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

115 lines
4.4 KiB
TypeScript

import { describe, it, expect, afterAll } from "vitest";
import { rm } from "node:fs/promises";
import { cpSync } from "node:fs";
import { startServer, type ServerConfig } from "../src/server.js";
import { ensureJournalFixture, ensureRefinedFixture, ensureCcFixture } from "./helpers.js";
// Two server instances: one WITHOUT relay config (push/refresh must refuse
// cleanly), one WITH a relay config pointing at an unreachable port (the relay
// call must throw a determinate error, not hang). Each gets its own journal
// LevelDB copy — classic-level takes an exclusive lock, so two servers can't
// open the same LevelDB dir at once.
const OUT_A = "/tmp/test-server-push-out-a";
const OUT_B = "/tmp/test-server-push-out-b";
const JOURNAL_A = "/tmp/test-server-push-journal-a";
const PORT_A = 17810;
const PORT_B = 17811;
const BASE_A = `http://127.0.0.1:${PORT_A}`;
const BASE_B = `http://127.0.0.1:${PORT_B}`;
const cfgA: ServerConfig = {
journal: JOURNAL_A,
refinedDir: ensureRefinedFixture(),
ccDir: ensureCcFixture(),
outDir: OUT_A,
mode: "dev",
port: PORT_A,
host: "127.0.0.1",
// no relayCfg -> push/refresh must refuse with a clear error
};
const cfgB: ServerConfig = {
journal: ensureJournalFixture(),
refinedDir: ensureRefinedFixture(),
ccDir: ensureCcFixture(),
outDir: OUT_B,
mode: "dev",
port: PORT_B,
host: "127.0.0.1",
relayCfg: { url: "http://127.0.0.1:1", apiKey: "test-key", clientId: "test-client" },
foundryCfg: { container: "foundry", dataDir: "/tmp/no-such-data", world: "mardonar" },
};
let hA: Awaited<ReturnType<typeof startServer>> | null = null;
let hB: Awaited<ReturnType<typeof startServer>> | null = null;
async function bootA(): Promise<void> {
if (!hA) {
await rm(JOURNAL_A, { recursive: true, force: true });
cpSync(ensureJournalFixture(), JOURNAL_A, { recursive: true });
hA = await startServer(cfgA);
}
}
async function bootB(): Promise<void> { if (!hB) hB = await startServer(cfgB); }
afterAll(async () => {
if (hA) { hA.server.close(); hA = null; }
if (hB) { hB.server.close(); hB = null; }
await rm(OUT_A, { recursive: true, force: true });
await rm(OUT_B, { recursive: true, force: true });
await rm(JOURNAL_A, { recursive: true, force: true });
});
async function jpost(base: string, p: string, body: unknown): Promise<any> {
const r = await fetch(base + p, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
return { status: r.status, json: await r.json() };
}
describe("dashboard live-push endpoints", () => {
it("/api/push refuses when relay is not configured", async () => {
await bootA();
const { status, json } = await jpost(BASE_A, "/api/push", { name: "Fenris of House Quche", dryRun: true });
expect(status).toBe(500);
expect(json.error).toContain("relay not configured");
});
it("/api/refresh refuses when relay is not configured", async () => {
await bootA();
const { status, json } = await jpost(BASE_A, "/api/refresh", {});
expect(status).toBe(500);
expect(json.error).toContain("relay not configured");
});
it("/api/push 400s on missing name", async () => {
await bootB();
const { status, json } = await jpost(BASE_B, "/api/push", { dryRun: true });
expect(status).toBe(400);
expect(json.error).toContain("missing name");
});
it("/api/push 400s for an unknown name", async () => {
await bootB();
const { status, json } = await jpost(BASE_B, "/api/push", { name: "Nobody Matches This", dryRun: true });
expect(status).toBe(400);
expect(json.error).toContain("no refined note");
});
it("/api/push on a known but unseeded note surfaces a clear error (no cc_uuid)", async () => {
await bootB();
// The fixture's Fenris note is matched but not seeded -> pushNote reads it,
// finds no foundry.cc_uuid, and throws before ever touching the relay.
const { status, json } = await jpost(BASE_B, "/api/push", { name: "Fenris of House Quche", dryRun: true });
expect(status).toBe(500);
expect(json.error).toContain("cc_uuid");
});
it("/api/refresh with an unreachable relay returns a determinate error (no hang)", async () => {
await bootB();
const { status, json } = await jpost(BASE_B, "/api/refresh", {});
expect(status).toBe(500);
// The relay /search fetch fails (connection refused on port 1) -> surfaced, not hung.
expect(typeof json.error).toBe("string");
expect(json.error.length).toBeGreaterThan(0);
}, 15000);
});