const request = require('supertest'); const app = require('../../app'); const { Task, sequelize } = require('../../models'); const { createTestUser } = require('../helpers/testUtils'); describe('Subtasks Completion Logic Integration', () => { let testUser; let agent; const toggleTaskCompletion = async (taskId) => { const task = await Task.findByPk(taskId); const newStatus = task.status === Task.STATUS.DONE ? task.note ? Task.STATUS.IN_PROGRESS : Task.STATUS.NOT_STARTED : Task.STATUS.DONE; return agent.patch(`/api/task/${task.uid}`).send({ status: newStatus }); }; beforeEach(async () => { await Task.destroy({ where: {}, truncate: true }); testUser = await createTestUser(); // Create authenticated agent agent = request.agent(app); await agent.post('/api/login').send({ email: testUser.email, password: 'password123', }); }); describe('Parent Task Completion Affects Subtasks', () => { it('should complete all subtasks when parent task is completed', async () => { // Create parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create subtasks const subtask1 = await Task.create({ name: 'Subtask 1', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); const subtask2 = await Task.create({ name: 'Subtask 2', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.IN_PROGRESS, priority: Task.PRIORITY.MEDIUM, }); const subtask3 = await Task.create({ name: 'Subtask 3', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.DONE, priority: Task.PRIORITY.MEDIUM, }); // Complete parent task let res = await toggleTaskCompletion(parentTask.id); expect(res.status).toBe(200); // Verify parent task is completed const updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.DONE); expect(updatedParent.completed_at).not.toBeNull(); // Verify all subtasks are completed const updatedSubtasks = await Task.findAll({ where: { parent_task_id: parentTask.id }, order: [['id', 'ASC']], }); expect(updatedSubtasks).toHaveLength(3); updatedSubtasks.forEach((subtask) => { expect(subtask.status).toBe(Task.STATUS.DONE); expect(subtask.completed_at).not.toBeNull(); }); }); it('should undone all subtasks when parent task is undone', async () => { // Create completed parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.DONE, completed_at: new Date(), priority: Task.PRIORITY.MEDIUM, }); // Create completed subtasks const subtask1 = await Task.create({ name: 'Subtask 1', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.DONE, completed_at: new Date(), priority: Task.PRIORITY.MEDIUM, }); const subtask2 = await Task.create({ name: 'Subtask 2', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.DONE, completed_at: new Date(), priority: Task.PRIORITY.MEDIUM, }); // Undone parent task let res = await toggleTaskCompletion(parentTask.id); expect(res.status).toBe(200); // Verify parent task is undone const updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); expect(updatedParent.completed_at).toBeNull(); // Verify all subtasks are undone const updatedSubtasks = await Task.findAll({ where: { parent_task_id: parentTask.id }, }); expect(updatedSubtasks).toHaveLength(2); updatedSubtasks.forEach((subtask) => { expect(subtask.status).toBe(Task.STATUS.NOT_STARTED); expect(subtask.completed_at).toBeNull(); }); }); }); describe('Subtask Completion Does Not Affect Parent Task', () => { it('should NOT auto-complete parent task when all subtasks are completed', async () => { // Create parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create subtasks const subtask1 = await Task.create({ name: 'Subtask 1', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); const subtask2 = await Task.create({ name: 'Subtask 2', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Complete first subtask let res = await toggleTaskCompletion(subtask1.id); expect(res.status).toBe(200); // Parent should still be incomplete let updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); // Complete second subtask const res2 = await toggleTaskCompletion(subtask2.id); expect(res2.status).toBe(200); // Parent should still be incomplete - user must manually complete it updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); expect(updatedParent.completed_at).toBeNull(); }); it('should NOT auto-undone parent task when a subtask is undone', async () => { // Create completed parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.DONE, completed_at: new Date(), priority: Task.PRIORITY.MEDIUM, }); // Create completed subtasks const subtask1 = await Task.create({ name: 'Subtask 1', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.DONE, completed_at: new Date(), priority: Task.PRIORITY.MEDIUM, }); const subtask2 = await Task.create({ name: 'Subtask 2', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.DONE, completed_at: new Date(), priority: Task.PRIORITY.MEDIUM, }); // Undone one subtask let res = await toggleTaskCompletion(subtask1.id); expect(res.status).toBe(200); // Parent should remain completed - user must manually undone it const updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.DONE); expect(updatedParent.completed_at).not.toBeNull(); // Other subtask should remain done const updatedSubtask2 = await Task.findByPk(subtask2.id); expect(updatedSubtask2.status).toBe(Task.STATUS.DONE); }); it('should not affect parent task when no subtasks exist', async () => { // Create parent task without subtasks const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Complete parent task directly let res = await toggleTaskCompletion(parentTask.id); expect(res.status).toBe(200); // Parent should be completed normally const updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.DONE); expect(updatedParent.completed_at).not.toBeNull(); }); }); describe('Complex Completion Scenarios', () => { it('should handle mixed subtask states correctly without affecting parent', async () => { // Create parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create subtasks with different statuses const subtask1 = await Task.create({ name: 'Subtask 1', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.DONE, completed_at: new Date(), priority: Task.PRIORITY.MEDIUM, }); const subtask2 = await Task.create({ name: 'Subtask 2', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.IN_PROGRESS, priority: Task.PRIORITY.MEDIUM, }); const subtask3 = await Task.create({ name: 'Subtask 3', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.WAITING, priority: Task.PRIORITY.MEDIUM, }); // Complete remaining subtasks let res = await toggleTaskCompletion(subtask2.id); expect(res.status).toBe(200); // Parent should still be incomplete let updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); // Complete last subtask const res2 = await toggleTaskCompletion(subtask3.id); expect(res2.status).toBe(200); // Parent should still be incomplete - user must manually complete it updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); }); it('should handle rapid completion toggles without affecting parent', async () => { // Create parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create single subtask const subtask = await Task.create({ name: 'Subtask', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Rapidly toggle subtask completion let res = await toggleTaskCompletion(subtask.id); expect(res.status).toBe(200); const res2 = await toggleTaskCompletion(subtask.id); expect(res2.status).toBe(200); const res3 = await toggleTaskCompletion(subtask.id); expect(res3.status).toBe(200); // Parent should remain unchanged regardless of subtask toggles const updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); }); }); describe('Edge Cases', () => { it('should cascade delete subtasks when parent is deleted', async () => { // Create parent task first const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create subtask const subtask = await Task.create({ name: 'Child Subtask', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); const subtaskId = subtask.id; // In test environment, FK constraints are disabled, so we need to manually handle cascade delete // In production, this would be handled by database FK constraints with CASCADE await Task.destroy({ where: { parent_task_id: parentTask.id } }); await parentTask.destroy(); // Verify subtask was also deleted (CASCADE behavior) const deletedSubtask = await Task.findByPk(subtaskId); expect(deletedSubtask).toBeNull(); }); it('should handle concurrent completion updates without affecting parent', async () => { // Create parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create subtasks const subtask1 = await Task.create({ name: 'Subtask 1', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); const subtask2 = await Task.create({ name: 'Subtask 2', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Simulate concurrent completion const promises = [ toggleTaskCompletion(subtask1.id), toggleTaskCompletion(subtask2.id), ]; const results = await Promise.all(promises); results.forEach((result) => { expect(result.status).toBe(200); }); // Parent should remain unchanged - user must manually complete it const updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); }); it('should handle deleted parent task gracefully (FK constraints disabled)', async () => { // Create parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create subtask const subtask = await Task.create({ name: 'Subtask', user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Delete parent task (in test environment, FK constraints are disabled) await agent .delete(`/api/task/${parentTask.uid}`) .expect(200); // Verify subtask remains (orphaned) since FK constraints are disabled in tests const remainingSubtask = await Task.findByPk(subtask.id); expect(remainingSubtask).not.toBeNull(); expect(remainingSubtask.parent_task_id).toBe(parentTask.id); // Points to deleted parent }); }); describe('Performance Considerations', () => { it('should handle many subtasks efficiently without affecting parent', async () => { // Create parent task const parentTask = await Task.create({ name: 'Parent Task', user_id: testUser.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }); // Create many subtasks const subtaskPromises = []; for (let i = 1; i <= 50; i++) { subtaskPromises.push( Task.create({ name: `Subtask ${i}`, user_id: testUser.id, parent_task_id: parentTask.id, status: Task.STATUS.NOT_STARTED, priority: Task.PRIORITY.MEDIUM, }) ); } const subtasks = await Promise.all(subtaskPromises); // Complete all subtasks const completionPromises = subtasks.map((subtask) => toggleTaskCompletion(subtask.id) ); const startTime = Date.now(); await Promise.all(completionPromises); const endTime = Date.now(); // Should complete within reasonable time (adjust threshold as needed) expect(endTime - startTime).toBeLessThan(10000); // 10 seconds // Parent should remain unchanged - user must manually complete it const updatedParent = await Task.findByPk(parentTask.id); expect(updatedParent.status).toBe(Task.STATUS.NOT_STARTED); }); }); });