Skip to content
This repository has been archived by the owner on Nov 8, 2024. It is now read-only.

Modularize validation #155

Merged
merged 38 commits into from
May 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b99fecf
refactor: adds "validateStatusCode" unit
artem-zakharchenko May 10, 2019
843dea1
refactor: adds "validateHeaders" unit
artem-zakharchenko May 10, 2019
788a824
refactor: adds "validateBody" unit
artem-zakharchenko May 10, 2019
f9f75c2
chore: allow leading underscore for arguments
artem-zakharchenko May 13, 2019
492d582
refactor: uses same signature for validation units
artem-zakharchenko May 13, 2019
6182a66
refactor: adds "isJsonSchema" predicate for validateBody
artem-zakharchenko May 14, 2019
3dd9b6d
refactor: removes "isJsonWeak" from "validateBody"
artem-zakharchenko May 14, 2019
846c45d
refactor: accepts "content-type" header directly in "getBodyType"
artem-zakharchenko May 14, 2019
e8959de
refactor: returns explicit null in "normalizeHeaders"
artem-zakharchenko May 14, 2019
558f7b7
refactor: delegate error handling to "validateBody" unit
artem-zakharchenko May 14, 2019
9a58c57
refactor: removes content-type parsing error handling from "getBodyType"
artem-zakharchenko May 14, 2019
98967c6
test: adds granular unit tests for "validateBody"
artem-zakharchenko May 15, 2019
79448a3
refactor: adds "evolve" utility
artem-zakharchenko May 15, 2019
350d8ca
fix: adds "normalizeHeaders"
artem-zakharchenko May 15, 2019
f2c2914
refactor: removes missing validator handling in "getBodyValidator"
artem-zakharchenko May 15, 2019
dd832ae
refactor: removes extra escaping for media type in "validateHeaders"
artem-zakharchenko May 15, 2019
b7b4452
refactor: adds "validateRequest"
artem-zakharchenko May 15, 2019
1038ec8
fix: fixes wrong strings interpolation in test suits
artem-zakharchenko May 15, 2019
46a5769
chore: updates to "[email protected]"
artem-zakharchenko May 16, 2019
6eea402
test: integrates new implementation into old test suits
artem-zakharchenko May 16, 2019
855a1d9
refactor: ensures same export type of modules
artem-zakharchenko May 16, 2019
8ddfa93
test: adds response test cases to "validateElement" tests
artem-zakharchenko May 16, 2019
e5c6dea
refactor: propagates internal exception to given callback
artem-zakharchenko May 16, 2019
cf73425
refactor: removes trimming of header values
artem-zakharchenko May 16, 2019
6c96930
refactor: removes try/catch blocks in "isJsonContentType"
artem-zakharchenko May 16, 2019
0bd7607
test: ensures proper return type of "evolve"
artem-zakharchenko May 16, 2019
7d50b13
refactor: renames "validateElement" to "validateMessage"
artem-zakharchenko May 16, 2019
c8018ea
refactor: prevents a call to callback within try/catch block
artem-zakharchenko May 16, 2019
c4a71c0
refactor: performs normalization of http messages in "validateMessage"
artem-zakharchenko May 16, 2019
85b6c22
refactor: simplifies "isset" util
artem-zakharchenko May 16, 2019
c77a427
refactor: parameterize http message origin of "getBodyType"
artem-zakharchenko May 16, 2019
bd2a906
test: asserts each missing header in "validateHeaders" test
artem-zakharchenko May 17, 2019
38776ba
test: polishes the test suits
artem-zakharchenko May 17, 2019
f42f615
test: adds JsonSchema test case for "validateBody"
artem-zakharchenko May 17, 2019
5589c2f
test: fixes typos in validation tests
artem-zakharchenko May 20, 2019
a0e3229
refactor: removes explicit normalization from "validateHeaders"
artem-zakharchenko May 20, 2019
98a9e6a
refactor: adds status code normalization to normalization layer
artem-zakharchenko May 20, 2019
20ec986
feat: adds new validation algorithm, release preparations
artem-zakharchenko May 20, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = {
node: true
},
rules: {
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this added? I don't see anything with leading underscores.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's used here currently:

const validator = validators.find(([_name, predicate]) => {

We can always swap the order of arguments, but it feels as a wrong compromise imho. The order of arguments if a functional design decision, and alerts on unused variables is a developer-friendly reminder. I would prioritize the first one, but am always open to discussion :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with it, but perhaps it doesn't deserve a global rule if it's at a single place :)

'consistent-return': 'off',
'class-methods-use-this': 'off',
'no-plusplus': 'off',
Expand Down
9 changes: 6 additions & 3 deletions lib/gavel.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
const { HttpRequest, ExpectedHttpRequest } = require('./model/http-request');
const { HttpResponse, ExpectedHttpResponse } = require('./model/http-response');

const { validate, isValid, isValidatable } = require('./validate');
const { isValid, isValidatable } = require('./validate');
const validate = require('./next/validate');

module.exports = {
// next
validate,

// legacy
HttpRequest,
HttpResponse,
ExpectedHttpRequest,
ExpectedHttpResponse,
validate,
isValid,
isValidatable
};
348 changes: 348 additions & 0 deletions lib/next/test/integration/validateMessage.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
const { assert } = require('chai');
const { validateMessage } = require('../../validateMessage');

describe('validateMessage', () => {
describe('with matching requests', () => {
const request = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: '{ "foo": "bar" }'
};
const result = validateMessage(request, request);

it('returns validation result object', () => {
assert.isObject(result);
});

it('contains all validatable keys', () => {
assert.hasAllKeys(result, ['headers', 'body']);
});

describe('headers', () => {
it('has "HeadersJsonExample" validator', () => {
assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample');
});

it('has "application/vnd.apiary.http-headers+json" real headers type', () => {
assert.propertyVal(
result.headers,
'realType',
'application/vnd.apiary.http-headers+json'
);
});

it('has "application/vnd.apiary.http-headers+json" expected headers type', () => {
assert.propertyVal(
result.headers,
'expectedType',
'application/vnd.apiary.http-headers+json'
);
});

it('has no errors', () => {
assert.lengthOf(result.headers.results, 0);
});
});

describe('body', () => {
it('has "JsonExample" validator', () => {
assert.propertyVal(result.body, 'validator', 'JsonExample');
});

it('has "application/json" real body type', () => {
assert.propertyVal(result.body, 'realType', 'application/json');
});

it('has "application/json" expected body type', () => {
assert.propertyVal(result.body, 'expectedType', 'application/json');
});

it('has no errors', () => {
assert.lengthOf(result.body.results, 0);
});
});
});

describe('with non-matching requests', () => {
const result = validateMessage(
{
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: '{ "lookHere": "foo" }'
},
{
method: 'PUT',
headers: '',
body: '2'
}
);

it('returns validation result object', () => {
assert.isObject(result);
});

it('contains all validatable keys', () => {
assert.hasAllKeys(result, ['headers', 'body']);
});

describe('method', () => {
// See https://github.com/apiaryio/gavel.js/issues/158
it.skip('compares methods');
});

describe('headers', () => {
it('has "HeadersJsonExample" validator', () => {
assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample');
});

it('has "application/vnd.apiary.http-headers+json" as real type', () => {
assert.propertyVal(
result.headers,
'realType',
'application/vnd.apiary.http-headers+json'
);
});

it('has "application/vnd.apiary.http-headers+json" expected type', () => {
assert.propertyVal(
result.headers,
'expectedType',
'application/vnd.apiary.http-headers+json'
);
});

it('has no errors', () => {
assert.lengthOf(result.headers.results, 0);
});
});

describe('body', () => {
it('has "JsonExample" validator', () => {
assert.propertyVal(result.body, 'validator', 'JsonExample');
});

it('has "application/json" real type', () => {
assert.propertyVal(result.body, 'realType', 'application/json');
});

it('has "application/json" expected type', () => {
assert.propertyVal(result.body, 'expectedType', 'application/json');
});

describe('produces an error', () => {
it('exactly one error', () => {
assert.lengthOf(result.body.results, 1);
});

it('has "error" severity', () => {
assert.propertyVal(result.body.results[0], 'severity', 'error');
});

it('has explanatory message', () => {
assert.propertyVal(
result.body.results[0],
'message',
`At '' Invalid type: object (expected integer)`
);
});
});
});
});

describe('with matching responses', () => {
const response = {
statusCode: 200,
headers: {
'Content-Type': 'application/json'
},
body: '{ "foo": "bar" }'
};
const result = validateMessage(response, response);

it('returns validation result object', () => {
assert.isObject(result);
});

it('contains all validatable keys', () => {
assert.hasAllKeys(result, ['statusCode', 'headers', 'body']);
});

describe('statusCode', () => {
it('has "TextDiff" validator', () => {
assert.propertyVal(result.statusCode, 'validator', 'TextDiff');
});

it('has "text/vnd.apiary.status-code" real type', () => {
assert.propertyVal(
result.statusCode,
'realType',
'text/vnd.apiary.status-code'
);
});

it('has "text/vnd.apiary.status-code" expected type', () => {
assert.propertyVal(
result.statusCode,
'expectedType',
'text/vnd.apiary.status-code'
);
});

it('has no errors', () => {
assert.lengthOf(result.statusCode.results, 0);
});
});

describe('headers', () => {
it('has "HeadersJsonExample" validator', () => {
assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample');
});

it('has "application/vnd.apiary.http-headers+json" real type', () => {
assert.propertyVal(
result.headers,
'realType',
'application/vnd.apiary.http-headers+json'
);
});

it('has "application/vnd.apiary.http-headers+json" expected type', () => {
assert.propertyVal(
result.headers,
'expectedType',
'application/vnd.apiary.http-headers+json'
);
});

it('has no errors', () => {
assert.lengthOf(result.headers.results, 0);
});
});

describe('body', () => {
it('has "JsonExample" validator', () => {
assert.propertyVal(result.body, 'validator', 'JsonExample');
});

it('has "application/json" real type', () => {
assert.propertyVal(result.body, 'realType', 'application/json');
});

it('has "application/json" expected type', () => {
assert.propertyVal(result.body, 'expectedType', 'application/json');
});

it('has no errors', () => {
assert.lengthOf(result.body.results, 0);
});
});
});

describe('with non-matching responses', () => {
const realResponse = {
statusCode: 400,
headers: {
'Content-Type': 'application/json'
}
};
const expectedResponse = {
statusCode: 200,
headers: {
'Accept-Language': 'en-US'
}
};
const result = validateMessage(realResponse, expectedResponse);

it('returns validation result object', () => {
assert.isObject(result);
});

it('contains all validatable keys', () => {
assert.hasAllKeys(result, ['statusCode', 'headers']);
});

describe('statusCode', () => {
it('has "TextDiff" validator', () => {
assert.propertyVal(result.statusCode, 'validator', 'TextDiff');
});

it('has "text/vnd.apiary.status-code" real type', () => {
assert.propertyVal(
result.statusCode,
'realType',
'text/vnd.apiary.status-code'
);
});

it('has "text/vnd.apiary.status-code" expected type', () => {
assert.propertyVal(
result.statusCode,
'expectedType',
'text/vnd.apiary.status-code'
);
});

describe('produces an error', () => {
it('exactly one error', () => {
assert.lengthOf(result.statusCode.results, 1);
});

it('has "error" severity', () => {
assert.propertyVal(result.statusCode.results[0], 'severity', 'error');
});

it('has explanatory message', () => {
assert.propertyVal(
result.statusCode.results[0],
'message',
'Real and expected data does not match.'
);
});
});
});

describe('headers', () => {
it('has "HeadersJsonExample" validator', () => {
assert.propertyVal(result.headers, 'validator', 'HeadersJsonExample');
});

it('has "application/vnd.apiary.http-headers+json" real type', () => {
assert.propertyVal(
result.headers,
'realType',
'application/vnd.apiary.http-headers+json'
);
});

it('has "application/vnd.apiary.http-headers+json" real type', () => {
assert.propertyVal(
result.headers,
'realType',
'application/vnd.apiary.http-headers+json'
);
});

describe('produces an error', () => {
it('exactly one error', () => {
assert.lengthOf(result.headers.results, 1);
});

it('has "error" severity', () => {
assert.propertyVal(result.headers.results[0], 'severity', 'error');
});

it('includes missing header in the message', () => {
assert.propertyVal(
result.headers.results[0],
'message',
`At '/accept-language' Missing required property: accept-language`
);
});
});
});
});
});
Loading