Half of #369 — the auto-poll piece. Inbox deferred (separate task). Both /requests (user) and /admin/requests (admin, 'approved' tab only) now refetch every 12s while at least one row has status='approved'. Stops automatically when all rows settle to pending/completed/rejected. TanStack Query's default refetchIntervalInBackground=false handles the visibility behavior — polling pauses when the tab is hidden and resumes on focus. Predicate hasInFlightRequest() is exported + tested so the polling logic is auditable independent of TanStack Query's internals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
186 lines
5.2 KiB
TypeScript
186 lines
5.2 KiB
TypeScript
import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
vi.mock('./client', () => ({
|
|
api: {
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
put: vi.fn(),
|
|
del: vi.fn()
|
|
},
|
|
apiFetch: vi.fn()
|
|
}));
|
|
|
|
import {
|
|
createRequest,
|
|
listMyRequests,
|
|
getRequest,
|
|
cancelRequest,
|
|
hasInFlightRequest
|
|
} from './requests';
|
|
import { api, apiFetch } from './client';
|
|
import { qk } from './queries';
|
|
import type { LidarrRequest } from './types';
|
|
|
|
const mockRow: LidarrRequest = {
|
|
id: 'r1',
|
|
user_id: 'u1',
|
|
status: 'pending',
|
|
kind: 'album',
|
|
lidarr_artist_mbid: 'art-mbid',
|
|
lidarr_album_mbid: 'alb-mbid',
|
|
lidarr_track_mbid: null,
|
|
artist_name: 'Aphex Twin',
|
|
album_title: 'Selected Ambient Works',
|
|
track_title: null,
|
|
quality_profile_id: null,
|
|
root_folder_path: null,
|
|
decided_at: null,
|
|
decided_by: null,
|
|
notes: null,
|
|
completed_at: null,
|
|
matched_track_id: null,
|
|
matched_album_id: null,
|
|
matched_artist_id: null,
|
|
requested_at: '2026-01-01T00:00:00Z',
|
|
updated_at: '2026-01-01T00:00:00Z',
|
|
imported_album_count: 0,
|
|
imported_track_count: 0
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('createRequest', () => {
|
|
test('POSTs /api/requests with full body for an album request', async () => {
|
|
(api.post as ReturnType<typeof vi.fn>).mockResolvedValueOnce(mockRow);
|
|
const out = await createRequest({
|
|
kind: 'album',
|
|
lidarr_artist_mbid: 'art-mbid',
|
|
lidarr_album_mbid: 'alb-mbid',
|
|
artist_name: 'Aphex Twin',
|
|
album_title: 'Selected Ambient Works'
|
|
});
|
|
expect(api.post).toHaveBeenCalledWith('/api/requests', {
|
|
kind: 'album',
|
|
lidarr_artist_mbid: 'art-mbid',
|
|
artist_name: 'Aphex Twin',
|
|
lidarr_album_mbid: 'alb-mbid',
|
|
album_title: 'Selected Ambient Works'
|
|
});
|
|
expect(out).toBe(mockRow);
|
|
});
|
|
|
|
test('omits empty / undefined optional MBID + title fields from wire body', async () => {
|
|
(api.post as ReturnType<typeof vi.fn>).mockResolvedValueOnce(mockRow);
|
|
await createRequest({
|
|
kind: 'artist',
|
|
lidarr_artist_mbid: 'art-mbid',
|
|
artist_name: 'Aphex Twin',
|
|
lidarr_album_mbid: '',
|
|
lidarr_track_mbid: ''
|
|
});
|
|
const body = (api.post as ReturnType<typeof vi.fn>).mock.calls[0][1] as Record<
|
|
string,
|
|
unknown
|
|
>;
|
|
expect(body).toEqual({
|
|
kind: 'artist',
|
|
lidarr_artist_mbid: 'art-mbid',
|
|
artist_name: 'Aphex Twin'
|
|
});
|
|
expect(body).not.toHaveProperty('lidarr_album_mbid');
|
|
expect(body).not.toHaveProperty('lidarr_track_mbid');
|
|
expect(body).not.toHaveProperty('album_title');
|
|
expect(body).not.toHaveProperty('track_title');
|
|
});
|
|
|
|
test('includes track fields for a track-kind request', async () => {
|
|
(api.post as ReturnType<typeof vi.fn>).mockResolvedValueOnce(mockRow);
|
|
await createRequest({
|
|
kind: 'track',
|
|
lidarr_artist_mbid: 'art-mbid',
|
|
lidarr_album_mbid: 'alb-mbid',
|
|
lidarr_track_mbid: 'trk-mbid',
|
|
artist_name: 'Aphex Twin',
|
|
album_title: 'Drukqs',
|
|
track_title: 'Avril 14th'
|
|
});
|
|
expect(api.post).toHaveBeenCalledWith('/api/requests', {
|
|
kind: 'track',
|
|
lidarr_artist_mbid: 'art-mbid',
|
|
artist_name: 'Aphex Twin',
|
|
lidarr_album_mbid: 'alb-mbid',
|
|
lidarr_track_mbid: 'trk-mbid',
|
|
album_title: 'Drukqs',
|
|
track_title: 'Avril 14th'
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('listMyRequests', () => {
|
|
test('GETs /api/requests', async () => {
|
|
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce([mockRow]);
|
|
const out = await listMyRequests();
|
|
expect(api.get).toHaveBeenCalledWith('/api/requests');
|
|
expect(out).toEqual([mockRow]);
|
|
});
|
|
});
|
|
|
|
describe('getRequest', () => {
|
|
test('GETs /api/requests/:id', async () => {
|
|
(api.get as ReturnType<typeof vi.fn>).mockResolvedValueOnce(mockRow);
|
|
const out = await getRequest('r1');
|
|
expect(api.get).toHaveBeenCalledWith('/api/requests/r1');
|
|
expect(out).toBe(mockRow);
|
|
});
|
|
});
|
|
|
|
describe('cancelRequest', () => {
|
|
test('DELETEs /api/requests/:id and returns the cancelled row', async () => {
|
|
const cancelled = { ...mockRow, status: 'rejected' as const };
|
|
(apiFetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cancelled);
|
|
const out = await cancelRequest('r1');
|
|
expect(apiFetch).toHaveBeenCalledWith('/api/requests/r1', { method: 'DELETE' });
|
|
expect(out).toBe(cancelled);
|
|
expect(out.status).toBe('rejected');
|
|
});
|
|
});
|
|
|
|
describe('qk.myRequests', () => {
|
|
test('returns the expected key tuple', () => {
|
|
expect(qk.myRequests()).toEqual(['myRequests']);
|
|
});
|
|
});
|
|
|
|
describe('hasInFlightRequest', () => {
|
|
test('false for undefined / empty', () => {
|
|
expect(hasInFlightRequest(undefined)).toBe(false);
|
|
expect(hasInFlightRequest([])).toBe(false);
|
|
});
|
|
|
|
test('false when no row is approved', () => {
|
|
expect(
|
|
hasInFlightRequest([
|
|
{ ...mockRow, status: 'pending' },
|
|
{ ...mockRow, status: 'completed' },
|
|
{ ...mockRow, status: 'rejected' }
|
|
])
|
|
).toBe(false);
|
|
});
|
|
|
|
test('true when at least one row is approved (mid-ingest)', () => {
|
|
expect(
|
|
hasInFlightRequest([
|
|
{ ...mockRow, status: 'pending' },
|
|
{ ...mockRow, status: 'approved' },
|
|
{ ...mockRow, status: 'completed' }
|
|
])
|
|
).toBe(true);
|
|
});
|
|
});
|