t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/AWS/Lambda/EventInvokeConfig.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

243 lines
7.4 KiB
TypeScript

import * as AWS from "@/AWS";
import * as Test from "@/Test/Alchemy";
import * as Lambda from "@distilled.cloud/aws/lambda";
import { expect } from "alchemy-test";
import * as Effect from "effect/Effect";
import * as Schedule from "effect/Schedule";
import { fileURLToPath } from "node:url";
const timeoutHandlerPath = fileURLToPath(
new URL("./timeout-handler.ts", import.meta.url),
);
const { test } = Test.make({ providers: AWS.providers() });
test.provider(
"syncs event invoke config on function and alias",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const program = ({
functionConfig,
alias,
}: {
functionConfig?: AWS.Lambda.EventInvokeConfig;
alias?: {
eventInvokeConfig?: AWS.Lambda.EventInvokeConfig;
};
}) =>
Effect.gen(function* () {
const queue = yield* AWS.SQS.Queue("FailureQueue", {
visibilityTimeout: "30 seconds",
});
const fn = yield* AWS.Lambda.Function("AsyncFn", {
main: timeoutHandlerPath,
handler: "handler",
isExternal: true,
functionUrl: false,
eventInvokeConfig: functionConfig,
});
yield* fn.bind("AllowEventInvokeDestination", {
policyStatements: [
{
Effect: "Allow",
Action: ["sqs:SendMessage"],
Resource: [queue.queueArn],
},
],
});
const version = yield* AWS.Lambda.Version("AsyncVersion", {
function: fn,
});
const live = alias
? yield* AWS.Lambda.Alias("LiveAlias", {
version,
aliasName: "live",
eventInvokeConfig: alias.eventInvokeConfig,
})
: undefined;
return { fn, queue, live };
});
// --- create with function-level config ---
const created = yield* stack.deploy(
program({
functionConfig: {
maximumRetryAttempts: 0,
maximumEventAge: "1 minute",
},
}),
);
const liveCreated = yield* expectConfig(created.fn.functionName, {
maximumRetryAttempts: 0,
maximumEventAgeInSeconds: 60,
});
expect(liveCreated.MaximumRetryAttempts).toBe(0);
expect(liveCreated.MaximumEventAgeInSeconds).toBe(60);
expect(liveCreated.DestinationConfig?.OnFailure?.Destination).toBe(
undefined,
);
// --- update retry behavior and add a failure destination ---
const updated = yield* stack.deploy(
program({
functionConfig: {
maximumRetryAttempts: 1,
maximumEventAge: "2 minutes",
destinationConfig: {
OnFailure: {
Destination: created.queue.queueArn,
},
},
},
}),
);
const liveUpdated = yield* expectConfig(updated.fn.functionName, {
maximumRetryAttempts: 1,
maximumEventAgeInSeconds: 120,
onFailureDestination: created.queue.queueArn,
});
expect(liveUpdated.MaximumRetryAttempts).toBe(1);
expect(liveUpdated.MaximumEventAgeInSeconds).toBe(120);
expect(liveUpdated.DestinationConfig?.OnFailure?.Destination).toBe(
created.queue.queueArn,
);
// --- omit the prop: the config is deleted, not left behind ---
const removed = yield* stack.deploy(program({}));
yield* expectNoConfig(removed.fn.functionName);
// --- alias-scoped config ---
const withAlias = yield* stack.deploy(
program({
alias: {
eventInvokeConfig: {
maximumRetryAttempts: 2,
maximumEventAge: "5 minutes",
destinationConfig: {
OnFailure: {
Destination: created.queue.queueArn,
},
},
},
},
}),
);
const liveAliasConfig = yield* expectConfig(
withAlias.fn.functionName,
{
maximumRetryAttempts: 2,
maximumEventAgeInSeconds: 300,
onFailureDestination: created.queue.queueArn,
},
withAlias.live!.aliasName,
);
expect(liveAliasConfig.MaximumRetryAttempts).toBe(2);
expect(liveAliasConfig.MaximumEventAgeInSeconds).toBe(300);
expect(liveAliasConfig.DestinationConfig?.OnFailure?.Destination).toBe(
created.queue.queueArn,
);
// The unqualified function stays unconfigured.
yield* expectNoConfig(withAlias.fn.functionName);
// --- omit the alias prop: the alias-scoped config is deleted ---
const aliasCleared = yield* stack.deploy(program({ alias: {} }));
yield* expectNoConfig(
aliasCleared.fn.functionName,
aliasCleared.live!.aliasName,
);
yield* stack.destroy();
// Out-of-band proof the destroy removed the host function (and with it
// the event invoke configs) from the cloud.
yield* Lambda.getFunction({
FunctionName: aliasCleared.fn.functionName,
}).pipe(
Effect.flatMap(() =>
Effect.fail(
new Error(`Function ${aliasCleared.fn.functionName} still exists`),
),
),
Effect.catchTag("ResourceNotFoundException", () => Effect.void),
Effect.retry({
schedule: Schedule.max([
Schedule.exponential(500),
Schedule.recurs(8),
]),
}),
);
}).pipe(
Effect.tap(() => stack.destroy()),
Effect.onError(() => stack.destroy().pipe(Effect.ignore)),
),
{
tags: ["provider:aws", "provider:aws:lambda", "provider:aws:sqs", "live"],
timeout: 360_000,
},
);
const getConfigOrUndefined = Effect.fn(function* (
functionName: string,
qualifier?: string,
) {
return yield* Lambda.getFunctionEventInvokeConfig({
FunctionName: functionName,
Qualifier: qualifier,
}).pipe(
Effect.catchTag("ResourceNotFoundException", () =>
Effect.succeed(undefined),
),
);
});
// Reads until the config matches the expected shape (updates propagate
// eventually), failing after a bounded number of attempts.
const expectConfig = Effect.fn(function* (
functionName: string,
expected: {
maximumRetryAttempts: number;
maximumEventAgeInSeconds: number;
onFailureDestination?: string;
},
qualifier?: string,
) {
return yield* getConfigOrUndefined(functionName, qualifier).pipe(
Effect.filterOrFail(
(config): config is Lambda.FunctionEventInvokeConfig =>
config !== undefined &&
config.MaximumRetryAttempts === expected.maximumRetryAttempts &&
config.MaximumEventAgeInSeconds === expected.maximumEventAgeInSeconds &&
config.DestinationConfig?.OnFailure?.Destination ===
expected.onFailureDestination,
() => new Error("Event invoke config update has not propagated yet"),
),
Effect.retry({
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(10)]),
}),
);
});
const expectNoConfig = Effect.fn(function* (
functionName: string,
qualifier?: string,
) {
yield* getConfigOrUndefined(functionName, qualifier).pipe(
Effect.filterOrFail(
(config) => config === undefined,
() => new Error("Event invoke config removal has not propagated yet"),
),
Effect.retry({
schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(10)]),
}),
);
});