t3-code-android-nightly/.repos/alchemy-effect/examples/aws-ecs/test/integ.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

121 lines
3.6 KiB
TypeScript

/**
* LIVE counterpart to test/dev.test.ts: deploys the exact same orders app
* to real AWS (Test.make + deploy, no CLI) — VPC, shared ALB, Fargate
* services, DynamoDB table, one-shot tasks — and drives the same HTTP
* surface against the real load balancer.
*
* Requires AWS credentials and docker (the Api/SeedTask/Report images are
* built locally and pushed to ECR). The first deploy provisions an ALB and
* two Fargate services (~5-10 minutes).
*/
import * as Alchemy from "alchemy";
import * as AWS from "alchemy/AWS";
import * as Test from "alchemy/Test/Bun";
import { expect } from "bun:test";
import * as Effect from "effect/Effect";
import * as Schedule from "effect/Schedule";
import * as HttpClient from "effect/http/HttpClient";
import Stack from "../alchemy.run.ts";
const { test, beforeAll, afterAll, deploy, destroy } = Test.make({
providers: AWS.providers(),
state: Alchemy.localState(),
});
// The first deploy builds + pushes container images and waits for two
// Fargate services to stabilize behind the ALB.
const stack = beforeAll(deploy(Stack), { timeout: 1_800_000 });
afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(Stack), {
timeout: 1_800_000,
});
type Outputs = {
url: string;
apiUrl: string;
seedUrl: string;
tableName: string;
};
const outputs = Effect.map(stack, (out) => out as unknown as Outputs);
/** GET with retries — fresh ALB targets take a moment to pass health checks. */
const getJson = Effect.fn(function* (url: string) {
const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
const res = yield* client.get(url).pipe(
Effect.retry({
schedule: Schedule.min([
Schedule.exponential("1 second"),
Schedule.spaced("5 seconds"),
]),
times: 30,
}),
);
return yield* res.json;
});
test(
"the Api service serves /api/orders through the shared ALB",
Effect.gen(function* () {
const { apiUrl } = yield* outputs;
const body = (yield* getJson(apiUrl)) as {
count: number;
orders: unknown[];
};
expect(body.count).toBeNumber();
}),
{ timeout: 300_000 },
);
test(
"the Web service serves the catch-all route",
Effect.gen(function* () {
const { url } = yield* outputs;
const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
const res = yield* client.get(url).pipe(
Effect.retry({
schedule: Schedule.min([
Schedule.exponential("1 second"),
Schedule.spaced("5 seconds"),
]),
times: 30,
}),
);
const text = yield* res.text;
expect(text).toContain("Server");
}),
{ timeout: 300_000 },
);
test(
"POST /api/seed launches the SeedTask and seeds the table",
Effect.gen(function* () {
const { apiUrl, seedUrl } = yield* outputs;
const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
const seedRes = yield* client.post(seedUrl).pipe(
Effect.retry({
schedule: Schedule.spaced("5 seconds"),
times: 12,
}),
);
const seed = (yield* seedRes.json) as {
taskArn?: string;
failures: unknown[];
};
expect(seed.failures).toEqual([]);
expect(seed.taskArn).toBeString();
// The one-shot Fargate task must pull its image and run to completion
// before the orders appear.
const count = yield* getJson(apiUrl).pipe(
Effect.map((body) => (body as { count: number }).count),
Effect.repeat({
schedule: Schedule.spaced("5 seconds"),
until: (count) => count >= 3,
times: 60,
}),
);
expect(count).toBeGreaterThanOrEqual(3);
}),
{ timeout: 600_000 },
);