112 lines
4.5 KiB
TypeScript
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);
|
|
});
|
|
});
|