diff --git a/package-lock.json b/package-lock.json index 01a97d6031..078f26115c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -304,6 +304,7 @@ "jsonschema", "semver" ], + "peer": true, "dependencies": { "jsonschema": "~1.4.1", "semver": "^7.7.2" @@ -540,6 +541,7 @@ "semver" ], "license": "Apache-2.0", + "peer": true, "dependencies": { "jsonschema": "~1.4.1", "semver": "^7.7.3" @@ -3012,6 +3014,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.975.0.tgz", "integrity": "sha512-xPFcBlpTDuTod9zAAnEsbezFOOqMfQfcd9RCl1LL4Q+qjmazuBSqlnzGE3Djr8Ax/PTV0TR3H2LuepO/ygXwsA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -10324,6 +10327,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.971.0.tgz", "integrity": "sha512-BBUne390fKa4C4QvZlUZ5gKcu+Uyid4IyQ20N4jl0vS7SK2xpfXlJcgKqPW5ts6kx6hWTQBk6sH5Lf12RvuJxg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", @@ -14503,6 +14507,19 @@ "node": ">=20.0.0" } }, + "node_modules/@aws/durable-execution-sdk-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aws/durable-execution-sdk-js/-/durable-execution-sdk-js-1.0.2.tgz", + "integrity": "sha512-KIYBVqV9gArkWdPEDYOjJqLlO+NecmCXOXadNBlOspJTmqztwuiyb6aZc468bYWSGuL4Me/gyTIK97ubkwFNCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-lambda": "^3.943.0" + }, + "engines": { + "node": ">=22" + } + }, "node_modules/@aws/lambda-invoke-store": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", @@ -16344,6 +16361,7 @@ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.6.2" }, @@ -17565,6 +17583,7 @@ "resolved": "https://registry.npmjs.org/aws-sdk-client-mock/-/aws-sdk-client-mock-4.1.0.tgz", "integrity": "sha512-h/tOYTkXEsAcV3//6C1/7U4ifSpKyJvb6auveAepqqNJl6TdZaPFEtKjBQNf8UxQdDP850knB2i/whq4zlsxJw==", "dev": true, + "peer": true, "dependencies": { "@types/sinon": "^17.0.3", "sinon": "^18.0.1", @@ -18032,7 +18051,8 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.5.tgz", "integrity": "sha512-fOoP70YLevMZr5avJHx2DU3LNYmC6wM8OwdrNewMZou1kZnPGOeVzBrRjZNgFDHUlulYUjkpFRSpTE3D+n+ZSg==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/core-util-is": { "version": "1.0.3", @@ -19303,6 +19323,7 @@ "resolved": "https://registry.npmjs.org/markdownlint-cli2/-/markdownlint-cli2-0.20.0.tgz", "integrity": "sha512-esPk+8Qvx/f0bzI7YelUeZp+jCtFOk3KjZ7s9iBQZ6HlymSXoTtWGiIRZP05/9Oy2ehIoIjenVwndxGtxOIJYQ==", "dev": true, + "peer": true, "dependencies": { "globby": "15.0.0", "js-yaml": "4.1.1", @@ -21067,6 +21088,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -21146,6 +21168,7 @@ "integrity": "sha512-x4xW77QC3i5DUFMBp0qjukOTnr/sSg+oEs86nB3LjDslvAmwe/PUGDWbe3GrIqt59oTqoXK5GRK9tAa0sYMiog==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@gerrit0/mini-shiki": "^3.17.0", "lunr": "^2.3.9", @@ -21178,6 +21201,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -21282,6 +21306,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -21390,6 +21415,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -21403,6 +21429,7 @@ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", @@ -21768,6 +21795,7 @@ "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.975.0", "@aws-sdk/lib-dynamodb": "^3.975.0", + "@aws/durable-execution-sdk-js": "^1.0.2", "aws-sdk-client-mock": "^4.1.0" }, "peerDependencies": { diff --git a/packages/idempotency/package.json b/packages/idempotency/package.json index 1ec2d9a8f4..ff98c0165d 100644 --- a/packages/idempotency/package.json +++ b/packages/idempotency/package.json @@ -154,6 +154,7 @@ "@aws-lambda-powertools/testing-utils": "file:../testing", "@aws-sdk/client-dynamodb": "^3.975.0", "@aws-sdk/lib-dynamodb": "^3.975.0", + "@aws/durable-execution-sdk-js": "^1.0.2", "aws-sdk-client-mock": "^4.1.0" } } diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts index 53cda832a3..f91d066e05 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts @@ -1,4 +1,8 @@ import { setTimeout } from 'node:timers/promises'; +import { + type DurableContext, + withDurableExecution, +} from '@aws/durable-execution-sdk-js'; import { Logger } from '@aws-lambda-powertools/logger'; import type { Context } from 'aws-lambda'; import { IdempotencyConfig } from '../../src/IdempotencyConfig.js'; @@ -111,3 +115,22 @@ export const handlerLambda = makeIdempotent( }), } ); + +export const handlerDurable = withDurableExecution( + makeIdempotent( + async (event: { foo: string }, context: DurableContext) => { + context.configureLogger({ customLogger: logger }); + + logger.info('Processing event', { foo: event.foo }); + + await context.wait({ seconds: 1 }); + + logger.info('After wait'); + + return event.foo; + }, + { + persistenceStore: dynamoDBPersistenceLayer, + } + ) +); diff --git a/packages/idempotency/tests/e2e/makeIdempotent.test.ts b/packages/idempotency/tests/e2e/makeIdempotent.test.ts index 380945509d..0d9117cb10 100644 --- a/packages/idempotency/tests/e2e/makeIdempotent.test.ts +++ b/packages/idempotency/tests/e2e/makeIdempotent.test.ts @@ -7,6 +7,7 @@ import { } from '@aws-lambda-powertools/testing-utils'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { ScanCommand } from '@aws-sdk/lib-dynamodb'; +import { Duration } from 'aws-cdk-lib'; import { AttributeType } from 'aws-cdk-lib/aws-dynamodb'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { IdempotencyTestNodejsFunctionAndDynamoTable } from '../helpers/resources.js'; @@ -77,6 +78,25 @@ describe('Idempotency E2E tests, wrapper function usage', () => { } ); + let functionNameDurable: string; + let tableNameDurable: string; + new IdempotencyTestNodejsFunctionAndDynamoTable( + testStack, + { + function: { + entry: lambdaFunctionCodeFilePath, + handler: 'handlerDurable', + durableConfig: { + executionTimeout: Duration.minutes(5), + retentionPeriod: Duration.days(1), + }, + }, + }, + { + nameSuffix: 'durable', + } + ); + const ddb = new DynamoDBClient({}); beforeAll(async () => { @@ -94,6 +114,8 @@ describe('Idempotency E2E tests, wrapper function usage', () => { testStack.findAndGetStackOutputValue('handlerFn'); tableNameLambdaHandler = testStack.findAndGetStackOutputValue('handlerTable'); + functionNameDurable = testStack.findAndGetStackOutputValue('durableFn'); + tableNameDurable = testStack.findAndGetStackOutputValue('durableTable'); }); it('when called twice with the same payload, it returns the same result', async () => { @@ -309,6 +331,35 @@ describe('Idempotency E2E tests, wrapper function usage', () => { expect(functionLogs[1]).toHaveLength(0); }); + it('calls an idempotent durable function and always returns the same result when called multiple times', async () => { + // Prepare + const payload = { + foo: 'bar', + }; + const payloadHash = createHash('md5') + .update(JSON.stringify(payload)) + .digest('base64'); + + // Act + await invokeFunction({ + functionName: `${functionNameDurable}:$LATEST`, + times: 2, + invocationMode: 'SEQUENTIAL', + payload, + }); + + // Assess + const idempotencyRecords = await ddb.send( + new ScanCommand({ + TableName: tableNameDurable, + }) + ); + expect(idempotencyRecords.Items?.length).toEqual(1); + expect(idempotencyRecords.Items?.[0].id).toEqual( + `${functionNameDurable}#${payloadHash}` + ); + }); + afterAll(async () => { if (!process.env.DISABLE_TEARDOWN) { await testStack.destroy(); diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index abe315ec5c..241a1b6d0f 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -66,28 +66,28 @@ describe('Class IdempotencyHandler', () => { 'There is already an execution in progress with idempotency key: idempotencyKey', case: 'pk only', }, - ])( - 'throws when the record is in progress and within expiry window ($case)', - ({ keys, expectedErrorMsg }) => { - // Prepare - const stubRecord = new IdempotencyRecord({ - ...keys, - expiryTimestamp: Date.now() + 1000, // should be in the future - inProgressExpiryTimestamp: 0, // less than current time in milliseconds - responseData: { responseData: 'responseData' }, - payloadHash: 'payloadHash', - status: IdempotencyRecordStatus.INPROGRESS, - }); - - // Act & Assess - expect(stubRecord.isExpired()).toBe(false); - expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); - expect(() => - idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) - ).toThrow(new IdempotencyAlreadyInProgressError(expectedErrorMsg)); - expect(mockResponseHook).not.toHaveBeenCalled(); - } - ); + ])('throws when the record is in progress and within expiry window ($case)', ({ + keys, + expectedErrorMsg, + }) => { + // Prepare + const stubRecord = new IdempotencyRecord({ + ...keys, + expiryTimestamp: Date.now() + 1000, // should be in the future + inProgressExpiryTimestamp: 0, // less than current time in milliseconds + responseData: { responseData: 'responseData' }, + payloadHash: 'payloadHash', + status: IdempotencyRecordStatus.INPROGRESS, + }); + + // Act & Assess + expect(stubRecord.isExpired()).toBe(false); + expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); + expect(() => + idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) + ).toThrow(new IdempotencyAlreadyInProgressError(expectedErrorMsg)); + expect(mockResponseHook).not.toHaveBeenCalled(); + }); it('throws when the record is in progress and outside expiry window', () => { // Prepare @@ -240,54 +240,52 @@ describe('Class IdempotencyHandler', () => { expect(mockProcessIdempotency).toHaveBeenCalledTimes(MAX_RETRIES + 1); }); - it("allows execution when isReplay is true and there is IN PROGRESS record", async ()=> { + it('allows execution when isReplay is true and there is IN PROGRESS record', async () => { // Prepare // Mock saveInProgress to simulate an existing IN_PROGRESS record - vi.spyOn(persistenceStore, 'saveInProgress') - .mockRejectedValueOnce( - new IdempotencyItemAlreadyExistsError( - 'Record exists', - new IdempotencyRecord({ - idempotencyKey: 'test-key', - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: Date.now() + 10000, - }) - ) - ); + vi.spyOn(persistenceStore, 'saveInProgress').mockRejectedValueOnce( + new IdempotencyItemAlreadyExistsError( + 'Record exists', + new IdempotencyRecord({ + idempotencyKey: 'test-key', + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: Date.now() + 10000, + }) + ) + ); // Act - await idempotentHandler.handle({isReplay: true}) + await idempotentHandler.handle({ isReplay: true }); // Assess - expect(mockFunctionToMakeIdempotent).toBeCalled() - }) - - it("raises an IdempotencyAlreadyInProgressError error when isReplay is false and there is an IN PROGRESS record", async ()=> { - // Prepare - // Mock saveInProgress to simulate an existing IN_PROGRESS record - vi.spyOn(persistenceStore, 'saveInProgress') - .mockRejectedValueOnce( - new IdempotencyItemAlreadyExistsError( - 'Record exists', - new IdempotencyRecord({ - idempotencyKey: 'test-key', - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: Date.now() + 10000, - }) - ) - ); - - // Act & Assess - await expect(idempotentHandler.handle({ isReplay: false })).rejects.toThrow(IdempotencyAlreadyInProgressError); - }) - - it("returns the result of the original durable execution when another durable execution with the same payload is invoked", async () => { + expect(mockFunctionToMakeIdempotent).toBeCalled(); + }); + it('raises an IdempotencyAlreadyInProgressError error when isReplay is false and there is an IN PROGRESS record', async () => { // Prepare - vi.spyOn( - persistenceStore, - 'saveInProgress' - ).mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + // Mock saveInProgress to simulate an existing IN_PROGRESS record + vi.spyOn(persistenceStore, 'saveInProgress').mockRejectedValueOnce( + new IdempotencyItemAlreadyExistsError( + 'Record exists', + new IdempotencyRecord({ + idempotencyKey: 'test-key', + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: Date.now() + 10000, + }) + ) + ); + + // Act & Assess + await expect( + idempotentHandler.handle({ isReplay: false }) + ).rejects.toThrow(IdempotencyAlreadyInProgressError); + }); + + it('returns the result of the original durable execution when another durable execution with the same payload is invoked', async () => { + // Prepare + vi.spyOn(persistenceStore, 'saveInProgress').mockRejectedValue( + new IdempotencyItemAlreadyExistsError() + ); const stubRecord = new IdempotencyRecord({ idempotencyKey: 'idempotencyKey', @@ -302,13 +300,13 @@ describe('Class IdempotencyHandler', () => { .mockResolvedValue(stubRecord); // Act - const result = await idempotentHandler.handle({isReplay: false}) + const result = await idempotentHandler.handle({ isReplay: false }); // Assess expect(result).toStrictEqual({ response: false }); expect(getRecordSpy).toHaveBeenCalledTimes(1); expect(getRecordSpy).toHaveBeenCalledWith(mockFunctionPayloadToBeHashed); - }) + }); }); describe('Method: getFunctionResult', () => { diff --git a/packages/idempotency/tests/unit/makeIdempotent.test.ts b/packages/idempotency/tests/unit/makeIdempotent.test.ts index b0640a4b41..986f50443e 100644 --- a/packages/idempotency/tests/unit/makeIdempotent.test.ts +++ b/packages/idempotency/tests/unit/makeIdempotent.test.ts @@ -1,3 +1,4 @@ +import type { DurableContext } from '@aws/durable-execution-sdk-js'; import context from '@aws-lambda-powertools/testing-utils/context'; import middy from '@middy/core'; import type { Context } from 'aws-lambda'; @@ -596,18 +597,20 @@ describe('Function: makeIdempotent', () => { expect(getRecordSpy).toHaveBeenCalledTimes(0); }); - it('registers the LambdaContext when provided a durable context', async () => { + it('registers the durable context when provided a durable context', async () => { // Prepare const registerLambdaContextSpy = vi.spyOn( IdempotencyConfig.prototype, 'registerLambdaContext' ); - const fn = async (_event: any, _context: any) => {}; - const handler = makeIdempotent(fn, mockIdempotencyOptions); + const handler = makeIdempotent( + async (_event: unknown, _context: DurableContext) => {}, + mockIdempotencyOptions + ); const mockDurableContext = { step: vi.fn(), lambdaContext: context }; // Act - await handler(event, mockDurableContext); + await handler(event, mockDurableContext as unknown as DurableContext); // Assess expect(registerLambdaContextSpy).toHaveBeenCalledOnce(); @@ -616,8 +619,10 @@ describe('Function: makeIdempotent', () => { it('passes isReplay=true to handler when durable context is in ReplayMode', async () => { // Prepare const handleSpy = vi.spyOn(IdempotencyHandler.prototype, 'handle'); - const fn = async (_event: any, _context: any) => {}; - const handler = makeIdempotent(fn, mockIdempotencyOptions); + const handler = makeIdempotent( + async (_event: unknown, _context: DurableContext) => {}, + mockIdempotencyOptions + ); const mockDurableContext = { step: vi.fn(), lambdaContext: context, @@ -625,7 +630,7 @@ describe('Function: makeIdempotent', () => { }; // Act - await handler(event, mockDurableContext); + await handler(event, mockDurableContext as unknown as DurableContext); // Assess expect(handleSpy).toHaveBeenCalledWith({ isReplay: true }); @@ -634,8 +639,10 @@ describe('Function: makeIdempotent', () => { it('passes isReplay=fale to handler when durable context is in ExecutionMode', async () => { // Prepare const handleSpy = vi.spyOn(IdempotencyHandler.prototype, 'handle'); - const fn = async (_event: any, _context: any) => {}; - const handler = makeIdempotent(fn, mockIdempotencyOptions); + const handler = makeIdempotent( + async (_event: unknown, _context: DurableContext) => {}, + mockIdempotencyOptions + ); const mockDurableContext = { step: vi.fn(), lambdaContext: context, @@ -643,7 +650,7 @@ describe('Function: makeIdempotent', () => { }; // Act - await handler(event, mockDurableContext); + await handler(event, mockDurableContext as unknown as DurableContext); // Assess expect(handleSpy).toHaveBeenCalledWith({ isReplay: false }); diff --git a/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts b/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts index 35121122e3..78ae70331e 100644 --- a/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts +++ b/packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts @@ -360,61 +360,60 @@ describe('Class: DynamoDBPersistenceLayer', () => { }, case: 'single key', }, - ])( - 'throws when called with a record that fails any condition ($case)', - async ({ keys }) => { - // Prepare - const { id, sortKey } = keys; - - const record = new IdempotencyRecord({ - idempotencyKey: id, - sortKey, - status: IdempotencyRecordStatus.EXPIRED, - expiryTimestamp: 0, - }); - const expiration = Date.now(); - client.on(PutItemCommand).rejects( - new ConditionalCheckFailedException({ - $metadata: { - httpStatusCode: 400, - requestId: 'someRequestId', - }, - message: 'Conditional check failed', - Item: { - id: { S: 'test-key' }, - ...(sortKey ? { sortKey: { S: sortKey } } : {}), - status: { S: 'INPROGRESS' }, - expiration: { N: expiration.toString() }, - }, + ])('throws when called with a record that fails any condition ($case)', async ({ + keys, + }) => { + // Prepare + const { id, sortKey } = keys; + + const record = new IdempotencyRecord({ + idempotencyKey: id, + sortKey, + status: IdempotencyRecordStatus.EXPIRED, + expiryTimestamp: 0, + }); + const expiration = Date.now(); + client.on(PutItemCommand).rejects( + new ConditionalCheckFailedException({ + $metadata: { + httpStatusCode: 400, + requestId: 'someRequestId', + }, + message: 'Conditional check failed', + Item: { + id: { S: 'test-key' }, + ...(sortKey ? { sortKey: { S: sortKey } } : {}), + status: { S: 'INPROGRESS' }, + expiration: { N: expiration.toString() }, + }, + }) + ); + const testPersistenceLayer = sortKey + ? new DynamoDBPersistenceLayerTestClass({ + tableName: dummyTableName, + sortKeyAttr: 'sortKey', }) - ); - const testPersistenceLayer = sortKey - ? new DynamoDBPersistenceLayerTestClass({ - tableName: dummyTableName, - sortKeyAttr: 'sortKey', - }) - : persistenceLayer; - - // Act & Assess - await expect( - testPersistenceLayer._putRecord(record) - ).rejects.toThrowError( - new IdempotencyItemAlreadyExistsError( - `Failed to put record for already existing idempotency key: ${ - sortKey - ? `${record.idempotencyKey} and sort key: ${sortKey}` - : record.idempotencyKey - }`, - new IdempotencyRecord({ - idempotencyKey: 'test-key', - sortKey, - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: expiration, - }) - ) - ); - } - ); + : persistenceLayer; + + // Act & Assess + await expect( + testPersistenceLayer._putRecord(record) + ).rejects.toThrowError( + new IdempotencyItemAlreadyExistsError( + `Failed to put record for already existing idempotency key: ${ + sortKey + ? `${record.idempotencyKey} and sort key: ${sortKey}` + : record.idempotencyKey + }`, + new IdempotencyRecord({ + idempotencyKey: 'test-key', + sortKey, + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: expiration, + }) + ) + ); + }); it('throws when encountering an unknown error', async () => { // Prepare diff --git a/packages/testing/src/invokeTestFunction.ts b/packages/testing/src/invokeTestFunction.ts index a6662188fd..293571dc68 100644 --- a/packages/testing/src/invokeTestFunction.ts +++ b/packages/testing/src/invokeTestFunction.ts @@ -27,9 +27,7 @@ const invokeFunctionOnce = async ({ if (result?.LogResult) { return new TestInvocationLogs(result?.LogResult); } - throw new Error( - 'No LogResult field returned in the response of Lambda invocation. This should not happen.' - ); + return new TestInvocationLogs(''); }; /**