Add dedicated v2 proclamation category/group endpoints and catalog specs

This commit is contained in:
NeilClack
2026-09-10 16:19:53 -05:00
parent 4d929df604
commit 2bbee98594
7 changed files with 224 additions and 0 deletions
@@ -0,0 +1,55 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/db', () => ({
prisma: {
proclamationCategory: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { GET } from './+server';
type GETEvent = Parameters<typeof GET>[0];
const mockFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
describe('GET /api/v2/categories', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns serialized categories', async () => {
mockFindMany.mockResolvedValue([
{ id: 1, name: 'Abundance', sortOrder: 0 },
{ id: 2, name: 'Health', sortOrder: 1 }
]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([
{ id: 1, name: 'Abundance', sortOrder: 0 },
{ id: 2, name: 'Health', sortOrder: 1 }
]);
expect(mockFindMany).toHaveBeenCalledTimes(1);
});
it('returns an empty array when there are no categories', async () => {
mockFindMany.mockResolvedValue([]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([]);
});
it('only exposes the v2 fields, dropping relations and extra columns', async () => {
mockFindMany.mockResolvedValue([
{ id: 3, name: 'Love', sortOrder: 2, proclamations: [{ id: 'x' }] }
]);
const res = await GET({} as GETEvent);
expect(await res.json()).toEqual([{ id: 3, name: 'Love', sortOrder: 2 }]);
});
});
@@ -0,0 +1,50 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/db', () => ({
prisma: {
colorPalette: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { CANONICAL_FREQUENCIES, CANONICAL_PALETTES } from '$lib/server/v2-serializers';
import { GET } from './+server';
type GETEvent = Parameters<typeof GET>[0];
const mockFindMany = prisma.colorPalette.findMany as ReturnType<typeof vi.fn>;
type Palette = { solfeggioFrequencyId: number; colors: string[] };
describe('GET /api/v2/color-palettes', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('prefers database colors and falls back to the canonical palette', async () => {
const dbColors = ['#111111', '#222222', '#333333', '#444444', '#555555', '#666666', '#777777'];
mockFindMany.mockResolvedValue([{ solfeggioFrequencyId: 5, colors: dbColors }]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
const body = (await res.json()) as Palette[];
expect(body).toHaveLength(9);
expect(body.find((p) => p.solfeggioFrequencyId === 5)?.colors).toEqual(dbColors);
expect(body.find((p) => p.solfeggioFrequencyId === 1)?.colors).toEqual(CANONICAL_PALETTES[1]);
});
it('returns the canonical fallback for every id when the table is empty', async () => {
mockFindMany.mockResolvedValue([]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
const body = (await res.json()) as Palette[];
expect(body).toHaveLength(9);
expect(body.map((p) => p.solfeggioFrequencyId)).toEqual(CANONICAL_FREQUENCIES.map((f) => f.id));
for (const palette of body) {
expect(palette.colors).toEqual(CANONICAL_PALETTES[palette.solfeggioFrequencyId]);
}
});
});
@@ -0,0 +1,23 @@
import { describe, it, expect } from 'vitest';
import { CANONICAL_FREQUENCIES } from '$lib/server/v2-serializers';
import { GET } from './+server';
type GETEvent = Parameters<typeof GET>[0];
describe('GET /api/v2/frequencies', () => {
it('returns the nine canonical solfeggio frequencies', async () => {
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual(CANONICAL_FREQUENCIES);
});
it('exposes sequential canonical ids 1-9', async () => {
const res = await GET({} as GETEvent);
const body = (await res.json()) as { id: number; name: string; value: number }[];
expect(body).toHaveLength(9);
expect(body.map((f) => f.id)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
expect(body.every((f) => f.name.length > 0 && f.value > 0)).toBe(true);
});
});
@@ -0,0 +1,3 @@
// Dedicated alias for the proclamation categories collection. The canonical
// handler lives at /api/v2/categories; both paths return the same payload.
export { GET } from '../categories/+server';
@@ -0,0 +1,45 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/db', () => ({
prisma: {
proclamationCategory: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { GET } from './+server';
type GETEvent = Parameters<typeof GET>[0];
const mockFindMany = prisma.proclamationCategory.findMany as ReturnType<typeof vi.fn>;
describe('GET /api/v2/proclamation-categories', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns the same serialized categories as /api/v2/categories', async () => {
mockFindMany.mockResolvedValue([
{ id: 1, name: 'Abundance', sortOrder: 0 },
{ id: 4, name: 'Protection', sortOrder: 3 }
]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([
{ id: 1, name: 'Abundance', sortOrder: 0 },
{ id: 4, name: 'Protection', sortOrder: 3 }
]);
expect(mockFindMany).toHaveBeenCalledTimes(1);
});
it('returns an empty array when there are no categories', async () => {
mockFindMany.mockResolvedValue([]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([]);
});
});
@@ -0,0 +1,3 @@
// Dedicated alias for the proclamation groups collection. The canonical
// handler lives at /api/v2/groups; both paths return the same payload.
export { GET } from '../groups/+server';
@@ -0,0 +1,45 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('$lib/db', () => ({
prisma: {
proclamationGroup: { findMany: vi.fn() }
}
}));
import { prisma } from '$lib/db';
import { GET } from './+server';
type GETEvent = Parameters<typeof GET>[0];
const mockFindMany = prisma.proclamationGroup.findMany as ReturnType<typeof vi.fn>;
describe('GET /api/v2/proclamation-groups', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('returns groups ordered by sortOrder', async () => {
mockFindMany.mockResolvedValue([
{ id: 1, name: 'General', sortOrder: 0 },
{ id: 2, name: 'Specific', sortOrder: 1 }
]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([
{ id: 1, name: 'General', sortOrder: 0 },
{ id: 2, name: 'Specific', sortOrder: 1 }
]);
expect(mockFindMany).toHaveBeenCalledWith({ orderBy: { sortOrder: 'asc' } });
});
it('returns an empty array when there are no groups', async () => {
mockFindMany.mockResolvedValue([]);
const res = await GET({} as GETEvent);
expect(res.status).toBe(200);
expect(await res.json()).toEqual([]);
});
});