@mbc-cqrs-serverless/core
Version:
CQRS and event base core
345 lines • 12.6 kB
JavaScript
;
/**
* Test Assertions
*
* Provides reusable assertion functions for AWS SDK error patterns
* and common test scenarios. These helpers ensure consistent
* validation across integration tests.
*
* Usage:
* import {
* assertIsRetriableError,
* assertIsThrottlingError,
* assertErrorMetadata
* } from './utilities/test-assertions'
*
* it('should be retriable', () => {
* assertIsRetriableError(error)
* })
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.assertIsRetriableError = assertIsRetriableError;
exports.assertIsNotRetriableError = assertIsNotRetriableError;
exports.assertIsThrottlingError = assertIsThrottlingError;
exports.assertIsNetworkError = assertIsNetworkError;
exports.assertIsTimeoutError = assertIsTimeoutError;
exports.assertErrorMetadata = assertErrorMetadata;
exports.assertHasRequestId = assertHasRequestId;
exports.assertIsClientFault = assertIsClientFault;
exports.assertIsServerFault = assertIsServerFault;
exports.assertResponseStructure = assertResponseStructure;
exports.assertDynamoDBResponseMetadata = assertDynamoDBResponseMetadata;
exports.assertThrowsErrorWithName = assertThrowsErrorWithName;
exports.assertThrowsErrorMatching = assertThrowsErrorMatching;
exports.assertCompletesWithin = assertCompletesWithin;
exports.assertTakesAtLeast = assertTakesAtLeast;
exports.forceGC = forceGC;
exports.getHeapUsed = getHeapUsed;
exports.assertMemoryIncreaseLessThan = assertMemoryIncreaseLessThan;
exports.assertNoMemoryLeak = assertNoMemoryLeak;
exports.assertArrayLength = assertArrayLength;
exports.assertAllMatch = assertAllMatch;
exports.assertUniqueBy = assertUniqueBy;
// ============================================================================
// Error Type Assertions
// ============================================================================
/**
* Asserts that an error is retriable based on AWS SDK v3 patterns
*/
function assertIsRetriableError(error) {
const awsError = error;
const isRetriable = awsError.$retryable !== undefined ||
(awsError.$metadata?.httpStatusCode &&
awsError.$metadata.httpStatusCode >= 500) ||
awsError.$metadata?.httpStatusCode === 429 ||
[
'ProvisionedThroughputExceededException',
'ThrottlingException',
'InternalServerError',
'ServiceUnavailable',
'RequestLimitExceeded',
'SlowDown',
'TooManyRequestsException',
].includes(awsError.name);
if (!isRetriable) {
throw new Error(`Expected error to be retriable, but got: ${awsError.name} (status: ${awsError.$metadata?.httpStatusCode})`);
}
}
/**
* Asserts that an error is NOT retriable
*/
function assertIsNotRetriableError(error) {
const awsError = error;
const nonRetriablePatterns = !awsError.$retryable &&
awsError.$metadata?.httpStatusCode !== undefined &&
awsError.$metadata.httpStatusCode < 500 &&
awsError.$metadata.httpStatusCode !== 429;
if (!nonRetriablePatterns) {
throw new Error(`Expected error to NOT be retriable, but got: ${awsError.name} (status: ${awsError.$metadata?.httpStatusCode}, $retryable: ${JSON.stringify(awsError.$retryable)})`);
}
}
/**
* Asserts that an error is a throttling error
*/
function assertIsThrottlingError(error) {
const awsError = error;
if (awsError.$retryable?.throttling !== true) {
throw new Error(`Expected throttling error, but got: ${awsError.name} ($retryable: ${JSON.stringify(awsError.$retryable)})`);
}
}
/**
* Asserts that an error is a network error
*/
function assertIsNetworkError(error) {
const err = error;
const networkErrorCodes = [
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'ENETUNREACH',
'ENOTFOUND',
'EPIPE',
'EAI_AGAIN',
];
const isNetwork = (err.code && networkErrorCodes.includes(err.code)) ||
err.message.toLowerCase().includes('socket hang up') ||
err.message.toLowerCase().includes('network error') ||
err.message.toLowerCase().includes('connection reset') ||
err.message.toLowerCase().includes('connection refused');
if (!isNetwork) {
throw new Error(`Expected network error, but got: ${err.message} (code: ${err.code})`);
}
}
/**
* Asserts that an error is a timeout error
*/
function assertIsTimeoutError(error) {
const err = error;
const isTimeout = err.code === 'ETIMEDOUT' ||
err.name === 'TimeoutError' ||
err.message.toLowerCase().includes('timeout') ||
err.message.toLowerCase().includes('timed out');
if (!isTimeout) {
throw new Error(`Expected timeout error, but got: ${err.message} (name: ${err.name}, code: ${err.code})`);
}
}
// ============================================================================
// Error Metadata Assertions
// ============================================================================
/**
* Asserts that an error has the expected metadata
*/
function assertErrorMetadata(error, expected) {
const awsError = error;
if (expected.httpStatusCode !== undefined) {
if (awsError.$metadata?.httpStatusCode !== expected.httpStatusCode) {
throw new Error(`Expected httpStatusCode ${expected.httpStatusCode}, but got ${awsError.$metadata?.httpStatusCode}`);
}
}
if (expected.requestId !== undefined) {
if (awsError.$metadata?.requestId !== expected.requestId) {
throw new Error(`Expected requestId ${expected.requestId}, but got ${awsError.$metadata?.requestId}`);
}
}
if (expected.name !== undefined) {
if (awsError.name !== expected.name) {
throw new Error(`Expected error name ${expected.name}, but got ${awsError.name}`);
}
}
if (expected.fault !== undefined) {
if (awsError.$fault !== expected.fault) {
throw new Error(`Expected fault ${expected.fault}, but got ${awsError.$fault}`);
}
}
}
/**
* Asserts that an error has a valid request ID
*/
function assertHasRequestId(error) {
const awsError = error;
if (!awsError.$metadata?.requestId) {
throw new Error('Expected error to have a requestId in $metadata');
}
}
/**
* Asserts that an error is a client fault (4xx)
*/
function assertIsClientFault(error) {
const awsError = error;
if (awsError.$fault !== 'client') {
throw new Error(`Expected client fault, but got ${awsError.$fault}`);
}
}
/**
* Asserts that an error is a server fault (5xx)
*/
function assertIsServerFault(error) {
const awsError = error;
if (awsError.$fault !== 'server') {
throw new Error(`Expected server fault, but got ${awsError.$fault}`);
}
}
// ============================================================================
// Response Assertions
// ============================================================================
/**
* Asserts that a response has the expected structure
*/
function assertResponseStructure(response, expectedKeys) {
if (typeof response !== 'object' || response === null) {
throw new Error(`Expected object response, but got ${typeof response}`);
}
const missingKeys = expectedKeys.filter((key) => !(key in response));
if (missingKeys.length > 0) {
throw new Error(`Response missing expected keys: ${missingKeys.join(', ')}. Got keys: ${Object.keys(response).join(', ')}`);
}
}
/**
* Asserts that a DynamoDB response has valid metadata
*/
function assertDynamoDBResponseMetadata(response) {
const resp = response;
if (!resp.$metadata) {
throw new Error('Expected $metadata in DynamoDB response');
}
if (resp.$metadata.httpStatusCode !== 200) {
throw new Error(`Expected httpStatusCode 200, but got ${resp.$metadata.httpStatusCode}`);
}
}
// ============================================================================
// Async Assertions
// ============================================================================
/**
* Asserts that an async function throws an error with the expected name
*/
async function assertThrowsErrorWithName(fn, expectedName) {
try {
await fn();
throw new Error(`Expected function to throw ${expectedName}, but it did not throw`);
}
catch (error) {
if (error.name !== expectedName) {
throw new Error(`Expected error name ${expectedName}, but got ${error.name}`);
}
}
}
/**
* Asserts that an async function throws an error matching a predicate
*/
async function assertThrowsErrorMatching(fn, predicate, description = 'custom predicate') {
try {
await fn();
throw new Error(`Expected function to throw error matching ${description}, but it did not throw`);
}
catch (error) {
if (!predicate(error)) {
throw new Error(`Error did not match ${description}: ${error.message}`);
}
}
}
// ============================================================================
// Timing Assertions
// ============================================================================
/**
* Asserts that an async operation completes within a time limit
*/
async function assertCompletesWithin(fn, timeoutMs) {
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
if (duration > timeoutMs) {
throw new Error(`Expected operation to complete within ${timeoutMs}ms, but took ${duration}ms`);
}
return result;
}
/**
* Asserts that an async operation takes at least a minimum time
* (useful for testing retry delays)
*/
async function assertTakesAtLeast(fn, minMs) {
const start = Date.now();
const result = await fn();
const duration = Date.now() - start;
if (duration < minMs) {
throw new Error(`Expected operation to take at least ${minMs}ms, but took ${duration}ms`);
}
return result;
}
// ============================================================================
// Memory Assertions
// ============================================================================
/**
* Forces garbage collection if available (requires --expose-gc flag)
*/
function forceGC() {
if (global.gc) {
global.gc();
}
}
/**
* Gets current heap memory usage in bytes
*/
function getHeapUsed() {
return process.memoryUsage().heapUsed;
}
/**
* Asserts that memory increase is within acceptable limits
*/
function assertMemoryIncreaseLessThan(initialMemory, maxIncreaseBytes) {
forceGC();
const currentMemory = getHeapUsed();
const increase = currentMemory - initialMemory;
if (increase > maxIncreaseBytes) {
const increaseMB = (increase / 1024 / 1024).toFixed(2);
const maxMB = (maxIncreaseBytes / 1024 / 1024).toFixed(2);
throw new Error(`Memory increased by ${increaseMB}MB, which exceeds limit of ${maxMB}MB`);
}
}
/**
* Runs a function and asserts memory usage stays within limits
*/
async function assertNoMemoryLeak(fn, maxIncreaseBytes = 50 * 1024 * 1024) {
forceGC();
const initialMemory = getHeapUsed();
const result = await fn();
assertMemoryIncreaseLessThan(initialMemory, maxIncreaseBytes);
return result;
}
// ============================================================================
// Array/Collection Assertions
// ============================================================================
/**
* Asserts that an array has the expected length
*/
function assertArrayLength(array, expectedLength) {
if (array.length !== expectedLength) {
throw new Error(`Expected array length ${expectedLength}, but got ${array.length}`);
}
}
/**
* Asserts that all items in an array match a predicate
*/
function assertAllMatch(array, predicate, description = 'predicate') {
const failingIndex = array.findIndex((item) => !predicate(item));
if (failingIndex !== -1) {
throw new Error(`Item at index ${failingIndex} did not match ${description}`);
}
}
/**
* Asserts that an array contains unique items based on a key function
*/
function assertUniqueBy(array, keyFn) {
const seen = new Set();
const duplicates = [];
array.forEach((item) => {
const key = keyFn(item);
if (seen.has(key)) {
duplicates.push(key);
}
seen.add(key);
});
if (duplicates.length > 0) {
throw new Error(`Found duplicate keys: ${duplicates.join(', ')}`);
}
}
//# sourceMappingURL=test-assertions.js.map