mirror of
https://github.com/VibedByKaKi/t3-code-android-nightly.git
synced 2026-10-18 16:27:43 +02:00
214 lines
7.9 KiB
TypeScript
214 lines
7.9 KiB
TypeScript
/**
|
|
* Floci local-emulator smoke test.
|
|
*
|
|
* Deploys real AWS resources (S3 Bucket, SQS Queue, DynamoDB Table) through
|
|
* the regular live providers, but pointed at a locally-running floci emulator
|
|
* via the floci-scoped environment the local AWS providers carry.
|
|
*
|
|
* ## How local mode is activated
|
|
*
|
|
* The AWS auth method is normally chosen by the profile entry in
|
|
* `~/.alchemy/profiles.json` (written by `alchemy profile edit`). Tests
|
|
* must not depend on the developer's on-disk profiles. `Test.make({ dev:
|
|
* true })` runs the stack the way `alchemy dev` does, so the local AWS
|
|
* providers are selected; they carry their own floci-scoped environment,
|
|
* which resolves dummy credentials (`test`/`test`), accountId
|
|
* `000000000000`, and endpoint `http://localhost:4566`, and `ensureFloci()`
|
|
* guarantees the emulator is serving (starting the `alchemy-floci`
|
|
* container if needed). Without `dev: true` the live providers run against
|
|
* whatever account the profile resolves — the real cloud.
|
|
*
|
|
* ## Why no real-AWS calls can happen
|
|
*
|
|
* The floci environment's credentials are hardcoded dummies and its accountId
|
|
* is fixed — it never calls STS. Every SDK call carries the emulator endpoint
|
|
* (via `Endpoint.fromEnvironment`); if any call ever escaped to real AWS it
|
|
* would fail auth immediately (the dummy keys exist in no real account).
|
|
*
|
|
* Requires Docker (floci runs as a container); skipped when the Docker
|
|
* daemon is unavailable.
|
|
*/
|
|
import * as AWS from "@/AWS";
|
|
import { Table } from "@/AWS/DynamoDB";
|
|
import { Bucket } from "@/AWS/S3";
|
|
import { Queue } from "@/AWS/SQS";
|
|
import * as Test from "@/Test/Alchemy";
|
|
import { expect } from "alchemy-test";
|
|
import * as Effect from "effect/Effect";
|
|
import * as HttpBody from "effect/http/HttpBody";
|
|
import * as HttpClient from "effect/http/HttpClient";
|
|
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
import { spawnSync } from "node:child_process";
|
|
|
|
const FLOCI_ENDPOINT = "http://localhost:4566";
|
|
|
|
// skipIf gate: floci runs as a Docker container. `docker info` proves the
|
|
// daemon is reachable (a mere `docker --version` would pass with the daemon
|
|
// down). Sync probe at collection time — skipIf needs a plain boolean.
|
|
const dockerAvailable = (() => {
|
|
try {
|
|
return (
|
|
spawnSync("docker", ["info"], { stdio: "ignore", timeout: 15_000 })
|
|
.status === 0
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
const providers = AWS.providers();
|
|
|
|
// `dev: true` runs the same topology as the real `alchemy dev` command
|
|
// (including the RPC sidecar default for RPC-backed providers).
|
|
const { test } = Test.make({ providers, dev: true });
|
|
|
|
/**
|
|
* Raw (non-distilled) call against the emulator gateway — out-of-band proof
|
|
* that a resource exists IN THE EMULATOR, not the real cloud. The dummy
|
|
* SigV4 Authorization header is never verified by the emulator; it only
|
|
* carries the service/region/account scoping the gateway routes by.
|
|
*/
|
|
const rawAwsJson = Effect.fn(function* (options: {
|
|
service: string;
|
|
region: string;
|
|
target: string;
|
|
contentType: string;
|
|
body: Record<string, unknown>;
|
|
}) {
|
|
const client = yield* HttpClient.HttpClient;
|
|
return yield* client.execute(
|
|
HttpClientRequest.post(`${FLOCI_ENDPOINT}/`).pipe(
|
|
HttpClientRequest.setHeaders({
|
|
"content-type": options.contentType,
|
|
"x-amz-target": options.target,
|
|
"x-amz-date": "20260101T000000Z",
|
|
authorization: `AWS4-HMAC-SHA256 Credential=test/20260101/${options.region}/${options.service}/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy`,
|
|
}),
|
|
HttpClientRequest.setBody(
|
|
HttpBody.text(JSON.stringify(options.body), options.contentType),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
|
|
/** Path-style S3 GET against the gateway (list the bucket's objects). */
|
|
const rawS3GetBucket = Effect.fn(function* (bucketName: string) {
|
|
const client = yield* HttpClient.HttpClient;
|
|
return yield* client.execute(
|
|
HttpClientRequest.get(`${FLOCI_ENDPOINT}/${bucketName}`).pipe(
|
|
HttpClientRequest.setHeaders({
|
|
"x-amz-date": "20260101T000000Z",
|
|
authorization:
|
|
"AWS4-HMAC-SHA256 Credential=test/20260101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=dummy",
|
|
}),
|
|
),
|
|
);
|
|
});
|
|
|
|
/** Region segment of an ARN (`arn:aws:sqs:REGION:ACCOUNT:name`). */
|
|
const regionOfArn = (arn: string) => arn.split(":")[3]!;
|
|
|
|
test.provider.skipIf(!dockerAvailable)(
|
|
"deploys S3 + SQS + DynamoDB into floci and destroys them",
|
|
(stack) =>
|
|
Effect.gen(function* () {
|
|
yield* stack.destroy();
|
|
|
|
const outputs = yield* stack.deploy(
|
|
Effect.gen(function* () {
|
|
const bucket = yield* Bucket("FlociBucket");
|
|
const queue = yield* Queue("FlociQueue");
|
|
const table = yield* Table("FlociTable", {
|
|
partitionKey: "id",
|
|
attributes: { id: "S" },
|
|
});
|
|
return { bucket, queue, table };
|
|
}),
|
|
);
|
|
|
|
// Attributes prove the providers ran their full reconcile loops. The
|
|
// dummy-account ARNs double as proof no real AWS account was involved.
|
|
expect(outputs.bucket.bucketName).toBeDefined();
|
|
expect(outputs.bucket.bucketArn).toContain(outputs.bucket.bucketName);
|
|
expect(outputs.queue.queueUrl).toContain(outputs.queue.queueName);
|
|
expect(outputs.queue.queueArn).toContain(":000000000000:");
|
|
expect(outputs.table.tableArn).toContain(":000000000000:");
|
|
|
|
const region = regionOfArn(outputs.queue.queueArn);
|
|
|
|
// --- Out-of-band verification: raw HTTP against the emulator gateway,
|
|
// no distilled SDK, no alchemy layers. If the resources had gone to
|
|
// real AWS these calls would not find them here.
|
|
const listQueues = yield* rawAwsJson({
|
|
service: "sqs",
|
|
region,
|
|
target: "AmazonSQS.ListQueues",
|
|
contentType: "application/x-amz-json-1.0",
|
|
body: {},
|
|
});
|
|
expect(listQueues.status).toBe(200);
|
|
const queues = (yield* listQueues.json) as { QueueUrls?: string[] };
|
|
expect(
|
|
queues.QueueUrls?.some((url) =>
|
|
url.endsWith(`/${outputs.queue.queueName}`),
|
|
),
|
|
).toBe(true);
|
|
|
|
const describeTable = yield* rawAwsJson({
|
|
service: "dynamodb",
|
|
region: regionOfArn(outputs.table.tableArn),
|
|
target: "DynamoDB_20120810.DescribeTable",
|
|
contentType: "application/x-amz-json-1.0",
|
|
body: { TableName: outputs.table.tableName },
|
|
});
|
|
expect(describeTable.status).toBe(200);
|
|
const described = (yield* describeTable.json) as {
|
|
Table?: { TableArn?: string };
|
|
};
|
|
expect(described.Table?.TableArn).toBe(outputs.table.tableArn);
|
|
|
|
const getBucket = yield* rawS3GetBucket(outputs.bucket.bucketName);
|
|
expect(getBucket.status).toBe(200);
|
|
|
|
// --- Destroy and verify gone from the emulator.
|
|
yield* stack.destroy();
|
|
|
|
const listQueuesAfter = yield* rawAwsJson({
|
|
service: "sqs",
|
|
region,
|
|
target: "AmazonSQS.ListQueues",
|
|
contentType: "application/x-amz-json-1.0",
|
|
body: {},
|
|
});
|
|
const queuesAfter = (yield* listQueuesAfter.json) as {
|
|
QueueUrls?: string[];
|
|
};
|
|
expect(
|
|
queuesAfter.QueueUrls?.some((url) =>
|
|
url.endsWith(`/${outputs.queue.queueName}`),
|
|
) ?? false,
|
|
).toBe(false);
|
|
|
|
const describeTableAfter = yield* rawAwsJson({
|
|
service: "dynamodb",
|
|
region: regionOfArn(outputs.table.tableArn),
|
|
target: "DynamoDB_20120810.DescribeTable",
|
|
contentType: "application/x-amz-json-1.0",
|
|
body: { TableName: outputs.table.tableName },
|
|
});
|
|
expect(describeTableAfter.status).toBe(400); // ResourceNotFoundException
|
|
|
|
const getBucketAfter = yield* rawS3GetBucket(outputs.bucket.bucketName);
|
|
expect(getBucketAfter.status).toBe(404); // NoSuchBucket
|
|
}),
|
|
{
|
|
tags: [
|
|
"provider:aws",
|
|
"provider:aws:dynamodb",
|
|
"provider:aws:s3",
|
|
"provider:aws:sqs",
|
|
"local",
|
|
],
|
|
timeout: 240_000,
|
|
},
|
|
);
|