b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
// The hyperdrive runtime adapter imports the bindings from cloudflare:workers,
|
|
// builds a node-postgres Pool, and routes per-request queries through the
|
|
// instrumentation logger. Mock all three so we can assert which binding's
|
|
// connection string the per-request pool is built from, without a real DB.
|
|
const { poolCalls, fakeEnv } = vi.hoisted(() => ({
|
|
poolCalls: [] as Array<{ connectionString: string }>,
|
|
fakeEnv: {
|
|
HYPERDRIVE: { connectionString: "postgres://primary/uncached" },
|
|
HYPERDRIVE_CACHED: { connectionString: "postgres://replica/cached" },
|
|
} as Record<string, { connectionString: string }>,
|
|
}));
|
|
|
|
vi.mock("cloudflare:workers", () => ({
|
|
env: fakeEnv,
|
|
waitUntil: (_p: Promise<unknown>) => {},
|
|
}));
|
|
|
|
vi.mock("pg", () => ({
|
|
Pool: class {
|
|
constructor(opts: { connectionString: string }) {
|
|
poolCalls.push({ connectionString: opts.connectionString });
|
|
}
|
|
// Kysely's PostgresDialect only needs these to construct; no query runs.
|
|
async end() {}
|
|
},
|
|
}));
|
|
|
|
vi.mock("emdash/database/instrumentation", () => ({
|
|
kyselyLogOption: () => undefined,
|
|
}));
|
|
|
|
import { createRequestScopedDb, selectBindingName } from "../../src/db/hyperdrive.js";
|
|
|
|
const cookies = {
|
|
get: () => undefined,
|
|
set: () => {},
|
|
};
|
|
const url = new URL("https://example.com/");
|
|
|
|
const publicUrl = new URL("https://example.com/posts");
|
|
const cfg = { binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_CACHED" };
|
|
|
|
describe("selectBindingName", () => {
|
|
it("uses the cached binding for anonymous reads of public paths", () => {
|
|
const name = selectBindingName(cfg, {
|
|
isAuthenticated: false,
|
|
isWrite: false,
|
|
url: publicUrl,
|
|
});
|
|
expect(name).toBe("HYPERDRIVE_CACHED");
|
|
});
|
|
|
|
it("uses the primary binding for authenticated reads", () => {
|
|
const name = selectBindingName(cfg, {
|
|
isAuthenticated: true,
|
|
isWrite: false,
|
|
url: publicUrl,
|
|
});
|
|
expect(name).toBe("HYPERDRIVE");
|
|
});
|
|
|
|
it("uses the primary binding for anonymous writes", () => {
|
|
const name = selectBindingName(cfg, {
|
|
isAuthenticated: false,
|
|
isWrite: true,
|
|
url: publicUrl,
|
|
});
|
|
expect(name).toBe("HYPERDRIVE");
|
|
});
|
|
|
|
it("uses the primary binding for anonymous GETs under /_emdash (setup/auth/admin APIs)", () => {
|
|
for (const path of [
|
|
"/_emdash",
|
|
"/_emdash/admin",
|
|
"/_emdash/admin/setup",
|
|
"/_emdash/api/setup/status",
|
|
"/_emdash/api/auth/me",
|
|
]) {
|
|
const name = selectBindingName(cfg, {
|
|
isAuthenticated: false,
|
|
isWrite: false,
|
|
url: new URL(`https://example.com${path}`),
|
|
});
|
|
expect(name, `path ${path} must use the primary binding`).toBe("HYPERDRIVE");
|
|
}
|
|
});
|
|
|
|
it("does not treat a public path that merely contains _emdash as internal", () => {
|
|
const name = selectBindingName(cfg, {
|
|
isAuthenticated: false,
|
|
isWrite: false,
|
|
url: new URL("https://example.com/posts/about-_emdash"),
|
|
});
|
|
expect(name).toBe("HYPERDRIVE_CACHED");
|
|
});
|
|
|
|
it("always uses the primary binding when no cachedBinding is set", () => {
|
|
expect(
|
|
selectBindingName(
|
|
{ binding: "HYPERDRIVE" },
|
|
{ isAuthenticated: false, isWrite: false, url: publicUrl },
|
|
),
|
|
).toBe("HYPERDRIVE");
|
|
expect(
|
|
selectBindingName(
|
|
{ binding: "HYPERDRIVE" },
|
|
{ isAuthenticated: true, isWrite: true, url: publicUrl },
|
|
),
|
|
).toBe("HYPERDRIVE");
|
|
});
|
|
});
|
|
|
|
describe("createRequestScopedDb binding routing", () => {
|
|
it("builds the pool from the cached binding for anonymous reads", () => {
|
|
poolCalls.length = 0;
|
|
const scoped = createRequestScopedDb({
|
|
config: { binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_CACHED" },
|
|
isAuthenticated: false,
|
|
isWrite: false,
|
|
cookies,
|
|
url,
|
|
});
|
|
expect(scoped).not.toBeNull();
|
|
expect(poolCalls).toHaveLength(1);
|
|
expect(poolCalls[0]!.connectionString).toBe("postgres://replica/cached");
|
|
});
|
|
|
|
it("builds the pool from the primary binding for authenticated reads", () => {
|
|
poolCalls.length = 0;
|
|
createRequestScopedDb({
|
|
config: { binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_CACHED" },
|
|
isAuthenticated: true,
|
|
isWrite: false,
|
|
cookies,
|
|
url,
|
|
});
|
|
expect(poolCalls[0]!.connectionString).toBe("postgres://primary/uncached");
|
|
});
|
|
|
|
it("builds the pool from the primary binding for writes", () => {
|
|
poolCalls.length = 0;
|
|
createRequestScopedDb({
|
|
config: { binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_CACHED" },
|
|
isAuthenticated: false,
|
|
isWrite: true,
|
|
cookies,
|
|
url,
|
|
});
|
|
expect(poolCalls[0]!.connectionString).toBe("postgres://primary/uncached");
|
|
});
|
|
|
|
it("builds the pool from the primary binding for anonymous /_emdash reads", () => {
|
|
poolCalls.length = 0;
|
|
createRequestScopedDb({
|
|
config: { binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_CACHED" },
|
|
isAuthenticated: false,
|
|
isWrite: false,
|
|
cookies,
|
|
url: new URL("https://example.com/_emdash/api/setup/status"),
|
|
});
|
|
expect(poolCalls[0]!.connectionString).toBe("postgres://primary/uncached");
|
|
});
|
|
|
|
it("falls back to the primary binding when the cached binding is missing at runtime", () => {
|
|
poolCalls.length = 0;
|
|
const scoped = createRequestScopedDb({
|
|
config: { binding: "HYPERDRIVE", cachedBinding: "HYPERDRIVE_MISSING" },
|
|
isAuthenticated: false,
|
|
isWrite: false,
|
|
cookies,
|
|
url,
|
|
});
|
|
expect(scoped).not.toBeNull();
|
|
expect(poolCalls[0]!.connectionString).toBe("postgres://primary/uncached");
|
|
});
|
|
|
|
it("returns null when the primary binding is absent (singleton fallback)", () => {
|
|
poolCalls.length = 0;
|
|
const scoped = createRequestScopedDb({
|
|
config: { binding: "NOPE" },
|
|
isAuthenticated: false,
|
|
isWrite: false,
|
|
cookies,
|
|
url,
|
|
});
|
|
expect(scoped).toBeNull();
|
|
expect(poolCalls).toHaveLength(0);
|
|
});
|
|
});
|