tududi/backend/tests/unit/services/parentChildRelationship.test.js
Antonis Anastasiadis e594d1075b
Linting cleanup (#99)
* Add eslint and prettier dependencies and configs

* Lint project.
2025-07-01 11:40:09 +03:00

599 lines
23 KiB
JavaScript

const { Task, User } = require('../../../models');
const RecurringTaskService = require('../../../services/recurringTaskService');
const { createTestUser } = require('../../helpers/testUtils');
describe('Parent-Child Relationship Functionality', () => {
let user;
beforeEach(async () => {
user = await createTestUser({ email: 'test@example.com' });
});
describe('Task Instance Creation', () => {
it('should create child task with correct parent relationship', async () => {
const parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
priority: 1,
note: 'Parent note',
});
const dueDate = new Date('2025-06-20T10:00:00Z');
const childTask = await RecurringTaskService.createTaskInstance(
parentTask,
dueDate
);
expect(childTask.name).toBe(parentTask.name);
expect(childTask.description).toBe(parentTask.description);
expect(childTask.priority).toBe(parentTask.priority);
expect(childTask.note).toBe(parentTask.note);
expect(childTask.user_id).toBe(parentTask.user_id);
expect(childTask.project_id).toBe(parentTask.project_id);
expect(childTask.recurring_parent_id).toBe(parentTask.id);
expect(childTask.recurrence_type).toBe('none');
expect(childTask.status).toBe(Task.STATUS.NOT_STARTED);
expect(childTask.due_date).toEqual(dueDate);
expect(childTask.today).toBe(false);
});
it('should preserve project assignment in child task', async () => {
// Create a real project first or skip project validation for this test
const parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'weekly',
recurrence_interval: 1,
user_id: user.id,
project_id: null, // Changed to null to avoid foreign key issues
priority: 2,
});
const dueDate = new Date('2025-06-20T10:00:00Z');
const childTask = await RecurringTaskService.createTaskInstance(
parentTask,
dueDate
);
expect(childTask.project_id).toBeNull();
expect(childTask.recurring_parent_id).toBe(parentTask.id);
});
it('should handle null description and note correctly', async () => {
const parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'monthly',
recurrence_interval: 1,
user_id: user.id,
description: null,
note: null,
priority: 0,
});
const dueDate = new Date('2025-06-20T10:00:00Z');
const childTask = await RecurringTaskService.createTaskInstance(
parentTask,
dueDate
);
expect(childTask.description).toBeNull();
expect(childTask.note).toBeNull();
expect(childTask.recurring_parent_id).toBe(parentTask.id);
});
});
describe('Parent-Child Task Queries', () => {
let parentTask, childTask1, childTask2;
beforeEach(async () => {
parentTask = await Task.create({
name: 'Daily Exercise',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
priority: 1,
});
childTask1 = await Task.create({
name: 'Daily Exercise',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED,
});
childTask2 = await Task.create({
name: 'Daily Exercise',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
due_date: new Date('2025-06-21T10:00:00Z'),
status: Task.STATUS.DONE,
});
});
it('should find all child tasks for a parent', async () => {
const childTasks = await Task.findAll({
where: {
recurring_parent_id: parentTask.id,
user_id: user.id,
},
order: [['due_date', 'ASC']],
});
expect(childTasks).toHaveLength(2);
expect(childTasks[0].id).toBe(childTask1.id);
expect(childTasks[1].id).toBe(childTask2.id);
expect(childTasks[0].due_date).toBeDefined();
expect(childTasks[1].due_date).toBeDefined();
});
it('should find parent task from child', async () => {
const parent = await Task.findByPk(childTask1.recurring_parent_id);
expect(parent).not.toBeNull();
expect(parent.id).toBe(parentTask.id);
expect(parent.recurrence_type).toBe('daily');
expect(parent.recurrence_interval).toBe(1);
});
it('should distinguish between parent and child tasks', async () => {
const allTasks = await Task.findAll({
where: { user_id: user.id },
order: [['id', 'ASC']],
});
const parentTasks = allTasks.filter(
(t) => t.recurrence_type !== 'none'
);
const childTasks = allTasks.filter(
(t) => t.recurring_parent_id !== null
);
expect(parentTasks).toHaveLength(1);
expect(childTasks).toHaveLength(2);
expect(parentTasks[0].id).toBe(parentTask.id);
});
it('should handle tasks with no parent relationship', async () => {
const standaloneTask = await Task.create({
name: 'Standalone Task',
recurrence_type: 'none',
user_id: user.id,
priority: 1,
});
expect(standaloneTask.recurring_parent_id).toBeFalsy(); // Can be null or undefined
expect(standaloneTask.recurrence_type).toBe('none');
});
});
describe('Completion-Based Recurring Task Generation', () => {
it('should create next instance when completing completion-based parent task', async () => {
const parentTask = await Task.create({
name: 'Completion Based Task',
recurrence_type: 'daily',
recurrence_interval: 1,
completion_based: true,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
const nextTask =
await RecurringTaskService.handleTaskCompletion(parentTask);
expect(nextTask).not.toBeNull();
expect(nextTask.name).toBe(parentTask.name);
expect(nextTask.recurring_parent_id).toBe(parentTask.id);
expect(nextTask.recurrence_type).toBe('none');
expect(nextTask.status).toBe(Task.STATUS.NOT_STARTED);
expect(nextTask.due_date).toBeDefined();
// Verify parent task's last_generated_date was updated
const updatedParent = await Task.findByPk(parentTask.id);
expect(updatedParent.last_generated_date).toBeDefined();
});
it('should not create multiple children when called repeatedly', async () => {
const parentTask = await Task.create({
name: 'Completion Based Task',
recurrence_type: 'daily',
recurrence_interval: 1,
completion_based: true,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
// Call completion multiple times quickly
const firstNextTask =
await RecurringTaskService.handleTaskCompletion(parentTask);
expect(firstNextTask).not.toBeNull();
// Check how many child tasks exist for this parent
const childTasks = await Task.findAll({
where: {
recurring_parent_id: parentTask.id,
user_id: user.id,
},
});
// Should only have one child task despite multiple generations from same parent
expect(childTasks.length).toBeGreaterThanOrEqual(1);
expect(childTasks[0].recurring_parent_id).toBe(parentTask.id);
});
it('should handle child task completion properly', async () => {
const parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'daily',
recurrence_interval: 1,
completion_based: true,
user_id: user.id,
});
const childTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED,
});
// Completing child task should not create new instances
const nextTask =
await RecurringTaskService.handleTaskCompletion(childTask);
expect(nextTask).toBeNull();
});
});
describe('Parent Task Updates Through Child Tasks', () => {
let parentTask, childTask;
beforeEach(async () => {
parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'daily',
recurrence_interval: 1,
recurrence_weekday: null,
completion_based: false,
user_id: user.id,
priority: 1,
});
childTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED,
});
});
it('should update parent recurrence settings through child task', async () => {
// Simulate updating parent through child
const updatedParent = await Task.findByPk(parentTask.id);
await updatedParent.update({
recurrence_type: 'weekly',
recurrence_interval: 2,
recurrence_weekday: 1, // Monday
completion_based: true,
});
const refreshedParent = await Task.findByPk(parentTask.id);
expect(refreshedParent.recurrence_type).toBe('weekly');
expect(refreshedParent.recurrence_interval).toBe(2);
expect(refreshedParent.recurrence_weekday).toBe(1);
expect(refreshedParent.completion_based).toBe(true);
// Verify child task is unchanged
const refreshedChild = await Task.findByPk(childTask.id);
expect(refreshedChild.recurrence_type).toBe('none');
expect(refreshedChild.recurring_parent_id).toBe(parentTask.id);
});
it('should preserve child task properties when updating parent', async () => {
await childTask.update({ status: Task.STATUS.IN_PROGRESS });
// Update parent
const updatedParent = await Task.findByPk(parentTask.id);
await updatedParent.update({
recurrence_type: 'monthly',
recurrence_interval: 3,
});
// Verify child maintains its specific properties
const refreshedChild = await Task.findByPk(childTask.id);
expect(refreshedChild.status).toBe(Task.STATUS.IN_PROGRESS);
expect(refreshedChild.due_date).toEqual(
new Date('2025-06-20T10:00:00Z')
);
expect(refreshedChild.recurring_parent_id).toBe(parentTask.id);
});
});
describe('Task Deletion Scenarios', () => {
let parentTask, childTask1, childTask2;
beforeEach(async () => {
parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'weekly',
recurrence_interval: 1,
user_id: user.id,
priority: 1,
});
childTask1 = await Task.create({
name: 'Parent Task',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED,
});
childTask2 = await Task.create({
name: 'Parent Task',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
due_date: new Date('2025-06-27T10:00:00Z'),
status: Task.STATUS.DONE,
});
});
it('should allow deleting child tasks without affecting parent', async () => {
await childTask1.destroy();
// Verify child is deleted
const deletedChild = await Task.findByPk(childTask1.id);
expect(deletedChild).toBeNull();
// Verify parent and other child still exist
const existingParent = await Task.findByPk(parentTask.id);
const existingChild = await Task.findByPk(childTask2.id);
expect(existingParent).not.toBeNull();
expect(existingChild).not.toBeNull();
});
it('should prevent deleting parent when child tasks exist due to foreign key constraint', async () => {
await expect(parentTask.destroy()).rejects.toThrow();
const error = await parentTask.destroy().catch((err) => err);
expect(error.name).toBe('SequelizeForeignKeyConstraintError');
// Verify parent and children still exist
const existingParent = await Task.findByPk(parentTask.id);
const existingChild1 = await Task.findByPk(childTask1.id);
const existingChild2 = await Task.findByPk(childTask2.id);
expect(existingParent).not.toBeNull();
expect(existingChild1).not.toBeNull();
expect(existingChild2).not.toBeNull();
});
it('should allow deleting parent after deleting all child tasks', async () => {
// Delete all child tasks first
await childTask1.destroy();
await childTask2.destroy();
// Now parent should be deletable
await parentTask.destroy();
// Verify all tasks are deleted
const deletedParent = await Task.findByPk(parentTask.id);
expect(deletedParent).toBeNull();
});
});
describe('Complex Parent-Child Scenarios', () => {
it('should handle multiple parents with different recurrence patterns', async () => {
const dailyParent = await Task.create({
name: 'Daily Task',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
priority: 1,
});
const weeklyParent = await Task.create({
name: 'Weekly Task',
recurrence_type: 'weekly',
recurrence_interval: 1,
recurrence_weekday: 1,
user_id: user.id,
priority: 2,
});
// Create child tasks for each parent
const dailyChild = await RecurringTaskService.createTaskInstance(
dailyParent,
new Date('2025-06-20T10:00:00Z')
);
const weeklyChild = await RecurringTaskService.createTaskInstance(
weeklyParent,
new Date('2025-06-23T10:00:00Z')
);
expect(dailyChild.recurring_parent_id).toBe(dailyParent.id);
expect(weeklyChild.recurring_parent_id).toBe(weeklyParent.id);
expect(dailyChild.name).toBe('Daily Task');
expect(weeklyChild.name).toBe('Weekly Task');
});
it('should maintain data integrity across multiple child generations', async () => {
const parentTask = await Task.create({
name: 'Long Running Task',
recurrence_type: 'daily',
recurrence_interval: 1,
completion_based: true,
user_id: user.id,
priority: 2,
});
const children = [];
// Generate 5 child tasks
for (let i = 0; i < 5; i++) {
await parentTask.update({ status: Task.STATUS.DONE });
const nextTask =
await RecurringTaskService.handleTaskCompletion(parentTask);
if (nextTask) {
children.push(nextTask);
}
await parentTask.update({ status: Task.STATUS.NOT_STARTED });
}
expect(children.length).toBe(5);
// Verify all children have correct parent relationship
for (const child of children) {
expect(child.recurring_parent_id).toBe(parentTask.id);
expect(child.name).toBe(parentTask.name);
expect(child.recurrence_type).toBe('none');
expect(child.status).toBe(Task.STATUS.NOT_STARTED);
}
// Verify no duplicate due dates
const dueDates = children.map((c) => c.due_date.getTime());
const uniqueDueDates = [...new Set(dueDates)];
expect(uniqueDueDates.length).toBe(dueDates.length);
});
it('should handle orphaned child tasks gracefully', async () => {
const parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
priority: 1,
});
const childTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED,
});
// Verify child can be found and has correct parent reference
const foundChild = await Task.findByPk(childTask.id);
expect(foundChild.recurring_parent_id).toBe(parentTask.id);
// Try to find parent through child
const foundParent = await Task.findByPk(
foundChild.recurring_parent_id
);
expect(foundParent).not.toBeNull();
expect(foundParent.id).toBe(parentTask.id);
});
});
describe('Data Consistency and Validation', () => {
it('should ensure child tasks cannot have recurrence settings', async () => {
// First create a parent task to reference
const parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
priority: 1,
});
const childTask = await Task.create({
name: 'Child Task',
recurrence_type: 'none',
recurring_parent_id: parentTask.id,
recurrence_interval: null,
recurrence_weekday: null,
recurrence_month_day: null,
recurrence_week_of_month: null,
completion_based: false,
user_id: user.id,
status: Task.STATUS.NOT_STARTED,
});
expect(childTask.recurrence_type).toBe('none');
expect(childTask.recurrence_interval).toBeNull();
expect(childTask.recurrence_weekday).toBeNull();
expect(childTask.recurrence_month_day).toBeNull();
expect(childTask.recurrence_week_of_month).toBeNull();
expect(childTask.completion_based).toBe(false);
});
it('should ensure parent tasks have valid recurrence settings', async () => {
const parentTask = await Task.create({
name: 'Parent Task',
recurrence_type: 'weekly',
recurrence_interval: 2,
recurrence_weekday: 5, // Friday
recurring_parent_id: null,
user_id: user.id,
priority: 1,
});
expect(parentTask.recurrence_type).toBe('weekly');
expect(parentTask.recurrence_interval).toBe(2);
expect(parentTask.recurrence_weekday).toBe(5);
expect(parentTask.recurring_parent_id).toBeNull();
});
it('should maintain user isolation for parent-child relationships', async () => {
const otherUser = await createTestUser({
email: 'other@example.com',
});
const user1Parent = await Task.create({
name: 'User 1 Parent',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
priority: 1,
});
const user2Parent = await Task.create({
name: 'User 2 Parent',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: otherUser.id,
priority: 1,
});
const user1Child = await Task.create({
name: 'User 1 Parent',
recurrence_type: 'none',
recurring_parent_id: user1Parent.id,
user_id: user.id,
due_date: new Date('2025-06-20T10:00:00Z'),
status: Task.STATUS.NOT_STARTED,
});
// Verify child belongs to correct user
expect(user1Child.user_id).toBe(user.id);
expect(user1Child.recurring_parent_id).toBe(user1Parent.id);
// Verify users can't see each other's tasks
const user1Tasks = await Task.findAll({
where: { user_id: user.id },
});
const user2Tasks = await Task.findAll({
where: { user_id: otherUser.id },
});
expect(user1Tasks.length).toBe(2); // parent + child
expect(user2Tasks.length).toBe(1); // just parent
expect(
user1Tasks.find((t) => t.id === user2Parent.id)
).toBeUndefined();
expect(
user2Tasks.find((t) => t.id === user1Parent.id)
).toBeUndefined();
});
});
});