1/**
2 * Floci local-emulator smoke test.
3 *
4 * Deploys real AWS resources (S3 Bucket, SQS Queue, DynamoDB Table) through
5 * the regular live providers, but pointed at a locally-running floci emulator
6 * via the floci-scoped environment the local AWS providers carry.
7 *
8 * ## How local mode is activated
9 *
10 * The AWS auth method is normally chosen by the profile entry in
11 * `~/.alchemy/profiles.json` (written by `alchemy profile edit`). Tests
12 * must not depend on the developer's on-disk profiles. `Test.make({ dev:
13 * true })` runs the stack the way `alchemy dev` does, so the local AWS
14 * providers are selected; they carry their own floci-scoped environment,
15 * which resolves dummy credentials (`test`/`test`), accountId
16 * `000000000000`, and endpoint `http://localhost:4566`, and `ensureFloci()`
17 * guarantees the emulator is serving (starting the `alchemy-floci`
18 * container if needed). Without `dev: true` the live providers run against
19 * whatever account the profile resolves — the real cloud.
20 *
21 * ## Why no real-AWS calls can happen
22 *
23 * The floci environment's credentials are hardcoded dummies and its accountId
24 * is fixed — it never calls STS. Every SDK call carries the emulator endpoint
25 * (via `Endpoint.fromEnvironment`); if any call ever escaped to real AWS it
26 * would fail auth immediately (the dummy keys exist in no real account).
27 *
28 * Requires Docker (floci runs as a container); skipped when the Docker
29 * daemon is unavailable.
30 */
31import * as AWS from "@/AWS";
32import { Table } from "@/AWS/DynamoDB";
33import { Bucket } from "@/AWS/S3";
34import { Queue } from "@/AWS/SQS";
35import * as Test from "@/Test/Alchemy";
36import { expect } from "alchemy-test";
37import * as Effect from "effect/Effect";
38import * as HttpBody from "effect/http/HttpBody";
39import * as HttpClient from "effect/http/HttpClient";
40import * as HttpClientRequest from "effect/http/HttpClientRequest";
41import { spawnSync } from "node:child_process";
42
43const FLOCI_ENDPOINT = "http://localhost:4566";
44
45// skipIf gate: floci runs as a Docker container. `docker info` proves the
46// daemon is reachable (a mere `docker --version` would pass with the daemon
47// down). Sync probe at collection time — skipIf needs a plain boolean.
48const dockerAvailable = (() => {
49 try {
50 return (
51 spawnSync("docker", ["info"], { stdio: "ignore", timeout: 15_000 })
52 .status === 0
53 );
54 } catch {
55 return false;
56 }
57})();
58
59const providers = AWS.providers();
60
61// `dev: true` runs the same topology as the real `alchemy dev` command
62// (including the RPC sidecar default for RPC-backed providers).
63const { test } = Test.make({ providers, dev: true });
64
65/**
66 * Raw (non-distilled) call against the emulator gateway — out-of-band proof
67 * that a resource exists IN THE EMULATOR, not the real cloud. The dummy
68 * SigV4 Authorization header is never verified by the emulator; it only
69 * carries the service/region/account scoping the gateway routes by.
70 */
71const rawAwsJson = Effect.fn(function* (options: {
72 service: string;
73 region: string;
74 target: string;
75 contentType: string;
76 body: Record<string, unknown>;
77}) {
78 const client = yield* HttpClient.HttpClient;
79 return yield* client.execute(
80 HttpClientRequest.post(`${FLOCI_ENDPOINT}/`).pipe(
81 HttpClientRequest.setHeaders({
82 "content-type": options.contentType,
83 "x-amz-target": options.target,
84 "x-amz-date": "20260101T000000Z",
85 authorization: `AWS4-HMAC-SHA256 Credential=test/20260101/${options.region}/${options.service}/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy`,
86 }),
87 HttpClientRequest.setBody(
88 HttpBody.text(JSON.stringify(options.body), options.contentType),
89 ),
90 ),
91 );
92});
93
94/** Path-style S3 GET against the gateway (list the bucket's objects). */
95const rawS3GetBucket = Effect.fn(function* (bucketName: string) {
96 const client = yield* HttpClient.HttpClient;
97 return yield* client.execute(
98 HttpClientRequest.get(`${FLOCI_ENDPOINT}/${bucketName}`).pipe(
99 HttpClientRequest.setHeaders({
100 "x-amz-date": "20260101T000000Z",
101 authorization:
102 "AWS4-HMAC-SHA256 Credential=test/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy",
103 }),
104 ),
105 );
106});
107
108/** Region segment of an ARN (`arn:aws:sqs:REGION:ACCOUNT:name`). */
109const regionOfArn = (arn: string) => arn.split(":")[3]!;
110
111test.provider.skipIf(!dockerAvailable)(
112 "deploys S3 + SQS + DynamoDB into floci and destroys them",
113 (stack) =>
114 Effect.gen(function* () {
115 yield* stack.destroy();
116
117 const outputs = yield* stack.deploy(
118 Effect.gen(function* () {
119 const bucket = yield* Bucket("FlociBucket");
120 const queue = yield* Queue("FlociQueue");
121 const table = yield* Table("FlociTable", {
122 partitionKey: "id",
123 attributes: { id: "S" },
124 });
125 return { bucket, queue, table };
126 }),
127 );
128
129 // Attributes prove the providers ran their full reconcile loops. The
130 // dummy-account ARNs double as proof no real AWS account was involved.
131 expect(outputs.bucket.bucketName).toBeDefined();
132 expect(outputs.bucket.bucketArn).toContain(outputs.bucket.bucketName);
133 expect(outputs.queue.queueUrl).toContain(outputs.queue.queueName);
134 expect(outputs.queue.queueArn).toContain(":000000000000:");
135 expect(outputs.table.tableArn).toContain(":000000000000:");
136
137 const region = regionOfArn(outputs.queue.queueArn);
138
139 // --- Out-of-band verification: raw HTTP against the emulator gateway,
140 // no distilled SDK, no alchemy layers. If the resources had gone to
141 // real AWS these calls would not find them here.
142 const listQueues = yield* rawAwsJson({
143 service: "sqs",
144 region,
145 target: "AmazonSQS.ListQueues",
146 contentType: "application/x-amz-json-1.0",
147 body: {},
148 });
149 expect(listQueues.status).toBe(200);
150 const queues = (yield* listQueues.json) as { QueueUrls?: string[] };
151 expect(
152 queues.QueueUrls?.some((url) =>
153 url.endsWith(`/${outputs.queue.queueName}`),
154 ),
155 ).toBe(true);
156
157 const describeTable = yield* rawAwsJson({
158 service: "dynamodb",
159 region: regionOfArn(outputs.table.tableArn),
160 target: "DynamoDB_20120810.DescribeTable",
161 contentType: "application/x-amz-json-1.0",
162 body: { TableName: outputs.table.tableName },
163 });
164 expect(describeTable.status).toBe(200);
165 const described = (yield* describeTable.json) as {
166 Table?: { TableArn?: string };
167 };
168 expect(described.Table?.TableArn).toBe(outputs.table.tableArn);
169
170 const getBucket = yield* rawS3GetBucket(outputs.bucket.bucketName);
171 expect(getBucket.status).toBe(200);
172
173 // --- Destroy and verify gone from the emulator.
174 yield* stack.destroy();
175
176 const listQueuesAfter = yield* rawAwsJson({
177 service: "sqs",
178 region,
179 target: "AmazonSQS.ListQueues",
180 contentType: "application/x-amz-json-1.0",
181 body: {},
182 });
183 const queuesAfter = (yield* listQueuesAfter.json) as {
184 QueueUrls?: string[];
185 };
186 expect(
187 queuesAfter.QueueUrls?.some((url) =>
188 url.endsWith(`/${outputs.queue.queueName}`),
189 ) ?? false,
190 ).toBe(false);
191
192 const describeTableAfter = yield* rawAwsJson({
193 service: "dynamodb",
194 region: regionOfArn(outputs.table.tableArn),
195 target: "DynamoDB_20120810.DescribeTable",
196 contentType: "application/x-amz-json-1.0",
197 body: { TableName: outputs.table.tableName },
198 });
199 expect(describeTableAfter.status).toBe(400); // ResourceNotFoundException
200
201 const getBucketAfter = yield* rawS3GetBucket(outputs.bucket.bucketName);
202 expect(getBucketAfter.status).toBe(404); // NoSuchBucket
203 }),
204 {
205 tags: [
206 "provider:aws",
207 "provider:aws:dynamodb",
208 "provider:aws:s3",
209 "provider:aws:sqs",
210 "local",
211 ],
212 timeout: 240_000,
213 },
214);