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-2076: Display Templates from S3 #910

Merged
merged 26 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b0af066
BHBC-2076: Add helper util for listing s3 files
curtisupshall Jan 9, 2023
e3c59e0
BHBC-2076: Created new endpoint for resources
curtisupshall Jan 9, 2023
4a07866
BHBC-2076: Implment new resources endpoint on resources page
curtisupshall Jan 9, 2023
b8cc4cd
BHBC-2076: Create object metadata util function for S3
curtisupshall Jan 10, 2023
5539edb
BHBC-2076: Enhanced typedefs for resource files
curtisupshall Jan 10, 2023
c6b0d63
BHBC-2076: Add filtering and metadata formatting to list resources en…
curtisupshall Jan 10, 2023
88f4626
BHBC-2076: add filename to resources list response
curtisupshall Jan 10, 2023
93627a7
BHBC-2076: view and download templates from S3
curtisupshall Jan 10, 2023
c252b24
BHBC-2076: Add spinner to frontend during resource loading
curtisupshall Jan 10, 2023
b42c42c
BHBC-2076: Test boilerplate; fix response validation for API
curtisupshall Jan 10, 2023
6c035ef
BHBC-2076: update list endpoint tests
curtisupshall Jan 11, 2023
bd0c3f8
BHBC-2076: Format and lint fixes
curtisupshall Jan 11, 2023
afc152f
BHBC-2076: Fix codesmells
curtisupshall Jan 11, 2023
f71465a
BHBC-2076: Add tests for ResourcesPage
curtisupshall Jan 12, 2023
7d6ad5c
BHBC-2076: Modify file utils to use getters rather than global scope
curtisupshall Jan 12, 2023
6b1166b
BHBC-2076: Jsdoc; Lint and format fixes
curtisupshall Jan 12, 2023
2b7b512
BHBC-2076: Fix tests, linting
curtisupshall Jan 12, 2023
de9d1e4
BHBC-2076: Added typedefs and jsdoc
curtisupshall Jan 12, 2023
769cf87
BHBC-2076: Resolve regex backtracking vuln
curtisupshall Jan 12, 2023
d13266c
BHBC-2076: Resolve regex backtracking vuln 2
curtisupshall Jan 12, 2023
826ba65
BHBC-2076: Fix test
curtisupshall Jan 12, 2023
d9dd16d
BHBC-2076: increase test coverage
curtisupshall Jan 12, 2023
f197c0b
BHBC-2076: Add test for useResourcesApi
curtisupshall Jan 12, 2023
998e1f8
Merge branch 'dev' into BHBC-2076
NickPhura Jan 13, 2023
03a6bc3
BHBC-2076: Fix download action bug
curtisupshall Jan 14, 2023
0159c57
BHBC-2076: Format fix
curtisupshall Jan 16, 2023
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
342 changes: 342 additions & 0 deletions api/src/paths/resources/list.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
import chai, { expect } from 'chai';
import { describe } from 'mocha';
import OpenAPIResponseValidator, { OpenAPIResponseValidatorArgs } from 'openapi-response-validator';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { HTTPError } from '../../errors/http-error';
import * as fileUtils from '../../utils/file-utils';
import { getRequestHandlerMocks } from '../../__mocks__/db';
import { GET, listResources } from './list';

chai.use(sinonChai);

describe('listResources', () => {
beforeEach(() => {
process.env.OBJECT_STORE_URL = 's3.host.example.com';
process.env.OBJECT_STORE_BUCKET_NAME = 'test-bucket';
});

afterEach(() => {
sinon.restore();
});

it('returns an empty array if no resources are found', async () => {
const listFilesStub = sinon.stub(fileUtils, 'listFilesFromS3').resolves({
Contents: []
});

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
const requestHandler = listResources();

await requestHandler(mockReq, mockRes, mockNext);

expect(listFilesStub).to.have.been.calledWith('templates/Current');
expect(mockRes.jsonValue).to.eql({ files: [] });
expect(mockRes.statusValue).to.equal(200);
});

it('returns an array of resources', async () => {
const mockMetadata = {
['key1']: {
'template-name': 'name1',
'template-type': 'type1',
species: 'species1'
},
['key2']: {
'template-name': 'name2',
'template-type': 'type2',
species: 'species2'
},
['key3']: {
'template-name': 'name3',
'template-type': 'type3',
species: 'species3'
}
};

sinon.stub(fileUtils, 'getObjectMeta').callsFake((key: string) => {
return Promise.resolve({
Metadata: mockMetadata[key]
});
});

const listFilesStub = sinon.stub(fileUtils, 'listFilesFromS3').resolves({
Contents: [
{
Key: 'key1',
LastModified: new Date('2023-01-01'),
Size: 5
},
{
Key: 'key2',
LastModified: new Date('2023-01-02'),
Size: 10
},
{
Key: 'key3',
LastModified: new Date('2023-01-03'),
Size: 15
}
]
});

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
const requestHandler = listResources();

await requestHandler(mockReq, mockRes, mockNext);

expect(listFilesStub).to.have.been.calledWith('templates/Current');
expect(mockRes.jsonValue).to.eql({
files: [
{
fileName: 'key1',
url: 's3.host.example.com/test-bucket/key1',
lastModified: new Date('2023-01-01').toISOString(),
fileSize: 5,
metadata: {
templateName: 'name1',
templateType: 'type1',
species: 'species1'
}
},
{
fileName: 'key2',
url: 's3.host.example.com/test-bucket/key2',
lastModified: new Date('2023-01-02').toISOString(),
fileSize: 10,
metadata: {
templateName: 'name2',
templateType: 'type2',
species: 'species2'
}
},
{
fileName: 'key3',
url: 's3.host.example.com/test-bucket/key3',
lastModified: new Date('2023-01-03').toISOString(),
fileSize: 15,
metadata: {
templateName: 'name3',
templateType: 'type3',
species: 'species3'
}
}
]
});
expect(mockRes.statusValue).to.equal(200);
});

it('should filter out directories from the s3 list respones', async () => {
sinon.stub(fileUtils, 'getObjectMeta').resolves({});

const listFilesStub = sinon.stub(fileUtils, 'listFilesFromS3').resolves({
Contents: [
{
Key: 'templates/Current/'
}
]
});

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();
const requestHandler = listResources();

await requestHandler(mockReq, mockRes, mockNext);

expect(listFilesStub).to.have.been.calledWith('templates/Current');
expect(mockRes.jsonValue).to.eql({ files: [] });
expect(mockRes.statusValue).to.equal(200);
});

it('catches error, and re-throws error', async () => {
sinon.stub(fileUtils, 'listFilesFromS3').rejects(new Error('an error occurred'));

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

try {
const requestHandler = listResources();

await requestHandler(mockReq, mockRes, mockNext);
expect.fail();
} catch (actualError) {
expect((actualError as HTTPError).message).to.equal('an error occurred');
}
});

describe('openApiSchema', () => {
describe('response validation', () => {
const responseValidator = new OpenAPIResponseValidator((GET.apiDoc as unknown) as OpenAPIResponseValidatorArgs);

describe('should succeed when', () => {
it('returns an empty response', async () => {
const apiResponse = { files: [] };
const response = responseValidator.validateResponse(200, apiResponse);

expect(response).to.equal(undefined);
});

it('optional values are not included', async () => {
const apiResponse = {
files: [
{
url: 'string1',
fileName: 'string1',
lastModified: 'string1',
fileSize: 0,
metadata: {}
}
]
};
const response = responseValidator.validateResponse(200, apiResponse);

expect(response).to.equal(undefined);
});

it('optional values are valid', async () => {
const apiResponse = {
files: [
{
url: 'string1',
fileName: 'string1',
lastModified: 'string1',
fileSize: 0,
metadata: {
templateName: 'string1',
templateType: 'string1',
species: 'string1'
}
}
]
};
const response = responseValidator.validateResponse(200, apiResponse);

expect(response).to.equal(undefined);
});
});

describe('should fail when', () => {
it('returns a null response', async () => {
const apiResponse = null;
const response = responseValidator.validateResponse(200, apiResponse);

expect(response.message).to.equal('The response was not valid.');
expect(response.errors[0].message).to.equal('must be object');
});

it('file has no fileName', async () => {
const apiResponse = {
files: [
{
url: 'string1',
lastModified: 'string1',
fileSize: 0,
metadata: {}
}
]
};

const response = responseValidator.validateResponse(200, apiResponse);
expect(response.message).to.equal('The response was not valid.');
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal("must have required property 'fileName'");
expect(response.errors[0].path).to.equal('files/0');
});

it('file has no url', async () => {
const apiResponse = {
files: [
{
fileName: 'string1',
lastModified: 'string1',
fileSize: 0,
metadata: {}
}
]
};

const response = responseValidator.validateResponse(200, apiResponse);
expect(response.message).to.equal('The response was not valid.');
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal("must have required property 'url'");
expect(response.errors[0].path).to.equal('files/0');
});

it('file has no lastModified', async () => {
const apiResponse = {
files: [
{
url: 'string1',
fileName: 'string1',
fileSize: 0,
metadata: {}
}
]
};

const response = responseValidator.validateResponse(200, apiResponse);
expect(response.message).to.equal('The response was not valid.');
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal("must have required property 'lastModified'");
expect(response.errors[0].path).to.equal('files/0');
});

it('file has no fileSize', async () => {
const apiResponse = {
files: [
{
url: 'string1',
fileName: 'string1',
lastModified: 'string1',
metadata: {}
}
]
};

const response = responseValidator.validateResponse(200, apiResponse);
expect(response.message).to.equal('The response was not valid.');
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal("must have required property 'fileSize'");
expect(response.errors[0].path).to.equal('files/0');
});

it('fileSize is not a number', async () => {
const apiResponse = {
files: [
{
url: 'string1',
fileName: 'string1',
lastModified: 'string1',
fileSize: '100 kB',
metadata: {}
}
]
};

const response = responseValidator.validateResponse(200, apiResponse);
expect(response.message).to.equal('The response was not valid.');
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal('must be number');
expect(response.errors[0].path).to.equal('files/0/fileSize');
});

it('file has no metadata', async () => {
const apiResponse = {
files: [
{
url: 'string1',
lastModified: 'string1',
fileName: 'string1',
fileSize: 0
}
]
};

const response = responseValidator.validateResponse(200, apiResponse);
expect(response.message).to.equal('The response was not valid.');
expect(response.errors.length).to.equal(1);
expect(response.errors[0].message).to.equal("must have required property 'metadata'");
expect(response.errors[0].path).to.equal('files/0');
});
});
});
});
});
Loading