T-HUB/apps/archive-bot/src/encryption-lint.test.ts

112 lines
4.5 KiB
TypeScript

import pino from "pino";
import { describe, expect, it } from "vitest";
import type { RoomEncryption } from "./events.js";
import {
lintEncryption,
logLintReport,
scanEncryption,
senderIsRoomAdmin,
toEncryptionHealth,
type MatrixLintPort,
} from "./encryption-lint.js";
import type { SyncResult, MediaDownload } from "./matrix.js";
import type { MatrixEvent } from "./events.js";
const MAPPED = "!mapped:example.org";
const logger = pino({ level: "silent" });
describe("lintEncryption", () => {
it("accepts deliberately unencrypted mapped rooms", () => {
const report = lintEncryption([MAPPED], new Map([[MAPPED, "unencrypted" as RoomEncryption]]), new Date(0));
expect(report.mappedUnencrypted).toEqual([MAPPED]);
expect(report.issues).toEqual([]);
});
it("flags encrypted/unknown mapped rooms as informational issues", () => {
const report = lintEncryption([MAPPED], new Map<string, RoomEncryption>([
[MAPPED, "encrypted"],
["!other:x", "unknown"],
]), new Date(0));
expect(report.issues.map((i) => i.kind)).toEqual(["mapped-encrypted", "unmapped-unknown"]);
});
it("WARNs on unencrypted rooms outside the mapping (policy violation)", () => {
const report = lintEncryption([MAPPED], new Map<string, RoomEncryption>([
[MAPPED, "unencrypted"],
["!stray:x", "unencrypted"],
]), new Date(0));
expect(report.issues).toEqual([{
roomId: "!stray:x",
kind: "unmapped-unencrypted",
encrypted: "unencrypted",
mapped: false,
}]);
expect(toEncryptionHealth(report)).toMatchObject({ checked: 2, violations: ["!stray:x"] });
});
it("produces an empty health snapshot before any scan", () => {
expect(toEncryptionHealth(null)).toEqual({ checked: 0, violations: [], mappedUnencrypted: [], checkedAt: null });
});
});
describe("logLintReport", () => {
it("emits WARN for violations and INFO for informational issues", () => {
const lines: { level: number; roomId?: string }[] = [];
const sink = pino(
{ level: "trace" },
{ write(line: string) {
const parsed = JSON.parse(line) as { level: number; roomId?: string };
lines.push({ level: parsed.level, roomId: parsed.roomId });
} },
);
logLintReport(lintEncryption([], new Map<string, RoomEncryption>([
["!violation:x", "unencrypted"],
["!info:x", "unknown"],
]), new Date(0)), sink);
const violation = lines.find((l) => l.roomId === "!violation:x");
const informational = lines.find((l) => l.roomId === "!info:x");
expect(violation?.level).toBe(40); // pino WARN
expect(informational?.level).toBe(30); // pino INFO
});
});
class FakeLintMatrix implements MatrixLintPort {
readonly userId = "@archive:example.org";
rooms: string[] = [];
states = new Map<string, RoomEncryption>();
async sync(): Promise<SyncResult> { throw new Error("unused"); }
async backfill(): Promise<MatrixEvent[]> { return []; }
async roomEncryption(roomId: string): Promise<RoomEncryption> { return this.states.get(roomId) ?? "unknown"; }
async joinRoom(): Promise<void> {}
async downloadMedia(): Promise<MediaDownload> { throw new Error("unused"); }
async sendNotice(): Promise<void> {}
async roomPowerLevels(): Promise<Record<string, unknown>> { return {}; }
async joinedRooms(): Promise<string[]> { return this.rooms; }
}
describe("scanEncryption", () => {
it("walks every joined room and produces the report", async () => {
const matrix = new FakeLintMatrix();
matrix.rooms = ["!a:x", "!b:x", "!c:x"];
matrix.states.set("!a:x", "encrypted");
matrix.states.set("!b:x", "unencrypted");
matrix.states.set("!c:x", "encrypted");
const report = await scanEncryption(matrix, ["!a:x"], logger, new AbortController().signal, new Date(0));
expect(report.checked).toBe(3);
expect(report.mappedUnencrypted).toEqual([]);
// "!a:x" is mapped but encrypted (informational); "!b:x" is the violation.
expect(report.issues).toEqual([
{ roomId: "!a:x", kind: "mapped-encrypted", encrypted: "encrypted", mapped: true },
{ roomId: "!b:x", kind: "unmapped-unencrypted", encrypted: "unencrypted", mapped: false },
]);
});
});
describe("senderIsRoomAdmin", () => {
it("defaults to 50 and reads users_default otherwise", () => {
expect(senderIsRoomAdmin({ users: { "@a:x": 50 } }, "@a:x")).toBe(true);
expect(senderIsRoomAdmin({ users_default: 50 }, "@b:x")).toBe(true);
expect(senderIsRoomAdmin({ users_default: 10 }, "@b:x")).toBe(false);
expect(senderIsRoomAdmin({}, "@b:x")).toBe(false);
});
});