Files
obsidian-foundry-sync/tests/relay.test.ts
2026-06-20 19:15:38 +00:00

120 lines
5.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { RelayClient } from "../src/relay/client.js";
// Minimal Response stand-in: the client only uses res.ok and res.text().
function resp(body: unknown, ok = true, status = 200) {
return {
ok,
status,
text: async () => (typeof body === "string" ? body : JSON.stringify(body)),
};
}
const cfg = { url: "https://relay.test", apiKey: "key123", clientId: "clientA" };
let calls: { method: string; url: string; body?: unknown }[] = [];
const realFetch = globalThis.fetch;
beforeEach(() => {
calls = [];
globalThis.fetch = vi.fn(async (url: string, init?: RequestInit) => {
calls.push({ method: init?.method ?? "GET", url, body: init?.body ? JSON.parse(String(init.body)) : undefined });
return resp({ data: { name: "fetched", _id: "x", flags: { "campaign-codex": { type: "npc" } } } });
}) as unknown as typeof fetch;
});
afterEach(() => { globalThis.fetch = realFetch; });
describe("RelayClient auth + routing", () => {
it("requires an api key", () => {
expect(() => new RelayClient({ url: cfg.url, apiKey: "", clientId: cfg.clientId })).toThrow(/RELAY_API_KEY/);
});
it("sends x-api-key header and clientId as a query param", async () => {
const c = new RelayClient(cfg);
await c.getEntry("JournalEntry.aaa");
const call = calls[0];
expect(call.method).toBe("GET");
expect(call.url).toContain("https://relay.test/get");
expect(call.url).toContain("clientId=clientA");
});
it("omits clientId when unset", async () => {
const c = new RelayClient({ url: cfg.url, apiKey: "k", clientId: "" });
await c.getEntry("JournalEntry.aaa");
expect(calls[0].url).not.toContain("clientId");
});
});
describe("RelayClient /get", () => {
it("parses the { data } envelope", async () => {
const c = new RelayClient(cfg);
const entry = await c.getEntry("JournalEntry.aaa");
expect(entry.name).toBe("fetched");
expect(entry.flags?.["campaign-codex"]?.type).toBe("npc");
expect(calls[0].url).toContain("uuid=JournalEntry.aaa");
});
it("throws when the envelope has no data", async () => {
globalThis.fetch = vi.fn(async () => resp({})) as unknown as typeof fetch;
const c = new RelayClient(cfg);
await expect(c.getEntry("JournalEntry.aaa")).rejects.toThrow(/no data/);
});
it("surfaces relay-level { error } on non-2xx", async () => {
globalThis.fetch = vi.fn(async () => resp({ error: "No connected Foundry clients found" }, false, 404)) as unknown as typeof fetch;
const c = new RelayClient(cfg);
await expect(c.getEntry("JournalEntry.aaa")).rejects.toThrow(/404.*No connected Foundry clients/);
});
});
describe("RelayClient /update", () => {
it("PUTs { data } and parses the { entity: [...] } envelope", async () => {
globalThis.fetch = vi.fn(async (_u: string, init?: RequestInit) => {
calls.push({ method: init?.method ?? "GET", url: _u, body: init?.body ? JSON.parse(String(init.body)) : undefined });
return resp({ entity: [{ name: "Updated", _id: "x" }] });
}) as unknown as typeof fetch;
const c = new RelayClient(cfg);
const updated = await c.updateEntry("JournalEntry.aaa", { name: "Updated", "flags.campaign-codex": { type: "npc" } });
expect(updated.name).toBe("Updated");
expect(calls[0].method).toBe("PUT");
expect(calls[0].url).toContain("/update");
expect(calls[0].url).toContain("uuid=JournalEntry.aaa");
expect((calls[0].body as Record<string, unknown>).data).toEqual({ name: "Updated", "flags.campaign-codex": { type: "npc" } });
});
it("throws when entity is missing", async () => {
globalThis.fetch = vi.fn(async () => resp({})) as unknown as typeof fetch;
const c = new RelayClient(cfg);
await expect(c.updateEntry("JournalEntry.aaa", { name: "x" })).rejects.toThrow(/no entity/);
});
});
describe("RelayClient /search", () => {
it("lists journal entries with the right filter params", async () => {
globalThis.fetch = vi.fn(async (u: string) => {
calls.push({ method: "GET", url: u });
return resp({ results: [{ uuid: "JournalEntry.aaa", id: "aaa", name: "Fenris", documentType: "JournalEntry" }] });
}) as unknown as typeof fetch;
const c = new RelayClient(cfg);
const results = await c.searchJournalEntries();
expect(results[0].name).toBe("Fenris");
const url = calls[0].url;
expect(url).toContain("filter=documentType%3AJournalEntry");
expect(url).toContain("excludeCompendiums=true");
expect(url).toContain("minified=true");
});
});
describe("RelayClient /create", () => {
it("POSTs entityType + data and parses { uuid, data }", async () => {
globalThis.fetch = vi.fn(async (_u: string, init?: RequestInit) => {
calls.push({ method: init?.method ?? "GET", url: _u, body: init?.body ? JSON.parse(String(init.body)) : undefined });
return resp({ uuid: "JournalEntry.new", data: { name: "My Entry", _id: "new" } });
}) as unknown as typeof fetch;
const c = new RelayClient(cfg);
const created = await c.createEntry("JournalEntry", { name: "My Entry" });
expect(created.uuid).toBe("JournalEntry.new");
expect(created.data.name).toBe("My Entry");
expect(calls[0].method).toBe("POST");
expect((calls[0].body as Record<string, unknown>).entityType).toBe("JournalEntry");
});
});