Build API v2

This commit is contained in:
NeilClack
2026-08-07 14:06:20 -05:00
parent b2091deae0
commit 64f7070ad7
50 changed files with 25534 additions and 26502 deletions
@@ -0,0 +1,177 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { isHttpError } from '@sveltejs/kit';
vi.mock('$lib/db', () => ({
prisma: {
proclamation: { findMany: vi.fn() },
licenseKey: { findUnique: vi.fn() },
user: { findUnique: vi.fn() },
userProclamations: { findMany: vi.fn() },
colorPalette: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { GET } from './+server';
type GETEvent = Parameters<typeof GET>[0];
const mockProclamationFindMany = prisma.proclamation.findMany as ReturnType<typeof vi.fn>;
const mockLicenseKey = prisma.licenseKey.findUnique as ReturnType<typeof vi.fn>;
const mockUser = prisma.user.findUnique as ReturnType<typeof vi.fn>;
const mockUserProclamations = prisma.userProclamations.findMany as ReturnType<typeof vi.fn>;
const mockColorPalette = prisma.colorPalette.findMany as ReturnType<typeof vi.fn>;
const palettes = [
{
solfeggioFrequencyId: 5,
colors: ['#32CD32', '#3CB371', '#90EE90', '#98FB98', '#ADFFA9', '#C8FFD2', '#F0FFF0']
},
{
solfeggioFrequencyId: 7,
colors: ['#00BFFF', '#1E90FF', '#4169E1', '#6495ED', '#87CEEB', '#ADD8E6', '#F0F8FF']
}
];
const peaceProclamation = {
id: 'b40fdcb9-703c-420e-af32-a081964c6f08',
name: 'Peace & Relaxation',
content: '<p>The text content</p>',
categoryId: 5,
subscriptionLevelId: 1,
solfeggioFrequencyId: 5,
solfeggioFrequency: { id: 5, name: 'Solar Plexus', value: 528 }
};
const crown936 = {
id: '936-hz-proclamation',
name: 'Crown 936',
content: '<p>no canonical id</p>',
categoryId: 4,
subscriptionLevelId: 1,
solfeggioFrequencyId: 1,
solfeggioFrequency: { id: 1, name: 'Crown', value: 936 }
};
function getRequest(headers: Record<string, string> = {}): Request {
return new Request('http://localhost/api/v2/proclamations', { headers });
}
async function expectHttpError(promise: unknown, status: number, message: string) {
try {
await promise;
} catch (e) {
expect(isHttpError(e)).toBe(true);
const err = e as { status: number; body: { message: string } };
expect(err.status).toBe(status);
expect(err.body.message).toBe(message);
return;
}
throw new Error('expected handler to throw an HttpError');
}
describe('GET /api/v2/proclamations', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns full proclamation records with canonical ids and embedded palettes', async () => {
mockProclamationFindMany.mockResolvedValue([peaceProclamation]);
mockColorPalette.mockResolvedValue(palettes);
const res = await GET({ request: getRequest() } as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([
{
id: peaceProclamation.id,
name: peaceProclamation.name,
content: peaceProclamation.content,
categoryId: 5,
solfeggioFrequencyId: 5,
subscriptionLevelId: 1,
colorPalette: palettes[0].colors
}
]);
});
it('drops proclamations whose frequency has no canonical id (936 Hz)', async () => {
mockProclamationFindMany.mockResolvedValue([peaceProclamation, crown936]);
mockColorPalette.mockResolvedValue(palettes);
const res = await GET({ request: getRequest() } as GETEvent);
const body = (await res.json()) as { id: string }[];
expect(body).toHaveLength(1);
expect(body[0].id).toBe(peaceProclamation.id);
});
it('returns the entitled proclamations as full records when headers are supplied', async () => {
mockLicenseKey.mockResolvedValue({ id: 'key-1', key: 'ABCD-EFGH-IJKL-MNOP' });
mockUser.mockResolvedValue({
id: 'user-1',
email: 'user@example.com',
licenseKeys: [{ id: 'key-1', key: 'ABCD-EFGH-IJKL-MNOP' }]
});
mockUserProclamations.mockResolvedValue([
{ userId: 'user-1', proclamationId: peaceProclamation.id, proclamation: peaceProclamation }
]);
mockColorPalette.mockResolvedValue(palettes);
const res = await GET({
request: getRequest({
'x-license-key': 'ABCD-EFGH-IJKL-MNOP',
'x-email': 'user@example.com'
})
} as GETEvent);
expect(res.status).toBe(200);
const body = (await res.json()) as { id: string }[];
expect(body).toHaveLength(1);
expect(body[0].id).toBe(peaceProclamation.id);
expect(mockUserProclamations).toHaveBeenCalledWith(
expect.objectContaining({ where: { userId: 'user-1' } })
);
});
it('rejects when only one entitlement header is supplied', async () => {
await expectHttpError(
GET({ request: getRequest({ 'x-license-key': 'ABCD-EFGH-IJKL-MNOP' }) } as GETEvent),
400,
'not-authorized'
);
});
it('rejects an unknown license key', async () => {
mockLicenseKey.mockResolvedValue(null);
await expectHttpError(
GET({
request: getRequest({
'x-license-key': 'UNKNOWN-KEY',
'x-email': 'user@example.com'
})
} as GETEvent),
400,
'not-authorized'
);
});
it('rejects a key that does not belong to the user', async () => {
mockLicenseKey.mockResolvedValue({ id: 'key-2', key: 'OTHER-KEY' });
mockUser.mockResolvedValue({
id: 'user-1',
email: 'user@example.com',
licenseKeys: [{ id: 'key-1', key: 'ABCD-EFGH-IJKL-MNOP' }]
});
await expectHttpError(
GET({
request: getRequest({
'x-license-key': 'OTHER-KEY',
'x-email': 'user@example.com'
})
} as GETEvent),
400,
"this key doesn't appear to belong to you"
);
});
});