UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

179 lines (151 loc) 6.14 kB
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, ); }); }); });