Files
Psychotoxical-psysonic/src/utils/themes/themeRegistry.test.ts
T
Psychotoxical 155ef88cc0 fix(themes): serve the Theme Store from GitHub raw, not jsDelivr (#1051)
Theme assets (registry.json, theme CSS, thumbnails) were served from the
jsDelivr CDN's mutable @main edge, which caches up to 12h. The registry is
purged on every themes push so updates were offered promptly — but the theme
CSS could still be served stale, so an update stored pre-update CSS under the
new version label and no further update was offered to correct it (a freshly
animated theme would install without its @keyframes).

Fetch everything from GitHub raw (permissive CORS, ~5-min server cache), which
is always current. jsDelivr is dropped entirely: our request volume does not
need a CDN, and its only real upside here (high-traffic edge serving) does not
apply, while its staleness actively caused the bug.

Rename cdnUrl -> assetUrl to reflect the source.
2026-06-09 20:24:43 +02:00

98 lines
3.4 KiB
TypeScript

/**
* Tests for the Theme Store registry client: TTL cache, force refresh,
* stale-on-error fallback, and malformed-cache tolerance.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { fetchRegistry, getCachedRegistry } from './themeRegistry';
const CACHE_KEY = 'psysonic_theme_registry_cache';
const NOW = 1_000_000_000;
const TTL = 12 * 60 * 60 * 1000;
const reg = (generatedAt = 't') => ({ schemaVersion: 1, generatedAt, themes: [] });
const okRes = (body: unknown) => ({ ok: true, status: 200, json: async () => body, text: async () => JSON.stringify(body) });
const failRes = () => ({ ok: false, status: 500, json: async () => ({}), text: async () => '' });
function writeCache(ts: number, generatedAt: string): void {
localStorage.setItem(CACHE_KEY, JSON.stringify({ ts, registry: reg(generatedAt) }));
}
beforeEach(() => {
localStorage.clear();
vi.spyOn(Date, 'now').mockReturnValue(NOW);
});
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
localStorage.clear();
});
describe('fetchRegistry', () => {
it('fetches and caches when there is no cache', async () => {
const fetchMock = vi.fn(async () => okRes(reg('fresh')));
vi.stubGlobal('fetch', fetchMock);
const r = await fetchRegistry();
expect(r.registry.generatedAt).toBe('fresh');
expect(r.stale).toBe(false);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(localStorage.getItem(CACHE_KEY)).toContain('fresh');
});
it('returns a fresh cache without fetching', async () => {
writeCache(NOW, 'cached');
const fetchMock = vi.fn(async () => okRes(reg('network')));
vi.stubGlobal('fetch', fetchMock);
const r = await fetchRegistry();
expect(r.registry.generatedAt).toBe('cached');
expect(fetchMock).not.toHaveBeenCalled();
});
it('force-refreshes even with a fresh cache', async () => {
writeCache(NOW, 'cached');
const fetchMock = vi.fn(async () => okRes(reg('forced')));
vi.stubGlobal('fetch', fetchMock);
const r = await fetchRegistry({ force: true });
expect(r.registry.generatedAt).toBe('forced');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('falls back to a stale cache when the fetch fails', async () => {
writeCache(NOW - TTL - 1, 'stale'); // older than TTL → not fresh
vi.stubGlobal('fetch', vi.fn(async () => failRes()));
const r = await fetchRegistry();
expect(r.registry.generatedAt).toBe('stale');
expect(r.stale).toBe(true);
});
it('throws when the fetch fails and there is no cache', async () => {
vi.stubGlobal('fetch', vi.fn(async () => failRes()));
await expect(fetchRegistry()).rejects.toThrow();
});
it('treats a malformed cache as no cache and fetches', async () => {
localStorage.setItem(CACHE_KEY, '{ not valid json');
expect(getCachedRegistry()).toBeNull();
const fetchMock = vi.fn(async () => okRes(reg('fresh2')));
vi.stubGlobal('fetch', fetchMock);
const r = await fetchRegistry();
expect(r.registry.generatedAt).toBe('fresh2');
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('fetches from GitHub raw, never a CDN', async () => {
const calls: string[] = [];
vi.stubGlobal('fetch', vi.fn(async (url: string) => { calls.push(url); return okRes(reg('fresh')); }));
await fetchRegistry({ force: true });
expect(calls).toHaveLength(1);
expect(calls[0]).toContain('raw.githubusercontent.com');
expect(calls[0]).not.toContain('jsdelivr');
});
});