Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BHBC-1689: Implement API Response Validation #737

Merged
merged 24 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8ed3b12
Add api response validation
NickPhura Apr 14, 2022
854fb7b
Updates
NickPhura Apr 14, 2022
62953f6
update reponse for the project attachments list
anissa-agahchen Apr 15, 2022
4c6efdf
BHBC-1689: Add validation to projects
curtisupshall Apr 21, 2022
489078e
BHBC-1689: Add response validation to surveys + attachments + observa…
curtisupshall Apr 25, 2022
7b3144b
BHBC-1689: Add response validation to project attachments.
curtisupshall Apr 25, 2022
ecf6d87
BHBC-1689: Fix publish_year type for report attachment uploading via …
curtisupshall Apr 25, 2022
b984714
BHBC-1689: Fix api response validation for user data response.
curtisupshall Apr 25, 2022
050cc67
BHBC-1689: Change year_published type from string to number in attach…
curtisupshall Apr 25, 2022
f47184d
BHBC-1689: Update tests
curtisupshall Apr 26, 2022
6ea5f61
Update occurrence submission response openapi
NickPhura Apr 26, 2022
443a2ea
Merge branch 'response_validation' of https://github.com/bcgov/biohub…
NickPhura Apr 26, 2022
cc071bb
BHBC-1689: Fix api response validation for uploads
curtisupshall Apr 26, 2022
c0cea84
Merge branch 'response_validation' of github.com:bcgov/biohubbc into …
curtisupshall Apr 26, 2022
aaa5d08
Update summary submission openapi and parse logic
NickPhura Apr 26, 2022
31a5a80
Merge branch 'response_validation' of https://github.com/bcgov/biohub…
NickPhura Apr 26, 2022
e5471d3
BHBC-1689: Fix all failing tests for reports
curtisupshall Apr 26, 2022
c1f49b7
BHBC-1689: Make lint-fix and make format-fix.
curtisupshall Apr 26, 2022
c9addc0
BHBC-1689: Remove console logs used for testing.
curtisupshall Apr 26, 2022
bd49318
Merge branch 'dev' into response_validation
curtisupshall Apr 26, 2022
b53c60c
api validation for view.ts and surveys.ts
anissa-agahchen Apr 26, 2022
3d49191
BHBC-1689: Fix validation for surveys.
curtisupshall Apr 26, 2022
2a28c73
BHBC-1689: Fix additional survey validation errors re publish_date.
curtisupshall Apr 26, 2022
d054b64
BHBC-1689: Fix additional survey validation for purpose_and_methodolg…
curtisupshall Apr 26, 2022
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
65 changes: 63 additions & 2 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import multer from 'multer';
import { OpenAPIV3 } from 'openapi-types';
import swaggerUIExperss from 'swagger-ui-express';
import { defaultPoolConfig, initDBPool } from './database/db';
import { ensureHTTPError } from './errors/custom-error';
import { ensureHTTPError, HTTPErrorType } from './errors/custom-error';
import { rootAPIDoc } from './openapi/root-api-doc';
import { authenticateRequest } from './request-handlers/security/authentication';
import { getLogger } from './utils/logger';
Expand Down Expand Up @@ -38,7 +38,11 @@ app.use(function (req: Request, res: Response, next: NextFunction) {

// Initialize express-openapi framework
const openAPIFramework = initialize({
apiDoc: rootAPIDoc as OpenAPIV3.Document, // base open api spec
apiDoc: {
...(rootAPIDoc as OpenAPIV3.Document), // base open api spec
'x-express-openapi-additional-middleware': [validateAllResponses],
'x-express-openapi-validation-strict': true
},
app: app, // express app to initialize
paths: './src/paths', // base folder for endpoint routes
pathsIgnore: new RegExp('.(spec|test)$'), // ignore test files in paths
Expand Down Expand Up @@ -107,3 +111,60 @@ try {
defaultLog.error({ label: 'start api', message: 'error', error });
process.exit(1);
}

/**
* Middleware to apply openapi response validation to all routes.
*
* Note: validates `<data>` sent via `res.status(<status>).json(<data>)` against the matching openapi response schema
* for `<status>`.
*
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
function validateAllResponses(req: Request, res: Response, next: NextFunction) {
const isStrictValidation = !!req['apiDoc']['x-express-openapi-validation-strict'] || false;

if (typeof res['validateResponse'] === 'function') {
const json = res.json;

res.json = (...args) => {
if (res.get('x-express-openapi-validation-error-for')) {
// Already validated, return
return json.apply(res, args);
}

const body = args[0];

const validationResult: { message: any; errors: any[] } | undefined = res['validateResponse'](
res.statusCode,
body
);

let validationMessage = '';
let errorList = [];

if (validationResult?.errors) {
validationMessage = `Invalid response for status code ${res.statusCode}`;

errorList = Array.from(validationResult.errors);

// Set to avoid a loop, and to provide the original status code
res.set('x-express-openapi-validation-error-for', res.statusCode.toString());
}

if (!isStrictValidation || !validationResult?.errors) {
return json.apply(res, args);
} else {
return res.status(500).json({
name: HTTPErrorType.INTERNAL_SERVER_ERROR,
status: 500,
message: validationMessage,
errors: errorList
});
}
};
}

next();
}
16 changes: 8 additions & 8 deletions api/src/models/project-survey-attachments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('PostReportAttachmentsMetaData', () => {
});

it('sets attachmentsData', function () {
expect(postReportAttachmentsData).to.eql({ title: null, year_published: null, authors: [], description: null });
expect(postReportAttachmentsData).to.eql({ title: null, year_published: 0, authors: [], description: null });
});
});

Expand All @@ -72,7 +72,7 @@ describe('PostReportAttachmentsMetaData', () => {

const input = {
title: 'Report 1',
year_published: '2000',
year_published: 2000,
authors: [{ first_name: 'John', last_name: 'Smith' }],
description: 'abstract of the report'
};
Expand All @@ -84,7 +84,7 @@ describe('PostReportAttachmentsMetaData', () => {
it('sets the report metadata', function () {
expect(postReportAttachmentsData).to.eql({
title: 'Report 1',
year_published: '2000',
year_published: 2000,
authors: [{ first_name: 'John', last_name: 'Smith' }],
description: 'abstract of the report'
});
Expand All @@ -98,7 +98,7 @@ describe('PutReportAttachmentMetaData', () => {
const putReportAttachmentData = new PutReportAttachmentMetadata(null);

expect(putReportAttachmentData.title).to.equal(null);
expect(putReportAttachmentData.year_published).to.equal(null);
expect(putReportAttachmentData.year_published).to.equal(0);
expect(putReportAttachmentData.authors).to.eql([]);
expect(putReportAttachmentData.description).to.equal(null);
expect(putReportAttachmentData.revision_count).to.equal(null);
Expand All @@ -108,7 +108,7 @@ describe('PutReportAttachmentMetaData', () => {
describe('All values provided', () => {
const input = {
title: 'Report 1',
year_published: '2000',
year_published: 2000,
authors: [{ first_name: 'John', last_name: 'Smith' }],
description: 'abstract of the report',
revision_count: 1
Expand All @@ -133,7 +133,7 @@ describe('GetReportAttachmentMetaData', () => {
expect(getReportAttachmentData).to.eql({
attachment_id: null,
title: null,
year_published: null,
year_published: 0,
authors: [],
description: null,
last_modified: null,
Expand All @@ -148,7 +148,7 @@ describe('GetReportAttachmentMetaData', () => {
title: 'My Report',
update_date: '2020-10-10',
description: 'abstract of the report',
year: '2020',
year_published: 2020,
revision_count: 2,
authors: [{ first_name: 'John', last_name: 'Smith' }]
};
Expand All @@ -157,7 +157,7 @@ describe('GetReportAttachmentMetaData', () => {
const getReportAttachmentData = new GetReportAttachmentMetadata(input);

expect(getReportAttachmentData.title).to.equal(input.title);
expect(getReportAttachmentData.year_published).to.equal(input.year);
expect(getReportAttachmentData.year_published).to.equal(input.year_published);
expect(getReportAttachmentData.description).to.equal(input.description);
expect(getReportAttachmentData.last_modified).to.equal(input.update_date);
expect(getReportAttachmentData.revision_count).to.equal(input.revision_count);
Expand Down
10 changes: 5 additions & 5 deletions api/src/models/project-survey-attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class GetAttachmentsData {
id: item.id,
fileName: item.file_name,
fileType: item.file_type || 'Report',
lastModified: item.update_date || item.create_date,
lastModified: (item.update_date || item.create_date).toString(),
size: item.file_size,
securityToken: item.security_token
};
Expand All @@ -37,13 +37,13 @@ export interface IReportAttachmentAuthor {

export class PostReportAttachmentMetadata {
title: string;
year_published: string;
year_published: number;
authors: IReportAttachmentAuthor[];
description: string;

constructor(obj?: any) {
this.title = (obj && obj?.title) || null;
this.year_published = (obj && obj?.year_published) || null;
this.year_published = Number((obj && obj?.year_published) || null);
this.authors = (obj?.authors?.length && obj.authors) || [];
this.description = (obj && obj?.description) || null;
}
Expand Down Expand Up @@ -71,9 +71,9 @@ export class GetReportAttachmentMetadata {
constructor(metaObj?: any, authorObj?: any) {
this.attachment_id = (metaObj && metaObj?.attachment_id) || null;
this.title = (metaObj && metaObj?.title) || null;
this.last_modified = (metaObj && metaObj?.update_date) || null;
this.last_modified = (metaObj && metaObj?.update_date.toString()) || null;
this.description = (metaObj && metaObj?.description) || null;
this.year_published = (metaObj && metaObj?.year) || null;
this.year_published = Number((metaObj && metaObj?.year_published) || null);
this.revision_count = (metaObj && metaObj?.revision_count) || null;
this.authors =
(authorObj &&
Expand Down
2 changes: 1 addition & 1 deletion api/src/models/project-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class GetProjectData {
moment(projectData.end_date).endOf('day').isBefore(moment()) &&
COMPLETION_STATUS.COMPLETED) ||
COMPLETION_STATUS.ACTIVE;
this.publish_date = projectData?.publish_date || '';
this.publish_date = String(projectData?.publish_date || '');
this.revision_count = projectData?.revision_count ?? null;
}
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/models/public/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class GetPublicAttachmentsData {
id: item.id,
fileName: item.file_name,
fileType: item.file_type || 'Report',
lastModified: item.update_date || item.create_date,
lastModified: (item.update_date || item.create_date).toString(),
size: item.file_size,
securityToken: item.is_secured
};
Expand Down
2 changes: 1 addition & 1 deletion api/src/models/survey-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export class GetUpdateSurveyDetailsData {
moment(surveyDetailsData.end_date).endOf('day').isBefore(moment()) &&
COMPLETION_STATUS.COMPLETED) ||
COMPLETION_STATUS.ACTIVE;
this.publish_date = surveyDetailsData?.publish_date || '';
this.publish_date = String(surveyDetailsData?.publish_date || '');
}
}

Expand Down
2 changes: 1 addition & 1 deletion api/src/models/survey-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export class GetViewSurveyDetailsData {
moment(surveyDetailsData.end_date).endOf('day').isBefore(moment()) &&
COMPLETION_STATUS.COMPLETED) ||
COMPLETION_STATUS.ACTIVE;
this.publish_date = surveyDetailsData?.publish_date || '';
this.publish_date = String(surveyDetailsData?.publish_date || '');
}
}

Expand Down
4 changes: 2 additions & 2 deletions api/src/openapi/schemas/administrative-activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const administrativeActivityResponseObject = {
type: 'number'
},
date: {
type: 'string',
description: 'The date this administrative activity was made'
description: 'The date this administrative activity was made',
oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }]
}
}
};
Expand Down
2 changes: 1 addition & 1 deletion api/src/openapi/schemas/permit-no-sampling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const permitNoSamplingPostBody = {
export const permitNoSamplingResponseBody = {
title: 'Permit no sampling Response Object',
type: 'object',
required: ['id'],
required: ['ids'],
properties: {
ids: {
type: 'array',
Expand Down
8 changes: 5 additions & 3 deletions api/src/paths/administrative-activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ GET.apiDoc = {
description: 'Administrative activity status type name'
},
description: {
type: 'string'
type: 'string',
nullable: true
},
notes: {
type: 'string'
type: 'string',
nullable: true
},
data: {
type: 'object',
Expand All @@ -105,7 +107,7 @@ GET.apiDoc = {
}
},
create_date: {
type: 'string'
oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }]
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions api/src/paths/dwc/view-occurrences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ POST.apiDoc = {
'application/json': {
schema: {
title: 'Occurrences spatial and metadata response object, for view purposes',
type: 'object',
properties: {}
type: 'array',
items: {}
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion api/src/paths/permit/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ GET.apiDoc = {
type: 'string'
},
project_name: {
type: 'string'
type: 'string',
nullable: true
}
}
},
Expand Down
59 changes: 29 additions & 30 deletions api/src/paths/project/{projectId}/attachments/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,35 @@ GET.apiDoc = {
content: {
'application/json': {
schema: {
type: 'array',
items: {
type: 'object',
required: ['projectId', 'fileName', 'fileType', 'lastModified', 'size', 'securityToken', 'revisionCount'],
properties: {
projectId: {
type: 'number'
},
fileName: {
description: 'The file name of the attachment',
type: 'string'
},
fileType: {
description: 'The file type of the attachment',
type: 'string'
},
lastModified: {
description: 'The date the object was last modified',
type: 'string'
},
size: {
description: 'The size of the attachment',
type: 'number'
},
securityToken: {
description: 'The security token of the attachment',
type: 'string'
},
revisionCount: {
type: 'number'
type: 'object',
properties: {
attachmentsList: {
type: 'array',
items: {
type: 'object',
required: ['id', 'fileName', 'fileType', 'lastModified', 'securityToken', 'size'],
properties: {
id: {
type: 'number'
},
fileName: {
type: 'string'
},
fileType: {
type: 'string'
},
lastModified: {
type: 'string'
},
securityToken: {
description: 'The security token of the attachment',
type: 'string',
nullable: true
},
size: {
type: 'number'
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ POST.apiDoc = {
type: 'string'
},
year_published: {
type: 'string'
type: 'string',
description:
'Year the report is published. (Note: Content-Type: multipart/form-data requires all parameters to be strings.)'
},
authors: {
type: 'array',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ POST.apiDoc = {
type: 'string'
},
securityToken: {
type: 'string'
type: 'string',
nullable: true
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe('gets metadata for a project report', () => {
title: 'My report',
update_date: '2020-10-10',
description: 'some description',
year: '2020',
year_published: 2020,
revision_count: '1'
}
]
Expand All @@ -153,7 +153,7 @@ describe('gets metadata for a project report', () => {
title: 'My report',
last_modified: '2020-10-10',
description: 'some description',
year_published: '2020',
year_published: 2020,
revision_count: '1',
authors: [{ first_name: 'John', last_name: 'Smith' }]
});
Expand Down
Loading