mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +00:00
155ef88cc0
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.
98 lines
3.4 KiB
TypeScript
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');
|
|
});
|
|
});
|