-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cb2-10457): add generic file retrieval (#55)
* feat(cb2-10457): add generic file retrieval * fix(cb2-10457): supress lint errors * fix(cb2-10457): auto lint updates
- Loading branch information
1 parent
c3419d1
commit a6fcac3
Showing
10 changed files
with
533 additions
and
106 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* eslint-disable @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-base-to-string */ | ||
|
||
import { S3, AWSError } from 'aws-sdk'; | ||
import { APIGatewayProxyResult } from 'aws-lambda'; | ||
import getObjectFromS3 from '../infrastructure/s3/s3PlateService'; | ||
import NoBodyError from '../errors/NoBodyError'; | ||
import MissingBucketNameError from '../errors/MissingBucketNameError'; | ||
import IncorrectFileTypeError from '../errors/IncorrectFileTypeError'; | ||
import MissingFolderNameError from '../errors/MissingFolderNameError'; | ||
import FileNameError from '../errors/FileNameError'; | ||
import FileDetails from '../interfaces/FileDetails'; | ||
|
||
function isAWSError(error: Error | AWSError): error is AWSError { | ||
return Object.prototype.hasOwnProperty.call(error, 'code') as boolean; | ||
} | ||
|
||
export default async ( | ||
event: FileDetails, | ||
s3: S3, | ||
bucketName: string | undefined, | ||
folder: string | undefined, | ||
currentEnvironment: string | undefined, | ||
): Promise<APIGatewayProxyResult> => { | ||
try { | ||
if (!bucketName) { | ||
throw new MissingBucketNameError(); | ||
} | ||
if (currentEnvironment !== 'local' && !folder) { | ||
throw new MissingFolderNameError(); | ||
} | ||
|
||
console.log(`Validating: ${event.fileName}`); | ||
|
||
if (!event.fileName) { | ||
throw new FileNameError(); | ||
} | ||
|
||
const file = await getObjectFromS3(s3, bucketName, folder, event.fileName); | ||
const response = file.toString('base64'); | ||
|
||
const headers = { | ||
'Content-type': 'application/pdf', | ||
'Access-Control-Allow-Origin': '*', | ||
'Access-Control-Allow-Credentials': true, | ||
'X-Content-Type-Options': 'nosniff', | ||
Vary: 'Origin', | ||
'X-XSS-Protection': '1; mode=block', | ||
}; | ||
|
||
return { | ||
headers, | ||
statusCode: 200, | ||
body: response, | ||
isBase64Encoded: true, | ||
}; | ||
} catch (e) { | ||
let code = 500; | ||
let message = ''; | ||
|
||
// Split into 50x and 40x errors. | ||
if (e instanceof NoBodyError || e instanceof MissingBucketNameError || e instanceof MissingFolderNameError) { | ||
message = e.message; | ||
} | ||
|
||
if (e instanceof FileNameError) { | ||
code = 400; | ||
message = e.message; | ||
} | ||
|
||
if (e instanceof IncorrectFileTypeError) { | ||
code = 404; | ||
message = e.message; | ||
} | ||
|
||
if (isAWSError(e)) { | ||
// S3 error that the key does not exist | ||
if (['NoSuchKey'].includes(e.code)) { | ||
code = 404; | ||
} | ||
|
||
// Any other AWS errors we get will always be a 500 because it will be an error on our part. | ||
message = e.code; | ||
} | ||
|
||
console.error(code); | ||
console.error(message); | ||
|
||
return { | ||
statusCode: code, | ||
body: message, | ||
}; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default class FileNameError extends Error { | ||
constructor() { | ||
super(); | ||
this.message = 'File name is missing or incorrect'; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { S3 } from 'aws-sdk'; | ||
import IncorrectFileTypeError from '../../errors/IncorrectFileTypeError'; | ||
import NoBodyError from '../../errors/NoBodyError'; | ||
|
||
export default async ( | ||
s3: S3, | ||
bucket: string, | ||
folder: string | undefined, | ||
fileName: string, | ||
): Promise<S3.Body> => { | ||
const key = folder ? `${folder}/${fileName}.pdf` : `${fileName}.pdf`; | ||
|
||
console.info(`Bucket name: ${bucket}`); | ||
console.info(`Item key: ${key}`); | ||
|
||
const response = await s3 | ||
.getObject({ | ||
Bucket: bucket, | ||
Key: key, | ||
}) | ||
.promise(); | ||
|
||
if (response.ContentType !== 'application/octet-stream' && response.ContentType !== 'application/pdf') { | ||
console.error(`Incorrect content-type: ${response.ContentType}`); | ||
throw new IncorrectFileTypeError(); | ||
} | ||
|
||
if (response.Body) { | ||
return response.Body; | ||
} | ||
|
||
throw new NoBodyError(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default interface FileDetails { | ||
fileName : string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import { S3 } from 'aws-sdk'; | ||
import MissingBucketNameError from '../../../src/errors/MissingBucketNameError'; | ||
import NoBodyError from '../../../src/errors/NoBodyError'; | ||
import IncorrectFileTypeError from '../../../src/errors/IncorrectFileTypeError'; | ||
import MissingFolderNameError from '../../../src/errors/MissingFolderNameError'; | ||
import FileDetails from '../../../src/interfaces/FileDetails'; | ||
import getFile from '../../../src/domain/getFile'; | ||
import FileNameError from '../../../src/errors/FileNameError'; | ||
|
||
describe('getFile', () => { | ||
it('returns an internal server error if the bucket is undefined', async () => { | ||
const response = await getFile({} as FileDetails, ({} as unknown) as S3, undefined, 'folder', 'test'); | ||
const error = new MissingBucketNameError(); | ||
|
||
expect(response.statusCode).toBe(500); | ||
expect(response.body).toEqual(error.message); | ||
}); | ||
|
||
it('returns an internal server error if the folder is undefined', async () => { | ||
const response = await getFile({} as FileDetails, ({} as unknown) as S3, 'bucket', undefined, 'test'); | ||
const error = new MissingFolderNameError(); | ||
|
||
expect(response.statusCode).toBe(500); | ||
expect(response.body).toEqual(error.message); | ||
}); | ||
|
||
it('returns a bad request if the file name is invalid', async () => { | ||
const event: FileDetails = { | ||
fileName: undefined, | ||
}; | ||
const response = await getFile(event, ({} as unknown) as S3, 'bucket', 'folder', 'test'); | ||
const error = new FileNameError(); | ||
|
||
expect(response.statusCode).toBe(400); | ||
expect(response.body).toEqual(error.message); | ||
}); | ||
|
||
it('returns an internal server error if there is no Body in the S3 request', async () => { | ||
const mockS3 = ({} as unknown) as S3; | ||
const mockPromise = jest.fn().mockReturnValue(Promise.resolve({ ContentType: 'application/octet-stream' })); | ||
const mockGetObject = jest.fn().mockReturnValue({ promise: mockPromise }); | ||
|
||
mockS3.getObject = mockGetObject; | ||
|
||
const event: FileDetails = { | ||
fileName: 'adr_pass_123_2024-01-22T11:48:16.035Z', | ||
}; | ||
const response = await getFile(event, mockS3, 'bucket', 'folder', 'test'); | ||
const error = new NoBodyError(); | ||
|
||
expect(response.statusCode).toBe(500); | ||
expect(response.body).toEqual(error.message); | ||
}); | ||
|
||
it('returns an 404 if the stored file is not a PDF', async () => { | ||
const mockS3 = ({} as unknown) as S3; | ||
const mockPromise = jest | ||
.fn() | ||
.mockReturnValue(Promise.resolve({ Body: 'This is an image', ContentType: 'image/jpg' })); | ||
const mockGetObject = jest.fn().mockReturnValue({ promise: mockPromise }); | ||
|
||
mockS3.getObject = mockGetObject; | ||
|
||
const event: FileDetails = { | ||
fileName: 'adr_pass_123_2024-01-22T11:48:16.035Z', | ||
}; | ||
const response = await getFile(event, mockS3, 'bucket', 'folder', 'test'); | ||
const error = new IncorrectFileTypeError(); | ||
|
||
expect(response.statusCode).toBe(404); | ||
expect(response.body).toEqual(error.message); | ||
}); | ||
|
||
it('returns a not found error if the file is not found', async () => { | ||
const mockS3 = ({} as unknown) as S3; | ||
const mockPromise = jest.fn().mockReturnValue(Promise.reject(({ code: 'NoSuchKey' } as unknown) as Error)); // eslint-disable-line prefer-promise-reject-errors | ||
const mockGetObject = jest.fn().mockReturnValue({ promise: mockPromise }); | ||
|
||
mockS3.getObject = mockGetObject; | ||
|
||
const event: FileDetails = { | ||
fileName: 'adr_pass_123_2024-01-22T11:48:16.035Z', | ||
}; | ||
const response = await getFile(event, mockS3, 'bucket', 'folder', 'test'); | ||
|
||
expect(response.statusCode).toBe(404); | ||
expect(response.body).toBe('NoSuchKey'); | ||
}); | ||
|
||
it('returns an internal server error if the S3 get fails for any other reason', async () => { | ||
const mockS3 = ({} as unknown) as S3; | ||
const mockPromise = jest.fn().mockReturnValue(Promise.reject(({ code: 'Generic Error' } as unknown) as Error)); // eslint-disable-line prefer-promise-reject-errors | ||
const mockGetObject = jest.fn().mockReturnValue({ promise: mockPromise }); | ||
|
||
mockS3.getObject = mockGetObject; | ||
|
||
const event: FileDetails = { | ||
fileName: 'adr_pass_123_2024-01-22T11:48:16.035Z', | ||
}; | ||
const response = await getFile(event, mockS3, 'bucket', 'folder', 'test'); | ||
|
||
expect(response.statusCode).toBe(500); | ||
expect(response.body).toBe('Generic Error'); | ||
}); | ||
|
||
it('returns a successful response if everything works', async () => { | ||
const mockS3 = ({} as unknown) as S3; | ||
const mockPromise = jest | ||
.fn() | ||
.mockReturnValue(Promise.resolve({ Body: 'Certificate Content', ContentType: 'application/octet-stream' })); | ||
const mockGetObject = jest.fn().mockReturnValue({ promise: mockPromise }); | ||
|
||
mockS3.getObject = mockGetObject; | ||
|
||
const event: FileDetails = { | ||
fileName: 'adr_pass_123_2024-01-22T11:48:16.035Z', | ||
}; | ||
const response = await getFile(event, mockS3, 'bucket', 'folder', 'test'); | ||
|
||
expect(response.statusCode).toBe(200); | ||
expect(response.body).toBe('Certificate Content'); | ||
}); | ||
|
||
it('base64 encodes the response', async () => { | ||
const mockS3 = ({} as unknown) as S3; | ||
const body = Buffer.from('Certificate Content'); | ||
const mockPromise = jest | ||
.fn() | ||
.mockReturnValue(Promise.resolve({ Body: body, ContentType: 'application/octet-stream' })); | ||
const mockGetObject = jest.fn().mockReturnValue({ promise: mockPromise }); | ||
|
||
mockS3.getObject = mockGetObject; | ||
|
||
const event: FileDetails = { | ||
fileName: 'adr_pass_123_2024-01-22T11:48:16.035Z', | ||
}; | ||
const response = await getFile(event, mockS3, 'bucket', 'folder', 'test'); | ||
|
||
expect(response.statusCode).toBe(200); | ||
expect(response.body).toEqual(body.toString('base64')); | ||
}); | ||
|
||
it('ignores the folder check if the current environment is "local". Required for local testing', async () => { | ||
const mockS3 = ({} as unknown) as S3; | ||
const mockPromise = jest | ||
.fn() | ||
.mockReturnValue(Promise.resolve({ Body: 'Certificate Content', ContentType: 'application/octet-stream' })); | ||
const mockGetObject = jest.fn().mockReturnValue({ promise: mockPromise }); | ||
|
||
mockS3.getObject = mockGetObject; | ||
|
||
const event: FileDetails = { | ||
fileName: 'adr_pass_123_2024-01-22T11:48:16.035Z', | ||
}; | ||
const response = await getFile(event, mockS3, 'bucket', undefined, 'local'); | ||
|
||
expect(response.statusCode).toBe(200); | ||
expect(response.body).toBe('Certificate Content'); | ||
}); | ||
}); |
Oops, something went wrong.