619 lines
23 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|