This commit is contained in:
235
packages/server/tests/local/compliance-templates.test.ts
Normal file
235
packages/server/tests/local/compliance-templates.test.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Compliance Template Routes Tests (M-03)
|
||||
*
|
||||
* Covers the 5-endpoint CRUD surface:
|
||||
* GET /api/compliance/templates
|
||||
* GET /api/compliance/templates/:id
|
||||
* POST /api/compliance/templates
|
||||
* PATCH /api/compliance/templates/:id
|
||||
* DELETE /api/compliance/templates/:id
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { MindDB, SessionStore, FrameStore } from '@waggle/core';
|
||||
import { buildLocalServer } from '../../src/local/index.js';
|
||||
import { injectWithAuth } from '../test-utils.js';
|
||||
|
||||
const ALL_ON = {
|
||||
interactions: true,
|
||||
oversight: true,
|
||||
models: true,
|
||||
provenance: true,
|
||||
riskAssessment: true,
|
||||
fria: true,
|
||||
};
|
||||
|
||||
const ALL_OFF = {
|
||||
interactions: false,
|
||||
oversight: false,
|
||||
models: false,
|
||||
provenance: false,
|
||||
riskAssessment: false,
|
||||
fria: false,
|
||||
};
|
||||
|
||||
describe('Compliance Template Routes (M-03)', () => {
|
||||
let server: FastifyInstance;
|
||||
let tmpDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-compliance-templates-test-'));
|
||||
const mind = new MindDB(path.join(tmpDir, 'personal.mind'));
|
||||
const sessions = new SessionStore(mind);
|
||||
const frames = new FrameStore(mind);
|
||||
const s = sessions.create('template-test-seed');
|
||||
frames.createIFrame(s.gop_id, 'seed', 'normal');
|
||||
mind.close();
|
||||
|
||||
server = await buildLocalServer({ dataDir: tmpDir });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.close();
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('POST /api/compliance/templates', () => {
|
||||
it('creates a template with 201 + returns the full row', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: {
|
||||
name: 'Test template',
|
||||
description: 'unit test',
|
||||
sections: ALL_ON,
|
||||
riskClassification: 'high-risk',
|
||||
orgName: 'Acme',
|
||||
footerText: 'Confidential',
|
||||
},
|
||||
});
|
||||
expect(res.statusCode).toBe(201);
|
||||
const body = res.json();
|
||||
expect(body.template.id).toBeGreaterThan(0);
|
||||
expect(body.template.name).toBe('Test template');
|
||||
expect(body.template.sections).toEqual(ALL_ON);
|
||||
expect(body.template.riskClassification).toBe('high-risk');
|
||||
});
|
||||
|
||||
it('400s on missing name', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: { sections: ALL_OFF },
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('400s on missing sections', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: { name: 'no sections' },
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
|
||||
it('400s on invalid risk classification', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: { name: 'bad risk', sections: ALL_OFF, riskClassification: 'catastrophic' },
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/compliance/templates', () => {
|
||||
it('returns the list of all templates', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/compliance/templates',
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
const body = res.json();
|
||||
expect(Array.isArray(body.templates)).toBe(true);
|
||||
expect(body.templates.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/compliance/templates/:id', () => {
|
||||
it('returns a specific template', async () => {
|
||||
const create = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: { name: 'fetch-me', sections: ALL_OFF },
|
||||
});
|
||||
const id = create.json().template.id;
|
||||
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: `/api/compliance/templates/${id}`,
|
||||
});
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json().template.id).toBe(id);
|
||||
});
|
||||
|
||||
it('404s on missing id', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/compliance/templates/999999',
|
||||
});
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('400s on non-numeric id', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: '/api/compliance/templates/not-a-number',
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /api/compliance/templates/:id', () => {
|
||||
it('updates partial fields and returns the new row', async () => {
|
||||
const create = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: { name: 'orig', description: 'old', sections: ALL_OFF, orgName: 'X' },
|
||||
});
|
||||
const id = create.json().template.id;
|
||||
|
||||
const patch = await injectWithAuth(server, {
|
||||
method: 'PATCH',
|
||||
url: `/api/compliance/templates/${id}`,
|
||||
payload: { name: 'renamed', orgName: null },
|
||||
});
|
||||
expect(patch.statusCode).toBe(200);
|
||||
const body = patch.json();
|
||||
expect(body.template.name).toBe('renamed');
|
||||
expect(body.template.description).toBe('old'); // preserved
|
||||
expect(body.template.orgName).toBeNull(); // cleared
|
||||
});
|
||||
|
||||
it('404s on missing id', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'PATCH',
|
||||
url: '/api/compliance/templates/999999',
|
||||
payload: { name: 'x' },
|
||||
});
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('400s on invalid body shape', async () => {
|
||||
const create = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: { name: 'for-bad-patch', sections: ALL_OFF },
|
||||
});
|
||||
const id = create.json().template.id;
|
||||
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'PATCH',
|
||||
url: `/api/compliance/templates/${id}`,
|
||||
payload: { sections: { interactions: true } }, // missing other 5 flags
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/compliance/templates/:id', () => {
|
||||
it('deletes an existing template + returns { deleted: true }', async () => {
|
||||
const create = await injectWithAuth(server, {
|
||||
method: 'POST',
|
||||
url: '/api/compliance/templates',
|
||||
payload: { name: 'doomed', sections: ALL_OFF },
|
||||
});
|
||||
const id = create.json().template.id;
|
||||
|
||||
const del = await injectWithAuth(server, {
|
||||
method: 'DELETE',
|
||||
url: `/api/compliance/templates/${id}`,
|
||||
});
|
||||
expect(del.statusCode).toBe(200);
|
||||
expect(del.json().deleted).toBe(true);
|
||||
|
||||
const refetch = await injectWithAuth(server, {
|
||||
method: 'GET',
|
||||
url: `/api/compliance/templates/${id}`,
|
||||
});
|
||||
expect(refetch.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('404s on missing id', async () => {
|
||||
const res = await injectWithAuth(server, {
|
||||
method: 'DELETE',
|
||||
url: '/api/compliance/templates/999999',
|
||||
});
|
||||
expect(res.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user