soulsync/webui/src/routes/stats/-stats.api.test.ts
Antti Kettunen ca84aa2e65
fix(webui): harden stats route fallback flows
- make listening status prefetch optional so /stats still renders on status failures
- normalize no-stats-yet cache responses into the existing empty stats state
- restore streaming fallback when library track resolution errors
- add route and API regression coverage for the review fixes
2026-05-23 21:23:32 +03:00

143 lines
4.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { HttpResponse, http, server } from '@/test/msw';
import {
fetchListeningStatsStatus,
fetchStatsCached,
fetchStatsDbStorage,
fetchStatsLibraryDiskUsage,
resolveStatsTrack,
streamStatsTrack,
triggerListeningStatsSync,
} from './-stats.api';
describe('stats api', () => {
it('fetches the cached stats payload for a range', async () => {
server.use(
http.get('/api/stats/cached', ({ request }) => {
const url = new URL(request.url);
expect(url.searchParams.get('range')).toBe('30d');
return HttpResponse.json({
success: true,
overview: { total_plays: 12 },
top_artists: [],
top_albums: [],
top_tracks: [],
timeline: [],
genres: [],
recent: [],
health: {},
});
}),
);
await expect(fetchStatsCached('30d')).resolves.toMatchObject({
overview: { total_plays: 12 },
});
});
it('returns an empty payload when cached stats are not available yet', async () => {
server.use(
http.get('/api/stats/cached', () =>
HttpResponse.json({ success: false, error: 'Listening stats not synced yet' }),
),
);
await expect(fetchStatsCached('7d')).resolves.toMatchObject({
success: true,
overview: { total_plays: 0 },
top_artists: [],
top_albums: [],
top_tracks: [],
timeline: [],
genres: [],
recent: [],
health: {},
});
});
it('returns an empty payload when the server reports a cache miss as an HTTP error', async () => {
server.use(
http.get('/api/stats/cached', () =>
HttpResponse.json({ error: 'No cached stats available yet' }, { status: 500 }),
),
);
await expect(fetchStatsCached('7d')).resolves.toMatchObject({
success: true,
overview: { total_plays: 0 },
top_artists: [],
top_albums: [],
top_tracks: [],
timeline: [],
genres: [],
recent: [],
health: {},
});
});
it('surfaces db storage and disk usage errors', async () => {
server.use(
http.get('/api/stats/db-storage', () =>
HttpResponse.json({ error: 'db unavailable' }, { status: 500 }),
),
http.get('/api/stats/library-disk-usage', () =>
HttpResponse.json({ error: 'disk unavailable' }, { status: 500 }),
),
);
await expect(fetchStatsDbStorage()).rejects.toThrow('db unavailable');
await expect(fetchStatsLibraryDiskUsage()).rejects.toThrow('disk unavailable');
});
it('reads listening status and triggers manual sync', async () => {
server.use(
http.get('/api/listening-stats/status', () =>
HttpResponse.json({ stats: { last_poll: '2026-05-14 10:00:00' } }),
),
http.post('/api/listening-stats/sync', () => HttpResponse.json({ success: true })),
);
await expect(fetchListeningStatsStatus()).resolves.toEqual({
stats: { last_poll: '2026-05-14 10:00:00' },
});
await expect(triggerListeningStatsSync()).resolves.toBeUndefined();
});
it('resolves and streams tracks through the stats playback helpers', async () => {
server.use(
http.post('/api/stats/resolve-track', async ({ request }) => {
await expect(request.json()).resolves.toEqual({
title: 'Track',
artist: 'Artist',
});
return HttpResponse.json({
success: true,
track: { id: 1, title: 'Track', file_path: '/music/track.flac' },
});
}),
http.post('/api/enhanced-search/stream-track', async ({ request }) => {
await expect(request.json()).resolves.toEqual({
track_name: 'Track',
artist_name: 'Artist',
album_name: 'Album',
duration_ms: 0,
});
return HttpResponse.json({
success: true,
result: { stream_url: '/api/stream/1' },
});
}),
);
await expect(resolveStatsTrack('Track', 'Artist')).resolves.toMatchObject({
id: 1,
title: 'Track',
});
await expect(streamStatsTrack('Track', 'Artist', 'Album')).resolves.toEqual({
stream_url: '/api/stream/1',
});
});
});