test: Add unit tests for project CRUD API endpoints
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user