import { describe, expect, it } from "vitest"; import { BadRequestException, ServiceUnavailableException } from "@nestjs/common"; import { Readable } from "node:stream"; import { decodeFileListCursor, encodeFileListCursor, } from "./file-cursor.js"; import { parseGroupFile, parseMatrixAttachmentCandidate, parseRetention, parseRetentionInput, parseSharedFileOrigin, requireMxid, requireMxcUri, requireRoomId, requireSha256, } from "./group-files.validation.js"; import { OcisVerificationError, verifyWhileStreaming } from "./verify-stream.js"; import { hashShareToken, isPlausibleShareToken, mintShareToken, sameToken } from "./share-token.js"; /** Deterministic 64-hex helper: repeat a pattern to exactly 64 chars. */ function hex64(seed: string): string { let out = ""; while (out.length < 64) out += Buffer.from(seed).toString("hex"); return out.slice(0, 64); } describe("group-files runtime validation (shared contracts)", () => { it("accepts the four SharedFileOrigin variants and rejects anything else", () => { expect(parseSharedFileOrigin({ kind: "matrix", mxcUri: "mxc://example.org/abc" })).toEqual({ kind: "matrix", mxcUri: "mxc://example.org/abc", }); expect( parseSharedFileOrigin({ kind: "bridge", mxcUri: "mxc://example.org/abc", providerId: "telegram" }), ).toEqual({ kind: "bridge", mxcUri: "mxc://example.org/abc", providerId: "telegram" }); expect(parseSharedFileOrigin({ kind: "bridge", mxcUri: "mxc://example.org/abc", providerId: "t", remoteEventId: "42" })).toEqual({ kind: "bridge", mxcUri: "mxc://example.org/abc", providerId: "t", remoteEventId: "42", }); expect(parseSharedFileOrigin({ kind: "upload" })).toEqual({ kind: "upload" }); expect(parseSharedFileOrigin({ kind: "pad-export", padId: "p", revision: "7" })).toEqual({ kind: "pad-export", padId: "p", revision: "7", }); for (const bad of [null, {}, { kind: "nope" }, { kind: "matrix" }, { kind: "matrix", mxcUri: "https://evil/x" }]) { expect(() => parseSharedFileOrigin(bad)).toThrow(BadRequestException); } }); it("validates MXIDs, room ids, mxc URIs and sha256 shape", () => { expect(requireMxid("@alice:example.org")).toBe("@alice:example.org"); expect(() => requireMxid("Alice (display)")).toThrow(BadRequestException); expect(() => requireMxid("#alias:example.org")).toThrow(BadRequestException); expect(requireRoomId("!archive:example.org")).toBe("!archive:example.org"); expect(() => requireRoomId("#alias:example.org")).toThrow(BadRequestException); // alias, never an identity expect(() => requireRoomId("")).toThrow(BadRequestException); expect(requireMxcUri("mxc://example.org/AbC123")).toBe("mxc://example.org/AbC123"); expect(() => requireMxcUri("mxc://example.org/")).toThrow(BadRequestException); expect(requireSha256(hex64("a"))).toBe(hex64("a")); expect(() => requireSha256("nope")).toThrow(BadRequestException); // hex digits include letters: an uppercase digest must be rejected const upperHex = "A".repeat(64); expect(/[A-F]/.test(upperHex)).toBe(true); expect(() => requireSha256(upperHex)).toThrow(BadRequestException); expect(() => requireSha256("0".repeat(63))).toThrow(BadRequestException); // wrong length }); it("keeps retention separate from link expiry and enforces shape coupling", () => { expect(parseRetention({ mode: "keep", legalHold: false })).toEqual({ mode: "keep", legalHold: false }); // legalHold is a FLAG alongside the mode, not a third mode (FileRetention contract) expect(parseRetention({ mode: "keep", legalHold: true })).toEqual({ mode: "keep", legalHold: true }); expect(parseRetention({ mode: "until", legalHold: true, expiresAt: "2030-01-01T00:00:00Z" })).toEqual({ mode: "until", legalHold: true, expiresAt: "2030-01-01T00:00:00Z", }); expect(() => parseRetention({ mode: "burn", legalHold: false })).toThrow(BadRequestException); expect(() => parseRetention({ mode: "until", legalHold: false })).toThrow(BadRequestException); expect(() => parseRetention({ mode: "keep", legalHold: false, expiresAt: "2030-01-01T00:00:00Z" })).toThrow(BadRequestException); expect(() => parseRetention({ mode: "until", legalHold: false, expiresAt: "next tuesday" })).toThrow(BadRequestException); // input variant expect(parseRetentionInput({ mode: "until", until: "2030-01-01T00:00:00Z" })).toEqual({ mode: "until", legalHold: false, expiresAt: "2030-01-01T00:00:00Z", }); expect(() => parseRetentionInput({ mode: "until" })).toThrow(BadRequestException); expect(() => parseRetentionInput({ mode: "keep", until: "2030-01-01T00:00:00Z" })).toThrow(BadRequestException); expect(parseRetentionInput({ mode: "keep", legalHold: true })).toEqual({ mode: "keep", legalHold: true }); expect(() => parseRetentionInput({ mode: "legalHold" })).toThrow(BadRequestException); // not a mode }); it("fails closed on Matrix attachment candidates with unknown/bad state", () => { const good = { roomId: "!a:example.org", eventId: "$1", senderId: "@s:example.org", eventType: "m.room.message", msgtype: "m.image", roomEncryption: "unencrypted", encryptedFile: false, redacted: false, mxcUri: "mxc://example.org/x", }; expect(parseMatrixAttachmentCandidate(good).roomEncryption).toBe("unencrypted"); // shape-level failures for (const patch of [ { roomId: "#alias:example.org" }, { senderId: "Bob" }, { mxcUri: "https://attacker/x" }, { encryptedFile: "yes" }, ]) { expect(() => parseMatrixAttachmentCandidate({ ...good, ...patch })).toThrow(BadRequestException); } // state-level eligibility is the CORE helper's call, not the parser's: // parse accepts encrypted/unknown/redacted states; canArchiveAttachment // is what fails closed on them (core test shared-files.test.ts). const parsed = parseMatrixAttachmentCandidate({ ...good, roomEncryption: "unknown" }); expect(parsed.roomEncryption).toBe("unknown"); }); it("round-trips a full GroupFile wire shape", () => { const value = { id: "b3a1c2d4-1111-4222-8333-444455556666", workspaceId: "c3a1c2d4-1111-4222-8333-444455556666", groupId: "d3a1c2d4-1111-4222-8333-444455556666", ownerId: null, folderId: null, createdBy: null, shared: false, roomId: "!r:example.org", senderId: "@s:example.org", eventId: "$evt", origin: { kind: "matrix", mxcUri: "mxc://example.org/x" }, name: "clip.mp4", mimeType: "video/mp4", mimeClass: "video", sizeBytes: 12, checksum: { algorithm: "sha256", value: hex64("z") }, createdAt: "2026-01-02T03:04:05Z", indexedAt: "2026-01-02T03:04:05Z", file: { spaceId: "sp", resourceId: "res", version: "v1" }, shareLink: { url: "https://files.example.org/f/res", access: "members", expiresAt: null }, retention: { mode: "keep", legalHold: false }, status: "available", }; expect(parseGroupFile(value)).toEqual(value); const personal = { ...value, groupId: null, ownerId: value.id, folderId: value.id, createdBy: value.id, file: { ...value.file, spaceId: null, storageKey: "sha256/key.mp4" } }; expect(parseGroupFile(personal)).toEqual(personal); const shared = { ...value, shared: true, origin: { kind: "upload", sharedFrom: "personal" } }; expect(parseGroupFile(shared)).toEqual(shared); expect(() => parseGroupFile({ ...value, sizeBytes: -1 })).toThrow(BadRequestException); expect(() => parseGroupFile({ ...value, senderId: "Bob" })).toThrow(BadRequestException); expect(() => parseGroupFile({ ...value, checksum: { algorithm: "sha256", value: "zz" } })).toThrow(BadRequestException); }); }); describe("file-cursor", () => { it("encodes/decodes (createdAt,id) and rejects malformed scopes", () => { const cursor = { createdAt: "2026-01-02T03:04:05Z", id: "b3a1c2d4-1111-4222-8333-444455556666" }; expect(decodeFileListCursor(encodeFileListCursor(cursor))).toEqual(cursor); for (const bad of ["", "!!!", Buffer.from(JSON.stringify({ createdAt: "nope", id: "x" })).toString("base64url")]) { expect(() => decodeFileListCursor(bad)).toThrow(BadRequestException); } // a cursor from another tenant carries no scope: it is just an empty page, // never a leak — the scope is re-bound server-side on every request. expect(decodeFileListCursor(encodeFileListCursor({ createdAt: "2026-01-02T03:04:05Z", id: "00000000-0000-4000-8000-000000000000" })).id).toBe( "00000000-0000-4000-8000-000000000000", ); }); }); describe("share tokens", () => { it("mints 256-bit capabilities and stores only sha256(token)", () => { const token = mintShareToken(); expect(token.length).toBeGreaterThanOrEqual(40); expect(isPlausibleShareToken(token)).toBe(true); expect(isPlausibleShareToken("short")).toBe(false); expect(isPlausibleShareToken("not base64url!!")).toBe(false); const hash = hashShareToken(token); expect(hash).toMatch(/^[0-9a-f]{64}$/); expect(hashShareToken(token)).toBe(hash); // deterministic expect(hashShareToken(mintShareToken())).not.toBe(hash); // unique per mint expect(sameToken(token, token)).toBe(true); expect(sameToken(token, mintShareToken())).toBe(false); // the plaintext can never be derived from the hash expect(hash).not.toContain(token); }); }); describe("verify-stream (storage-agnostic streaming verification)", () => { it("verifyWhileStreaming passes on match and aborts on tamper", async () => { const { createHash } = await import("node:crypto"); const payload = Buffer.from("0123456789"); const good = createHash("sha256").update(payload).digest("hex"); const sinkChunks: Buffer[] = []; const out = await verifyWhileStreaming( { stream: Readable.from([payload.subarray(0, 4), payload.subarray(4)]), sizeBytes: payload.byteLength, contentType: "application/octet-stream" }, good, (chunk) => sinkChunks.push(chunk), ); expect(out.sizeBytes).toBe(10); expect(Buffer.concat(sinkChunks).toString()).toBe("0123456789"); await expect( verifyWhileStreaming( { stream: Readable.from([Buffer.from("tampered")]), sizeBytes: 8, contentType: null }, good, () => undefined, ), ).rejects.toBeInstanceOf(OcisVerificationError); }); });