tududi/backend/tests/integration/project-sharing.test.js
2025-12-08 16:33:49 +02:00

619 lines
23 KiB
JavaScript

const request = require('supertest');
const app = require('../../app');
const {
User,
Project,
Task,
Note,
Permission,
TaskAttachment,
sequelize,
} = require('../../models');
const { createTestUser } = require('../helpers/testUtils');
describe('Project Sharing Integration Tests', () => {
let ownerUser, sharedUser, ownerAgent, sharedUserAgent, project;
beforeEach(async () => {
// Create test users using test helper
ownerUser = await createTestUser({
email: `owner_${Date.now()}@test.com`,
name: 'Owner',
timezone: 'UTC',
});
sharedUser = await createTestUser({
email: `shared_${Date.now()}@test.com`,
name: 'Shared User',
timezone: 'UTC',
});
// Create agents for both users (maintains sessions)
ownerAgent = request.agent(app);
sharedUserAgent = request.agent(app);
// Login as owner
await ownerAgent
.post('/api/login')
.send({ email: ownerUser.email, password: 'password123' });
// Login as shared user
await sharedUserAgent
.post('/api/login')
.send({ email: sharedUser.email, password: 'password123' });
// Create a project as owner
const projectResponse = await ownerAgent.post('/api/project').send({
name: 'Shared Test Project',
description: 'Project for sharing tests',
});
project = projectResponse.body;
// Share the project with read-write access
await ownerAgent.post('/api/shares').send({
resource_type: 'project',
resource_uid: project.uid,
target_user_email: sharedUser.email,
access_level: 'rw',
});
});
afterAll(async () => {
await sequelize.close();
});
describe('Project visibility', () => {
test('shared user should see shared project in /api/projects', async () => {
const response = await sharedUserAgent.get('/api/projects');
expect(response.status).toBe(200);
expect(response.body.projects).toBeDefined();
const sharedProjectUids = response.body.projects.map((p) => p.uid);
expect(sharedProjectUids).toContain(project.uid);
});
test('shared project should have is_shared flag and share_count in /api/projects', async () => {
const response = await ownerAgent.get('/api/projects');
expect(response.status).toBe(200);
expect(response.body.projects).toBeDefined();
const sharedProject = response.body.projects.find(
(p) => p.uid === project.uid
);
expect(sharedProject).toBeDefined();
expect(sharedProject.is_shared).toBe(true);
expect(sharedProject.share_count).toBeGreaterThan(0);
});
test('owner can fetch share list with emails via /api/shares', async () => {
const response = await ownerAgent.get(
`/api/shares?resource_type=project&resource_uid=${project.uid}`
);
expect(response.status).toBe(200);
expect(response.body.shares).toBeDefined();
expect(Array.isArray(response.body.shares)).toBe(true);
// Should include owner
const ownerShare = response.body.shares.find(
(s) => s.is_owner === true
);
expect(ownerShare).toBeDefined();
expect(ownerShare.email).toBe(ownerUser.email);
expect(ownerShare.access_level).toBe('owner');
// Avatar field should exist (may be null if no avatar set)
expect(ownerShare).toHaveProperty('avatar_image');
// Should include shared user
const sharedUserShare = response.body.shares.find(
(s) => s.email === sharedUser.email
);
expect(sharedUserShare).toBeDefined();
expect(sharedUserShare.is_owner).toBe(false);
expect(sharedUserShare.email).toBe(sharedUser.email);
expect(['ro', 'rw']).toContain(sharedUserShare.access_level);
// Avatar field should exist (may be null if no avatar set)
expect(sharedUserShare).toHaveProperty('avatar_image');
});
});
describe('Issue 1: Task and Note Visibility in Shared Projects', () => {
test('shared user should see tasks in shared project', async () => {
// Owner creates a task in the shared project
const taskResponse = await ownerAgent.post('/api/task').send({
name: 'Task by owner in shared project',
project_id: project.id,
priority: 1,
status: 0,
});
const taskInSharedProject = taskResponse.body;
// Shared user should see this task
const response = await sharedUserAgent.get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
const foundTask = response.body.tasks.find(
(t) => t.id === taskInSharedProject.id
);
expect(foundTask).toBeDefined();
expect(foundTask.name).toBe('Task by owner in shared project');
});
test('shared user should see notes in shared project', async () => {
// Owner creates a note in the shared project
const noteResponse = await ownerAgent.post('/api/note').send({
title: 'Note by owner in shared project',
content: 'This note should be visible to shared user',
project_uid: project.uid,
});
const noteInSharedProject = noteResponse.body;
// Shared user should see this note
const response = await sharedUserAgent.get('/api/notes');
expect(response.status).toBe(200);
expect(response.body).toBeDefined();
const foundNote = response.body.find(
(n) => n.id === noteInSharedProject.id
);
expect(foundNote).toBeDefined();
expect(foundNote.title).toBe('Note by owner in shared project');
});
test('shared user should NOT see tasks in non-shared projects', async () => {
// Create another project (not shared)
const privateProjectResponse = await ownerAgent
.post('/api/project')
.send({
name: 'Private Project',
description: 'This should not be visible',
});
const privateProject = privateProjectResponse.body;
// Create task in private project
const privateTaskResponse = await ownerAgent
.post('/api/task')
.send({
name: 'Private task',
project_id: privateProject.id,
priority: 1,
status: 0,
});
const privateTask = privateTaskResponse.body;
// Shared user should NOT see this task
const response = await sharedUserAgent.get('/api/tasks');
expect(response.status).toBe(200);
const foundTask = response.body.tasks.find(
(t) => t.id === privateTask.id
);
expect(foundTask).toBeUndefined();
});
test('shared user should see tasks due today in shared projects', async () => {
// Create a task due today in shared project
const today = new Date().toISOString().split('T')[0];
const taskDueTodayResponse = await ownerAgent
.post('/api/task')
.send({
name: 'Task due today in shared project',
project_id: project.id,
due_date: today,
priority: 1,
status: 0,
});
const taskDueToday = taskDueTodayResponse.body;
// Fetch today's tasks as shared user with include_lists to get tasks_due_today
const response = await sharedUserAgent.get(
'/api/tasks?type=today&include_lists=true'
);
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
// Task should appear in tasks_due_today since it has a due date but today=false
expect(response.body.tasks_due_today).toBeDefined();
const foundTask = response.body.tasks_due_today.find(
(t) => t.id === taskDueToday.id
);
expect(foundTask).toBeDefined();
expect(foundTask.name).toBe('Task due today in shared project');
});
});
describe('Issue 2: Task and Note Creation in Shared Projects', () => {
test('shared user with RW access can create tasks in shared project', async () => {
const response = await sharedUserAgent.post('/api/task').send({
name: 'Task created by shared user',
project_id: project.id,
priority: 1,
status: 0,
});
expect(response.status).toBe(201);
expect(response.body).toBeDefined();
expect(response.body.name).toBe('Task created by shared user');
expect(response.body.project_id).toBe(project.id);
});
test('shared user with RW access can create notes in shared project', async () => {
const response = await sharedUserAgent.post('/api/note').send({
title: 'Note created by shared user',
content: 'Content of the note',
project_uid: project.uid,
});
expect(response.status).toBe(201);
expect(response.body).toBeDefined();
expect(response.body.title).toBe('Note created by shared user');
});
test('shared user with RO access cannot create tasks', async () => {
// Change permission to read-only
await Permission.update(
{ access_level: 'ro' },
{
where: {
resource_uid: project.uid,
user_id: sharedUser.id,
},
}
);
const response = await sharedUserAgent.post('/api/task').send({
name: 'Task that should fail',
project_id: project.id,
priority: 1,
status: 0,
});
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
});
test('shared user with RO access cannot create notes', async () => {
// Change permission to read-only
await Permission.update(
{ access_level: 'ro' },
{
where: {
resource_uid: project.uid,
user_id: sharedUser.id,
},
}
);
const response = await sharedUserAgent.post('/api/note').send({
title: 'Note that should fail',
content: 'Content',
project_uid: project.uid,
});
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
});
});
describe('Task Timeline Access', () => {
test('shared user can access task timeline in shared project', async () => {
// Create a task in the shared project
const taskResponse = await ownerAgent.post('/api/task').send({
name: 'Task with timeline',
project_id: project.id,
priority: 1,
status: 0,
});
const taskInSharedProject = taskResponse.body;
// Shared user should be able to access the timeline
const response = await sharedUserAgent.get(
`/api/task/${taskInSharedProject.uid}/timeline`
);
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
});
test('shared user cannot access task timeline in non-shared project', async () => {
// Create a private project and task
const privateProjectResponse = await ownerAgent
.post('/api/project')
.send({
name: 'Private Project for Timeline',
});
const privateProject = privateProjectResponse.body;
const privateTaskResponse = await ownerAgent
.post('/api/task')
.send({
name: 'Private task',
project_id: privateProject.id,
priority: 1,
status: 0,
});
const privateTask = privateTaskResponse.body;
// Shared user should NOT access this timeline
const response = await sharedUserAgent.get(
`/api/task/${privateTask.uid}/timeline`
);
expect(response.status).toBe(404);
});
test('shared user can access completion time analytics', async () => {
// Create a task in the shared project
const taskResponse = await ownerAgent.post('/api/task').send({
name: 'Task for completion analytics',
project_id: project.id,
priority: 1,
status: 0,
});
const taskInSharedProject = taskResponse.body;
const response = await sharedUserAgent.get(
`/api/task/${taskInSharedProject.uid}/completion-time`
);
// Should return 404 if not completed, or 200 with data if completed
expect([200, 404]).toContain(response.status);
});
});
describe('Owner sees their own tasks correctly', () => {
test('owner should see all their tasks including those in shared projects', async () => {
// Create a task in the shared project
await ownerAgent.post('/api/task').send({
name: 'Owner task in shared project',
project_id: project.id,
priority: 1,
status: 0,
});
const response = await ownerAgent.get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
expect(response.body.tasks.length).toBeGreaterThan(0);
});
test('owner should see tasks due today including shared project tasks', async () => {
const today = new Date().toISOString().split('T')[0];
await ownerAgent.post('/api/task').send({
name: 'Owner task due today',
project_id: project.id,
due_date: today,
priority: 1,
status: 0,
});
const response = await ownerAgent.get('/api/tasks?type=today');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
});
});
describe('Task Attachments in Shared Projects', () => {
let taskInSharedProject;
beforeEach(async () => {
// Owner creates a task in the shared project
const taskResponse = await ownerAgent.post('/api/task').send({
name: 'Task with attachments in shared project',
project_id: project.id,
priority: 1,
status: 0,
});
taskInSharedProject = taskResponse.body;
});
test('shared user with RW access can view attachments in shared project task', async () => {
// Owner creates an attachment on the task
const attachment = await TaskAttachment.create({
task_id: taskInSharedProject.id,
user_id: ownerUser.id,
original_filename: 'shared-document.pdf',
stored_filename: 'task-shared-123.pdf',
file_size: 1024,
mime_type: 'application/pdf',
file_path: 'tasks/task-shared-123.pdf',
});
// Shared user should be able to view attachments
const response = await sharedUserAgent.get(
`/api/tasks/${taskInSharedProject.uid}/attachments`
);
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBe(1);
expect(response.body[0].original_filename).toBe(
'shared-document.pdf'
);
});
test('shared user with RW access can upload attachments to shared project task', async () => {
const path = require('path');
const fs = require('fs').promises;
const testFilesDir = path.join(__dirname, '../test-files');
// Ensure test files directory exists
await fs.mkdir(testFilesDir, { recursive: true });
// Create a test file
const testFilePath = path.join(testFilesDir, 'test-upload.pdf');
await fs.writeFile(
testFilePath,
'Test PDF content for shared user'
);
// Shared user uploads attachment
const response = await sharedUserAgent
.post('/api/upload/task-attachment')
.field('taskUid', taskInSharedProject.uid)
.attach('file', testFilePath);
expect(response.status).toBe(201);
expect(response.body.original_filename).toBe('test-upload.pdf');
expect(response.body.task_id).toBe(taskInSharedProject.id);
});
test('shared user with RW access can delete attachments from shared project task', async () => {
// Owner creates an attachment on the task
const attachment = await TaskAttachment.create({
task_id: taskInSharedProject.id,
user_id: ownerUser.id,
original_filename: 'to-delete.pdf',
stored_filename: 'task-delete-shared-123.pdf',
file_size: 1024,
mime_type: 'application/pdf',
file_path: 'tasks/task-delete-shared-123.pdf',
});
// Shared user deletes the attachment
const response = await sharedUserAgent.delete(
`/api/tasks/${taskInSharedProject.uid}/attachments/${attachment.uid}`
);
expect(response.status).toBe(200);
expect(response.body.message).toBe(
'Attachment deleted successfully'
);
});
test('shared user with RW access can download attachments from shared project task', async () => {
const path = require('path');
const fs = require('fs').promises;
const uploadPath = path.join(__dirname, '../../uploads/tasks');
// Create upload directory
await fs.mkdir(uploadPath, { recursive: true });
// Create a test file
const testFilePath = path.join(
uploadPath,
'task-download-shared.pdf'
);
await fs.writeFile(testFilePath, 'test download content');
// Owner creates an attachment on the task
const attachment = await TaskAttachment.create({
task_id: taskInSharedProject.id,
user_id: ownerUser.id,
original_filename: 'download-shared.pdf',
stored_filename: 'task-download-shared.pdf',
file_size: 1024,
mime_type: 'application/pdf',
file_path: 'tasks/task-download-shared.pdf',
});
// Shared user downloads the attachment
const response = await sharedUserAgent.get(
`/api/attachments/${attachment.uid}/download`
);
expect(response.status).toBe(200);
});
test('shared user with RO access can view attachments but not upload', async () => {
// Change permission to read-only
await Permission.update(
{ access_level: 'ro' },
{
where: {
resource_uid: project.uid,
user_id: sharedUser.id,
},
}
);
// Owner creates an attachment on the task
await TaskAttachment.create({
task_id: taskInSharedProject.id,
user_id: ownerUser.id,
original_filename: 'readonly-doc.pdf',
stored_filename: 'task-readonly-123.pdf',
file_size: 1024,
mime_type: 'application/pdf',
file_path: 'tasks/task-readonly-123.pdf',
});
// Shared user can view attachments
const viewResponse = await sharedUserAgent.get(
`/api/tasks/${taskInSharedProject.uid}/attachments`
);
expect(viewResponse.status).toBe(200);
expect(viewResponse.body.length).toBe(1);
// But cannot upload
const path = require('path');
const fs = require('fs').promises;
const testFilesDir = path.join(__dirname, '../test-files');
await fs.mkdir(testFilesDir, { recursive: true });
const testFilePath = path.join(testFilesDir, 'test-ro-upload.pdf');
await fs.writeFile(testFilePath, 'Test content');
const uploadResponse = await sharedUserAgent
.post('/api/upload/task-attachment')
.field('taskUid', taskInSharedProject.uid)
.attach('file', testFilePath);
expect(uploadResponse.status).toBe(403);
});
test('shared user with RO access can download but not delete attachments', async () => {
// Change permission to read-only
await Permission.update(
{ access_level: 'ro' },
{
where: {
resource_uid: project.uid,
user_id: sharedUser.id,
},
}
);
const path = require('path');
const fs = require('fs').promises;
const uploadPath = path.join(__dirname, '../../uploads/tasks');
await fs.mkdir(uploadPath, { recursive: true });
const testFilePath = path.join(uploadPath, 'task-ro-download.pdf');
await fs.writeFile(testFilePath, 'test content');
// Owner creates an attachment
const attachment = await TaskAttachment.create({
task_id: taskInSharedProject.id,
user_id: ownerUser.id,
original_filename: 'ro-test.pdf',
stored_filename: 'task-ro-download.pdf',
file_size: 1024,
mime_type: 'application/pdf',
file_path: 'tasks/task-ro-download.pdf',
});
// Can download
const downloadResponse = await sharedUserAgent.get(
`/api/attachments/${attachment.uid}/download`
);
expect(downloadResponse.status).toBe(200);
// Cannot delete
const deleteResponse = await sharedUserAgent.delete(
`/api/tasks/${taskInSharedProject.uid}/attachments/${attachment.uid}`
);
expect(deleteResponse.status).toBe(403);
});
});
});