1import test from "node:test";2import assert from "node:assert/strict";3import fs from "node:fs";4import vm from "node:vm";56const source = fs.readFileSync("tinyclient/file-catalog.js", "utf8");7const pubkey = "a".repeat(64);8const hash = "b".repeat(64);910function setup({path = "/r/test/files", actor = pubkey} = {}) {11 const values = new Map();12 const rows = [];13 const document = {14 querySelector(selector) {15 if (selector === "[data-pubkey]") return {getAttribute: () => actor};16 if (selector === "#file-library") return {querySelectorAll: () => rows};17 return null;18 },19 createElement() { return {textContent: "", append() {}}; }20 };21 const location = {href: "https://relay.test" + path, origin: "https://relay.test", pathname: path};22 const history = {replaceState(_state, _title, value) { location.href = "https://relay.test" + value; location.pathname = new URL(location.href).pathname; location.hash = new URL(location.href).hash; }};23 const context = {24 location, history, document,25 localStorage: {getItem: key => values.get(key) || null, setItem: (key, value) => values.set(key, value)},26 URL, URLSearchParams, Date, Number, String, Array, Boolean, Object, RegExp, JSON,27 globalThis: null, tiny: {localPath: value => "/r/test" + value}28 };29 context.globalThis = context;30 vm.runInNewContext(source, context);31 return {context, values, rows};32}3334const link = () => `https://relay.test/r/test/file/${hash}#key=${"c".repeat(64)}&iv=${"d".repeat(16)}&name=video.mp4&type=video%2Fmp4`;3536test("saves encrypted metadata per tenant and account and restores it after reload", () => {37 const first = setup({path: "/r/test/file/" + hash});38 assert.equal(first.context.tiny.files.catalog.save({hash, link: link(), name: "video.mp4", type: "video/mp4", size: 104857600}, pubkey), true);39 assert.equal(first.context.tiny.files.catalog.get(hash, pubkey).name, "video.mp4");40 assert.equal(first.context.tiny.files.catalog.get(hash, "e".repeat(64)), null);41 assert.equal(first.context.tiny.files.catalog.restore(pubkey).name, "video.mp4");42 assert.match(first.context.location.href, /#key=/);43});4445test("rejects unsafe links, names and cross-tenant links", () => {46 const {context} = setup();47 assert.equal(context.tiny.files.catalog.save({hash, link: "https://evil.test/r/test/file/" + hash + "#key=x", name: "x"}, pubkey), false);48 assert.equal(context.tiny.files.catalog.save({hash, link: link(), name: "../secret"}, pubkey), false);49 assert.equal(context.tiny.files.catalog.save({hash, link: link().replace("/r/test/", "/r/other/"), name: "x"}, pubkey), false);50 assert.equal(context.tiny.files.catalog.save({hash, link: link().replace("/file/" + hash, "/file?hash=" + hash), name: "x"}, pubkey), false);51 assert.equal(context.tiny.files.catalog.save({hash, link: link().replace("/file/" + hash, "/file/" + "f".repeat(64)), name: "x"}, pubkey), false);52});