Skip to content

Commit

Permalink
Merge pull request #1937 from balena-io/aws-mock
Browse files Browse the repository at this point in the history
Tests: Allow passing in different mocks to aws-mock
  • Loading branch information
Page- authored Jan 30, 2025
2 parents a2981aa + c35f229 commit 0ece31c
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 120 deletions.
283 changes: 164 additions & 119 deletions test/test-lib/aws-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,143 +7,188 @@ import {
} from '../../src/lib/config.js';
import { TEST_MOCK_ONLY } from '../../src/features/device-types/storage/aws-sdk-wrapper.js';

import $getObjectMocks from '../fixtures/s3/getObject.json' with { type: 'json' };
import listObjectsV2Mocks from '../fixtures/s3/listObjectsV2.json' with { type: 'json' };

// AWS S3 Client getObject results have a Buffer on their Body prop
// and a Date on their LastModified prop so we have to reconstruct
// them from the strings that the mock object holds
const getObjectMocks: Dictionary<AWS.S3.Types.GetObjectOutput> = _.mapValues(
$getObjectMocks,
(
getObjectMock: (typeof $getObjectMocks)[keyof typeof $getObjectMocks],
): AWS.S3.Types.GetObjectOutput => {
return {
...getObjectMock,

Body:
'Body' in getObjectMock && getObjectMock.Body
? Buffer.from(getObjectMock.Body)
: undefined,
LastModified:
'LastModified' in getObjectMock && getObjectMock.LastModified
? new Date(getObjectMock.LastModified)
: undefined,
};
},
);

class NotFoundError extends Error {
public statusCode = 404;

constructor() {
super('NotFound');
}
}

const toReturnType = <T extends (...args: any[]) => any>(
result:
| Error
| {
[key: string]: any;
},
) => {
return {
// eslint-disable-next-line @typescript-eslint/require-await -- We need to return a promise for mocking reasons but we don't need to await.
promise: async () => {
if (result instanceof Error) {
throw result;
}
if (result.Error) {
const error = new Error();
Object.assign(error, result.Error);
type MockedError = {
Error: {
statusCode: number;
};
};

throw error;
}
return result;
export default (
$getObjectMocks: Dictionary<
| (Omit<AWS.S3.Types.GetObjectOutput, 'LastModified' | 'Body'> & {
LastModified?: string;
Body?: string;
})
| MockedError
>,
$listObjectsV2Mocks: Dictionary<
| (Omit<AWS.S3.Types.ListObjectsV2Output, 'Contents'> & {
Contents?: Array<
Omit<AWS.S3.Types.ListObjectsV2Output['Contents'], 'LastModified'> & {
LastModified: string;
}
>;
})
| MockedError
>,
) => {
// AWS S3 Client getObject results have a Buffer on their Body prop
// and a Date on their LastModified prop so we have to reconstruct
// them from the strings that the mock object holds
const getObjectMocks: Dictionary<AWS.S3.Types.GetObjectOutput | MockedError> =
_.mapValues(
$getObjectMocks,
(
getObjectMock: (typeof $getObjectMocks)[keyof typeof $getObjectMocks],
): AWS.S3.Types.GetObjectOutput | MockedError => {
return {
...getObjectMock,

Body:
'Body' in getObjectMock && getObjectMock.Body
? Buffer.from(getObjectMock.Body)
: undefined,
LastModified:
'LastModified' in getObjectMock && getObjectMock.LastModified
? new Date(getObjectMock.LastModified)
: undefined,
};
},
);
const listObjectsV2Mocks: Dictionary<
AWS.S3.Types.ListObjectsV2Output | MockedError
> = _.mapValues(
$listObjectsV2Mocks,
(
listObjectsV2Mock: (typeof $listObjectsV2Mocks)[keyof typeof $listObjectsV2Mocks],
): AWS.S3.Types.ListObjectsV2Output | MockedError => {
return {
...listObjectsV2Mock,

Contents:
'Contents' in listObjectsV2Mock && listObjectsV2Mock.Contents
? listObjectsV2Mock.Contents.map((contents) => {
return {
...contents,
LastModified:
'LastModified' in contents && contents.LastModified
? new Date(contents.LastModified)
: undefined,
};
})
: undefined,
};
},
} as ReturnType<T>;
};
);

interface UnauthenticatedRequestParams {
[key: string]: any;
}
class NotFoundError extends Error {
public statusCode = 404;

class S3Mock {
constructor(params: AWS.S3.Types.ClientConfiguration) {
assert(
params.accessKeyId === IMAGE_STORAGE_ACCESS_KEY,
'S3 access key not matching',
);
assert(
params.secretAccessKey === IMAGE_STORAGE_SECRET_KEY,
'S3 secret key not matching',
);
constructor() {
super('NotFound');
}
}

public makeUnauthenticatedRequest(
operation: string,
params?: UnauthenticatedRequestParams,
): AWS.Request<any, AWS.AWSError> {
if (operation === 'headObject') {
return this.headObject(params as AWS.S3.Types.HeadObjectRequest);
}
if (operation === 'getObject') {
return this.getObject(params as AWS.S3.Types.GetObjectRequest);
}
if (operation === 'listObjectsV2') {
return this.listObjectsV2(params as AWS.S3.Types.ListObjectsV2Request);
}
throw new Error(`AWS Mock: Operation ${operation} isn't implemented`);
const toReturnType = <T extends (...args: any[]) => any>(
result:
| Error
| MockedError
| AWS.S3.Types.GetObjectOutput
| AWS.S3.Types.ListObjectsV2Output,
) => {
return {
// eslint-disable-next-line @typescript-eslint/require-await -- We need to return a promise for mocking reasons but we don't need to await.
promise: async () => {
if (result instanceof Error) {
throw result;
}
if ('Error' in result && result.Error) {
const error = new Error();
Object.assign(error, result.Error);

throw error;
}
return result;
},
} as ReturnType<T>;
};

interface UnauthenticatedRequestParams {
[key: string]: any;
}

public headObject(
params: AWS.S3.Types.HeadObjectRequest,
): ReturnType<AWS.S3['headObject']> {
const mock = getObjectMocks[params.Key as keyof typeof getObjectMocks];
if (mock) {
const trimmedMock = _.omit(mock, 'Body', 'ContentRange', 'TagCount');
return toReturnType<AWS.S3['headObject']>(trimmedMock);
class S3Mock {
constructor(params: AWS.S3.Types.ClientConfiguration) {
assert(
params.accessKeyId === IMAGE_STORAGE_ACCESS_KEY,
'S3 access key not matching',
);
assert(
params.secretAccessKey === IMAGE_STORAGE_SECRET_KEY,
'S3 secret key not matching',
);
}

// treat not found IGNORE file mocks as 404
if (_.endsWith(params.Key, '/IGNORE')) {
return toReturnType<AWS.S3['headObject']>(new NotFoundError());
public makeUnauthenticatedRequest(
operation: string,
params?: UnauthenticatedRequestParams,
): AWS.Request<any, AWS.AWSError> {
if (operation === 'headObject') {
return this.headObject(params as AWS.S3.Types.HeadObjectRequest);
}
if (operation === 'getObject') {
return this.getObject(params as AWS.S3.Types.GetObjectRequest);
}
if (operation === 'listObjectsV2') {
return this.listObjectsV2(params as AWS.S3.Types.ListObjectsV2Request);
}
throw new Error(`AWS Mock: Operation ${operation} isn't implemented`);
}

throw new Error(
`aws mock: headObject could not find a mock for ${params.Key}`,
);
}
public headObject(
params: AWS.S3.Types.HeadObjectRequest,
): ReturnType<AWS.S3['headObject']> {
const mock = getObjectMocks[params.Key as keyof typeof getObjectMocks];
if (mock) {
const trimmedMock = _.omit(mock, 'Body', 'ContentRange', 'TagCount');
return toReturnType<AWS.S3['headObject']>(trimmedMock);
}

// treat not found IGNORE file mocks as 404
if (_.endsWith(params.Key, '/IGNORE')) {
return toReturnType<AWS.S3['headObject']>(new NotFoundError());
}

public getObject(
params: AWS.S3.Types.GetObjectRequest,
): ReturnType<AWS.S3['getObject']> {
const mock = getObjectMocks[params.Key as keyof typeof getObjectMocks];
if (!mock) {
throw new Error(
`aws mock: getObject could not find a mock for ${params.Key}`,
`aws mock: headObject could not find a mock for ${params.Key}`,
);
}
return toReturnType<AWS.S3['getObject']>(mock);
}

public listObjectsV2(
params: AWS.S3.Types.ListObjectsV2Request,
): ReturnType<AWS.S3['listObjectsV2']> {
const mock =
listObjectsV2Mocks[params.Prefix as keyof typeof listObjectsV2Mocks];
if (!mock) {
throw new Error(
`aws mock: listObjectsV2 could not find a mock for ${params.Prefix}`,
);
public getObject(
params: AWS.S3.Types.GetObjectRequest,
): ReturnType<AWS.S3['getObject']> {
const mock = getObjectMocks[params.Key as keyof typeof getObjectMocks];
if (!mock) {
throw new Error(
`aws mock: getObject could not find a mock for ${params.Key}`,
);
}
return toReturnType<AWS.S3['getObject']>(mock);
}

public listObjectsV2(
params: AWS.S3.Types.ListObjectsV2Request,
): ReturnType<AWS.S3['listObjectsV2']> {
const mock =
listObjectsV2Mocks[params.Prefix as keyof typeof listObjectsV2Mocks];
if (!mock) {
throw new Error(
`aws mock: listObjectsV2 could not find a mock for ${params.Prefix}`,
);
}
return toReturnType<AWS.S3['listObjectsV2']>(mock);
}
return toReturnType<AWS.S3['listObjectsV2']>(mock);
}
}

export const AWSSdkMock = {
S3: S3Mock,
TEST_MOCK_ONLY.S3 = S3Mock as typeof AWS.S3;
};

TEST_MOCK_ONLY.S3 = S3Mock as typeof AWS.S3;
7 changes: 6 additions & 1 deletion test/test-lib/init-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ import {
} from '../../src/features/contracts/index.js';
import { getUserFromToken } from './api-helpers.js';
import * as config from '../../src/lib/config.js';
import $getObjectMocks from '../fixtures/s3/getObject.json' with { type: 'json' };
import listObjectsV2Mocks from '../fixtures/s3/listObjectsV2.json' with { type: 'json' };
import awsMockSetup from './aws-mock.js';

const version = 'resin';

export const preInit = async () => {
augmentStatusAssertionError();
await import('./aws-mock.js');

awsMockSetup($getObjectMocks, listObjectsV2Mocks);

await import('./contracts-mock.js');

config.TEST_MOCK_ONLY.ASYNC_TASKS_ENABLED = true;
Expand Down

0 comments on commit 0ece31c

Please sign in to comment.