Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

93 lines
3.4 KiB
TypeScript

/**
* Backup route tests
*
* - Route registration for all four backup endpoints
* - Authorization: every endpoint requires the admin-only backups:manage
* permission
* - The public media file route must never serve keys under backups/
* (archives contain the site's full content export)
*/
import { describe, expect, it, vi } from "vitest";
import { injectCoreRoutes } from "../../../src/astro/integration/routes.js";
import { GET as mediaFileGet } from "../../../src/astro/routes/api/media/file/[...key].js";
import {
DELETE as archiveDelete,
GET as archiveGet,
} from "../../../src/astro/routes/api/settings/backups/archives/[name].js";
import { POST as archivesPost } from "../../../src/astro/routes/api/settings/backups/archives/index.js";
import { GET as exportGet } from "../../../src/astro/routes/api/settings/backups/export.js";
import {
GET as backupsGet,
PUT as backupsPut,
} from "../../../src/astro/routes/api/settings/backups/index.js";
// Minimal APIContext stand-in; routes only touch locals/params/request.
// eslint-disable-next-line typescript/no-explicit-any -- test double for APIContext
function ctx(overrides: Record<string, unknown>): any {
return {
locals: { emdash: { db: {}, storage: {} }, user: null },
params: {},
request: new Request("https://example.com"),
...overrides,
};
}
describe("backup route registration", () => {
it("registers all backup routes", () => {
const injectRoute = vi.fn();
injectCoreRoutes(injectRoute);
const patterns = injectRoute.mock.calls.map((call) => (call[0] as { pattern: string }).pattern);
expect(patterns).toContain("/_emdash/api/settings/backups");
expect(patterns).toContain("/_emdash/api/settings/backups/export");
expect(patterns).toContain("/_emdash/api/settings/backups/archives");
expect(patterns).toContain("/_emdash/api/settings/backups/archives/[name]");
});
});
describe("backup route authorization", () => {
const cases: [string, (c: unknown) => Promise<Response>][] = [
["GET /settings/backups", (c) => backupsGet(c as never)],
["PUT /settings/backups", (c) => backupsPut(c as never)],
["GET /settings/backups/export", (c) => exportGet(c as never)],
["POST /settings/backups/archives", (c) => archivesPost(c as never)],
["GET /settings/backups/archives/[name]", (c) => archiveGet(c as never)],
["DELETE /settings/backups/archives/[name]", (c) => archiveDelete(c as never)],
];
for (const [label, invoke] of cases) {
it(`${label} rejects anonymous requests`, async () => {
const res = await invoke(
ctx({ params: { name: "emdash-backup-2026-07-09T08-45-12-abcd1234.json" } }),
);
expect(res.status).toBe(401);
});
it(`${label} rejects editors (below admin)`, async () => {
const res = await invoke(
ctx({
locals: { emdash: { db: {}, storage: {} }, user: { id: "u1", role: 40 } },
params: { name: "emdash-backup-2026-07-09T08-45-12-abcd1234.json" },
}),
);
expect(res.status).toBe(403);
});
}
});
describe("media file route denies backup archives", () => {
it("returns 404 for keys under backups/ without touching storage", async () => {
const download = vi.fn();
const res = await mediaFileGet(
ctx({
params: { key: "backups/emdash-backup-2026-07-09T08-45-12-abcd1234.json" },
locals: { emdash: { storage: { download } } },
}) as never,
);
expect(res.status).toBe(404);
expect(download).not.toHaveBeenCalled();
});
});