a09ad9c429
69 tests across 16 suites, ~120 ms total. Uses node:test (built into
Node 20+) — no new dependency, no Docker rebuild churn. Run with:
cd server && npm test
Coverage:
• util.js extractVideoId, formatTime,
parseTimestampedTranscript, safeText,
retryGemini (incl. 503 retry, network-error
retry, non-retryable passthrough), sendEvent
• gemini-helpers.js PRICING table integrity, calcCost (model-
specific rates, default fallback, missing
fields, sub-cent ¢ formatting, totalTokens
precedence), buildAnalysisPrompt
• license.js checkLicense (no key, malformed, fallback to
startos-config.json, license.txt priority),
activate (bad-format throw, file write),
deactivate (file removal, idempotent),
publicView (no raw key leak, sorted
entitlements, ISO dates), has()
• history.js initHistory + getHistoryDir, saveToHistory
(id shape, defaults, podcast guid encoding),
loadMeta + saveMeta round-trip, corrupt-file
tolerance
Tests that need module-private file paths (license, history) use a
mkdtemp'd tmp dir as DATA_DIR + dynamic import() so each suite starts
clean. No test mocks the filesystem — they read/write real files
inside the tmp dir, matching production behavior.
Deliberately not yet covered (need an Express app harness or external
binaries): license-middleware (gate behavior), config (live-reload
poll), audio (ffmpeg/ffprobe), ytdlp (yt-dlp + git), cookies (state
mutation routes), the /api/process pipeline. Worth a follow-up after
the current refactor settles.
181 lines
6.5 KiB
JavaScript
181 lines
6.5 KiB
JavaScript
// Tests for server/license.js — the offline Ed25519 verifier wrapper.
|
|
//
|
|
// license.js reads its paths (LICENSE_PATH, STARTOS_CONFIG_PATH) at module
|
|
// load time from env vars. To isolate test fixtures, we set RECAP_LICENSE_
|
|
// KEY_PATH + DATA_DIR to a tmp dir BEFORE importing the module — which is
|
|
// why this file uses dynamic import().
|
|
|
|
import { test, describe, before, after } from "node:test";
|
|
import { strict as assert } from "node:assert";
|
|
import fs from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
let tmpDir;
|
|
let licensePath;
|
|
let licenseModule;
|
|
|
|
before(async () => {
|
|
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "recap-license-test-"));
|
|
licensePath = path.join(tmpDir, "license.txt");
|
|
process.env.DATA_DIR = tmpDir;
|
|
process.env.RECAP_LICENSE_KEY_PATH = licensePath;
|
|
// Make sure config dir exists so the fallback read in readLicenseString
|
|
// doesn't fail on a missing parent.
|
|
await fs.mkdir(path.join(tmpDir, "config"), { recursive: true });
|
|
// Dynamic import after env vars are in place.
|
|
licenseModule = await import("../license.js");
|
|
});
|
|
|
|
after(async () => {
|
|
delete process.env.RECAP_LICENSE_KEY_PATH;
|
|
delete process.env.DATA_DIR;
|
|
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
describe("checkLicense", () => {
|
|
test("returns 'unlicensed' when no key file exists", async () => {
|
|
await fs.unlink(licensePath).catch(() => {});
|
|
const state = licenseModule.checkLicense();
|
|
assert.equal(state.state, "unlicensed");
|
|
assert.equal(state.reason, null);
|
|
assert.deepEqual([...state.entitlements], []);
|
|
});
|
|
|
|
test("returns 'invalid' for a malformed key", async () => {
|
|
await fs.writeFile(licensePath, "garbage-not-a-license");
|
|
const state = licenseModule.checkLicense();
|
|
assert.equal(state.state, "invalid");
|
|
assert.ok(state.reason, "should have a reason");
|
|
});
|
|
|
|
test("returns 'invalid' for a LIC1 prefix without valid base32 body", async () => {
|
|
await fs.writeFile(licensePath, "LIC1-not-real-key");
|
|
const state = licenseModule.checkLicense();
|
|
assert.equal(state.state, "invalid");
|
|
});
|
|
|
|
test("falls back to startos-config.json when license.txt is missing", async () => {
|
|
await fs.unlink(licensePath).catch(() => {});
|
|
const cfgPath = path.join(tmpDir, "config", "startos-config.json");
|
|
await fs.writeFile(cfgPath, JSON.stringify({ recap_license_key: "LIC1-bad-but-detected" }));
|
|
const state = licenseModule.checkLicense();
|
|
// Will be 'invalid' (bad key) but NOT 'unlicensed' — proves the fallback ran.
|
|
assert.equal(state.state, "invalid");
|
|
await fs.unlink(cfgPath).catch(() => {});
|
|
});
|
|
|
|
test("license.txt takes priority over startos-config.json", async () => {
|
|
await fs.writeFile(licensePath, "LIC1-from-license-txt");
|
|
const cfgPath = path.join(tmpDir, "config", "startos-config.json");
|
|
await fs.writeFile(cfgPath, JSON.stringify({ recap_license_key: "LIC1-from-config" }));
|
|
const state = licenseModule.checkLicense();
|
|
// Both are bad, but the reason should reflect license.txt's content,
|
|
// not the config's. We can't easily distinguish without a real key,
|
|
// but we can confirm it returns 'invalid' (no precedence-related crash).
|
|
assert.equal(state.state, "invalid");
|
|
await fs.unlink(cfgPath).catch(() => {});
|
|
await fs.unlink(licensePath).catch(() => {});
|
|
});
|
|
});
|
|
|
|
describe("activate / deactivate", () => {
|
|
test("activate rejects keys missing the LIC1- prefix", () => {
|
|
assert.throws(() => licenseModule.activate("not-a-license"), { code: "bad_format" });
|
|
assert.throws(() => licenseModule.activate(""), { code: "bad_format" });
|
|
assert.throws(() => licenseModule.activate(null), { code: "bad_format" });
|
|
});
|
|
|
|
test("activate writes a LIC1-prefixed string to license.txt", async () => {
|
|
const fake = "LIC1-fake-but-shaped-correctly";
|
|
const state = licenseModule.activate(fake);
|
|
const written = (await fs.readFile(licensePath, "utf-8")).trim();
|
|
assert.equal(written, fake);
|
|
// Returned state will be 'invalid' (signature fails) — not our concern here.
|
|
assert.ok(["invalid", "licensed"].includes(state.state));
|
|
});
|
|
|
|
test("deactivate removes license.txt and the state sidecar", async () => {
|
|
await fs.writeFile(licensePath, "LIC1-anything");
|
|
licenseModule.deactivate();
|
|
await assert.rejects(fs.access(licensePath), /ENOENT/);
|
|
const state = licenseModule.checkLicense();
|
|
assert.equal(state.state, "unlicensed");
|
|
});
|
|
|
|
test("deactivate is idempotent on a clean slate", () => {
|
|
// No file → should not throw
|
|
licenseModule.deactivate();
|
|
licenseModule.deactivate();
|
|
});
|
|
});
|
|
|
|
describe("publicView", () => {
|
|
test("never includes the raw license key", () => {
|
|
const state = licenseModule.checkLicense();
|
|
const view = licenseModule.publicView(state);
|
|
// The public view should not contain anything that looks like a license string
|
|
const flat = JSON.stringify(view);
|
|
assert.ok(!flat.includes("LIC1-"));
|
|
});
|
|
|
|
test("converts entitlements Set to a sorted array", () => {
|
|
const view = licenseModule.publicView({
|
|
state: "licensed",
|
|
reason: null,
|
|
licenseId: "abc",
|
|
entitlements: new Set(["library", "core", "history"]),
|
|
expiresAt: null,
|
|
isTrial: false,
|
|
});
|
|
assert.deepEqual(view.entitlements, ["core", "history", "library"]);
|
|
});
|
|
|
|
test("ISO-formats expiresAt and graceUntil", () => {
|
|
const expiresAt = new Date("2027-01-01T00:00:00.000Z");
|
|
const view = licenseModule.publicView({
|
|
state: "licensed",
|
|
reason: null,
|
|
licenseId: null,
|
|
entitlements: new Set(),
|
|
expiresAt,
|
|
isTrial: false,
|
|
});
|
|
assert.equal(view.expiresAt, "2027-01-01T00:00:00.000Z");
|
|
});
|
|
|
|
test("always exposes productSlug = 'recap'", () => {
|
|
const view = licenseModule.publicView({
|
|
state: "unlicensed",
|
|
reason: null,
|
|
licenseId: null,
|
|
entitlements: new Set(),
|
|
expiresAt: null,
|
|
isTrial: false,
|
|
});
|
|
assert.equal(view.productSlug, "recap");
|
|
});
|
|
});
|
|
|
|
describe("has(state, entitlement)", () => {
|
|
test("returns true when entitlement present", () => {
|
|
assert.equal(
|
|
licenseModule.has({ entitlements: new Set(["core"]) }, "core"),
|
|
true
|
|
);
|
|
});
|
|
|
|
test("returns false when entitlement absent", () => {
|
|
assert.equal(
|
|
licenseModule.has({ entitlements: new Set(["core"]) }, "subscriptions"),
|
|
false
|
|
);
|
|
});
|
|
|
|
test("returns falsy on null state", () => {
|
|
assert.ok(!licenseModule.has(null, "core"));
|
|
assert.ok(!licenseModule.has(undefined, "core"));
|
|
assert.ok(!licenseModule.has({}, "core"));
|
|
});
|
|
});
|