alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
981 lines (846 loc) • 29.9 kB
text/typescript
import { Alepha } from "alepha";
import { DateTimeProvider } from "alepha/datetime";
import {
FileSystemProvider,
MemoryFileSystemProvider,
MemoryShellProvider,
ShellProvider,
} from "alepha/system";
import { describe, test } from "vitest";
import { CloudflareAdapter } from "../adapters/CloudflareAdapter.ts";
import type { PlatformContext } from "../adapters/PlatformAdapter.ts";
import { platformOptions } from "../atoms/platformOptions.ts";
import { CloudflareApi } from "../services/CloudflareApi.ts";
import { NamingService } from "../services/NamingService.ts";
/**
* In-memory CloudflareApi for testing.
*
* Stores resources in maps and implements the same interface
* without making any HTTP calls.
*/
class MemoryCloudflareApi extends CloudflareApi {
public d1Databases: Array<{ uuid: string; name: string }> = [];
public kvNamespaces: Array<{ id: string; title: string }> = [];
public r2Buckets: Array<{ name: string; creation_date: string }> = [];
public queues: Array<{ queue_id: string; queue_name: string }> = [];
public hyperdriveConfigs: Array<{
id: string;
name: string;
origin: { host: string };
}> = [];
public secrets: Map<string, Array<{ name: string; type: string }>> =
new Map();
public override async resolveToken(): Promise<string> {
return "test-token";
}
public override async resolveAccountId(): Promise<string> {
return "test-account-id";
}
public override async listD1() {
return this.d1Databases;
}
public override async createD1(name: string) {
const db = { uuid: `d1-${name}-uuid`, name };
this.d1Databases.push(db);
return db;
}
public override async deleteD1(databaseId: string) {
this.d1Databases = this.d1Databases.filter((db) => db.uuid !== databaseId);
}
public override async listKV() {
return this.kvNamespaces;
}
public override async createKV(title: string) {
const ns = { id: `kv-${title}-id`, title };
this.kvNamespaces.push(ns);
return ns;
}
public override async deleteKV(namespaceId: string) {
this.kvNamespaces = this.kvNamespaces.filter((ns) => ns.id !== namespaceId);
}
public override async listR2() {
return this.r2Buckets;
}
public override async createR2(name: string) {
this.r2Buckets.push({ name, creation_date: new Date().toISOString() });
}
public override async deleteR2(name: string) {
this.r2Buckets = this.r2Buckets.filter((b) => b.name !== name);
}
public override async listQueues() {
return this.queues;
}
public override async createQueue(name: string) {
const queue = { queue_id: `q-${name}-id`, queue_name: name };
this.queues.push(queue);
return queue;
}
public override async deleteQueue(queueId: string) {
this.queues = this.queues.filter((q) => q.queue_id !== queueId);
}
public override async listQueueConsumers() {
return [];
}
public override async deleteQueueConsumer() {}
public override async listHyperdrive() {
return this.hyperdriveConfigs;
}
public override async createHyperdrive(name: string) {
const config = { id: `hd-${name}-id`, name, origin: { host: "localhost" } };
this.hyperdriveConfigs.push(config);
return config;
}
public override async deleteHyperdrive(configId: string) {
this.hyperdriveConfigs = this.hyperdriveConfigs.filter(
(c) => c.id !== configId,
);
}
public override async listSecrets(scriptName: string) {
return this.secrets.get(scriptName) ?? [];
}
public override async putSecret(
scriptName: string,
name: string,
_value: string,
) {
const existing = this.secrets.get(scriptName) ?? [];
if (!existing.some((s) => s.name === name)) {
existing.push({ name, type: "secret_text" });
}
this.secrets.set(scriptName, existing);
}
// Full binding set (all types), used by the bulk-PATCH path. Existing
// tests read `api.secrets` which we keep as a secret_text-only projection
// of this map.
public bindings: Map<
string,
Array<{ type: string; name: string; text?: string }>
> = new Map();
public override async getWorkerSettings(scriptName: string) {
return { bindings: this.bindings.get(scriptName) ?? [] };
}
public override async patchWorkerBindings(
scriptName: string,
bindings: Array<{ type: string; name: string; text?: string }>,
) {
this.bindings.set(scriptName, bindings);
const secretView = bindings
.filter((b) => b.type === "secret_text")
.map((b) => ({ name: b.name, type: "secret_text" as const }));
this.secrets.set(scriptName, secretView);
}
public override async listDeployments() {
return [];
}
public override async listVersions() {
return [];
}
public override async deleteWorker() {}
}
describe("CloudflareAdapter", () => {
const createTestEnv = () => {
// Ensure D1 path (not Hyperdrive) — isPostgres() checks process.env.DATABASE_URL
delete process.env.DATABASE_URL;
const alepha = Alepha.create()
.with({ provide: FileSystemProvider, use: MemoryFileSystemProvider })
.with({ provide: ShellProvider, use: MemoryShellProvider })
.with({ provide: CloudflareApi, use: MemoryCloudflareApi });
const fs = alepha.inject(MemoryFileSystemProvider);
const shell = alepha.inject(MemoryShellProvider);
const dateTime = alepha.inject(DateTimeProvider);
const adapter = alepha.inject(CloudflareAdapter);
const naming = alepha.inject(NamingService);
const api = alepha.inject(MemoryCloudflareApi);
// Pre-seed package.json so ensureDependency finds wrangler already installed
fs.files.set(
"/project/package.json",
Buffer.from(
JSON.stringify({
name: "test",
devDependencies: { wrangler: "^3.0.0" },
}),
),
);
return { alepha, fs, shell, dateTime, adapter, naming, api };
};
const makeCtx = (
naming: NamingService,
overrides: Partial<PlatformContext> = {},
): PlatformContext => ({
project: "acme-portal",
env: "production",
envConfig: { adapter: "cloudflare" },
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
root: "/project",
naming: naming.forContext("acme-portal", "production"),
...overrides,
});
describe("authenticate", () => {
test("always validates token but skips account resolution when cache is fresh", async ({
expect,
}) => {
const { adapter, shell, dateTime, naming, api } = createTestEnv();
const ctx = makeCtx(naming);
dateTime.pause();
shell.outputs.set(
"wrangler auth token --json",
JSON.stringify({ type: "oauth", token: "test-token" }),
);
let resolveAccountCalls = 0;
const originalResolve = api.resolveAccountId.bind(api);
api.resolveAccountId = async () => {
resolveAccountCalls++;
return originalResolve();
};
const run = createMockRun();
await adapter.authenticate(ctx, run);
expect(resolveAccountCalls).toBe(1);
// Second call — token still validated, but account resolution skipped
shell.calls.length = 0;
resolveAccountCalls = 0;
await adapter.authenticate(ctx, run);
expect(shell.wasCalled("wrangler auth token --json")).toBe(true);
expect(resolveAccountCalls).toBe(0);
});
test("checks auth token when cache is stale", async ({ expect }) => {
const { adapter, shell, dateTime, naming } = createTestEnv();
const ctx = makeCtx(naming);
shell.outputs.set(
"wrangler auth token --json",
JSON.stringify({ type: "oauth", token: "test-token" }),
);
dateTime.pause();
const run = createMockRun();
await adapter.authenticate(ctx, run);
await dateTime.travel(5 * 60 * 60 * 1000); // 5 hours
shell.calls.length = 0;
await adapter.authenticate(ctx, run);
expect(shell.wasCalled("wrangler auth token --json")).toBe(true);
});
});
describe("provision", () => {
test("creates D1 database via REST API when app has database", async ({
expect,
}) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: true,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
const run = createMockRun();
await adapter.provision(ctx, run);
expect(api.d1Databases).toHaveLength(1);
expect(api.d1Databases[0].name).toBe("acme-portal-production");
});
test("skips D1 creation when database already exists", async ({
expect,
}) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: true,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
// Pre-seed existing database
api.d1Databases.push({
name: "acme-portal-production",
uuid: "existing-uuid",
});
const run = createMockRun();
await adapter.provision(ctx, run);
// Should still be 1 (not 2)
expect(api.d1Databases).toHaveLength(1);
expect(api.d1Databases[0].uuid).toBe("existing-uuid");
});
test("creates R2 bucket via REST API when app has bucket", async ({
expect,
}) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: true,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
const run = createMockRun();
await adapter.provision(ctx, run);
expect(api.r2Buckets).toHaveLength(1);
expect(api.r2Buckets[0].name).toBe("acme-portal-production");
});
test("creates KV namespace via REST API when app has KV", async ({
expect,
}) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: true,
hasQueue: false,
hasCron: false,
},
});
const run = createMockRun();
await adapter.provision(ctx, run);
expect(api.kvNamespaces).toHaveLength(1);
expect(api.kvNamespaces[0].title).toBe("acme-portal-production");
});
test("creates queue via REST API when app has queue", async ({
expect,
}) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: true,
hasCron: false,
},
});
const run = createMockRun();
await adapter.provision(ctx, run);
expect(api.queues).toHaveLength(1);
expect(api.queues[0].queue_name).toBe("acme-portal-production");
});
});
describe("secrets", () => {
test("pushes non-binding env vars via REST putSecret", async ({
expect,
}) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
await fs.writeFile(
"/project/.env.production",
[
"GOOGLE_API_KEY=sk-123",
"APP_SECRET=my-secret",
"DATABASE_URL=d1://mydb",
"R2_BUCKET_NAME=my-bucket",
"CLOUDFLARE_DOMAIN=example.com",
"VITE_PUBLIC_KEY=public-abc",
"NODE_ENV=production",
"",
].join("\n"),
);
const run = createMockRun();
await adapter.secrets(ctx, run);
const pushed = api.secrets.get("acme-portal-production") ?? [];
const names = pushed.map((s) => s.name).sort();
expect(names).toEqual(["APP_SECRET", "GOOGLE_API_KEY"]);
});
test("with platform.secrets.keys, resolves the allowlist from process.env (no .env file) and ignores ambient vars", async ({
expect,
}) => {
const { adapter, alepha, naming, api } = createTestEnv();
// Declare an explicit allowlist — the CI shape: secrets arrive via the
// job environment, there is no .env.production on the runner.
alepha.set(platformOptions, {
secrets: { keys: ["APP_SECRET", "GOOGLE_CLIENT_ID", "EMAIL_FROM"] },
environments: { production: { adapter: "cloudflare" } },
} as any);
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
process.env.APP_SECRET = "from-env-secret";
process.env.GOOGLE_CLIENT_ID = "from-env-google";
// EMAIL_FROM intentionally unset → declared but unresolved, not pushed.
// PATH-style ambient var that must never leak into worker secrets.
process.env.AMBIENT_RUNNER_VAR = "leak-me-not";
try {
const run = createMockRun();
await adapter.secrets(ctx, run);
} finally {
delete process.env.APP_SECRET;
delete process.env.GOOGLE_CLIENT_ID;
delete process.env.AMBIENT_RUNNER_VAR;
}
const pushed = api.secrets.get("acme-portal-production") ?? [];
const names = pushed.map((s) => s.name).sort();
expect(names).toEqual(["APP_SECRET", "GOOGLE_CLIENT_ID"]);
});
test("with platform.secrets.keys, the .env file overrides process.env per key", async ({
expect,
}) => {
const { adapter, alepha, fs, naming, api } = createTestEnv();
alepha.set(platformOptions, {
secrets: { keys: ["APP_SECRET", "GOOGLE_CLIENT_ID"] },
environments: { production: { adapter: "cloudflare" } },
} as any);
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
// APP_SECRET in the file → file wins. GOOGLE_CLIENT_ID only in env → env.
await fs.writeFile(
"/project/.env.production",
["APP_SECRET=from-file", ""].join("\n"),
);
process.env.APP_SECRET = "from-env";
process.env.GOOGLE_CLIENT_ID = "env-google";
try {
const run = createMockRun();
await adapter.secrets(ctx, run);
} finally {
delete process.env.APP_SECRET;
delete process.env.GOOGLE_CLIENT_ID;
}
// Secret *values* land in the full binding set (api.bindings); the
// api.secrets projection only keeps names.
const bindings = api.bindings.get("acme-portal-production") ?? [];
const byName = Object.fromEntries(
bindings
.filter((b) => b.type === "secret_text")
.map((b) => [b.name, b.text]),
);
expect(byName.APP_SECRET).toBe("from-file");
expect(byName.GOOGLE_CLIENT_ID).toBe("env-google");
});
test("uses dist/manifest.json `env` as the default allowlist, resolved from process.env", async ({
expect,
}) => {
const { adapter, fs, naming, api } = createTestEnv();
// No platform.secrets.keys and no .env file → the manifest's declared
// env list is the allowlist. This is the CI shape.
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
await fs.writeFile(
"/project/dist/manifest.json",
JSON.stringify({
env: [
"APP_SECRET",
"GOOGLE_CLIENT_ID",
"CLOUDFLARE_ZONE",
"LOG_LEVEL",
],
}),
);
process.env.APP_SECRET = "s1";
process.env.GOOGLE_CLIENT_ID = "g1";
process.env.CLOUDFLARE_ZONE = "example.com"; // declared but EXCLUDED
// LOG_LEVEL is declared + ambient in the runner, but EXCLUDED (infra knob).
try {
const run = createMockRun();
await adapter.secrets(ctx, run);
} finally {
delete process.env.APP_SECRET;
delete process.env.GOOGLE_CLIENT_ID;
delete process.env.CLOUDFLARE_ZONE;
}
const pushed = api.secrets.get("acme-portal-production") ?? [];
expect(pushed.map((s) => s.name).sort()).toEqual([
"APP_SECRET",
"GOOGLE_CLIENT_ID",
]);
});
test("pushes per-deploy keys from .env.<env>.local even when not in the manifest", async ({
expect,
}) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
// Manifest declares only APP_SECRET…
await fs.writeFile(
"/project/dist/manifest.json",
JSON.stringify({ env: ["APP_SECRET"] }),
);
// …but an orchestrator (Rocket) injected CLUB_CONFIG_JSON into the
// per-deploy override file. It must still be pushed.
await fs.writeFile(
"/project/.env.production.local",
["APP_SECRET=s1", 'CLUB_CONFIG_JSON={"id":"b14"}', ""].join("\n"),
);
const run = createMockRun();
await adapter.secrets(ctx, run);
const pushed = api.secrets.get("acme-portal-production") ?? [];
expect(pushed.map((s) => s.name).sort()).toEqual([
"APP_SECRET",
"CLUB_CONFIG_JSON",
]);
});
test("platform.secrets.keys overrides the manifest `env` allowlist", async ({
expect,
}) => {
const { adapter, alepha, fs, naming, api } = createTestEnv();
alepha.set(platformOptions, {
secrets: { keys: ["APP_SECRET"] }, // narrow override
environments: { production: { adapter: "cloudflare" } },
} as any);
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
// Manifest lists more keys, but the explicit override wins.
await fs.writeFile(
"/project/dist/manifest.json",
JSON.stringify({ env: ["APP_SECRET", "GOOGLE_CLIENT_ID"] }),
);
process.env.APP_SECRET = "s1";
process.env.GOOGLE_CLIENT_ID = "g1";
try {
const run = createMockRun();
await adapter.secrets(ctx, run);
} finally {
delete process.env.APP_SECRET;
delete process.env.GOOGLE_CLIENT_ID;
}
const pushed = api.secrets.get("acme-portal-production") ?? [];
expect(pushed.map((s) => s.name)).toEqual(["APP_SECRET"]);
});
test("auto-derives PUBLIC_URL from the configured domain", async ({
expect,
}) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
envConfig: { adapter: "cloudflare", domain: "lore.alepha.dev" },
});
await fs.writeFile(
"/project/.env.production",
["APP_SECRET=my-secret", ""].join("\n"),
);
const run = createMockRun();
await adapter.secrets(ctx, run);
const bindings = api.bindings.get("acme-portal-production") ?? [];
const publicUrl = bindings.find((b) => b.name === "PUBLIC_URL");
expect(publicUrl?.text).toBe("https://lore.alepha.dev");
});
test("derives PUBLIC_URL for a tenant subdomain", async ({ expect }) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
envConfig: { adapter: "cloudflare", domain: "alepha.dev" },
tenant: "acme",
});
await fs.writeFile(
"/project/.env.production",
["APP_SECRET=my-secret", ""].join("\n"),
);
const run = createMockRun();
await adapter.secrets(ctx, run);
const bindings = api.bindings.get("acme-portal-production") ?? [];
const publicUrl = bindings.find((b) => b.name === "PUBLIC_URL");
expect(publicUrl?.text).toBe("https://acme.alepha.dev");
});
test("honors an explicit PUBLIC_URL over the derived one", async ({
expect,
}) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
envConfig: { adapter: "cloudflare", domain: "lore.alepha.dev" },
});
await fs.writeFile(
"/project/.env.production",
["PUBLIC_URL=https://custom.example.com", ""].join("\n"),
);
const run = createMockRun();
await adapter.secrets(ctx, run);
const bindings = api.bindings.get("acme-portal-production") ?? [];
const publicUrl = bindings.find((b) => b.name === "PUBLIC_URL");
expect(publicUrl?.text).toBe("https://custom.example.com");
});
test("skips when no env file exists", async ({ expect }) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
const run = createMockRun();
await adapter.secrets(ctx, run);
expect(api.secrets.size).toBe(0);
});
test("skips comments and empty lines", async ({ expect }) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
await fs.writeFile(
"/project/.env.production",
["# This is a comment", "", "ONLY_SECRET=value"].join("\n"),
);
const run = createMockRun();
await adapter.secrets(ctx, run);
const pushed = api.secrets.get("acme-portal-production") ?? [];
expect(pushed.map((s) => s.name)).toEqual(["ONLY_SECRET"]);
});
test("skips PATCH when ALEPHA_SECRETS_HASH binding matches", async ({
expect,
}) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
await fs.writeFile(
"/project/.env.production",
["APP_SECRET=my-secret", "GOOGLE_API_KEY=sk-123"].join("\n"),
);
const run = createMockRun();
// First push lands the hash binding on the worker.
await adapter.secrets(ctx, run);
const firstBindings = api.bindings.get("acme-portal-production") ?? [];
const hashBinding = firstBindings.find(
(b) => b.name === "ALEPHA_SECRETS_HASH",
);
expect(hashBinding?.type).toBe("plain_text");
expect(hashBinding?.text).toMatch(/^[a-f0-9]{64}$/);
// Mutate the bindings to a sentinel set we can watch for accidental
// overwrites. If the second `secrets()` call decides to PATCH again,
// the sentinel would be wiped.
api.bindings.set("acme-portal-production", [
...firstBindings,
{ type: "plain_text", name: "__SENTINEL__", text: "untouched" },
]);
await adapter.secrets(ctx, run);
const afterBindings = api.bindings.get("acme-portal-production") ?? [];
expect(afterBindings.find((b) => b.name === "__SENTINEL__")?.text).toBe(
"untouched",
);
});
test("PATCHes when secrets changed (hash mismatch)", async ({ expect }) => {
const { adapter, fs, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: false,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
await fs.writeFile("/project/.env.production", "APP_SECRET=v1");
const run = createMockRun();
await adapter.secrets(ctx, run);
const firstHash = api.bindings
.get("acme-portal-production")
?.find((b) => b.name === "ALEPHA_SECRETS_HASH")?.text;
// Same key, different value → hash should change → fresh PATCH.
await fs.writeFile("/project/.env.production", "APP_SECRET=v2");
await adapter.secrets(ctx, run);
const secondHash = api.bindings
.get("acme-portal-production")
?.find((b) => b.name === "ALEPHA_SECRETS_HASH")?.text;
expect(firstHash).toBeTruthy();
expect(secondHash).toBeTruthy();
expect(secondHash).not.toBe(firstHash);
});
});
describe("inspect", () => {
test("returns state of all expected resources via REST API", async ({
expect,
}) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: true,
hasBucket: true,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
// Pre-seed existing resources
api.d1Databases.push({
name: "acme-portal-production",
uuid: "db-uuid",
});
api.r2Buckets.push({
name: "acme-portal-production",
creation_date: "2025-01-01",
});
const run = createMockRun();
const state = await adapter.inspect(ctx, run);
expect(state.databases).toEqual([
{
name: "acme-portal-production",
exists: true,
id: "db-uuid",
},
]);
expect(state.buckets).toEqual([
{
name: "acme-portal-production",
exists: true,
id: "2025-01-01",
},
]);
});
});
describe("teardown", () => {
test("deletes resources via REST API", async ({ expect }) => {
const { adapter, naming, api } = createTestEnv();
const ctx = makeCtx(naming, {
entry: { root: "/project", server: "src/main.ts" },
resources: {
hasDatabase: true,
hasBucket: true,
hasKV: true,
hasQueue: false,
hasCron: false,
},
});
// Pre-seed existing resources
api.d1Databases.push({
name: "acme-portal-production",
uuid: "db-uuid",
});
api.r2Buckets.push({
name: "acme-portal-production",
creation_date: "2025-01-01",
});
api.kvNamespaces.push({
id: "kv-id",
title: "acme-portal-production",
});
const run = createMockRun();
await adapter.teardown(ctx, run);
expect(api.d1Databases).toHaveLength(0);
// expect(api.r2Buckets).toHaveLength(0); DISABLED FOR NOW
expect(api.kvNamespaces).toHaveLength(0);
});
});
describe("exportDb", () => {
test("dumps remote D1 and imports it into a local SQLite snapshot", async ({
expect,
}) => {
const { adapter, naming } = createTestEnv();
const ctx = makeCtx(naming, {
resources: {
hasDatabase: true,
hasBucket: false,
hasKV: false,
hasQueue: false,
hasCron: false,
},
});
// Record the raw shell strings exportDb issues via run().
const commands: string[] = [];
const run: any = async (cmd: any) => {
if (typeof cmd === "string") commands.push(cmd);
};
run.end = () => {};
await adapter.exportDb(ctx, run, {
output: "/tmp/snap.db",
keepSql: true,
});
expect(
commands.some((c) => /^wrangler d1 export .+ --remote /.test(c)),
).toBe(true);
expect(commands.some((c) => /sqlite3 '\/tmp\/snap\.db' < /.test(c))).toBe(
true,
);
});
test("refuses when no database is detected", async ({ expect }) => {
const { adapter, naming } = createTestEnv();
const ctx = makeCtx(naming); // resources.hasDatabase defaults to false
const run = createMockRun();
await expect(adapter.exportDb(ctx, run)).rejects.toThrow(/no database/i);
});
});
});
/**
* Create a mock RunnerMethod that just executes handlers directly.
*/
function createMockRun(): any {
const run: any = async (task: any) => {
if (Array.isArray(task)) {
await Promise.all(
task.map((t) =>
typeof t === "object" && t.handler ? t.handler() : Promise.resolve(),
),
);
}
if (typeof task === "object" && task.handler) {
await task.handler();
}
};
run.end = () => {};
return run;
}