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

235 lines
7.4 KiB
TypeScript

import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
EnvCredentialStore,
FileCredentialStore,
MemoryCredentialStore,
ReadOnlyCredentialStoreError,
type Did,
type PublisherSession,
} from "../src/credentials/index.js";
const session = (overrides: Partial<PublisherSession> = {}): PublisherSession => ({
did: "did:plc:abc123" as Did,
handle: "alice.example.com",
pds: "https://pds.example.com",
updatedAt: 1_700_000_000_000,
...overrides,
});
describe("MemoryCredentialStore", () => {
it("starts empty", async () => {
const store = new MemoryCredentialStore();
expect(await store.current()).toBeNull();
expect(await store.list()).toEqual([]);
});
it("makes the first put the current session", async () => {
const store = new MemoryCredentialStore();
const s = session();
await store.put(s);
expect(await store.current()).toEqual(s);
});
it("does not change current on subsequent puts", async () => {
const store = new MemoryCredentialStore();
const a = session({ did: "did:plc:aaa" as Did, handle: "a.test" });
const b = session({ did: "did:plc:bbb" as Did, handle: "b.test" });
await store.put(a);
await store.put(b);
expect((await store.current())?.did).toBe(a.did);
expect(await store.list()).toHaveLength(2);
});
it("setCurrent switches the active session", async () => {
const store = new MemoryCredentialStore();
const a = session({ did: "did:plc:aaa" as Did });
const b = session({ did: "did:plc:bbb" as Did });
await store.put(a);
await store.put(b);
await store.setCurrent(b.did);
expect((await store.current())?.did).toBe(b.did);
});
it("setCurrent rejects unknown DIDs", async () => {
const store = new MemoryCredentialStore();
await expect(store.setCurrent("did:plc:nope" as Did)).rejects.toThrow(/no stored session/);
});
it("remove clears current when removing the active session", async () => {
const store = new MemoryCredentialStore();
const a = session();
await store.put(a);
await store.remove(a.did);
expect(await store.current()).toBeNull();
expect(await store.list()).toEqual([]);
});
it("isolates put-result from later mutation (defensive copy)", async () => {
const store = new MemoryCredentialStore();
const s = session();
await store.put(s);
s.handle = "mutated.test"; // mutate after put
const stored = await store.current();
expect(stored?.handle).toBe("alice.example.com");
});
});
describe("FileCredentialStore", () => {
let dir: string;
let path: string;
beforeEach(async () => {
dir = await mkdtemp(join(tmpdir(), "emdash-credentials-"));
path = join(dir, "credentials.json");
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
});
it("returns null current when file does not exist", async () => {
const store = new FileCredentialStore({ path });
expect(await store.current()).toBeNull();
});
it("persists across instances", async () => {
const a = new FileCredentialStore({ path });
await a.put(session());
const b = new FileCredentialStore({ path });
const current = await b.current();
expect(current?.handle).toBe("alice.example.com");
});
it("writes the file with restrictive mode", async () => {
const store = new FileCredentialStore({ path });
await store.put(session());
const stat = await import("node:fs/promises").then((m) => m.stat(path));
// File mode includes type bits; mask to permission bits only.
expect(stat.mode & 0o777).toBe(0o600);
});
it("uses atomic rename for writes (no .tmp leftover after success)", async () => {
const store = new FileCredentialStore({ path });
await store.put(session());
const fs = await import("node:fs/promises");
await expect(fs.access(`${path}.tmp`)).rejects.toThrow();
});
it("preserves currentDid across remove of non-current session", async () => {
const store = new FileCredentialStore({ path });
const a = session({ did: "did:plc:aaa" as Did });
const b = session({ did: "did:plc:bbb" as Did });
await store.put(a);
await store.put(b);
await store.remove(b.did);
expect((await store.current())?.did).toBe(a.did);
});
it("throws helpfully on a non-JSON file", async () => {
const fs = await import("node:fs/promises");
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path, "this is not json");
const store = new FileCredentialStore({ path });
await expect(store.current()).rejects.toThrow(/not valid JSON/);
});
it("throws helpfully on JSON with the wrong shape", async () => {
const fs = await import("node:fs/promises");
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(path, JSON.stringify({ unrelated: "data" }));
const store = new FileCredentialStore({ path });
await expect(store.current()).rejects.toThrow(/unrecognised shape/);
});
it("writes a JSON envelope with version, currentDid, and sessions", async () => {
const store = new FileCredentialStore({ path });
await store.put(session());
const raw = await readFile(path, "utf8");
const parsed = JSON.parse(raw);
expect(parsed).toMatchObject({
version: 1,
currentDid: "did:plc:abc123",
sessions: {
"did:plc:abc123": {
did: "did:plc:abc123",
handle: "alice.example.com",
},
},
});
});
});
describe("EnvCredentialStore", () => {
it("returns null when env vars are unset", async () => {
const store = new EnvCredentialStore({ env: {} });
expect(await store.current()).toBeNull();
expect(await store.list()).toEqual([]);
});
it("returns a session when all required env vars are present", async () => {
const store = new EnvCredentialStore({
env: {
EMDASH_PUBLISHER_DID: "did:plc:abc",
EMDASH_PUBLISHER_HANDLE: "alice.example.com",
EMDASH_PUBLISHER_PDS: "https://pds.example.com",
},
});
const current = await store.current();
expect(current).toMatchObject({
did: "did:plc:abc",
handle: "alice.example.com",
pds: "https://pds.example.com",
});
});
it("requires all three identity env vars", async () => {
const store = new EnvCredentialStore({
env: {
EMDASH_PUBLISHER_DID: "did:plc:abc",
// missing handle and pds
},
});
expect(await store.current()).toBeNull();
});
it("rejects malformed DIDs loudly", async () => {
const store = new EnvCredentialStore({
env: {
EMDASH_PUBLISHER_DID: "not-a-did",
EMDASH_PUBLISHER_HANDLE: "alice.example.com",
EMDASH_PUBLISHER_PDS: "https://pds.example.com",
},
});
await expect(store.current()).rejects.toThrow(/not a valid DID/);
});
it("throws ReadOnlyCredentialStoreError on put/setCurrent/remove", async () => {
const store = new EnvCredentialStore({ env: {} });
await expect(store.put(session())).rejects.toBeInstanceOf(ReadOnlyCredentialStoreError);
await expect(store.setCurrent("did:plc:x" as Did)).rejects.toBeInstanceOf(
ReadOnlyCredentialStoreError,
);
await expect(store.remove("did:plc:x" as Did)).rejects.toBeInstanceOf(
ReadOnlyCredentialStoreError,
);
});
it("get(did) only returns the env session when the DID matches", async () => {
const store = new EnvCredentialStore({
env: {
EMDASH_PUBLISHER_DID: "did:plc:abc",
EMDASH_PUBLISHER_HANDLE: "alice.example.com",
EMDASH_PUBLISHER_PDS: "https://pds.example.com",
},
});
expect(await store.get("did:plc:abc" as Did)).not.toBeNull();
expect(await store.get("did:plc:other" as Did)).toBeNull();
});
});