Files
portal/src/lib/svelte-admin/crud.spec.ts
T
NeilClack 1d11de8cec Refactor admin to shared generic CRUD; restructure proclamation categories/groups and APIs
- Replace per-model admin forms with the shared generic AdminTable/AdminModal CRUD
- Add ProclamationGroup model and admin + API v2 routes (replaces subscription levels)
- Drop subscription levels and flatten proclamation categories
- Update v2 serializers and seed data accordingly
2026-08-27 12:03:40 -05:00

228 lines
7.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createLocals, mockUser } from '$lib/test-utils';
import { getModelConfig } from './registry';
// Ensure superValidate can be imported from server path in Node.js/SSR context
import { superValidate } from 'sveltekit-superforms/server';
import { zod4 } from 'sveltekit-superforms/adapters';
vi.mock('$lib/db', () => {
const mkdel = () => ({
findMany: vi.fn(),
create: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
findUnique: vi.fn(),
findFirst: vi.fn()
});
return {
prisma: {
userRole: mkdel(),
proclamation: mkdel(),
proclamationCategory: mkdel(),
proclamationGroup: mkdel(),
solfeggioFrequency: mkdel()
}
};
});
import { prisma } from '$lib/db';
import { loadRecords, upsertRecord, deleteRecord } from './crud';
const userRoleFindMany = prisma.userRole.findMany as ReturnType<typeof vi.fn>;
const create = prisma.userRole.create as ReturnType<typeof vi.fn>;
const update = prisma.userRole.update as ReturnType<typeof vi.fn>;
const deletes = prisma.userRole.delete as ReturnType<typeof vi.fn>;
const findUnique = prisma.userRole.findUnique as ReturnType<typeof vi.fn>;
const findFirst = prisma.userRole.findFirst as ReturnType<typeof vi.fn>;
const config = getModelConfig('user-roles')!;
describe('crud loadRecords', () => {
beforeEach(() => {
userRoleFindMany.mockReset();
});
it('returns records', async () => {
userRoleFindMany.mockResolvedValue([{ id: 1, name: 'Admin' }]);
const locals = createLocals(mockUser() as any);
const result = await loadRecords(locals, config);
expect(result.records).toEqual([{ id: 1, name: 'Admin' }]);
});
it('returns empty records array', async () => {
userRoleFindMany.mockResolvedValue([]);
const locals = createLocals(mockUser() as any);
const result = await loadRecords(locals, config);
expect(result.records).toEqual([]);
});
});
describe('crud upsertRecord', () => {
beforeEach(() => {
create.mockReset();
update.mockReset();
findUnique.mockReset();
findFirst.mockReset();
});
it('creates a new record when no id', async () => {
create.mockResolvedValue({ id: 2, name: 'Editor' });
const fd = new FormData();
fd.set('name', 'Editor');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(create).toHaveBeenCalledWith({ data: { name: 'Editor' } });
expect(result).toEqual({ success: true });
});
it('updates an existing record when id is present', async () => {
findUnique.mockResolvedValue({ id: 1, name: 'Old Name' });
const fd = new FormData();
fd.set('id', '1');
fd.set('name', 'New Name');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(update).toHaveBeenCalledWith({ where: { id: 1 }, data: { name: 'New Name' } });
expect(result).toEqual({ success: true });
});
it('returns error when updating non-existent record', async () => {
findUnique.mockResolvedValue(null);
const fd = new FormData();
fd.set('id', '999');
fd.set('name', 'Ghost');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(result).toEqual({ success: false, error: 'Record not found' });
});
});
describe('crud deleteRecord', () => {
beforeEach(() => {
deletes.mockReset();
});
it('deletes a record by id', async () => {
deletes.mockResolvedValue({ id: 1, name: 'Admin' });
const fd = new FormData();
fd.set('id', '1');
const locals = createLocals(mockUser() as any);
const result = await deleteRecord(fd, config, locals);
expect(deletes).toHaveBeenCalledWith({ where: { id: 1 } });
expect(result).toEqual({ success: true });
});
it('returns error when id is missing', async () => {
const fd = new FormData();
const locals = createLocals(mockUser() as any);
const result = await deleteRecord(fd, config, locals);
expect(result).toEqual({ success: false, error: 'Missing id' });
});
});
describe('P2002 unique constraint handling', () => {
beforeEach(() => {
create.mockReset();
});
it('returns error with field names when meta.target is populated', async () => {
const p2002 = new Error('Unique constraint failed');
(p2002 as any).code = 'P2002';
(p2002 as any).meta = { target: ['name'], modelName: 'UserRole' };
create.mockRejectedValue(p2002);
const fd = new FormData();
fd.set('name', 'Admin');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(result).toEqual({
success: false,
error: 'A record with this name already exists.',
fieldErrors: { name: 'Already taken' }
});
});
it('returns generic message when meta.target is empty', async () => {
const p2002 = new Error('Unique constraint failed');
(p2002 as any).code = 'P2002';
(p2002 as any).meta = { target: [], modelName: 'UserRole' };
create.mockRejectedValue(p2002);
const fd = new FormData();
fd.set('name', 'Admin');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(result).toEqual({
success: false,
error: 'A database constraint violation occurred. The record could not be created.'
});
});
it('returns generic message when meta.target is missing', async () => {
const p2002 = new Error('Unique constraint failed');
(p2002 as any).code = 'P2002';
(p2002 as any).meta = { modelName: 'UserRole' };
create.mockRejectedValue(p2002);
const fd = new FormData();
fd.set('name', 'Admin');
const locals = createLocals(mockUser() as any);
const result = await upsertRecord(fd, config, locals);
expect(result).toEqual({
success: false,
error: 'A database constraint violation occurred. The record could not be created.'
});
});
});
describe('sveltekit-superforms import compatibility', () => {
it('imports superValidate from server path without pulling in SuperDebug.svelte', async () => {
expect(superValidate).toBeDefined();
});
it('calls superValidate with a Request and validates against a Zod schema', async () => {
const { z } = await import('zod');
const schema = z.object({ name: z.string().min(1) });
const body = new URLSearchParams({ name: 'test' });
const request = new Request('http://localhost', {
method: 'POST',
body,
headers: { 'content-type': 'application/x-www-form-urlencoded' }
});
const form = await superValidate(request, zod4(schema));
expect(form.valid).toBe(true);
expect(form.data).toEqual({ name: 'test' });
});
});
describe('prisma client version consistency', () => {
it('generated client references existing query compiler files', async () => {
const fs = await import('fs');
const path = await import('path');
const runtimeDir = path.resolve('node_modules/@prisma/client/runtime');
const generatedFile = path.resolve('src/lib/generated/prisma/internal/class.ts');
const content = fs.readFileSync(generatedFile, 'utf-8');
const compilerMatch = content.match(/import\(["']@prisma\/client\/runtime\/([^"']+)["']\)/);
expect(compilerMatch).not.toBeNull();
const compilerPath = path.join(runtimeDir, compilerMatch![1]);
expect(fs.existsSync(compilerPath)).toBe(true);
});
});