alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
179 lines (151 loc) • 6.14 kB
text/typescript
import { Alepha } from "alepha";
import { FileSystemProvider, MemoryFileSystemProvider } from "alepha/system";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { BuildCloudflareTask } from "../tasks/BuildCloudflareTask.ts";
/**
* Exposes the protected binding enhancers so the wrangler.jsonc shape can be
* asserted directly without driving a full build.
*/
class TestBuildCloudflareTask extends BuildCloudflareTask {
public testEnhanceD1 = this.enhanceD1.bind(this);
public testEnhanceR2 = this.enhanceR2.bind(this);
public testEnhanceQueue = this.enhanceQueue.bind(this);
public testWriteWorkerEntryPoint = this.writeWorkerEntryPoint.bind(this);
}
describe("BuildCloudflareTask", () => {
const createTask = () => {
const alepha = Alepha.create().with({
provide: FileSystemProvider,
use: MemoryFileSystemProvider,
});
return alepha.inject(TestBuildCloudflareTask);
};
const createTaskWithFs = () => {
const alepha = Alepha.create().with({
provide: FileSystemProvider,
use: MemoryFileSystemProvider,
});
return {
task: alepha.inject(TestBuildCloudflareTask),
fs: alepha.inject(MemoryFileSystemProvider),
};
};
// Snapshot + restore the env vars these enhancers read so tests don't leak.
const ENV_KEYS = [
"DATABASE_URL",
"R2_BUCKET_NAME",
"CLOUDFLARE_JURISDICTION",
"CLOUDFLARE_QUEUE_NAME",
"CLOUDFLARE_QUEUE_DLQ_NAME",
"CLOUDFLARE_QUEUE_MAX_RETRIES",
] as const;
const saved: Record<string, string | undefined> = {};
beforeEach(() => {
for (const k of ENV_KEYS) saved[k] = process.env[k];
});
afterEach(() => {
for (const k of ENV_KEYS) {
if (saved[k] === undefined) delete process.env[k];
else process.env[k] = saved[k];
}
});
describe("enhanceD1", () => {
it("does not emit `jurisdiction` on the D1 binding (wrangler rejects it)", () => {
// D1 jurisdiction is applied at database-creation time, not on the
// binding — wrangler warns on the unexpected field. See CloudflareApi.
process.env.DATABASE_URL = "d1://my-db:db-id-123";
process.env.CLOUDFLARE_JURISDICTION = "eu";
const wrangler: Record<string, any> = {};
createTask().testEnhanceD1(wrangler);
expect(wrangler.d1_databases).toEqual([
{ binding: "DB", database_name: "my-db", database_id: "db-id-123" },
]);
expect(wrangler.d1_databases[0]).not.toHaveProperty("jurisdiction");
expect(wrangler.vars.DATABASE_URL).toBe("d1://DB");
});
it("ignores non-d1 DATABASE_URL", () => {
process.env.DATABASE_URL = "postgres://localhost/db";
const wrangler: Record<string, any> = {};
createTask().testEnhanceD1(wrangler);
expect(wrangler.d1_databases).toBeUndefined();
});
});
describe("enhanceR2", () => {
it("keeps `jurisdiction` on the R2 binding (wrangler accepts it)", () => {
process.env.R2_BUCKET_NAME = "my-bucket";
process.env.CLOUDFLARE_JURISDICTION = "eu";
const wrangler: Record<string, any> = {};
createTask().testEnhanceR2(wrangler);
expect(wrangler.r2_buckets).toEqual([
{ binding: "my-bucket", bucket_name: "my-bucket", jurisdiction: "eu" },
]);
});
});
describe("enhanceQueue", () => {
/**
* The worker's queue handler calls `msg.retry()` on any throw. Without a
* `dead_letter_queue`, CF burns its retries and then DISCARDS the message —
* a poison job disappears with no record and no signal.
*/
it("gives the consumer a dead-letter queue and a retry ceiling", () => {
process.env.CLOUDFLARE_QUEUE_NAME = "my-queue";
const wrangler: Record<string, any> = {};
createTask().testEnhanceQueue(wrangler);
expect(wrangler.queues.consumers).toEqual([
{
queue: "my-queue",
dead_letter_queue: "my-queue-dlq",
max_retries: 3,
},
]);
});
it("honours an explicit dead-letter queue and retry ceiling", () => {
process.env.CLOUDFLARE_QUEUE_NAME = "my-queue";
process.env.CLOUDFLARE_QUEUE_DLQ_NAME = "shared-dlq";
process.env.CLOUDFLARE_QUEUE_MAX_RETRIES = "5";
const wrangler: Record<string, any> = {};
createTask().testEnhanceQueue(wrangler);
expect(wrangler.queues.consumers[0]).toEqual({
queue: "my-queue",
dead_letter_queue: "shared-dlq",
max_retries: 5,
});
});
it("ignores a non-numeric retry ceiling rather than emitting NaN", () => {
process.env.CLOUDFLARE_QUEUE_NAME = "my-queue";
process.env.CLOUDFLARE_QUEUE_MAX_RETRIES = "not-a-number";
const wrangler: Record<string, any> = {};
createTask().testEnhanceQueue(wrangler);
expect(wrangler.queues.consumers[0].max_retries).toBe(3);
});
});
describe("writeWorkerEntryPoint", () => {
const ENTRY = "/root/dist/main.cloudflare.js";
/**
* A single CF isolate serves concurrent invocations. Stashing the
* per-invocation `executionCtx.waitUntil` on the shared Alepha store means
* request B overwrites request A's, and A's background work then calls B's
* (already-returned) context — "waitUntil after response" — silently
* killing it. The handle must ride the per-invocation async context.
*/
it("does not stash waitUntil on the shared global store", async () => {
const { task, fs } = createTaskWithFs();
await task.testWriteWorkerEntryPoint("/root", "dist");
expect(
fs.wasWrittenMatching(
ENTRY,
/__alepha\.set\(\s*["']cloudflare\.waitUntil["']/,
),
).toBe(false);
});
it("scopes waitUntil to each invocation's async context", async () => {
const { task, fs } = createTaskWithFs();
await task.testWriteWorkerEntryPoint("/root", "dist");
// Every entry point (fetch / scheduled / queue) must run its body inside
// an Alepha fork seeded with that invocation's waitUntil.
expect(fs.wasWrittenMatching(ENTRY, /__alepha\.context\.run\(/)).toBe(
true,
);
});
});
});