import { describe, expect, it } from "vitest"; import { ConfigError, loadConfig, parseExportConfig, parseRoomMappings, toGroupFileMapping } from "./config.js"; const WS = "11111111-1111-4111-8111-111111111111"; const GROUP = "22222222-2222-4222-8222-222222222222"; describe("parseRoomMappings", () => { it("accepts immutable room IDs with UUID workspace/group", () => { expect(parseRoomMappings(JSON.stringify([{ roomId: "!abc:example.org", workspaceId: WS, groupId: GROUP }]))) .toEqual([{ roomId: "!abc:example.org", workspaceId: WS, groupId: GROUP }]); }); it("rejects aliases, non-UUIDs, duplicates, empty and malformed input", () => { expect(() => parseRoomMappings(JSON.stringify([{ roomId: "#archive:example.org", workspaceId: WS, groupId: GROUP }]))) .toThrow(ConfigError); expect(() => parseRoomMappings(JSON.stringify([{ roomId: "!a:x", workspaceId: "ws", groupId: GROUP }]))).toThrow(ConfigError); const dup = { roomId: "!a:x", workspaceId: WS, groupId: GROUP }; expect(() => parseRoomMappings(JSON.stringify([dup, dup]))).toThrow(/duplicate/); expect(() => parseRoomMappings("[]")).toThrow(ConfigError); expect(() => parseRoomMappings("{nope")).toThrow(ConfigError); }); it("maps to an unencrypted-archive GroupFileMapping kept forever", () => { const mapping = toGroupFileMapping({ roomId: "!a:x", workspaceId: WS, groupId: GROUP }); expect(mapping.archivePolicy).toBe("unencrypted-archive"); expect(mapping.retention).toEqual({ legalHold: false, mode: "keep" }); }); }); const env = { DATABASE_URL: "postgres://bot@db/hub", MATRIX_HOMESERVER_URL: "https://matrix.example.org", MATRIX_USER: "archive-bot", MATRIX_PASSWORD: "pw", ROOM_MAPPINGS: JSON.stringify([{ roomId: "!a:x", workspaceId: WS, groupId: GROUP }]), FILES_OCID_WEBDAV_URL: "https://files.example.org", FILES_OCID_USER: "svc", FILES_OCID_PASSWORD: "pw2", ARCHIVE_BOT_HEALTH_PORT: "9100", MATRIX_MEDIA_REDIRECT_HOSTS: " R2.example.com ,", }; void env; describe("loadConfig", () => { it("parses the full env contract", () => { const config = loadConfig(env); expect(config.healthPort).toBe(9100); expect(config.mediaRedirectHosts).toEqual(["r2.example.com"]); expect(config.matrixDeviceId).toBe("HUB_ARCHIVE_BOT"); }); it("fails on missing secrets or a bad port", () => { expect(() => loadConfig({ ...env, MATRIX_PASSWORD: "" })).toThrow(/MATRIX_PASSWORD/); expect(() => loadConfig({ ...env, ARCHIVE_BOT_HEALTH_PORT: "70000" })).toThrow(/PORT/); expect(() => loadConfig({ ...env, FILES_OCID_WEBDAV_URL: "ftp://x" })).toThrow(/http/); }); }); describe("parseExportConfig (phase-B encrypted exports)", () => { const R = "age1" + "q".repeat(58); const base = { ARCHIVE_EXPORT_AGE_RECIPIENTS: R, ARCHIVE_EXPORT_R2_ACCESS_KEY: "k", ARCHIVE_EXPORT_R2_SECRET_KEY: "s", }; it("is null when nothing is configured", () => { expect(parseExportConfig({})).toBeNull(); }); it("is null when explicitly disabled, even with credentials present", () => { expect(parseExportConfig({ ...base, ARCHIVE_EXPORT_ENABLED: "false" })).toBeNull(); }); it("parses recipients, R2 credentials and defaults", () => { const cfg = parseExportConfig(base); expect(cfg).toMatchObject({ enabled: true, ageRecipients: [R], every: 500, r2: { region: "auto", bucket: "hub-matrix-media" }, }); }); it("de-duplicates recipients and honours ARCHIVE_EXPORT_EVERY", () => { const cfg = parseExportConfig({ ...base, ARCHIVE_EXPORT_AGE_RECIPIENTS: `${R}, ${R}`, ARCHIVE_EXPORT_EVERY: "50" }); expect(cfg?.ageRecipients).toEqual([R]); expect(cfg?.every).toBe(50); }); it("rejects malformed recipients and missing credentials", () => { expect(() => parseExportConfig({ ...base, ARCHIVE_EXPORT_AGE_RECIPIENTS: "AGE-SECRET-KEY-1X" })).toThrow(ConfigError); expect(() => parseExportConfig({ ARCHIVE_EXPORT_AGE_RECIPIENTS: R })).toThrow(/ACCESS_KEY/); expect(() => parseExportConfig({ ...base, ARCHIVE_EXPORT_EVERY: "-1" })).toThrow(ConfigError); expect(() => parseExportConfig({ ...base, ARCHIVE_EXPORT_R2_ENDPOINT: "http://insecure" })).toThrow(ConfigError); }); it("wires into loadConfig", () => { const cfg = loadConfig({ ...env, ...base }); expect(cfg.export).not.toBeNull(); expect(cfg.export?.ageRecipients).toEqual([R]); // and stays null in the default case expect(loadConfig(env).export).toBeNull(); }); });