From dde704d764d2c37f1d25d8675eb7478cdf9ef3be Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Wed, 22 Oct 2025 12:10:50 +0300 Subject: [PATCH 1/4] Fix an issue with tasks permissions on shared project --- backend/routes/task-events.js | 30 ++++++++++- backend/services/permissionsService.js | 74 +++++++++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/backend/routes/task-events.js b/backend/routes/task-events.js index 16a26df..3e01d3a 100644 --- a/backend/routes/task-events.js +++ b/backend/routes/task-events.js @@ -16,8 +16,21 @@ router.get('/task/:uid/timeline', async (req, res) => { if (!isValidUid(req.params.uid)) return res.status(400).json({ error: 'Invalid UID' }); + const permissionsService = require('../services/permissionsService'); + + // Check if user has access to the task (either owns it or has access through shared project) + const access = await permissionsService.getAccess( + req.currentUser.id, + 'task', + req.params.uid + ); + + if (access === 'none') { + return res.status(404).json({ error: 'Task not found' }); + } + const task = await Task.findOne({ - where: { uid: req.params.uid, user_id: req.currentUser.id }, + where: { uid: req.params.uid }, }); if (!task) { @@ -39,8 +52,21 @@ router.get('/task/:uid/completion-time', async (req, res) => { if (!isValidUid(req.params.uid)) return res.status(400).json({ error: 'Invalid UID' }); + const permissionsService = require('../services/permissionsService'); + + // Check if user has access to the task (either owns it or has access through shared project) + const access = await permissionsService.getAccess( + req.currentUser.id, + 'task', + req.params.uid + ); + + if (access === 'none') { + return res.status(404).json({ error: 'Task not found' }); + } + const task = await Task.findOne({ - where: { uid: req.params.uid, user_id: req.currentUser.id }, + where: { uid: req.params.uid }, }); if (!task) { diff --git a/backend/services/permissionsService.js b/backend/services/permissionsService.js index 3d971fd..d3d5333 100644 --- a/backend/services/permissionsService.js +++ b/backend/services/permissionsService.js @@ -29,19 +29,57 @@ async function getAccess(userId, resourceType, resourceUid) { } else if (resourceType === 'task') { const t = await Task.findOne({ where: { uid: resourceUid }, - attributes: ['user_id'], + attributes: ['user_id', 'project_id'], raw: true, }); if (!t) return ACCESS.NONE; if (t.user_id === userId) return ACCESS.RW; + + // Check if user has access through the parent project + if (t.project_id) { + const project = await Project.findOne({ + where: { id: t.project_id }, + attributes: ['uid'], + raw: true, + }); + if (project) { + const projectAccess = await getAccess( + userId, + 'project', + project.uid + ); + if (projectAccess !== ACCESS.NONE) { + return projectAccess; // Inherit access from project + } + } + } } else if (resourceType === 'note') { const n = await Note.findOne({ where: { uid: resourceUid }, - attributes: ['user_id'], + attributes: ['user_id', 'project_id'], raw: true, }); if (!n) return ACCESS.NONE; if (n.user_id === userId) return ACCESS.RW; + + // Check if user has access through the parent project + if (n.project_id) { + const project = await Project.findOne({ + where: { id: n.project_id }, + attributes: ['uid'], + raw: true, + }); + if (project) { + const projectAccess = await getAccess( + userId, + 'project', + project.uid + ); + if (projectAccess !== ACCESS.NONE) { + return projectAccess; // Inherit access from project + } + } + } } // shared @@ -64,6 +102,38 @@ async function ownershipOrPermissionWhere(resourceType, userId) { } const sharedUids = await getSharedUidsForUser(resourceType, userId); + + // For tasks and notes, also include items from shared projects + if (resourceType === 'task' || resourceType === 'note') { + const sharedProjectUids = await getSharedUidsForUser('project', userId); + + // Get the project IDs for shared projects + let sharedProjectIds = []; + if (sharedProjectUids.length > 0) { + const projects = await Project.findAll({ + where: { uid: { [Op.in]: sharedProjectUids } }, + attributes: ['id'], + raw: true, + }); + sharedProjectIds = projects.map((p) => p.id); + } + + const conditions = [ + { user_id: userId }, // Items owned by user + ]; + + if (sharedUids.length > 0) { + conditions.push({ uid: { [Op.in]: sharedUids } }); // Items directly shared with user + } + + if (sharedProjectIds.length > 0) { + conditions.push({ project_id: { [Op.in]: sharedProjectIds } }); // Items in shared projects + } + + return { [Op.or]: conditions }; + } + + // For other resource types (projects, etc.), use the original logic return { [Op.or]: [ { user_id: userId }, From bbc4615ee3b999f6e11d8526c257897943d45b7f Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Wed, 22 Oct 2025 13:29:37 +0300 Subject: [PATCH 2/4] Fix an issue with sharing permissions --- backend/middleware/auth.js | 7 +++++ backend/routes/tasks.js | 36 ++++++++++++---------- backend/services/permissionsService.js | 42 +++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 17 deletions(-) diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 58c5415..f2ada0e 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -18,6 +18,13 @@ const requireAuth = async (req, res, next) => { return res.status(401).json({ error: 'User not found' }); } + // Debug logging to verify correct user is authenticated + if (req.path.includes('/tasks') && req.method === 'GET') { + console.log( + `[AUTH DEBUG] ${req.method} ${req.path} - User: ${user.email} (ID: ${user.id})` + ); + } + req.currentUser = user; next(); } catch (error) { diff --git a/backend/routes/tasks.js b/backend/routes/tasks.js index 445dedc..52bebcb 100644 --- a/backend/routes/tasks.js +++ b/backend/routes/tasks.js @@ -838,24 +838,28 @@ async function computeTaskMetrics(userId, userTimezone = 'UTC') { const tasksDueToday = await Task.findAll({ where: { - ...visibleTasksWhere, - status: { - [Op.notIn]: [ - Task.STATUS.DONE, - Task.STATUS.ARCHIVED, - 'done', - 'archived', - ], - }, - parent_task_id: null, // Exclude subtasks - recurring_parent_id: null, // Exclude recurring instances - [Op.or]: [ - { due_date: { [Op.lte]: todayBounds.end } }, - sequelize.literal(`EXISTS ( - SELECT 1 FROM projects - WHERE projects.id = Task.project_id + [Op.and]: [ + visibleTasksWhere, + { + status: { + [Op.notIn]: [ + Task.STATUS.DONE, + Task.STATUS.ARCHIVED, + 'done', + 'archived', + ], + }, + parent_task_id: null, // Exclude subtasks + recurring_parent_id: null, // Exclude recurring instances + [Op.or]: [ + { due_date: { [Op.lte]: todayBounds.end } }, + sequelize.literal(`EXISTS ( + SELECT 1 FROM projects + WHERE projects.id = Task.project_id AND projects.due_date_at <= '${todayBounds.end.toISOString()}' )`), + ], + }, ], }, include: [ diff --git a/backend/services/permissionsService.js b/backend/services/permissionsService.js index d3d5333..f55935e 100644 --- a/backend/services/permissionsService.js +++ b/backend/services/permissionsService.js @@ -97,15 +97,47 @@ async function getAccess(userId, resourceType, resourceUid) { async function ownershipOrPermissionWhere(resourceType, userId) { // Admin users can see all resources - if (await isAdmin(userId)) { + // Note: isAdmin expects a UID, but we might receive a numeric ID + // Get the user's UID if we received a numeric ID + let userUid = userId; + if (typeof userId === 'number' || !isNaN(parseInt(userId))) { + const { User } = require('../models'); + const user = await User.findByPk(userId, { + attributes: ['uid', 'email'], + }); + if (user) { + userUid = user.uid; + console.log( + `[PERMISSIONS DEBUG] User lookup: ID=${userId}, UID=${userUid}, Email=${user.email}` + ); + } + } + + const isUserAdmin = await isAdmin(userUid); + console.log( + `[PERMISSIONS DEBUG] Resource: ${resourceType}, UserId: ${userId}, IsAdmin: ${isUserAdmin}` + ); + + if (isUserAdmin) { + console.log( + `[PERMISSIONS DEBUG] User is admin, returning empty where clause (all resources visible)` + ); return {}; // empty where clause = no restriction } const sharedUids = await getSharedUidsForUser(resourceType, userId); + console.log( + `[PERMISSIONS DEBUG] Shared ${resourceType} UIDs for user ${userId}:`, + sharedUids + ); // For tasks and notes, also include items from shared projects if (resourceType === 'task' || resourceType === 'note') { const sharedProjectUids = await getSharedUidsForUser('project', userId); + console.log( + `[PERMISSIONS DEBUG] Shared project UIDs for user ${userId}:`, + sharedProjectUids + ); // Get the project IDs for shared projects let sharedProjectIds = []; @@ -116,6 +148,10 @@ async function ownershipOrPermissionWhere(resourceType, userId) { raw: true, }); sharedProjectIds = projects.map((p) => p.id); + console.log( + `[PERMISSIONS DEBUG] Shared project IDs for user ${userId}:`, + sharedProjectIds + ); } const conditions = [ @@ -130,6 +166,10 @@ async function ownershipOrPermissionWhere(resourceType, userId) { conditions.push({ project_id: { [Op.in]: sharedProjectIds } }); // Items in shared projects } + console.log( + `[PERMISSIONS DEBUG] Final where conditions for ${resourceType}:`, + JSON.stringify(conditions) + ); return { [Op.or]: conditions }; } From 02633da70420a45abc91c5b5a1d6f20bb139c3f6 Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Wed, 22 Oct 2025 15:56:48 +0300 Subject: [PATCH 3/4] Add tests --- .../tests/integration/project-sharing.test.js | 366 ++++++++++++++++++ backend/tests/integration/users.test.js | 4 +- .../services/parentChildRelationship.test.js | 4 +- package-lock.json | 20 +- package.json | 1 + 5 files changed, 389 insertions(+), 6 deletions(-) create mode 100644 backend/tests/integration/project-sharing.test.js diff --git a/backend/tests/integration/project-sharing.test.js b/backend/tests/integration/project-sharing.test.js new file mode 100644 index 0000000..43cae1d --- /dev/null +++ b/backend/tests/integration/project-sharing.test.js @@ -0,0 +1,366 @@ +const request = require('supertest'); +const app = require('../../app'); +const { User, Project, Task, Note, Permission, 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('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 + const response = await sharedUserAgent + .get('/api/tasks?type=today'); + + expect(response.status).toBe(200); + expect(response.body.tasks).toBeDefined(); + + // Check if the task appears in the main tasks or metrics + const allTasks = [ + ...response.body.tasks, + ...(response.body.metrics?.tasks_due_today || []), + ]; + + const foundTask = allTasks.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(); + }); + }); +}); diff --git a/backend/tests/integration/users.test.js b/backend/tests/integration/users.test.js index 65acc61..78da05e 100644 --- a/backend/tests/integration/users.test.js +++ b/backend/tests/integration/users.test.js @@ -8,13 +8,13 @@ describe('Users Routes', () => { beforeEach(async () => { user = await createTestUser({ - email: 'test@example.com', + email: `test_${Date.now()}@example.com`, }); // Create authenticated agent agent = request.agent(app); await agent.post('/api/login').send({ - email: 'test@example.com', + email: user.email, password: 'password123', }); }); diff --git a/backend/tests/unit/services/parentChildRelationship.test.js b/backend/tests/unit/services/parentChildRelationship.test.js index 8469cfb..c2943c9 100644 --- a/backend/tests/unit/services/parentChildRelationship.test.js +++ b/backend/tests/unit/services/parentChildRelationship.test.js @@ -466,13 +466,13 @@ describe('Parent-Child Relationship Functionality', () => { const uniqueDueDates = [...new Set(dueDates)]; expect(uniqueDueDates.length).toBe(dueDates.length); - // Verify children have sequential due dates (within tolerance for floating point) + // Verify children have sequential due dates (within tolerance for DST transitions) const sortedDueDates = dueDates.sort(); for (let i = 1; i < sortedDueDates.length; i++) { const dayDiff = (sortedDueDates[i] - sortedDueDates[i - 1]) / (24 * 60 * 60 * 1000); - expect(Math.abs(dayDiff - 1)).toBeLessThan(0.001); // Each task should be ~1 day apart + expect(Math.abs(dayDiff - 1)).toBeLessThan(0.05); // Each task should be ~1 day apart (allowing for DST) } }); diff --git a/package-lock.json b/package-lock.json index bb90ecc..bbeeea1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tududi", - "version": "v0.84.1-rc1", + "version": "v0.84.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tududi", - "version": "v0.84.1-rc1", + "version": "v0.84.1", "license": "ISC", "dependencies": { "@heroicons/react": "^2.1.5", @@ -89,6 +89,7 @@ "sequelize-cli": "~6.6.2", "style-loader": "^4.0.0", "supertest": "~7.1.1", + "supertest-session": "^5.0.1", "swc-loader": "^0.2.6", "tailwindcss": "^3.4.13", "ts-jest": "^29.0.0", @@ -17934,6 +17935,21 @@ "node": ">=14.18.0" } }, + "node_modules/supertest-session": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/supertest-session/-/supertest-session-5.0.1.tgz", + "integrity": "sha512-RpR8tGQZGreQsOCiW3YMSPKMwPlAB8lA0Jyat+8VUSJaYvLHTMqhMW6gooJ2htzjr3w/kgqJTQDnmuFenzA9JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookiejar": "^2.1.2", + "methods": "^1.1.2", + "object-assign": "^4.0.1" + }, + "peerDependencies": { + "supertest": ">= 3.1.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 6c4c52b..27bcbd2 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "sequelize-cli": "~6.6.2", "style-loader": "^4.0.0", "supertest": "~7.1.1", + "supertest-session": "^5.0.1", "swc-loader": "^0.2.6", "tailwindcss": "^3.4.13", "ts-jest": "^29.0.0", From 08f56ceb44dd0178e460e7ea9c6caefb04a36d91 Mon Sep 17 00:00:00 2001 From: Chris Veleris Date: Wed, 22 Oct 2025 16:28:13 +0300 Subject: [PATCH 4/4] fixup! Add tests --- .../tests/integration/project-sharing.test.js | 202 ++++++++---------- 1 file changed, 91 insertions(+), 111 deletions(-) diff --git a/backend/tests/integration/project-sharing.test.js b/backend/tests/integration/project-sharing.test.js index 43cae1d..947e342 100644 --- a/backend/tests/integration/project-sharing.test.js +++ b/backend/tests/integration/project-sharing.test.js @@ -1,6 +1,13 @@ const request = require('supertest'); const app = require('../../app'); -const { User, Project, Task, Note, Permission, sequelize } = require('../../models'); +const { + User, + Project, + Task, + Note, + Permission, + sequelize, +} = require('../../models'); const { createTestUser } = require('../helpers/testUtils'); describe('Project Sharing Integration Tests', () => { @@ -35,23 +42,19 @@ describe('Project Sharing Integration Tests', () => { .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', - }); + 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', - }); + await ownerAgent.post('/api/shares').send({ + resource_type: 'project', + resource_uid: project.uid, + target_user_email: sharedUser.email, + access_level: 'rw', + }); }); afterAll(async () => { @@ -61,19 +64,16 @@ describe('Project Sharing Integration Tests', () => { 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 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'); + const response = await sharedUserAgent.get('/api/tasks'); expect(response.status).toBe(200); expect(response.body.tasks).toBeDefined(); @@ -87,18 +87,15 @@ describe('Project Sharing Integration Tests', () => { 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 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'); + const response = await sharedUserAgent.get('/api/notes'); expect(response.status).toBe(200); expect(response.body).toBeDefined(); @@ -132,8 +129,7 @@ describe('Project Sharing Integration Tests', () => { const privateTask = privateTaskResponse.body; // Shared user should NOT see this task - const response = await sharedUserAgent - .get('/api/tasks'); + const response = await sharedUserAgent.get('/api/tasks'); expect(response.status).toBe(200); const foundTask = response.body.tasks.find( @@ -157,8 +153,7 @@ describe('Project Sharing Integration Tests', () => { const taskDueToday = taskDueTodayResponse.body; // Fetch today's tasks as shared user - const response = await sharedUserAgent - .get('/api/tasks?type=today'); + const response = await sharedUserAgent.get('/api/tasks?type=today'); expect(response.status).toBe(200); expect(response.body.tasks).toBeDefined(); @@ -177,14 +172,12 @@ describe('Project Sharing Integration Tests', () => { 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, - }); + 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(); @@ -193,13 +186,11 @@ describe('Project Sharing Integration Tests', () => { }); 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, - }); + 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(); @@ -218,14 +209,12 @@ describe('Project Sharing Integration Tests', () => { } ); - const response = await sharedUserAgent - .post('/api/task') - .send({ - name: 'Task that should fail', - project_id: project.id, - priority: 1, - status: 0, - }); + 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'); @@ -243,13 +232,11 @@ describe('Project Sharing Integration Tests', () => { } ); - const response = await sharedUserAgent - .post('/api/note') - .send({ - title: 'Note that should fail', - content: 'Content', - project_uid: project.uid, - }); + 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'); @@ -259,19 +246,18 @@ describe('Project Sharing Integration Tests', () => { 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 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`); + const response = await sharedUserAgent.get( + `/api/task/${taskInSharedProject.uid}/timeline` + ); expect(response.status).toBe(200); expect(Array.isArray(response.body)).toBe(true); @@ -297,26 +283,26 @@ describe('Project Sharing Integration Tests', () => { const privateTask = privateTaskResponse.body; // Shared user should NOT access this timeline - const response = await sharedUserAgent - .get(`/api/task/${privateTask.uid}/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 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`); + 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); @@ -326,17 +312,14 @@ describe('Project Sharing Integration Tests', () => { 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, - }); + 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'); + const response = await ownerAgent.get('/api/tasks'); expect(response.status).toBe(200); expect(response.body.tasks).toBeDefined(); @@ -346,18 +329,15 @@ describe('Project Sharing Integration Tests', () => { 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, - }); + 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'); + const response = await ownerAgent.get('/api/tasks?type=today'); expect(response.status).toBe(200); expect(response.body.tasks).toBeDefined();