import { test, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import type { MatrixClient } from 'matrix-js-sdk'; import { resolveClipObjectUrl, clearSoundboardClipCache } from './soundboardClips'; // [Gitea #57] resolveClipObjectUrl/clearSoundboardClipCache talk to // URL.createObjectURL/revokeObjectURL and `fetch` — neither exists in this // Node test environment, so each case stubs minimal fakes and restores them. const originalCreateObjectURL = URL.createObjectURL; const originalRevokeObjectURL = URL.revokeObjectURL; const originalFetch = globalThis.fetch; let nextUrlId = 0; let revoked: string[] = []; const fakeMx = { getHomeserverUrl: () => 'https://example.org', } as unknown as MatrixClient; beforeEach(() => { nextUrlId = 0; revoked = []; (URL as unknown as { createObjectURL: (b: Blob) => string }).createObjectURL = () => `blob:fake-${nextUrlId++}`; (URL as unknown as { revokeObjectURL: (u: string) => void }).revokeObjectURL = (url: string) => { revoked.push(url); }; globalThis.fetch = (async () => ({ ok: true, blob: async () => new Blob(['clip']), }) as Response) as typeof fetch; }); afterEach(() => { clearSoundboardClipCache(); URL.createObjectURL = originalCreateObjectURL; URL.revokeObjectURL = originalRevokeObjectURL; globalThis.fetch = originalFetch; }); test('caches the resolved object URL per mxc and does not re-download', async () => { let downloads = 0; globalThis.fetch = (async () => { downloads += 1; return { ok: true, blob: async () => new Blob(['clip']) } as Response; }) as typeof fetch; const first = await resolveClipObjectUrl(fakeMx, 'mxc://example.org/clip1'); const second = await resolveClipObjectUrl(fakeMx, 'mxc://example.org/clip1'); assert.equal(first, second); assert.equal(downloads, 1); }); test('clearSoundboardClipCache revokes every cached URL and empties the map', async () => { const urlA = await resolveClipObjectUrl(fakeMx, 'mxc://example.org/a'); const urlB = await resolveClipObjectUrl(fakeMx, 'mxc://example.org/b'); clearSoundboardClipCache(); assert.deepEqual(new Set(revoked), new Set([urlA, urlB])); // Cache is empty, so resolving the same mxc again re-downloads (new blob URL). const urlAAgain = await resolveClipObjectUrl(fakeMx, 'mxc://example.org/a'); assert.notEqual(urlAAgain, urlA); }); test('evicts the least-recently-used entry once the cache exceeds its cap', async () => { // Cap is 64; fill it, then add one more and confirm exactly one eviction. for (let i = 0; i < 64; i += 1) { // eslint-disable-next-line no-await-in-loop await resolveClipObjectUrl(fakeMx, `mxc://example.org/clip-${i}`); } assert.equal(revoked.length, 0); const firstUrl = await resolveClipObjectUrl(fakeMx, 'mxc://example.org/clip-0'); // Re-resolving clip-0 above marked it as recently used, so the least-recently // used entry is now clip-1 — adding a new clip should evict that one. await resolveClipObjectUrl(fakeMx, 'mxc://example.org/clip-new'); assert.equal(revoked.length, 1); // clip-0 must not have been evicted since it was refreshed just before. assert.equal(revoked.includes(firstUrl), false); });