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