Skip to content

Commit

Permalink
Merge 32f3864 into 07c8c81
Browse files Browse the repository at this point in the history
  • Loading branch information
honzajavorek committed Mar 5, 2019
2 parents 07c8c81 + 32f3864 commit 7df327d
Show file tree
Hide file tree
Showing 21 changed files with 365 additions and 270 deletions.
84 changes: 34 additions & 50 deletions docs/usage-js.rst
Expand Up @@ -33,71 +33,55 @@ Let’s have a look at an example configuration first. (Please also see the :ref
{
server: 'http://127.0.0.1:3000/api', // your URL to API endpoint the tests will run against
options: {
'path': [], // Required Array if Strings; filepaths to API description documents, can use glob wildcards
path: [], // Required Array if Strings; filepaths to API description documents, can use glob wildcards
'dry-run': false, // Boolean, do not run any real HTTP transaction
'names': false, // Boolean, Print Transaction names and finish, similar to dry-run
'loglevel': 'warning', // String, logging level (debug, warning, error, silent)
'only': [], // Array of Strings, run only transaction that match these names
'header': [], // Array of Strings, these strings are then added as headers (key:value) to every transaction
'user': null, // String, Basic Auth credentials in the form username:password
'hookfiles': [], // Array of Strings, filepaths to files containing hooks (can use glob wildcards)
'reporter': ['dot', 'html'], // Array of possible reporters, see folder lib/reporters
'output': [], // Array of Strings, filepaths to files used for output of file-based reporters
names: false, // Boolean, Print Transaction names and finish, similar to dry-run
loglevel: 'warning', // String, logging level (debug, warning, error, silent)
only: [], // Array of Strings, run only transaction that match these names
header: [], // Array of Strings, these strings are then added as headers (key:value) to every transaction
user: null, // String, Basic Auth credentials in the form username:password
hookfiles: [], // Array of Strings, filepaths to files containing hooks (can use glob wildcards)
reporter: ['dot', 'html'], // Array of possible reporters, see folder lib/reporters
output: [], // Array of Strings, filepaths to files used for output of file-based reporters
'inline-errors': false, // Boolean, If failures/errors are display immediately in Dredd run
'require': null, // String, When using nodejs hooks, require the given module before executing hooks
'color': true,
require: null, // String, When using nodejs hooks, require the given module before executing hooks
color: true,
},
emitter: new EventEmitter(), // listen to test progress, your own instance of EventEmitter
apiDescriptions: ['FORMAT: 1A\n# Sample API\n']
}
'emitter': EventEmitterInstance, // optional - listen to test progress, your own instance of EventEmitter
.. js:data:: configuration

'data': {
'path/to/file': '...'
}
}
.. js:attribute:: configuration.server

Properties
----------
The HTTP(S) address of the API server to test against the API description(s). A valid URL is expected, e.g. ``http://127.0.0.1:8000``

server (string)
~~~~~~~~~~~~~~~
:type: string
:required: yes

Your choice of the API endpoint to test the API description against. It must be a valid URL (you can specify ``port``, ``path`` and http or https ``protocol``).
.. js:attribute:: configuration.options

options (object)
~~~~~~~~~~~~~~~~
Because :js:attr:`configuration.options.path` array is required, you must specify options. You’ll end with errors otherwise.

Because ``options.path`` array is required, you must specify options. You’ll end with errors otherwise.
:type: object
:required: yes

.. _optionspath-object:
.. js:attribute:: configuration.options.path

options.path (object)
^^^^^^^^^^^^^^^^^^^^^
Array of paths or URLs to API description documents.

**Required** Array of filepaths to API description documents. Or it can also be an URL to download the API description from internet via http(s) protocol.
:type: array
:required: yes

data (object)
^^^^^^^^^^^^^
.. js:attribute:: configuration.emitter

**Optional** Object with keys as ``filename`` and value as ``blueprint``-code.
Listen to test progress by providing your own instance of `EventEmitter <https://nodejs.org/api/events.html#events_class_eventemitter>`__.

Useful when you don’t want to operate on top of filesystem and want to pass code of your API description as a string. You get the point.
:type: EventEmitter

.. code-block:: javascript
.. js:attribute:: configuration.apiDescriptions

{
'data': {
'./api-description.apib': 'FORMAT: 1A\n\n# My String API\n\nGET /url\n+ Response 200\n\n Some content',
'./directory/another-api-description.apib': '# Another API\n\n## Group Machines\n\n### Machine [/machine]\n\n#### Read machine [GET]\n\n...'
}
}
API descriptions as strings. Useful when you don't want to operate on top of the filesystem.

:type: array
87 changes: 31 additions & 56 deletions lib/Dredd.js
Expand Up @@ -20,15 +20,11 @@ const PROXY_ENV_VARIABLES = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY'];
const FILE_DOWNLOAD_TIMEOUT = 5000;


function removeDuplicates(arr) {
return arr.reduce((alreadyProcessed, currentItem) => {
if (alreadyProcessed.indexOf(currentItem) === -1) {
return alreadyProcessed.concat(currentItem);
}
return alreadyProcessed;
}, []);
function unique(array) {
return Array.from(new Set(array));
}


class Dredd {
constructor(config) {
this.init(config);
Expand All @@ -49,6 +45,7 @@ class Dredd {
end: 0,
duration: 0,
};
this.configuration.files = [];
this.transactions = [];
this.runner = new Runner(this.configuration);
this.logger = logger;
Expand Down Expand Up @@ -77,37 +74,7 @@ https://dredd.org/en/latest/how-it-works/#using-https-proxy
}

run(callback) {
this.configDataIsEmpty = true;

if (!this.configuration.files) { this.configuration.files = []; }
if (!this.configuration.data) { this.configuration.data = {}; }

const passedConfigData = {};

const object = this.configuration.data || {};
for (const key of Object.keys(object || {})) {
const val = object[key];
this.configDataIsEmpty = false;
if (typeof val === 'string') {
passedConfigData[key] = {
filename: key,
raw: val,
};
} else if (typeof val === 'object' && val.raw && val.filename) {
passedConfigData[val.filename] = {
filename: val.filename,
raw: val.raw,
};
}
}

if (!this.configDataIsEmpty) {
this.configuration.data = passedConfigData;
}

// Remove duplicate paths
this.configuration.options.path = removeDuplicates(this.configuration.options.path);

// Take care of --require
if (this.configuration.options.require) {
let mod = this.configuration.options.require;
const abs = fs.existsSync(mod) || fs.existsSync(`${mod}.js`);
Expand Down Expand Up @@ -155,6 +122,8 @@ https://dredd.org/en/latest/how-it-works/#using-https-proxy
}

// Expand all globs
// TODO use the same mechanism as in 'resolveHookfiles', this is unnecessary,
// duplicate work
expandGlobs(callback) {
async.each(this.configuration.options.path, (globToExpand, globCallback) => {
if (/^http(s)?:\/\//.test(globToExpand)) {
Expand All @@ -178,7 +147,7 @@ https://dredd.org/en/latest/how-it-works/#using-https-proxy
(err) => {
if (err) { return callback(err, this.stats); }

if (this.configDataIsEmpty && this.configuration.files.length === 0) {
if (this.configuration.apiDescriptions.length === 0 && this.configuration.files.length === 0) {
err = new Error(`
API description document (or documents) not found on path:
'${this.configuration.options.path}'
Expand All @@ -187,7 +156,7 @@ API description document (or documents) not found on path:
}

// Remove duplicate filenames
this.configuration.files = removeDuplicates(this.configuration.files);
this.configuration.files = unique(this.configuration.files);
callback(null, this.stats);
});
}
Expand All @@ -204,8 +173,7 @@ API description document (or documents) not found on path:
} else {
this.readLocalFile(fileUrlOrPath, loadCallback);
}
},
callback);
}, callback);
}

downloadFile(fileUrl, callback) {
Expand All @@ -230,7 +198,10 @@ Server did not send any blueprint back and responded with status code ${res.stat
`);
return callback(err, this.stats);
}
this.configuration.data[fileUrl] = { raw: body, filename: fileUrl };
this.configuration.apiDescriptions.push({
location: fileUrl,
content: body,
});
callback(null, this.stats);
});
}
Expand All @@ -244,7 +215,10 @@ Is the provided path correct?
`);
return callback(err);
}
this.configuration.data[filePath] = { raw: data, filename: filePath };
this.configuration.apiDescriptions.push({
location: filePath,
content: data,
});
callback(null, this.stats);
});
}
Expand All @@ -254,30 +228,31 @@ Is the provided path correct?
this.transactions = [];

// Compile HTTP transactions for each API description
async.each(Object.keys(this.configuration.data), (filename, next) => {
const fileData = this.configuration.data[filename];
if (!fileData.annotations) { fileData.annotations = []; }
async.each(this.configuration.apiDescriptions, (apiDescription, next) => {
apiDescription.annotations = [];

this.logger.debug(`Parsing API description: ${filename}`);
parse(fileData.raw, (parseErr, parseResult) => {
this.logger.debug(`Parsing API description: ${apiDescription.location}`);
parse(apiDescription.content, (parseErr, parseResult) => {
if (parseErr) { next(parseErr); return; }

this.logger.debug(`Compiling HTTP transactions from API description: ${filename}`);
this.logger.debug(`Compiling HTTP transactions from API description: ${apiDescription.location}`);
let compileResult;
try {
compileResult = compile(parseResult.mediaType, parseResult.apiElements, filename);
compileResult = compile(parseResult.mediaType, parseResult.apiElements, apiDescription.location);
} catch (compileErr) {
next(compileErr);
return;
}
fileData.mediaType = compileResult.mediaType;
fileData.annotations = fileData.annotations.concat(compileResult.annotations);
this.transactions = this.transactions.concat(compileResult.transactions);
apiDescription.mediaType = compileResult.mediaType;
apiDescription.annotations = apiDescription.annotations.concat(compileResult.annotations);
this.transactions = this.transactions
.concat(compileResult.transactions)
.map(transaction => (Object.assign({ apiDescriptionMediaType: compileResult.mediaType }, transaction)));
next();
});
},
(runtimeError) => {
if (!runtimeError) { runtimeError = handleRuntimeProblems(this.configuration.data, this.logger); }
if (!runtimeError) { runtimeError = handleRuntimeProblems(this.configuration.apiDescriptions, this.logger); }
callback(runtimeError, this.stats);
});
}
Expand All @@ -289,7 +264,7 @@ Is the provided path correct?

// When event 'start' is emitted, function in callback is executed for each
// reporter registered by listeners
this.configuration.emitter.emit('start', this.configuration.data, (reporterError) => {
this.configuration.emitter.emit('start', this.configuration.apiDescriptions, (reporterError) => {
if (reporterError) { this.logger.error(reporterError.message); }

// Last called reporter callback function starts the runner
Expand Down
11 changes: 3 additions & 8 deletions lib/TransactionRunner.js
Expand Up @@ -38,7 +38,7 @@ class TransactionRunner {

config(config) {
this.configuration = config;
this.multiBlueprint = Object.keys(this.configuration.data).length > 1;
this.multiBlueprint = this.configuration.apiDescriptions.length > 1;
}

run(transactions, callback) {
Expand Down Expand Up @@ -197,13 +197,7 @@ class TransactionRunner {

configureTransaction(transaction) {
const { configuration } = this;

const { origin, request, response } = transaction;
const mediaType = (
configuration.data[origin.filename]
? configuration.data[origin.filename].mediaType
: undefined
) || 'text/vnd.apiblueprint';

// Parse the server URL (just once, caching it in @parsedUrl)
if (!this.parsedUrl) { this.parsedUrl = this.parseServerUrl(configuration.server); }
Expand Down Expand Up @@ -247,12 +241,13 @@ class TransactionRunner {
// Transaction skipping (can be modified in hooks). If the input format
// is OpenAPI 2, non-2xx transactions should be skipped by default.
let skip = false;
if (mediaType.indexOf('swagger') !== -1) {
if (transaction.apiDescriptionMediaType.includes('swagger')) {
const status = parseInt(response.status, 10);
if ((status < 200) || (status >= 300)) {
skip = true;
}
}
delete transaction.apiDescriptionMediaType;

const configuredTransaction = {
name: transaction.name,
Expand Down
36 changes: 35 additions & 1 deletion lib/configuration.js
Expand Up @@ -21,6 +21,11 @@ function coerceToBoolean(value) {
}


function unique(array) {
return Array.from(new Set(array));
}


function applyLoggingOptions(options) {
if (options.color === false) {
logger.transports.console.colorize = false;
Expand Down Expand Up @@ -126,6 +131,27 @@ function coerceRemovedOptions(config = {}) {
config.options.path = [].concat([config.blueprintPath], coerceToArray(config.options.path));
delete config.blueprintPath;
}
if (config.data) {
warnings.push("DEPRECATED: The 'data' configuration property is deprecated "
+ "in favor of 'apiDescriptions', please see https://dredd.org");

const apiDescriptions = Object.keys(config.data).map((location) => {
if (typeof config.data[location] === 'string') {
return {
location,
content: config.data[location],
};
}
return {
location: config.data[location].filename,
content: config.data[location].raw,
};
});
config.apiDescriptions = config.apiDescriptions
? config.apiDescriptions.concat(apiDescriptions)
: apiDescriptions;
delete config.data;
}

return { errors, warnings };
}
Expand All @@ -139,6 +165,7 @@ function applyConfiguration(inConfig) {
// Keep commented-out, so these values are actually set by CLI
// cwd: process.cwd()
},
apiDescriptions: [],
options: {
'dry-run': false,
reporter: null,
Expand Down Expand Up @@ -166,6 +193,13 @@ function applyConfiguration(inConfig) {
},
};

// Transform apiDescriptions from strings to an array of objects
outConfig.apiDescriptions = coerceToArray(inConfig.apiDescriptions)
.map((apiDescription, i) => ({
location: `configuration.apiDescriptions[${i}]`,
content: apiDescription,
}));

// Gracefully deal with the removed options
const coerceResult = coerceRemovedOptions(inConfig);

Expand All @@ -188,7 +222,7 @@ function applyConfiguration(inConfig) {
outConfig.options.header = coerceToArray(outConfig.options.header);
outConfig.options.method = coerceToArray(outConfig.options.method);
outConfig.options.only = coerceToArray(outConfig.options.only);
outConfig.options.path = coerceToArray(outConfig.options.path);
outConfig.options.path = unique(coerceToArray(outConfig.options.path));

outConfig.options.method = outConfig.options.method
.map(method => method.toUpperCase());
Expand Down

0 comments on commit 7df327d

Please sign in to comment.