soulsync/webui/src/routes/issues/-issues.api.test.ts
Antti Kettunen 4b2c10fd12
Add MSW-backed issue API tests
- Add a shared MSW server to the Vitest setup
- Cover issue API request, success, and error scenarios
- Add msw as a dev dependency for future API-layer tests
2026-05-13 22:26:24 +03:00

168 lines
4.3 KiB
TypeScript

import { describe, expect, it } from 'vite-plus/test';
import { HttpResponse, http, server } from '@/test/msw';
import type { IssueRecord } from './-issues.types';
import {
createIssue,
deleteIssue,
fetchIssue,
fetchIssueCounts,
fetchIssueList,
updateIssue,
} from './-issues.api';
const counts = {
open: 4,
in_progress: 2,
resolved: 1,
dismissed: 3,
total: 10,
};
const issue: IssueRecord = {
id: 17,
profile_id: 2,
entity_type: 'track',
entity_id: '987',
category: 'wrong_metadata',
title: 'Wrong metadata',
description: 'Title is incorrect',
status: 'open',
priority: 'normal',
snapshot_data: null,
created_at: '2026-05-01T10:00:00.000Z',
updated_at: '2026-05-01T10:30:00.000Z',
};
describe('issue api', () => {
it('fetches issue counts with the profile header', async () => {
server.use(
http.get('/api/issues/counts', ({ request }) => {
expect(request.headers.get('X-Profile-Id')).toBe('42');
return HttpResponse.json({
success: true,
counts,
});
}),
);
await expect(fetchIssueCounts(42)).resolves.toEqual(counts);
});
it('includes list filters and surfaces backend error messages', async () => {
server.use(
http.get('/api/issues', ({ request }) => {
const url = new URL(request.url);
expect(request.headers.get('X-Profile-Id')).toBe('7');
expect(url.searchParams.get('limit')).toBe('100');
expect(url.searchParams.get('status')).toBe('open');
expect(url.searchParams.get('category')).toBe('wrong_metadata');
return HttpResponse.json(
{
error: 'Issue list unavailable',
},
{ status: 500 },
);
}),
);
await expect(
fetchIssueList(7, {
status: 'open',
category: 'wrong_metadata',
}),
).rejects.toThrow('Issue list unavailable');
});
it('falls back when an issue payload is missing the record', async () => {
server.use(
http.get('/api/issues/:issueId', ({ params, request }) => {
expect(request.headers.get('X-Profile-Id')).toBe('8');
expect(params.issueId).toBe('19');
return HttpResponse.json({
success: false,
});
}),
);
await expect(fetchIssue(8, 19)).rejects.toThrow('Issue not found');
});
it('normalizes create issue payloads before posting', async () => {
server.use(
http.post('/api/issues', async ({ request }) => {
expect(request.headers.get('X-Profile-Id')).toBe('13');
expect(request.headers.get('Content-Type')).toContain('application/json');
await expect(request.json()).resolves.toEqual({
entity_type: 'album',
entity_id: 'album-55',
category: 'wrong_cover',
title: 'Missing cover',
description: '',
priority: 'normal',
});
return HttpResponse.json({
success: true,
issue,
});
}),
);
await expect(
createIssue(13, {
entity_type: 'album',
entity_id: 'album-55',
category: 'wrong_cover',
title: 'Missing cover',
}),
).resolves.toEqual(issue);
});
it('posts issue updates to the correct endpoint', async () => {
server.use(
http.put('/api/issues/:issueId', async ({ params, request }) => {
expect(request.headers.get('X-Profile-Id')).toBe('21');
expect(params.issueId).toBe('17');
await expect(request.json()).resolves.toEqual({
status: 'resolved',
admin_response: 'Fixed upstream',
});
return HttpResponse.json({
success: true,
});
}),
);
await expect(
updateIssue(21, 17, {
status: 'resolved',
admin_response: 'Fixed upstream',
}),
).resolves.toBeUndefined();
});
it('surfaces delete errors from the server', async () => {
server.use(
http.delete('/api/issues/:issueId', ({ params, request }) => {
expect(request.headers.get('X-Profile-Id')).toBe('5');
expect(params.issueId).toBe('91');
return HttpResponse.json(
{
error: 'Cannot delete issue',
},
{ status: 403 },
);
}),
);
await expect(deleteIssue(5, 91)).rejects.toThrow('Cannot delete issue');
});
});