import { describe, it, expect } from 'vitest'; import { z } from 'zod'; import { parsePositiveIntParam, validateBody } from '../../src/lib/server/api-helpers'; // SvelteKit's `error()` throws an HttpError shape with { status, body }. // We verify both — wrapping these everywhere costs nothing and keeps the // API contract stable. function expectHttpError(fn: () => unknown, status: number, message?: string) { try { fn(); } catch (err) { const e = err as { status?: number; body?: { message?: string } }; expect(e.status, `status should be ${status}`).toBe(status); if (message !== undefined) { expect(e.body?.message).toBe(message); } return; } throw new Error('expected fn to throw, but it returned normally'); } describe('parsePositiveIntParam', () => { it('parses a valid positive integer', () => { expect(parsePositiveIntParam('42', 'id')).toBe(42); expect(parsePositiveIntParam('1', 'id')).toBe(1); expect(parsePositiveIntParam('999999', 'id')).toBe(999999); }); it('throws 400 for zero', () => { expectHttpError(() => parsePositiveIntParam('0', 'id'), 400, 'Invalid id'); }); it('throws 400 for negative numbers', () => { expectHttpError(() => parsePositiveIntParam('-1', 'id'), 400, 'Invalid id'); }); it('throws 400 for non-integer', () => { expectHttpError(() => parsePositiveIntParam('1.5', 'id'), 400, 'Invalid id'); }); it('throws 400 for non-numeric strings', () => { expectHttpError(() => parsePositiveIntParam('abc', 'id'), 400, 'Invalid id'); }); it('throws 400 for empty string', () => { expectHttpError(() => parsePositiveIntParam('', 'id'), 400, 'Invalid id'); }); it('throws 400 for null', () => { expectHttpError(() => parsePositiveIntParam(null, 'id'), 400, 'Missing id'); }); it('throws 400 for undefined', () => { expectHttpError(() => parsePositiveIntParam(undefined, 'id'), 400, 'Missing id'); }); it('uses the provided field name in error messages', () => { expectHttpError(() => parsePositiveIntParam('foo', 'recipe_id'), 400, 'Invalid recipe_id'); expectHttpError(() => parsePositiveIntParam(null, 'recipe_id'), 400, 'Missing recipe_id'); }); }); describe('validateBody', () => { const Schema = z.object({ name: z.string().min(1), age: z.number().int().nonnegative() }); it('returns parsed data when valid', () => { const result = validateBody({ name: 'foo', age: 42 }, Schema); expect(result).toEqual({ name: 'foo', age: 42 }); }); it('throws 400 with message and issues on schema mismatch', () => { try { validateBody({ name: '', age: -1 }, Schema); throw new Error('expected throw'); } catch (err) { const e = err as { status?: number; body?: { message?: string; issues?: unknown[] } }; expect(e.status).toBe(400); expect(e.body?.message).toBe('Invalid body'); expect(Array.isArray(e.body?.issues)).toBe(true); expect(e.body?.issues?.length).toBeGreaterThan(0); } }); it('throws 400 for null body (request.json failure case)', () => { expectHttpError(() => validateBody(null, Schema), 400, 'Invalid body'); }); it('throws 400 for primitive non-object body', () => { expectHttpError(() => validateBody('a string', Schema), 400, 'Invalid body'); }); });