import { describe, expect, test, vi, beforeEach } from 'vitest'; vi.mock('./client', () => ({ api: { get: vi.fn(), post: vi.fn(), put: vi.fn(), del: vi.fn() } })); import { getLidarrConfig, putLidarrConfig, testLidarrConnection, listQualityProfiles, listRootFolders, listAdminRequests, approveRequest, rejectRequest, listAdminQuarantine, resolveQuarantine, deleteQuarantineFile, deleteQuarantineViaLidarr, listQuarantineActions } from './admin'; import { api } from './client'; import { qk } from './queries'; import type { ActionResult, AdminQuarantineRow, LidarrConfig, LidarrQualityProfile, LidarrQuarantineActionRow, LidarrRequest, LidarrRootFolder } from './types'; const baseConfig: LidarrConfig = { enabled: true, base_url: 'http://lidarr.lan:8686', api_key: '***', default_quality_profile_id: 1, default_metadata_profile_id: 1, default_root_folder_path: '/music' }; const baseRow: 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: 'Drukqs', 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(); }); describe('getLidarrConfig', () => { test('GETs /api/admin/lidarr/config', async () => { (api.get as ReturnType).mockResolvedValueOnce(baseConfig); const out = await getLidarrConfig(); expect(api.get).toHaveBeenCalledWith('/api/admin/lidarr/config'); expect(out).toBe(baseConfig); }); }); describe('putLidarrConfig', () => { test('PUTs /api/admin/lidarr/config with the full config body', async () => { const next: LidarrConfig = { ...baseConfig, api_key: 'plain-key' }; (api.put as ReturnType).mockResolvedValueOnce(next); const out = await putLidarrConfig(next); expect(api.put).toHaveBeenCalledWith('/api/admin/lidarr/config', next); expect(out).toBe(next); }); }); describe('testLidarrConnection', () => { test('POSTs /api/admin/lidarr/test with empty body when no overrides', async () => { (api.post as ReturnType).mockResolvedValueOnce({ ok: true, version: '2.0.0' }); const out = await testLidarrConnection(); expect(api.post).toHaveBeenCalledWith('/api/admin/lidarr/test', {}); expect(out).toEqual({ ok: true, version: '2.0.0' }); }); test('forwards override base_url + api_key body', async () => { (api.post as ReturnType).mockResolvedValueOnce({ ok: true, version: '2.0.0' }); await testLidarrConnection({ base_url: 'http://other:8686', api_key: 'try-this' }); expect(api.post).toHaveBeenCalledWith('/api/admin/lidarr/test', { base_url: 'http://other:8686', api_key: 'try-this' }); }); test('returns ok:false branch without throwing', async () => { (api.post as ReturnType).mockResolvedValueOnce({ ok: false, error: 'auth failed' }); const out = await testLidarrConnection(); expect(out).toEqual({ ok: false, error: 'auth failed' }); }); }); describe('listQualityProfiles', () => { test('GETs /api/admin/lidarr/quality-profiles', async () => { const profiles: LidarrQualityProfile[] = [{ id: 1, name: 'Lossless' }]; (api.get as ReturnType).mockResolvedValueOnce(profiles); const out = await listQualityProfiles(); expect(api.get).toHaveBeenCalledWith('/api/admin/lidarr/quality-profiles'); expect(out).toBe(profiles); }); }); describe('listRootFolders', () => { test('GETs /api/admin/lidarr/root-folders', async () => { const folders: LidarrRootFolder[] = [ { path: '/music', accessible: true, free_space: 100 } ]; (api.get as ReturnType).mockResolvedValueOnce(folders); const out = await listRootFolders(); expect(api.get).toHaveBeenCalledWith('/api/admin/lidarr/root-folders'); expect(out).toBe(folders); }); }); describe('listAdminRequests', () => { test('no args -> /api/admin/requests with no query string', async () => { (api.get as ReturnType).mockResolvedValueOnce([baseRow]); await listAdminRequests(); expect(api.get).toHaveBeenCalledWith('/api/admin/requests'); }); test('status only -> ?status=', async () => { (api.get as ReturnType).mockResolvedValueOnce([baseRow]); await listAdminRequests('approved'); expect(api.get).toHaveBeenCalledWith('/api/admin/requests?status=approved'); }); test('status + limit -> ?status=&limit=', async () => { (api.get as ReturnType).mockResolvedValueOnce([baseRow]); await listAdminRequests('pending', 25); expect(api.get).toHaveBeenCalledWith( '/api/admin/requests?status=pending&limit=25' ); }); test('limit only -> ?limit=', async () => { (api.get as ReturnType).mockResolvedValueOnce([baseRow]); await listAdminRequests(undefined, 10); expect(api.get).toHaveBeenCalledWith('/api/admin/requests?limit=10'); }); }); describe('approveRequest', () => { test('POSTs to /api/admin/requests/:id/approve with empty body by default', async () => { (api.post as ReturnType).mockResolvedValueOnce(baseRow); await approveRequest('r1'); expect(api.post).toHaveBeenCalledWith('/api/admin/requests/r1/approve', {}); }); test('forwards quality_profile_id + root_folder_path overrides', async () => { (api.post as ReturnType).mockResolvedValueOnce(baseRow); await approveRequest('r1', { quality_profile_id: 2, root_folder_path: '/music' }); expect(api.post).toHaveBeenCalledWith('/api/admin/requests/r1/approve', { quality_profile_id: 2, root_folder_path: '/music' }); }); }); describe('rejectRequest', () => { test('POSTs to /api/admin/requests/:id/reject with empty body when no notes', async () => { (api.post as ReturnType).mockResolvedValueOnce(baseRow); await rejectRequest('r1'); expect(api.post).toHaveBeenCalledWith('/api/admin/requests/r1/reject', {}); }); test('includes notes when provided', async () => { (api.post as ReturnType).mockResolvedValueOnce(baseRow); await rejectRequest('r1', 'duplicate of r0'); expect(api.post).toHaveBeenCalledWith('/api/admin/requests/r1/reject', { notes: 'duplicate of r0' }); }); }); describe('qk admin keys', () => { test('lidarrConfig key', () => { expect(qk.lidarrConfig()).toEqual(['lidarrConfig']); }); test('lidarrQualityProfiles key', () => { expect(qk.lidarrQualityProfiles()).toEqual(['lidarrQualityProfiles']); }); test('lidarrRootFolders key', () => { expect(qk.lidarrRootFolders()).toEqual(['lidarrRootFolders']); }); test('adminRequests key uses "all" when status omitted', () => { // listAdminRequests sends no status param when called without one (returns // every status), so the cache key must distinguish that from any specific // status — otherwise an unfiltered call would falsely cache as `pending`. expect(qk.adminRequests()).toEqual(['adminRequests', { status: 'all' }]); }); test('adminRequests key includes the status filter', () => { expect(qk.adminRequests('approved')).toEqual([ 'adminRequests', { status: 'approved' } ]); }); }); const baseQuarantineRow: AdminQuarantineRow = { track_id: 't1', track_title: 'Avril 14th', artist_name: 'Aphex Twin', album_title: 'Drukqs', album_id: 'alb1', lidarr_album_mbid: null, report_count: 1, latest_at: '2026-01-01T00:00:00Z', reason_counts: { bad_rip: 1 }, reports: [] }; const baseAction: ActionResult = { action_id: 'a1', affected_users: 1 }; const baseActionRow: LidarrQuarantineActionRow = { id: 'a1', track_id: 't1', track_title: 'Avril 14th', artist_name: 'Aphex Twin', album_title: 'Drukqs', action: 'resolved', admin_id: 'admin1', lidarr_album_mbid: null, affected_users: 1, created_at: '2026-01-01T00:00:00Z' }; describe('listAdminQuarantine', () => { test('GETs /api/admin/quarantine', async () => { (api.get as ReturnType).mockResolvedValueOnce([baseQuarantineRow]); const out = await listAdminQuarantine(); expect(api.get).toHaveBeenCalledWith('/api/admin/quarantine'); expect(out).toEqual([baseQuarantineRow]); }); }); describe('resolveQuarantine', () => { test('POSTs /api/admin/quarantine/:trackID/resolve with empty body', async () => { (api.post as ReturnType).mockResolvedValueOnce(baseAction); const out = await resolveQuarantine('t1'); expect(api.post).toHaveBeenCalledWith('/api/admin/quarantine/t1/resolve', {}); expect(out).toBe(baseAction); }); }); describe('deleteQuarantineFile', () => { test('POSTs /api/admin/quarantine/:trackID/delete-file with empty body', async () => { (api.post as ReturnType).mockResolvedValueOnce(baseAction); const out = await deleteQuarantineFile('t1'); expect(api.post).toHaveBeenCalledWith( '/api/admin/quarantine/t1/delete-file', {} ); expect(out).toBe(baseAction); }); }); describe('deleteQuarantineViaLidarr', () => { test('POSTs /api/admin/quarantine/:trackID/delete-via-lidarr with empty body', async () => { (api.post as ReturnType).mockResolvedValueOnce(baseAction); const out = await deleteQuarantineViaLidarr('t1'); expect(api.post).toHaveBeenCalledWith( '/api/admin/quarantine/t1/delete-via-lidarr', {} ); expect(out).toBe(baseAction); }); }); describe('listQuarantineActions', () => { test('defaults to limit=50 when no argument', async () => { (api.get as ReturnType).mockResolvedValueOnce([baseActionRow]); await listQuarantineActions(); expect(api.get).toHaveBeenCalledWith( '/api/admin/quarantine/actions?limit=50' ); }); test('forwards explicit limit', async () => { (api.get as ReturnType).mockResolvedValueOnce([baseActionRow]); await listQuarantineActions(10); expect(api.get).toHaveBeenCalledWith( '/api/admin/quarantine/actions?limit=10' ); }); }); describe('qk admin quarantine keys', () => { test('adminQuarantine key', () => { expect(qk.adminQuarantine()).toEqual(['adminQuarantine']); }); test('adminQuarantineActions key defaults to limit 50', () => { expect(qk.adminQuarantineActions()).toEqual([ 'adminQuarantineActions', { limit: 50 } ]); }); test('adminQuarantineActions key reflects custom limit', () => { expect(qk.adminQuarantineActions(25)).toEqual([ 'adminQuarantineActions', { limit: 25 } ]); }); });