fix: replace hardcoded localhost:3001 with relative API URL + add canvas mock and integration test
- Fix frontend Network error: API_BASE now defaults to empty string (same-origin) - Fix 3 files: api.ts, AuthContext.tsx, Dashboard.tsx - Add Canvas 2D context mock in tests/setup.ts for jsdom - Fix -0 vs +0 in ZoomPanController.getViewport() - Add IntegrationWorkflow.test.ts (35 tests, full CAD workflow)
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* AuthService Unit Tests – Register / Login / ChangePassword / Session lifecycle
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { AuthService } from '../src/auth/AuthService.js';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let db: SqliteAdapter;
|
||||
let auth: AuthService;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
auth = new AuthService(db);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Register ───────────────────────────────────────
|
||||
|
||||
describe('register()', () => {
|
||||
it('should register a new user successfully', async () => {
|
||||
const result = await auth.register('register@example.com', 'Password123!', 'Register User', 'planer');
|
||||
expect(result.user).toBeDefined();
|
||||
expect(result.user.email).toBe('register@example.com');
|
||||
expect(result.user.name).toBe('Register User');
|
||||
expect(result.user.password_hash).toBeUndefined();
|
||||
expect(result.session).toBeDefined();
|
||||
expect(result.session.token).toBeDefined();
|
||||
expect(result.session.userId).toBe(result.user.id);
|
||||
});
|
||||
|
||||
it('should throw on duplicate email', async () => {
|
||||
await expect(
|
||||
auth.register('register@example.com', 'AnotherPass!', 'Another User', 'planer'),
|
||||
).rejects.toThrow('Email already registered');
|
||||
});
|
||||
|
||||
it('should default role to planer when not specified', async () => {
|
||||
const result = await auth.register('default-role@example.com', 'Pass123!', 'Default Role User');
|
||||
expect(result.user.role).toBe('planer');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Login ─────────────────────────────────────────
|
||||
|
||||
describe('login()', () => {
|
||||
it('should login with correct credentials', async () => {
|
||||
const result = await auth.login('register@example.com', 'Password123!');
|
||||
expect(result.user).toBeDefined();
|
||||
expect(result.user.email).toBe('register@example.com');
|
||||
expect(result.session.token).toBeDefined();
|
||||
expect(result.session.userId).toBe(result.user.id);
|
||||
});
|
||||
|
||||
it('should throw on wrong password', async () => {
|
||||
await expect(
|
||||
auth.login('register@example.com', 'WrongPassword!'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
|
||||
it('should throw on non-existent email', async () => {
|
||||
await expect(
|
||||
auth.login('nobody@example.com', 'SomePassword!'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Change Password ───────────────────────────────
|
||||
|
||||
describe('changePassword()', () => {
|
||||
it('should change password with correct old password', async () => {
|
||||
const result = await auth.register('changepw@example.com', 'OldPass123!', 'ChangePW User', 'admin');
|
||||
const userId = result.user.id;
|
||||
|
||||
await auth.changePassword(userId, 'OldPass123!', 'NewPass456!');
|
||||
// Should now be able to login with new password
|
||||
const loginResult = await auth.login('changepw@example.com', 'NewPass456!');
|
||||
expect(loginResult.user.id).toBe(userId);
|
||||
});
|
||||
|
||||
it('should throw on wrong old password', async () => {
|
||||
const result = await auth.register('changepw2@example.com', 'OriginalPass!', 'User2', 'planer');
|
||||
await expect(
|
||||
auth.changePassword(result.user.id, 'WrongOldPass!', 'NewPass!'),
|
||||
).rejects.toThrow('Invalid current password');
|
||||
});
|
||||
|
||||
it('should throw on non-existent user', async () => {
|
||||
await expect(
|
||||
auth.changePassword('non-existent-user-id', 'OldPass!', 'NewPass!'),
|
||||
).rejects.toThrow('User not found');
|
||||
});
|
||||
|
||||
it('should reject old password after change', async () => {
|
||||
const result = await auth.register('changepw3@example.com', 'Original123!', 'User3', 'planer');
|
||||
await auth.changePassword(result.user.id, 'Original123!', 'Changed456!');
|
||||
await expect(
|
||||
auth.login('changepw3@example.com', 'Original123!'),
|
||||
).rejects.toThrow('Invalid credentials');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Session Management ────────────────────────────
|
||||
|
||||
describe('Session lifecycle', () => {
|
||||
it('should create and validate a session', async () => {
|
||||
const regResult = await auth.register('session@example.com', 'Pass123!', 'Session User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
const session = auth.validateSession(token);
|
||||
expect(session).not.toBeNull();
|
||||
expect(session!.token).toBe(token);
|
||||
expect(session!.userId).toBe(regResult.user.id);
|
||||
});
|
||||
|
||||
it('should return null for invalid session token', () => {
|
||||
const session = auth.validateSession('invalid-token-xyz');
|
||||
expect(session).toBeNull();
|
||||
});
|
||||
|
||||
it('should destroy a session and invalidate it', async () => {
|
||||
const regResult = await auth.register('destroy@example.com', 'Pass123!', 'Destroy User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
// Session should be valid
|
||||
expect(auth.validateSession(token)).not.toBeNull();
|
||||
|
||||
// Destroy it
|
||||
auth.destroySession(token);
|
||||
|
||||
// Session should now be invalid
|
||||
expect(auth.validateSession(token)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle destroying a non-existent session gracefully', () => {
|
||||
// Should not throw
|
||||
auth.destroySession('non-existent-token');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── getUserFromSession ─────────────────────────────
|
||||
|
||||
describe('getUserFromSession()', () => {
|
||||
it('should return user from valid session', async () => {
|
||||
const regResult = await auth.register('getuser@example.com', 'Pass123!', 'GetUser User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
const user = auth.getUserFromSession(token);
|
||||
expect(user).not.toBeNull();
|
||||
expect(user!.email).toBe('getuser@example.com');
|
||||
expect(user!.password_hash).toBeDefined(); // returns full DBUser including password_hash
|
||||
});
|
||||
|
||||
it('should return null for invalid session token', () => {
|
||||
const user = auth.getUserFromSession('invalid-token-xyz');
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null after session is destroyed', async () => {
|
||||
const regResult = await auth.register('getuser2@example.com', 'Pass123!', 'GetUser2 User', 'planer');
|
||||
const token = regResult.session.token;
|
||||
|
||||
auth.destroySession(token);
|
||||
const user = auth.getUserFromSession(token);
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── createSession (standalone) ─────────────────────
|
||||
|
||||
describe('createSession()', () => {
|
||||
it('should create a session with a unique token', async () => {
|
||||
const regResult = await auth.register('createsession@example.com', 'Pass123!', 'CS User', 'planer');
|
||||
const session1 = auth.createSession(regResult.user.id);
|
||||
const session2 = auth.createSession(regResult.user.id);
|
||||
|
||||
expect(session1.token).not.toBe(session2.token);
|
||||
expect(session1.userId).toBe(regResult.user.id);
|
||||
expect(session2.userId).toBe(regResult.user.id);
|
||||
expect(session1.expiresAt).toBeGreaterThan(Date.now());
|
||||
expect(session2.expiresAt).toBeGreaterThan(Date.now());
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* SqliteAdapter Unit Tests – init, CRUD for all entities, close
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
|
||||
let idCounter = 0;
|
||||
function uniqueId(prefix: string): string {
|
||||
idCounter++;
|
||||
return `${prefix}-${Date.now()}-${idCounter}`;
|
||||
}
|
||||
|
||||
describe('SqliteAdapter', () => {
|
||||
let db: SqliteAdapter;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Init & Close ───────────────────────────────────
|
||||
|
||||
describe('init() and close()', () => {
|
||||
it('should initialize the database without errors', async () => {
|
||||
const testDb = new SqliteAdapter(':memory:');
|
||||
await testDb.init();
|
||||
const users = testDb.listUsers();
|
||||
expect(Array.isArray(users)).toBe(true);
|
||||
testDb.close();
|
||||
});
|
||||
|
||||
it('should seed a default user on init', async () => {
|
||||
const defaultUser = db.getUser('user-default');
|
||||
expect(defaultUser).not.toBeNull();
|
||||
expect(defaultUser!.email).toBe('default@webcad.local');
|
||||
expect(defaultUser!.role).toBe('admin');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Users CRUD ─────────────────────────────────────
|
||||
|
||||
describe('Users CRUD', () => {
|
||||
it('should create and get a user', () => {
|
||||
const uid = uniqueId('user');
|
||||
const user = db.createUser({
|
||||
id: uid,
|
||||
email: `${uid}@example.com`,
|
||||
password_hash: 'hash123',
|
||||
name: 'CRUD User',
|
||||
role: 'planer',
|
||||
});
|
||||
expect(user.id).toBe(uid);
|
||||
expect(user.email).toBe(`${uid}@example.com`);
|
||||
|
||||
const fetched = db.getUser(user.id);
|
||||
expect(fetched).not.toBeNull();
|
||||
expect(fetched!.email).toBe(`${uid}@example.com`);
|
||||
});
|
||||
|
||||
it('should get user by email', () => {
|
||||
const uid = uniqueId('user');
|
||||
db.createUser({ id: uid, email: `${uid}@example.com`, password_hash: 'hash', name: 'Email User', role: 'planer' });
|
||||
const user = db.getUserByEmail(`${uid}@example.com`);
|
||||
expect(user).not.toBeNull();
|
||||
expect(user!.name).toBe('Email User');
|
||||
});
|
||||
|
||||
it('should return null for non-existent user', () => {
|
||||
expect(db.getUser('non-existent-id')).toBeNull();
|
||||
expect(db.getUserByEmail('nobody@nowhere.com')).toBeNull();
|
||||
});
|
||||
|
||||
it('should update a user', () => {
|
||||
const uid = uniqueId('user');
|
||||
db.createUser({ id: uid, email: `${uid}@example.com`, password_hash: 'hash', name: 'Original Name', role: 'planer' });
|
||||
const updated = db.updateUser(uid, { name: 'Updated Name', role: 'admin' });
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.name).toBe('Updated Name');
|
||||
expect(updated!.role).toBe('admin');
|
||||
});
|
||||
|
||||
it('should list users', () => {
|
||||
const users = db.listUsers();
|
||||
expect(users.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should delete a user', () => {
|
||||
const uid = uniqueId('user');
|
||||
db.createUser({ id: uid, email: `${uid}@example.com`, password_hash: 'hash', name: 'Delete Me', role: 'planer' });
|
||||
const ok = db.deleteUser(uid);
|
||||
expect(ok).toBe(true);
|
||||
expect(db.getUser(uid)).toBeNull();
|
||||
});
|
||||
|
||||
it('should return false when deleting non-existent user', () => {
|
||||
expect(db.deleteUser('non-existent-id')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Projects CRUD ──────────────────────────────────
|
||||
|
||||
describe('Projects CRUD', () => {
|
||||
it('should create and get a project', () => {
|
||||
const pid = uniqueId('proj');
|
||||
const project = db.createProject({ id: pid, name: 'Test Project', description: 'Test desc' });
|
||||
expect(project.id).toBe(pid);
|
||||
expect(project.name).toBe('Test Project');
|
||||
|
||||
const fetched = db.getProject(pid);
|
||||
expect(fetched).not.toBeNull();
|
||||
expect(fetched!.name).toBe('Test Project');
|
||||
});
|
||||
|
||||
it('should return null for non-existent project', () => {
|
||||
expect(db.getProject('non-existent-id')).toBeNull();
|
||||
});
|
||||
|
||||
it('should update a project', () => {
|
||||
const pid = uniqueId('proj');
|
||||
db.createProject({ id: pid, name: 'Update Project' });
|
||||
const updated = db.updateProject(pid, { name: 'Updated Project', description: 'New desc' });
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.name).toBe('Updated Project');
|
||||
expect(updated!.description).toBe('New desc');
|
||||
});
|
||||
|
||||
it('should list projects', () => {
|
||||
const projects = db.listProjects();
|
||||
expect(projects.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should delete a project', () => {
|
||||
const pid = uniqueId('proj');
|
||||
db.createProject({ id: pid, name: 'Delete Project' });
|
||||
const ok = db.deleteProject(pid);
|
||||
expect(ok).toBe(true);
|
||||
expect(db.getProject(pid)).toBeNull();
|
||||
});
|
||||
|
||||
it('should cascade delete drawings when project is deleted', () => {
|
||||
const pid = uniqueId('proj');
|
||||
const did = uniqueId('draw');
|
||||
db.createProject({ id: pid, name: 'Cascade Project' });
|
||||
db.createDrawing({ id: did, project_id: pid, name: 'Cascade Drawing' });
|
||||
db.deleteProject(pid);
|
||||
expect(db.getDrawing(did)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Drawings CRUD ──────────────────────────────────
|
||||
|
||||
describe('Drawings CRUD', () => {
|
||||
let projectId: string;
|
||||
|
||||
it('should create and get a drawing', () => {
|
||||
projectId = uniqueId('proj');
|
||||
const did = uniqueId('draw');
|
||||
db.createProject({ id: projectId, name: 'Drawing Project' });
|
||||
const drawing = db.createDrawing({ id: did, project_id: projectId, name: 'Floor 1' });
|
||||
expect(drawing.id).toBe(did);
|
||||
expect(drawing.name).toBe('Floor 1');
|
||||
expect(drawing.project_id).toBe(projectId);
|
||||
|
||||
const fetched = db.getDrawing(did);
|
||||
expect(fetched).not.toBeNull();
|
||||
expect(fetched!.name).toBe('Floor 1');
|
||||
});
|
||||
|
||||
it('should list drawings by project', () => {
|
||||
const did1 = uniqueId('draw');
|
||||
const did2 = uniqueId('draw');
|
||||
db.createDrawing({ id: did1, project_id: projectId, name: 'Drawing A' });
|
||||
db.createDrawing({ id: did2, project_id: projectId, name: 'Drawing B' });
|
||||
const drawings = db.listDrawings(projectId);
|
||||
expect(drawings.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('should update a drawing', () => {
|
||||
const did = uniqueId('draw');
|
||||
db.createDrawing({ id: did, project_id: projectId, name: 'Update Me' });
|
||||
const updated = db.updateDrawing(did, { name: 'Updated Drawing', data_json: '{"x":1}' });
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.name).toBe('Updated Drawing');
|
||||
expect(updated!.data_json).toBe('{"x":1}');
|
||||
});
|
||||
|
||||
it('should delete a drawing', () => {
|
||||
const did = uniqueId('draw');
|
||||
db.createDrawing({ id: did, project_id: projectId, name: 'Delete Me' });
|
||||
const ok = db.deleteDrawing(did);
|
||||
expect(ok).toBe(true);
|
||||
expect(db.getDrawing(did)).toBeNull();
|
||||
});
|
||||
|
||||
it('should cascade delete layers when drawing is deleted', () => {
|
||||
const did = uniqueId('draw');
|
||||
const lid = uniqueId('layer');
|
||||
db.createDrawing({ id: did, project_id: projectId, name: 'Cascade Drawing' });
|
||||
db.createLayer({ id: lid, drawing_id: did, name: 'Cascade Layer' });
|
||||
db.deleteDrawing(did);
|
||||
const layers = db.listLayers(did);
|
||||
expect(layers.find((l) => l.id === lid)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Layers CRUD ────────────────────────────────────
|
||||
|
||||
describe('Layers CRUD', () => {
|
||||
let drawingId: string;
|
||||
|
||||
it('should create and get a layer', () => {
|
||||
const pid = uniqueId('proj');
|
||||
drawingId = uniqueId('draw');
|
||||
const lid = uniqueId('layer');
|
||||
db.createProject({ id: pid, name: 'Layer Project' });
|
||||
db.createDrawing({ id: drawingId, project_id: pid, name: 'Layer Drawing' });
|
||||
const layer = db.createLayer({ id: lid, drawing_id: drawingId, name: 'Layer 1', color: '#ff0000' });
|
||||
expect(layer.id).toBe(lid);
|
||||
expect(layer.name).toBe('Layer 1');
|
||||
expect(layer.color).toBe('#ff0000');
|
||||
expect(layer.visible).toBe(1);
|
||||
expect(layer.locked).toBe(0);
|
||||
});
|
||||
|
||||
it('should list layers by drawing', () => {
|
||||
const lid2 = uniqueId('layer');
|
||||
db.createLayer({ id: lid2, drawing_id: drawingId, name: 'Layer 2' });
|
||||
const layers = db.listLayers(drawingId);
|
||||
expect(layers.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should update a layer', () => {
|
||||
const lid = uniqueId('layer');
|
||||
db.createLayer({ id: lid, drawing_id: drawingId, name: 'Update Layer' });
|
||||
const updated = db.updateLayer(lid, { name: 'Updated Layer', visible: 0, locked: 1 });
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.name).toBe('Updated Layer');
|
||||
expect(updated!.visible).toBe(0);
|
||||
expect(updated!.locked).toBe(1);
|
||||
});
|
||||
|
||||
it('should delete a layer', () => {
|
||||
const lid = uniqueId('layer');
|
||||
db.createLayer({ id: lid, drawing_id: drawingId, name: 'Delete Layer' });
|
||||
const ok = db.deleteLayer(lid);
|
||||
expect(ok).toBe(true);
|
||||
const layers = db.listLayers(drawingId);
|
||||
expect(layers.find((l) => l.id === lid)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Elements CRUD ──────────────────────────────────
|
||||
|
||||
describe('Elements CRUD', () => {
|
||||
let drawingId: string;
|
||||
let layerId: string;
|
||||
|
||||
it('should create and get an element', () => {
|
||||
const pid = uniqueId('proj');
|
||||
drawingId = uniqueId('draw');
|
||||
layerId = uniqueId('layer');
|
||||
const eid = uniqueId('elem');
|
||||
db.createProject({ id: pid, name: 'Element Project' });
|
||||
db.createDrawing({ id: drawingId, project_id: pid, name: 'Element Drawing' });
|
||||
db.createLayer({ id: layerId, drawing_id: drawingId, name: 'Element Layer' });
|
||||
|
||||
const element = db.createElement({
|
||||
id: eid,
|
||||
drawing_id: drawingId,
|
||||
layer_id: layerId,
|
||||
type: 'rect',
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 50,
|
||||
});
|
||||
expect(element.id).toBe(eid);
|
||||
expect(element.type).toBe('rect');
|
||||
expect(element.x).toBe(10);
|
||||
expect(element.y).toBe(20);
|
||||
});
|
||||
|
||||
it('should list elements by drawing', () => {
|
||||
const eid2 = uniqueId('elem');
|
||||
db.createElement({ id: eid2, drawing_id: drawingId, layer_id: layerId, type: 'circle' });
|
||||
const elements = db.listElements(drawingId);
|
||||
expect(elements.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should update an element', () => {
|
||||
const eid = uniqueId('elem');
|
||||
db.createElement({ id: eid, drawing_id: drawingId, layer_id: layerId, type: 'line' });
|
||||
const updated = db.updateElement(eid, { x: 500, y: 600, width: 200 });
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.x).toBe(500);
|
||||
expect(updated!.y).toBe(600);
|
||||
expect(updated!.width).toBe(200);
|
||||
});
|
||||
|
||||
it('should delete an element', () => {
|
||||
const eid = uniqueId('elem');
|
||||
db.createElement({ id: eid, drawing_id: drawingId, layer_id: layerId, type: 'text' });
|
||||
const ok = db.deleteElement(eid);
|
||||
expect(ok).toBe(true);
|
||||
const elements = db.listElements(drawingId);
|
||||
expect(elements.find((e) => e.id === eid)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Blocks CRUD ────────────────────────────────────
|
||||
|
||||
describe('Blocks CRUD', () => {
|
||||
let drawingId: string;
|
||||
|
||||
it('should create and get a block', () => {
|
||||
const pid = uniqueId('proj');
|
||||
drawingId = uniqueId('draw');
|
||||
const bid = uniqueId('block');
|
||||
db.createProject({ id: pid, name: 'Block Project' });
|
||||
db.createDrawing({ id: drawingId, project_id: pid, name: 'Block Drawing' });
|
||||
|
||||
const block = db.createBlock({ id: bid, drawing_id: drawingId, name: 'Table Block', category: 'Mobiliar' });
|
||||
expect(block.id).toBe(bid);
|
||||
expect(block.name).toBe('Table Block');
|
||||
expect(block.category).toBe('Mobiliar');
|
||||
expect(block.elements_json).toBe('[]');
|
||||
});
|
||||
|
||||
it('should list blocks by drawing', () => {
|
||||
const bid2 = uniqueId('block');
|
||||
db.createBlock({ id: bid2, drawing_id: drawingId, name: 'Chair Block' });
|
||||
const blocks = db.listBlocks(drawingId);
|
||||
expect(blocks.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should update a block', () => {
|
||||
const bid = uniqueId('block');
|
||||
db.createBlock({ id: bid, drawing_id: drawingId, name: 'Update Block' });
|
||||
const updated = db.updateBlock(bid, { name: 'Updated Block', category: 'Bühne' });
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated!.name).toBe('Updated Block');
|
||||
expect(updated!.category).toBe('Bühne');
|
||||
});
|
||||
|
||||
it('should delete a block', () => {
|
||||
const bid = uniqueId('block');
|
||||
db.createBlock({ id: bid, drawing_id: drawingId, name: 'Delete Block' });
|
||||
const ok = db.deleteBlock(bid);
|
||||
expect(ok).toBe(true);
|
||||
const blocks = db.listBlocks(drawingId);
|
||||
expect(blocks.find((b) => b.id === bid)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Settings CRUD ──────────────────────────────────
|
||||
|
||||
describe('Settings CRUD', () => {
|
||||
it('should set and get a setting', () => {
|
||||
db.setSetting('test-key', 'test-value');
|
||||
const setting = db.getSetting('test-key');
|
||||
expect(setting).not.toBeNull();
|
||||
expect(setting!.key).toBe('test-key');
|
||||
expect(setting!.value).toBe('test-value');
|
||||
expect(setting!.updated_at).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return null for non-existent setting', () => {
|
||||
expect(db.getSetting('non-existent-key')).toBeNull();
|
||||
});
|
||||
|
||||
it('should upsert (update existing setting)', () => {
|
||||
db.setSetting('upsert-key', 'initial');
|
||||
db.setSetting('upsert-key', 'updated');
|
||||
const setting = db.getSetting('upsert-key');
|
||||
expect(setting).not.toBeNull();
|
||||
expect(setting!.value).toBe('updated');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* AI API Tests – Auth guard, validation, and service availability checks
|
||||
* Does NOT test actual OpenRouter API calls.
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { createServer } from '../src/server.js';
|
||||
|
||||
describe('AI API', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: SqliteAdapter;
|
||||
let authToken: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
app = await createServer({ db, port: 0 });
|
||||
await app.ready();
|
||||
|
||||
// Register a user to get an auth token
|
||||
const registerRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/auth/register',
|
||||
payload: {
|
||||
email: 'ai-test@example.com',
|
||||
password: 'TestPass123!',
|
||||
name: 'AI Test User',
|
||||
role: 'planer',
|
||||
},
|
||||
});
|
||||
const body = JSON.parse(registerRes.body);
|
||||
authToken = body.session.token;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Auth Guard ─────────────────────────────────────
|
||||
|
||||
describe('POST /api/ai/chat – Authentication', () => {
|
||||
it('should return 401 without authorization header', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/ai/chat',
|
||||
payload: { messages: [{ role: 'user', content: 'hello' }] },
|
||||
});
|
||||
expect(response.statusCode).toBe(401);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('Authentication required');
|
||||
});
|
||||
|
||||
it('should return 401 with invalid token', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/ai/chat',
|
||||
headers: { authorization: 'Bearer invalid-token-xxx' },
|
||||
payload: { messages: [{ role: 'user', content: 'hello' }] },
|
||||
});
|
||||
expect(response.statusCode).toBe(401);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('Invalid or expired session');
|
||||
});
|
||||
|
||||
it('should return 401 with malformed authorization header', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/ai/chat',
|
||||
headers: { authorization: 'NotBearer sometoken' },
|
||||
payload: { messages: [{ role: 'user', content: 'hello' }] },
|
||||
});
|
||||
expect(response.statusCode).toBe(401);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Validation ─────────────────────────────────────
|
||||
|
||||
describe('POST /api/ai/chat – Validation', () => {
|
||||
it('should return 400 when messages array is missing', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/ai/chat',
|
||||
headers: { authorization: `Bearer ${authToken}` },
|
||||
payload: {},
|
||||
});
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('Messages array is required');
|
||||
});
|
||||
|
||||
it('should return 400 when messages array is empty', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/ai/chat',
|
||||
headers: { authorization: `Bearer ${authToken}` },
|
||||
payload: { messages: [] },
|
||||
});
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('Messages array is required');
|
||||
});
|
||||
|
||||
it('should return 400 when messages is not an array', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/ai/chat',
|
||||
headers: { authorization: `Bearer ${authToken}` },
|
||||
payload: { messages: 'not an array' },
|
||||
});
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Service Availability ───────────────────────────
|
||||
|
||||
describe('POST /api/ai/chat – Service availability', () => {
|
||||
it('should return 503 when API_KEY_OPENROUTER is not set', async () => {
|
||||
// Ensure env var is not set for this test
|
||||
const originalValue = process.env.API_KEY_OPENROUTER;
|
||||
delete process.env.API_KEY_OPENROUTER;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/ai/chat',
|
||||
headers: { authorization: `Bearer ${authToken}` },
|
||||
payload: { messages: [{ role: 'user', content: 'hello' }] },
|
||||
});
|
||||
expect(response.statusCode).toBe(503);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('AI service not configured');
|
||||
|
||||
// Restore original value if it existed
|
||||
if (originalValue !== undefined) {
|
||||
process.env.API_KEY_OPENROUTER = originalValue;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* Blocks API Tests – CRUD (List / Create / Update / Delete)
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { createServer } from '../src/server.js';
|
||||
|
||||
describe('Blocks API', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: SqliteAdapter;
|
||||
let drawingId: string;
|
||||
let createdBlockId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
app = await createServer({ db, port: 0 });
|
||||
await app.ready();
|
||||
|
||||
// Create project → drawing hierarchy
|
||||
const projRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Blocks Test Project' },
|
||||
});
|
||||
const projectId = JSON.parse(projRes.body).id;
|
||||
|
||||
const drawRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projectId}/drawings`,
|
||||
payload: { name: 'Blocks Test Drawing' },
|
||||
});
|
||||
drawingId = JSON.parse(drawRes.body).id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Create ─────────────────────────────────────────
|
||||
|
||||
describe('POST /api/drawings/:drawingId/blocks', () => {
|
||||
it('should create a block with 201', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/blocks`,
|
||||
payload: { name: 'Standard Table', category: 'Mobiliar', description: 'A standard round table' },
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.id).toBeDefined();
|
||||
expect(body.name).toBe('Standard Table');
|
||||
expect(body.category).toBe('Mobiliar');
|
||||
expect(body.description).toBe('A standard round table');
|
||||
expect(body.drawing_id).toBe(drawingId);
|
||||
createdBlockId = body.id;
|
||||
});
|
||||
|
||||
it('should create a block with default values', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/blocks`,
|
||||
payload: { name: 'Minimal Block' },
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Minimal Block');
|
||||
expect(body.category).toBe('Allgemein');
|
||||
expect(body.description).toBeNull();
|
||||
expect(body.elements_json).toBe('[]');
|
||||
});
|
||||
|
||||
it('should reject block without name with 400', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/blocks`,
|
||||
payload: { category: 'Test' },
|
||||
});
|
||||
expect(response.statusCode).toBe(400);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('Name is required');
|
||||
});
|
||||
|
||||
it('should reject block with empty name with 400', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/blocks`,
|
||||
payload: { name: '' },
|
||||
});
|
||||
expect(response.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── List ───────────────────────────────────────────
|
||||
|
||||
describe('GET /api/drawings/:drawingId/blocks', () => {
|
||||
it('should list blocks for a drawing', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${drawingId}/blocks`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should return empty array for drawing with no blocks', async () => {
|
||||
const projRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Empty Blocks Project' },
|
||||
});
|
||||
const projId = JSON.parse(projRes.body).id;
|
||||
const drawRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projId}/drawings`,
|
||||
payload: { name: 'Empty Drawing' },
|
||||
});
|
||||
const emptyDrawId = JSON.parse(drawRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${emptyDrawId}/blocks`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update ─────────────────────────────────────────
|
||||
|
||||
describe('PATCH /api/blocks/:id', () => {
|
||||
it('should update block name', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/blocks/${createdBlockId}`,
|
||||
payload: { name: 'Updated Block Name' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Updated Block Name');
|
||||
});
|
||||
|
||||
it('should update block category and description', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/blocks/${createdBlockId}`,
|
||||
payload: { category: 'Bühne', description: 'Stage equipment' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.category).toBe('Bühne');
|
||||
expect(body.description).toBe('Stage equipment');
|
||||
});
|
||||
|
||||
it('should update block elements_json', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/blocks/${createdBlockId}`,
|
||||
payload: { elements_json: '[{"type":"rect"}]' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.elements_json).toBe('[{"type":"rect"}]');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent block update', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: '/api/blocks/non-existent-id',
|
||||
payload: { name: 'New Name' },
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Delete ─────────────────────────────────────────
|
||||
|
||||
describe('DELETE /api/blocks/:id', () => {
|
||||
it('should delete a block', async () => {
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/blocks`,
|
||||
payload: { name: 'To Be Deleted' },
|
||||
});
|
||||
const blockId = JSON.parse(createRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/blocks/${blockId}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(204);
|
||||
|
||||
// Verify it's gone via list
|
||||
const listRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${drawingId}/blocks`,
|
||||
});
|
||||
const blocks = JSON.parse(listRes.body);
|
||||
expect(blocks.find((b: any) => b.id === blockId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent block delete', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/blocks/non-existent-id',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Drawings API Tests – CRUD (List / Get / Create / Update / Delete)
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { createServer } from '../src/server.js';
|
||||
|
||||
describe('Drawings API', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: SqliteAdapter;
|
||||
let projectId: string;
|
||||
let createdDrawingId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
app = await createServer({ db, port: 0 });
|
||||
await app.ready();
|
||||
|
||||
// Create a project first (drawings belong to projects)
|
||||
const projectRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Drawings Test Project' },
|
||||
});
|
||||
projectId = JSON.parse(projectRes.body).id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Create ─────────────────────────────────────────
|
||||
|
||||
describe('POST /api/projects/:projectId/drawings', () => {
|
||||
it('should create a drawing with 201', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projectId}/drawings`,
|
||||
payload: { name: 'Ground Floor' },
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.id).toBeDefined();
|
||||
expect(body.name).toBe('Ground Floor');
|
||||
expect(body.project_id).toBe(projectId);
|
||||
createdDrawingId = body.id;
|
||||
});
|
||||
|
||||
it('should create a drawing with default name', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projectId}/drawings`,
|
||||
payload: {},
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Unbenannt');
|
||||
expect(body.project_id).toBe(projectId);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── List ───────────────────────────────────────────
|
||||
|
||||
describe('GET /api/projects/:projectId/drawings', () => {
|
||||
it('should list drawings for a project', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/projects/${projectId}/drawings`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should return empty array for project with no drawings', async () => {
|
||||
// Create a new empty project
|
||||
const projRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Empty Project' },
|
||||
});
|
||||
const emptyProjId = JSON.parse(projRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/projects/${emptyProjId}/drawings`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Get ────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/drawings/:id', () => {
|
||||
it('should get a single drawing', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${createdDrawingId}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.id).toBe(createdDrawingId);
|
||||
expect(body.name).toBe('Ground Floor');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent drawing', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/drawings/non-existent-id',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update ─────────────────────────────────────────
|
||||
|
||||
describe('PATCH /api/drawings/:id', () => {
|
||||
it('should update drawing name', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/drawings/${createdDrawingId}`,
|
||||
payload: { name: 'First Floor' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('First Floor');
|
||||
});
|
||||
|
||||
it('should update drawing data_json', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/drawings/${createdDrawingId}`,
|
||||
payload: { data_json: '{"layers":[]}' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.data_json).toBe('{"layers":[]}');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent drawing update', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: '/api/drawings/non-existent-id',
|
||||
payload: { name: 'New Name' },
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Delete ─────────────────────────────────────────
|
||||
|
||||
describe('DELETE /api/drawings/:id', () => {
|
||||
it('should delete a drawing', async () => {
|
||||
// Create a drawing to delete
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projectId}/drawings`,
|
||||
payload: { name: 'To Be Deleted' },
|
||||
});
|
||||
const drawingId = JSON.parse(createRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/drawings/${drawingId}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(204);
|
||||
|
||||
// Verify it's gone
|
||||
const getRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${drawingId}`,
|
||||
});
|
||||
expect(getRes.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent drawing delete', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/drawings/non-existent-id',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Elements API Tests – CRUD (List / Create / Update / Delete)
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { createServer } from '../src/server.js';
|
||||
|
||||
describe('Elements API', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: SqliteAdapter;
|
||||
let drawingId: string;
|
||||
let layerId: string;
|
||||
let createdElementId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
app = await createServer({ db, port: 0 });
|
||||
await app.ready();
|
||||
|
||||
// Create project → drawing → layer hierarchy
|
||||
const projRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Elements Test Project' },
|
||||
});
|
||||
const projectId = JSON.parse(projRes.body).id;
|
||||
|
||||
const drawRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projectId}/drawings`,
|
||||
payload: { name: 'Elements Test Drawing' },
|
||||
});
|
||||
drawingId = JSON.parse(drawRes.body).id;
|
||||
|
||||
const layerRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/layers`,
|
||||
payload: { name: 'Elements Layer' },
|
||||
});
|
||||
layerId = JSON.parse(layerRes.body).id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Create ─────────────────────────────────────────
|
||||
|
||||
describe('POST /api/drawings/:drawingId/elements', () => {
|
||||
it('should create an element with 201', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/elements`,
|
||||
payload: { layer_id: layerId, type: 'rect', x: 10, y: 20, width: 100, height: 50 },
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.id).toBeDefined();
|
||||
expect(body.type).toBe('rect');
|
||||
expect(body.x).toBe(10);
|
||||
expect(body.y).toBe(20);
|
||||
expect(body.width).toBe(100);
|
||||
expect(body.height).toBe(50);
|
||||
expect(body.drawing_id).toBe(drawingId);
|
||||
expect(body.layer_id).toBe(layerId);
|
||||
createdElementId = body.id;
|
||||
});
|
||||
|
||||
it('should create an element with default values', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/elements`,
|
||||
payload: { layer_id: layerId, type: 'circle' },
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.type).toBe('circle');
|
||||
expect(body.x).toBe(0);
|
||||
expect(body.y).toBe(0);
|
||||
expect(body.width).toBe(0);
|
||||
expect(body.height).toBe(0);
|
||||
expect(body.properties_json).toBe('{}');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── List ───────────────────────────────────────────
|
||||
|
||||
describe('GET /api/drawings/:drawingId/elements', () => {
|
||||
it('should list elements for a drawing', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${drawingId}/elements`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should return empty array for drawing with no elements', async () => {
|
||||
// Create a new drawing with no elements
|
||||
const projRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Empty Elements Project' },
|
||||
});
|
||||
const projId = JSON.parse(projRes.body).id;
|
||||
const drawRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projId}/drawings`,
|
||||
payload: { name: 'Empty Drawing' },
|
||||
});
|
||||
const emptyDrawId = JSON.parse(drawRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${emptyDrawId}/elements`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update ─────────────────────────────────────────
|
||||
|
||||
describe('PATCH /api/elements/:id', () => {
|
||||
it('should update element position', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/elements/${createdElementId}`,
|
||||
payload: { x: 100, y: 200 },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.x).toBe(100);
|
||||
expect(body.y).toBe(200);
|
||||
});
|
||||
|
||||
it('should update element dimensions', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/elements/${createdElementId}`,
|
||||
payload: { width: 300, height: 150 },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.width).toBe(300);
|
||||
expect(body.height).toBe(150);
|
||||
});
|
||||
|
||||
it('should update element properties_json', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/elements/${createdElementId}`,
|
||||
payload: { properties_json: '{"color":"red"}' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.properties_json).toBe('{"color":"red"}');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent element update', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: '/api/elements/non-existent-id',
|
||||
payload: { x: 0 },
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Delete ─────────────────────────────────────────
|
||||
|
||||
describe('DELETE /api/elements/:id', () => {
|
||||
it('should delete an element', async () => {
|
||||
// Create an element to delete
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/elements`,
|
||||
payload: { layer_id: layerId, type: 'line' },
|
||||
});
|
||||
const elemId = JSON.parse(createRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/elements/${elemId}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(204);
|
||||
|
||||
// Verify it's gone via list
|
||||
const listRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${drawingId}/elements`,
|
||||
});
|
||||
const elements = JSON.parse(listRes.body);
|
||||
expect(elements.find((e: any) => e.id === elemId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent element delete', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/elements/non-existent-id',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Layers API Tests – CRUD (List / Create / Update / Delete)
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { createServer } from '../src/server.js';
|
||||
|
||||
describe('Layers API', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: SqliteAdapter;
|
||||
let drawingId: string;
|
||||
let createdLayerId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
app = await createServer({ db, port: 0 });
|
||||
await app.ready();
|
||||
|
||||
// Create project → drawing hierarchy
|
||||
const projRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Layers Test Project' },
|
||||
});
|
||||
const projectId = JSON.parse(projRes.body).id;
|
||||
|
||||
const drawRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projectId}/drawings`,
|
||||
payload: { name: 'Layers Test Drawing' },
|
||||
});
|
||||
drawingId = JSON.parse(drawRes.body).id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Create ─────────────────────────────────────────
|
||||
|
||||
describe('POST /api/drawings/:drawingId/layers', () => {
|
||||
it('should create a layer with 201', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/layers`,
|
||||
payload: { name: 'Background', color: '#ff0000' },
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.id).toBeDefined();
|
||||
expect(body.name).toBe('Background');
|
||||
expect(body.color).toBe('#ff0000');
|
||||
expect(body.drawing_id).toBe(drawingId);
|
||||
createdLayerId = body.id;
|
||||
});
|
||||
|
||||
it('should create a layer with default values', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/layers`,
|
||||
payload: {},
|
||||
});
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Layer');
|
||||
expect(body.visible).toBe(1);
|
||||
expect(body.locked).toBe(0);
|
||||
expect(body.color).toBe('#ffffff');
|
||||
expect(body.line_type).toBe('solid');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── List ───────────────────────────────────────────
|
||||
|
||||
describe('GET /api/drawings/:drawingId/layers', () => {
|
||||
it('should list layers for a drawing', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${drawingId}/layers`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it('should return empty array for drawing with no layers', async () => {
|
||||
// Create a new drawing with no layers
|
||||
const projRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/api/projects',
|
||||
payload: { name: 'Empty Layers Project' },
|
||||
});
|
||||
const projId = JSON.parse(projRes.body).id;
|
||||
const drawRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/projects/${projId}/drawings`,
|
||||
payload: { name: 'Empty Drawing' },
|
||||
});
|
||||
const emptyDrawId = JSON.parse(drawRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${emptyDrawId}/layers`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update ─────────────────────────────────────────
|
||||
|
||||
describe('PATCH /api/layers/:id', () => {
|
||||
it('should update layer name', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/layers/${createdLayerId}`,
|
||||
payload: { name: 'Updated Layer Name' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Updated Layer Name');
|
||||
});
|
||||
|
||||
it('should update layer visibility', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/layers/${createdLayerId}`,
|
||||
payload: { visible: 0, locked: 1 },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.visible).toBe(0);
|
||||
expect(body.locked).toBe(1);
|
||||
});
|
||||
|
||||
it('should update layer color and line_type', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/layers/${createdLayerId}`,
|
||||
payload: { color: '#00ff00', line_type: 'dashed' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.color).toBe('#00ff00');
|
||||
expect(body.line_type).toBe('dashed');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent layer update', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: '/api/layers/non-existent-id',
|
||||
payload: { name: 'New Name' },
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Delete ─────────────────────────────────────────
|
||||
|
||||
describe('DELETE /api/layers/:id', () => {
|
||||
it('should delete a layer', async () => {
|
||||
// Create a layer to delete
|
||||
const createRes = await app.inject({
|
||||
method: 'POST',
|
||||
url: `/api/drawings/${drawingId}/layers`,
|
||||
payload: { name: 'To Be Deleted' },
|
||||
});
|
||||
const layerId = JSON.parse(createRes.body).id;
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/layers/${layerId}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(204);
|
||||
|
||||
// Verify it's gone via list
|
||||
const listRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/drawings/${drawingId}/layers`,
|
||||
});
|
||||
const layers = JSON.parse(listRes.body);
|
||||
expect(layers.find((l: any) => l.id === layerId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent layer delete', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/layers/non-existent-id',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Settings API Tests – Key/Value settings (Get / Put / Upsert)
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { createServer } from '../src/server.js';
|
||||
|
||||
describe('Settings API', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: SqliteAdapter;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
app = await createServer({ db, port: 0 });
|
||||
await app.ready();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── Get (missing key → 404) ─────────────────────────
|
||||
|
||||
describe('GET /api/settings/:key', () => {
|
||||
it('should return 404 for non-existent setting', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/non-existent-key',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('Setting not found');
|
||||
});
|
||||
|
||||
it('should return a setting that was previously set', async () => {
|
||||
// First set a value
|
||||
await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/test-key',
|
||||
payload: { value: 'test-value' },
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/test-key',
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.key).toBe('test-key');
|
||||
expect(body.value).toBe('test-value');
|
||||
expect(body.updated_at).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Put (upsert) ───────────────────────────────────
|
||||
|
||||
describe('PUT /api/settings/:key', () => {
|
||||
it('should create a new setting', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/new-setting',
|
||||
payload: { value: 'new-value' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.key).toBe('new-setting');
|
||||
expect(body.value).toBe('new-value');
|
||||
expect(body.updated_at).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update an existing setting (upsert)', async () => {
|
||||
// Create initial
|
||||
await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/upsert-key',
|
||||
payload: { value: 'initial' },
|
||||
});
|
||||
|
||||
// Update it
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/upsert-key',
|
||||
payload: { value: 'updated' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.key).toBe('upsert-key');
|
||||
expect(body.value).toBe('updated');
|
||||
|
||||
// Verify via GET
|
||||
const getRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/upsert-key',
|
||||
});
|
||||
const getBody = JSON.parse(getRes.body);
|
||||
expect(getBody.value).toBe('updated');
|
||||
});
|
||||
|
||||
it('should reject setting without value', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/no-value-key',
|
||||
payload: {},
|
||||
});
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.error).toContain('Value is required');
|
||||
});
|
||||
|
||||
it('should handle complex JSON values', async () => {
|
||||
const complexValue = JSON.stringify({ nested: { object: true }, array: [1, 2, 3] });
|
||||
const response = await app.inject({
|
||||
method: 'PUT',
|
||||
url: '/api/settings/complex-config',
|
||||
payload: { value: complexValue },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const getRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/settings/complex-config',
|
||||
});
|
||||
const body = JSON.parse(getRes.body);
|
||||
expect(body.value).toBe(complexValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Users API Tests – List / Get / Update / Delete (password_hash stripping)
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||
import { createServer } from '../src/server.js';
|
||||
|
||||
describe('Users API', () => {
|
||||
let app: FastifyInstance;
|
||||
let db: SqliteAdapter;
|
||||
let testUserId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
db = new SqliteAdapter(':memory:');
|
||||
await db.init();
|
||||
app = await createServer({ db, port: 0 });
|
||||
await app.ready();
|
||||
|
||||
// Create a test user directly via DB (bypassing AuthService)
|
||||
const user = db.createUser({
|
||||
email: 'test-admin@example.com',
|
||||
password_hash: 'fake-hash-for-testing',
|
||||
name: 'Test Admin',
|
||||
role: 'admin',
|
||||
});
|
||||
testUserId = user.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
db.close();
|
||||
});
|
||||
|
||||
// ─── List ───────────────────────────────────────────
|
||||
|
||||
describe('GET /api/users', () => {
|
||||
it('should list all users', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/users',
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBeGreaterThanOrEqual(2); // default user + test user
|
||||
});
|
||||
|
||||
it('should strip password_hash from list responses', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/users',
|
||||
});
|
||||
const body = JSON.parse(response.body);
|
||||
for (const user of body) {
|
||||
expect(user.password_hash).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Get ────────────────────────────────────────────
|
||||
|
||||
describe('GET /api/users/:id', () => {
|
||||
it('should get a single user', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/users/${testUserId}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.id).toBe(testUserId);
|
||||
expect(body.email).toBe('test-admin@example.com');
|
||||
expect(body.name).toBe('Test Admin');
|
||||
expect(body.role).toBe('admin');
|
||||
});
|
||||
|
||||
it('should strip password_hash from single user response', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/users/${testUserId}`,
|
||||
});
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.password_hash).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent user', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/api/users/non-existent-id',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Update ─────────────────────────────────────────
|
||||
|
||||
describe('PATCH /api/users/:id', () => {
|
||||
it('should update user name', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/users/${testUserId}`,
|
||||
payload: { name: 'Updated Name' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Updated Name');
|
||||
expect(body.id).toBe(testUserId);
|
||||
});
|
||||
|
||||
it('should update user role', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/users/${testUserId}`,
|
||||
payload: { role: 'planer' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.role).toBe('planer');
|
||||
});
|
||||
|
||||
it('should strip password_hash from update response', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/users/${testUserId}`,
|
||||
payload: { name: 'Another Name' },
|
||||
});
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.password_hash).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not update password_hash via PATCH endpoint', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/users/${testUserId}`,
|
||||
payload: { password_hash: 'hacked-hash' },
|
||||
});
|
||||
expect(response.statusCode).toBe(200);
|
||||
// Verify the password_hash was NOT changed in DB
|
||||
const dbUser = db.getUser(testUserId);
|
||||
expect(dbUser!.password_hash).toBe('fake-hash-for-testing');
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent user update', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'PATCH',
|
||||
url: '/api/users/non-existent-id',
|
||||
payload: { name: 'New Name' },
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Delete ─────────────────────────────────────────
|
||||
|
||||
describe('DELETE /api/users/:id', () => {
|
||||
it('should delete a user', async () => {
|
||||
// Create a user to delete
|
||||
const user = db.createUser({
|
||||
email: 'delete-me@example.com',
|
||||
password_hash: 'temp-hash',
|
||||
name: 'Delete Me',
|
||||
role: 'planer',
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/users/${user.id}`,
|
||||
});
|
||||
expect(response.statusCode).toBe(204);
|
||||
|
||||
// Verify it's gone
|
||||
const getRes = await app.inject({
|
||||
method: 'GET',
|
||||
url: `/api/users/${user.id}`,
|
||||
});
|
||||
expect(getRes.statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it('should return 404 for non-existent user delete', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'DELETE',
|
||||
url: '/api/users/non-existent-id',
|
||||
});
|
||||
expect(response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user