test: Add unit tests for project CRUD API endpoints

This commit is contained in:
2026-06-22 05:48:54 +00:00
parent 00b59368cc
commit 7b532e61bd
@@ -0,0 +1,551 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import fastify from 'fastify';
import projectsRoutes from '../projects';
import { DatabaseInterface } from '../../database/DatabaseInterface';
// Mock database interface
const mockDb = {
getSessionByToken: vi.fn(),
getUserById: vi.fn(),
getProjectsByOwner: vi.fn(),
createProject: vi.fn(),
createProjectMember: vi.fn(),
getProjectById: vi.fn(),
updateProject: vi.fn(),
deleteProject: vi.fn(),
getProjectMembers: vi.fn(),
getUserByEmail: vi.fn(),
isProjectMember: vi.fn(),
getProjectMemberById: vi.fn(),
updateProjectMember: vi.fn(),
deleteProjectMember: vi.fn(),
getVersionsByProject: vi.fn(),
createVersion: vi.fn(),
getVersionById: vi.fn(),
};
// Mock fastify instance
const app = fastify();
app.decorate('db', mockDb);
// Register routes
app.register(projectsRoutes, { prefix: '/projects' });
// Test data
const mockUser = {
id: 'user-1',
email: 'test@example.com',
password_hash: 'hash',
display_name: 'Test User',
role: 'Planner',
is_active: 1,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
};
const mockProject = {
id: 'project-1',
name: 'Test Project',
description: 'Test Description',
owner_id: 'user-1',
units: 'm',
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
thumbnail_path: 'path/to/thumbnail',
};
const mockVersion = {
id: 'version-1',
project_id: 'project-1',
label: 'v1.0',
description: 'Initial version',
snapshot_data: Buffer.from('{}'),
created_by: 'user-1',
created_at: '2023-01-01T00:00:00Z',
};
const mockMember = {
id: 'member-1',
project_id: 'project-1',
user_id: 'user-2',
role: 'Betrachter',
invited_at: '2023-01-01T00:00:00Z',
accepted_at: '2023-01-01T00:00:00Z',
};
const mockSession = {
id: 'session-1',
user_id: 'user-1',
token_hash: 'token-hash',
expires_at: '2026-01-01T00:00:00Z',
created_at: '2023-01-01T00:00:00Z',
ip_address: '127.0.0.1',
user_agent: 'test-agent',
};
// Tests
describe('Project Routes', () => {
beforeEach(() => {
// Clear all mocks
vi.clearAllMocks();
// Setup default mock responses
mockDb.getSessionByToken.mockResolvedValue(mockSession);
mockDb.getUserById.mockResolvedValue(mockUser);
mockDb.isProjectMember.mockResolvedValue(true);
});
describe('GET /projects', () => {
it('should return user projects when authenticated', async () => {
mockDb.getProjectsByOwner.mockResolvedValue([mockProject]);
const response = await app.inject({
method: 'GET',
url: '/projects',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual([mockProject]);
expect(mockDb.getProjectsByOwner).toHaveBeenCalledWith('user-1');
});
it('should return 401 when not authenticated', async () => {
const response = await app.inject({
method: 'GET',
url: '/projects',
});
expect(response.statusCode).toBe(401);
});
});
describe('POST /projects', () => {
it('should create a new project when authenticated', async () => {
mockDb.createProject.mockResolvedValue(mockProject);
mockDb.createProjectMember.mockResolvedValue({
id: 'member-1',
project_id: 'project-1',
user_id: 'user-1',
role: 'Admin',
invited_at: '2023-01-01T00:00:00Z',
accepted_at: '2023-01-01T00:00:00Z',
});
const response = await app.inject({
method: 'POST',
url: '/projects',
headers: {
authorization: 'Bearer valid-token',
},
body: {
name: 'Test Project',
description: 'Test Description',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual(mockProject);
expect(mockDb.createProject).toHaveBeenCalled();
expect(mockDb.createProjectMember).toHaveBeenCalled();
});
it('should return 401 when not authenticated', async () => {
const response = await app.inject({
method: 'POST',
url: '/projects',
body: {
name: 'Test Project',
},
});
expect(response.statusCode).toBe(401);
});
});
describe('GET /projects/:id', () => {
it('should return project details when user is a member', async () => {
mockDb.getProjectById.mockResolvedValue(mockProject);
const response = await app.inject({
method: 'GET',
url: '/projects/project-1',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual(mockProject);
});
it('should return 404 when project not found', async () => {
mockDb.getProjectById.mockResolvedValue(null);
const response = await app.inject({
method: 'GET',
url: '/projects/project-2',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(404);
});
});
describe('PUT /projects/:id', () => {
it('should update project when user is Planner or Admin and a member', async () => {
mockDb.getProjectById.mockResolvedValue(mockProject);
mockDb.updateProject.mockResolvedValue({
...mockProject,
name: 'Updated Project',
});
const response = await app.inject({
method: 'PUT',
url: '/projects/project-1',
headers: {
authorization: 'Bearer valid-token',
},
body: {
name: 'Updated Project',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body).name).toBe('Updated Project');
});
it('should return 403 when user is not Planner or Admin', async () => {
mockDb.getUserById.mockResolvedValue({
...mockUser,
role: 'Betrachter',
});
const response = await app.inject({
method: 'PUT',
url: '/projects/project-1',
headers: {
authorization: 'Bearer valid-token',
},
body: {
name: 'Updated Project',
},
});
expect(response.statusCode).toBe(403);
});
});
describe('DELETE /projects/:id', () => {
it('should delete project when user is Admin and a member', async () => {
mockDb.getProjectById.mockResolvedValue(mockProject);
const response = await app.inject({
method: 'DELETE',
url: '/projects/project-1',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual({ message: 'Project deleted successfully' });
expect(mockDb.deleteProject).toHaveBeenCalledWith('project-1');
});
it('should return 403 when user is not Admin', async () => {
mockDb.getUserById.mockResolvedValue({
...mockUser,
role: 'Planner',
});
const response = await app.inject({
method: 'DELETE',
url: '/projects/project-1',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(403);
});
});
describe('GET /projects/:id/members', () => {
it('should return project members when user is a member', async () => {
mockDb.getProjectMembers.mockResolvedValue([mockMember]);
const response = await app.inject({
method: 'GET',
url: '/projects/project-1/members',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual([mockMember]);
});
});
describe('POST /projects/:id/members', () => {
it('should invite member when user is Admin and a member', async () => {
mockDb.getUserByEmail.mockResolvedValue({
id: 'user-2',
email: 'invitee@example.com',
password_hash: 'hash',
display_name: 'Invitee User',
role: 'Betrachter',
is_active: 1,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
});
mockDb.isProjectMember.mockImplementation((projectId, userId) =>
Promise.resolve(userId !== 'user-2')
);
mockDb.createProjectMember.mockResolvedValue(mockMember);
const response = await app.inject({
method: 'POST',
url: '/projects/project-1/members',
headers: {
authorization: 'Bearer valid-token',
},
body: {
email: 'invitee@example.com',
role: 'Betrachter',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual(mockMember);
});
it('should return 403 when user is not Admin', async () => {
mockDb.getUserById.mockResolvedValue({
...mockUser,
role: 'Planner',
});
const response = await app.inject({
method: 'POST',
url: '/projects/project-1/members',
headers: {
authorization: 'Bearer valid-token',
},
body: {
email: 'invitee@example.com',
role: 'Betrachter',
},
});
expect(response.statusCode).toBe(403);
});
});
describe('PUT /projects/:id/members/:userId', () => {
it('should update member role when user is Admin and a member', async () => {
mockDb.getProjectMemberById.mockResolvedValue(mockMember);
mockDb.updateProjectMember.mockResolvedValue({
...mockMember,
role: 'Planner',
});
const response = await app.inject({
method: 'PUT',
url: '/projects/project-1/members/user-2',
headers: {
authorization: 'Bearer valid-token',
},
body: {
role: 'Planner',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body).role).toBe('Planner');
});
it('should return 403 when user is not Admin', async () => {
mockDb.getUserById.mockResolvedValue({
...mockUser,
role: 'Planner',
});
const response = await app.inject({
method: 'PUT',
url: '/projects/project-1/members/user-2',
headers: {
authorization: 'Bearer valid-token',
},
body: {
role: 'Planner',
},
});
expect(response.statusCode).toBe(403);
});
});
describe('DELETE /projects/:id/members/:userId', () => {
it('should remove member when user is Admin and a member', async () => {
mockDb.getProjectMemberById.mockResolvedValue(mockMember);
const response = await app.inject({
method: 'DELETE',
url: '/projects/project-1/members/user-2',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual({ message: 'Member removed successfully' });
expect(mockDb.deleteProjectMember).toHaveBeenCalledWith('user-2');
});
it('should return 403 when user is not Admin', async () => {
mockDb.getUserById.mockResolvedValue({
...mockUser,
role: 'Planner',
});
const response = await app.inject({
method: 'DELETE',
url: '/projects/project-1/members/user-2',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(403);
});
});
describe('GET /projects/:id/versions', () => {
it('should return project versions when user is a member', async () => {
mockDb.getVersionsByProject.mockResolvedValue([mockVersion]);
const response = await app.inject({
method: 'GET',
url: '/projects/project-1/versions',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual([mockVersion]);
});
});
describe('POST /projects/:id/versions', () => {
it('should create version when user is Planner or Admin and a member', async () => {
mockDb.createVersion.mockResolvedValue(mockVersion);
const response = await app.inject({
method: 'POST',
url: '/projects/project-1/versions',
headers: {
authorization: 'Bearer valid-token',
},
body: {
label: 'v1.0',
description: 'Initial version',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual(mockVersion);
});
it('should return 403 when user is not Planner or Admin', async () => {
mockDb.getUserById.mockResolvedValue({
...mockUser,
role: 'Betrachter',
});
const response = await app.inject({
method: 'POST',
url: '/projects/project-1/versions',
headers: {
authorization: 'Bearer valid-token',
},
body: {
label: 'v1.0',
},
});
expect(response.statusCode).toBe(403);
});
});
describe('GET /projects/:id/versions/:vId', () => {
it('should return version details when user is a member', async () => {
mockDb.getVersionById.mockResolvedValue(mockVersion);
const response = await app.inject({
method: 'GET',
url: '/projects/project-1/versions/version-1',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual(mockVersion);
});
it('should return 404 when version not found', async () => {
mockDb.getVersionById.mockResolvedValue(null);
const response = await app.inject({
method: 'GET',
url: '/projects/project-1/versions/version-2',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(404);
});
});
describe('POST /projects/:id/versions/:vId/restore', () => {
it('should restore version when user is Planner or Admin and a member', async () => {
mockDb.getVersionById.mockResolvedValue(mockVersion);
const response = await app.inject({
method: 'POST',
url: '/projects/project-1/versions/version-1/restore',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(200);
expect(JSON.parse(response.body)).toEqual({ message: 'Version restored successfully' });
});
it('should return 403 when user is not Planner or Admin', async () => {
mockDb.getUserById.mockResolvedValue({
...mockUser,
role: 'Betrachter',
});
const response = await app.inject({
method: 'POST',
url: '/projects/project-1/versions/version-1/restore',
headers: {
authorization: 'Bearer valid-token',
},
});
expect(response.statusCode).toBe(403);
});
});
});