Add tests for API endpoints, liked store, listen tracker and paged list

This commit is contained in:
2026-09-24 14:04:43 +02:00
parent b5c9658d65
commit e5ce63a192
6 changed files with 423 additions and 0 deletions
@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest';
vi.mock('$lib/server/config', () => ({ apiConfig: { baseUrl: 'http://api.test' } }));
import { POST } from './+server';
type PostEvent = Parameters<typeof POST>[0];
function event(body: unknown, options: { token?: string | null; api?: Response } = {}) {
const { token = 'token', api = Response.json(true) } = options;
const fetchFn = vi.fn().mockResolvedValue(api);
const request = new Request('http://x/api/likes', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: typeof body === 'string' ? body : JSON.stringify(body)
});
return {
event: {
request,
locals: { accessToken: token, user: null },
fetch: fetchFn
} as unknown as PostEvent,
fetchFn
};
}
describe('POST /api/likes', () => {
it('returns 401 without a token', async () => {
const { event: e, fetchFn } = event({ trackId: 't1' }, { token: null });
expect((await POST(e)).status).toBe(401);
expect(fetchFn).not.toHaveBeenCalled();
});
it.each([[{}], [{ trackId: '' }], [{ trackId: 5 }], ['not json']])(
'returns 400 for an invalid body %j',
async (body) => {
const { event: e, fetchFn } = event(body);
expect((await POST(e)).status).toBe(400);
expect(fetchFn).not.toHaveBeenCalled();
}
);
it('returns the liked state from the API', async () => {
const { event: e } = event({ trackId: 't1' });
const res = await POST(e);
expect(res.status).toBe(200);
expect(await res.json()).toEqual({ liked: true });
});
it('returns 502 when the API fails', async () => {
const { event: e } = event(
{ trackId: 't1' },
{ api: Response.json({ title: 'boom' }, { status: 500 }) }
);
expect((await POST(e)).status).toBe(502);
});
});
@@ -0,0 +1,61 @@
import { describe, expect, it, vi } from 'vitest';
vi.mock('$lib/server/config', () => ({ apiConfig: { baseUrl: 'http://api.test' } }));
import { PUT } from './+server';
type PutEvent = Parameters<typeof PUT>[0];
function event(body: string, api: Response = new Response(null, { status: 204 })) {
const fetchFn = vi.fn().mockResolvedValue(api);
const request = new Request('http://x/api/listens/l1/progress', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body
});
return {
event: {
request,
params: { id: 'l1' },
locals: { accessToken: 'token', user: null },
fetch: fetchFn
} as unknown as PutEvent,
fetchFn
};
}
describe('PUT /api/listens/[id]/progress', () => {
it('returns 401 without a token', async () => {
const { event: e } = event('{"playedSeconds":5}');
(e.locals as { accessToken: string | null }).accessToken = null;
expect((await PUT(e)).status).toBe(401);
});
it.each([
['invalid JSON', 'nope'],
['negative seconds', '{"playedSeconds":-1}'],
['string seconds', '{"playedSeconds":"5"}'],
['missing seconds', '{}'],
['null seconds', '{"playedSeconds":null}']
])('returns 400 for %s', async (_name, body) => {
const { event: e, fetchFn } = event(body);
expect((await PUT(e)).status).toBe(400);
expect(fetchFn).not.toHaveBeenCalled();
});
it('returns 204 when the API accepts the progress', async () => {
const { event: e, fetchFn } = event('{"playedSeconds":12.5}');
expect((await PUT(e)).status).toBe(204);
expect(fetchFn).toHaveBeenCalledTimes(1);
});
it('returns 404 when the API returns 404', async () => {
const { event: e } = event('{"playedSeconds":5}', Response.json({}, { status: 404 }));
expect((await PUT(e)).status).toBe(404);
});
it('returns 502 for other API failures', async () => {
const { event: e } = event('{"playedSeconds":5}', Response.json({}, { status: 500 }));
expect((await PUT(e)).status).toBe(502);
});
});
@@ -0,0 +1,42 @@
import { describe, expect, it, vi } from 'vitest';
import { EXPLORE_PAGE_SIZE, MAX_PAGE_SIZE } from '$lib/config';
vi.mock('$lib/server/config', () => ({ apiConfig: { baseUrl: 'http://api.test' } }));
import { GET } from './+server';
type GetEvent = Parameters<typeof GET>[0];
async function requestedQuery(search: string) {
const fetchFn = vi
.fn()
.mockResolvedValue(
Response.json({ items: [], pageNumber: 1, hasNextPage: false, totalItemCount: 0 })
);
const res = await GET({
url: new URL(`http://x/api/tracks${search}`),
locals: { accessToken: null, user: null },
fetch: fetchFn
} as unknown as GetEvent);
expect(res.status).toBe(200);
const request = fetchFn.mock.calls[0][0] as Request;
return new URL(request.url).searchParams;
}
describe('GET /api/tracks', () => {
it('caps pageSize at MAX_PAGE_SIZE', async () => {
const query = await requestedQuery('?pageSize=100000');
expect(query.get('pageSize')).toBe(String(MAX_PAGE_SIZE));
});
it('uses the default page size for invalid values', async () => {
expect((await requestedQuery('?pageSize=abc')).get('pageSize')).toBe(String(EXPLORE_PAGE_SIZE));
expect((await requestedQuery('')).get('pageSize')).toBe(String(EXPLORE_PAGE_SIZE));
});
it('resets an invalid pageNumber to 1', async () => {
expect((await requestedQuery('?pageNumber=-3')).get('pageNumber')).toBe('1');
expect((await requestedQuery('?pageNumber=abc')).get('pageNumber')).toBe('1');
expect((await requestedQuery('?pageNumber=4')).get('pageNumber')).toBe('4');
});
});