tududi/backend/tests/integration/tasks.test.js
Chris 0265f6e70c
fix: prevent task name truncation when creating from inbox (#1020)
* fix: prevent Telegram polling errors from blocking container startup (#989)

This fix addresses the issue where the container gets stuck in an endless
loop of Telegram connection errors when the bot is configured but Telegram
is unreachable during startup.

Changes:
- Add 10-second startup delay before initializing Telegram polling to allow
  the system to settle
- Implement exponential backoff (5s to 5min) when Telegram connection fails
- Add rate-limited error logging (max once per minute per user) to reduce
  log spam and prevent event loop blocking
- Track error state per user to manage backoff independently
- Auto-reset error state on successful connection
- Update tests to account for new error state tracking

Fixes #989

* fix: prevent task name truncation when creating from inbox

Changes:
- Change task.name from VARCHAR(255) to TEXT to prevent any potential truncation
- Change inbox_items.title and cleaned_content from VARCHAR(255) to TEXT
- Add integration test to verify long task names are preserved
- Add unit test to verify cleaned_content doesn't truncate

While investigation showed no actual truncation occurring in the codebase,
this defensive fix ensures unlimited text length for task names and inbox
content, eliminating any possibility of truncation at the database level.

Fixes #1016
2026-04-13 23:14:52 +03:00

516 lines
18 KiB
JavaScript

const request = require('supertest');
const app = require('../../app');
const { Task, User } = require('../../models');
const { createTestUser } = require('../helpers/testUtils');
describe('Tasks Routes', () => {
let user, agent;
beforeEach(async () => {
user = await createTestUser({
email: 'test@example.com',
});
// Create authenticated agent
agent = request.agent(app);
await agent.post('/api/login').send({
email: 'test@example.com',
password: 'password123',
});
});
describe('POST /api/task', () => {
it('should create a new task', async () => {
const taskData = {
name: 'Test Task',
note: 'Test Note',
priority: 1,
status: 0,
};
const response = await agent.post('/api/task').send(taskData);
expect(response.status).toBe(201);
expect(response.body.id).toBeDefined();
expect(response.body.name).toBe(taskData.name);
expect(response.body.note).toBe(taskData.note);
expect(response.body.priority).toBe(taskData.priority);
expect(response.body.status).toBe(taskData.status);
expect(response.body.user_id).toBe(user.id);
});
it('should require authentication', async () => {
const taskData = {
name: 'Test Task',
};
const response = await request(app)
.post('/api/task')
.send(taskData);
expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required');
});
it('should require task name', async () => {
// Mock console.error to suppress expected error log in test output
const originalConsoleError = console.error;
console.error = jest.fn();
const taskData = {
note: 'Test Note',
};
const response = await agent.post('/api/task').send(taskData);
expect(response.status).toBe(400);
// Restore original console.error
console.error = originalConsoleError;
});
it('should not truncate long task names', async () => {
const longTaskName =
'This task has a long name, very long name, with lots of stuff';
const taskData = {
name: longTaskName,
priority: 'low',
status: 'not_started',
};
const response = await agent.post('/api/task').send(taskData);
expect(response.status).toBe(201);
expect(response.body.name).toBe(longTaskName);
expect(response.body.name.length).toBe(longTaskName.length);
const retrievedTask = await Task.findOne({
where: { id: response.body.id },
});
expect(retrievedTask.name).toBe(longTaskName);
});
});
describe('GET /api/tasks', () => {
let task1, task2;
beforeEach(async () => {
task1 = await Task.create({
name: 'Task 1',
user_id: user.id,
status: Task.STATUS.IN_PROGRESS, // Active status shows in today view
});
task2 = await Task.create({
name: 'Task 2',
user_id: user.id,
status: Task.STATUS.NOT_STARTED, // Not active, won't show in today view
});
});
it('should get all user tasks', async () => {
const response = await agent.get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
expect(response.body.tasks.length).toBe(2);
expect(response.body.tasks.map((t) => t.id)).toContain(task1.id);
expect(response.body.tasks.map((t) => t.id)).toContain(task2.id);
});
it('should filter today tasks (returns tasks with active status)', async () => {
const response = await agent.get('/api/tasks?type=today');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
expect(response.body.tasks.length).toBe(1);
expect(response.body.tasks[0].id).toBe(task1.id);
});
it('should require authentication', async () => {
const response = await request(app).get('/api/tasks');
expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required');
});
});
// Note: No individual task GET route exists in the current API
describe('PATCH /api/task/:id', () => {
let task;
beforeEach(async () => {
task = await Task.create({
name: 'Test Task',
priority: 0,
status: 0,
user_id: user.id,
});
});
it('should update task', async () => {
const updateData = {
name: 'Updated Task',
note: 'Updated Note',
priority: 2,
status: 1,
};
const response = await agent
.patch(`/api/task/${task.uid}`)
.send(updateData);
expect(response.status).toBe(200);
expect(response.body.id).toBeDefined();
expect(response.body.name).toBe(updateData.name);
expect(response.body.note).toBe(updateData.note);
expect(response.body.priority).toBe(updateData.priority);
expect(response.body.status).toBe(updateData.status);
});
it('should update recurring task name without transformation', async () => {
// Create a recurring task
const recurringTask = await Task.create({
name: 'My Daily Task',
recurrence_type: 'daily',
recurrence_interval: 1,
user_id: user.id,
status: 0,
});
const updateData = {
name: 'Updated Daily Task Name',
};
const response = await agent
.patch(`/api/task/${recurringTask.uid}`)
.send(updateData);
expect(response.status).toBe(200);
expect(response.body.id).toBeDefined();
// The response should contain the actual updated name, not a transformed name like "Daily"
expect(response.body.name).toBe(updateData.name);
expect(response.body.original_name).toBe(updateData.name);
});
it('should return 403 for non-existent task', async () => {
const response = await agent
.patch('/api/task/nonexistent-uid-12345')
.send({ name: 'Updated' });
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
});
it("should not allow updating other user's tasks", async () => {
const bcrypt = require('bcrypt');
const otherUser = await User.create({
email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10),
});
const otherTask = await Task.create({
name: 'Other Task',
user_id: otherUser.id,
});
const response = await agent
.patch(`/api/task/${otherTask.uid}`)
.send({ name: 'Updated' });
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
});
it('should require authentication', async () => {
const response = await request(app)
.patch(`/api/task/${task.uid}`)
.send({ name: 'Updated' });
expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required');
});
});
describe('DELETE /api/task/:id', () => {
let task;
beforeEach(async () => {
task = await Task.create({
name: 'Test Task',
user_id: user.id,
});
});
it('should delete task', async () => {
const response = await agent.delete(`/api/task/${task.uid}`);
expect(response.status).toBe(200);
expect(response.body.message).toBe('Task successfully deleted');
// Verify task is deleted
const deletedTask = await Task.findByPk(task.id);
expect(deletedTask).toBeNull();
}, 10000); // 10 second timeout for DELETE operations
it('should return 403 for non-existent task', async () => {
const response = await agent.delete(
'/api/task/nonexistent-uid-12345'
);
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
});
it("should not allow deleting other user's tasks", async () => {
const bcrypt = require('bcrypt');
const otherUser = await User.create({
email: 'other@example.com',
password_digest: await bcrypt.hash('password123', 10),
});
const otherTask = await Task.create({
name: 'Other Task',
user_id: otherUser.id,
});
const response = await agent.delete(`/api/task/${otherTask.uid}`);
expect(response.status).toBe(403);
expect(response.body.error).toBe('Forbidden');
}, 10000); // 10 second timeout for this specific test
it('should require authentication', async () => {
const response = await request(app).delete(`/api/task/${task.uid}`);
expect(response.status).toBe(401);
expect(response.body.error).toBe('Authentication required');
});
});
describe('Task with tags', () => {
it('should create task with tags', async () => {
const taskData = {
name: 'Test Task',
tags: [{ name: 'work' }, { name: 'urgent' }],
};
const response = await agent.post('/api/task').send(taskData);
expect(response.status).toBe(201);
expect(response.body.Tags).toBeDefined();
expect(response.body.Tags.length).toBe(2);
expect(response.body.Tags.map((t) => t.name)).toContain('work');
expect(response.body.Tags.map((t) => t.name)).toContain('urgent');
});
it('should return all tags when filtering by a specific tag', async () => {
const taskData = {
name: 'Task with multiple tags',
tags: [{ name: 'alpha' }, { name: 'beta' }],
};
const createResponse = await agent.post('/api/task').send(taskData);
expect(createResponse.status).toBe(201);
const response = await agent.get('/api/tasks?tag=alpha');
expect(response.status).toBe(200);
expect(response.body.tasks.length).toBe(1);
const [task] = response.body.tasks;
const tagNames = (task.tags || []).map((t) => t.name);
expect(tagNames).toEqual(expect.arrayContaining(['alpha', 'beta']));
expect(tagNames.length).toBe(2);
});
});
describe('Subtasks filtering', () => {
it('should not include subtasks at first level when retrieving /tasks', async () => {
// Create a parent task
const parentTask = await Task.create({
name: 'Parent Task',
user_id: user.id,
status: 0,
});
// Create subtasks
const subtask1 = await Task.create({
name: 'Subtask 1',
user_id: user.id,
parent_task_id: parentTask.id,
status: 0,
});
const subtask2 = await Task.create({
name: 'Subtask 2',
user_id: user.id,
parent_task_id: parentTask.id,
status: 0,
});
// Create another regular task (not a subtask)
const regularTask = await Task.create({
name: 'Regular Task',
user_id: user.id,
status: 0,
});
const response = await agent.get('/api/tasks');
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
// Should only return parent and regular tasks, not subtasks
const taskIds = response.body.tasks.map((t) => t.id);
const taskNames = response.body.tasks.map((t) => t.name);
expect(taskIds).toContain(parentTask.id);
expect(taskIds).toContain(regularTask.id);
expect(taskIds).not.toContain(subtask1.id);
expect(taskIds).not.toContain(subtask2.id);
expect(taskNames).toContain('Parent Task');
expect(taskNames).toContain('Regular Task');
expect(taskNames).not.toContain('Subtask 1');
expect(taskNames).not.toContain('Subtask 2');
});
});
describe('Recurring task search functionality', () => {
it('should include recurring task instances in search results', async () => {
const today = new Date();
today.setHours(0, 0, 0, 0);
// Create a recurring task template (set for today to ensure it's included)
const recurringTemplate = await Task.create({
name: 'RecurringTask',
user_id: user.id,
recurrence_type: 'daily',
recurrence_interval: 1,
due_date: today,
status: 0,
});
// Create a recurring task instance (simulating what the recurring task service would create)
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const recurringInstance = await Task.create({
name: 'RecurringTask',
user_id: user.id,
recurring_parent_id: recurringTemplate.id,
due_date: tomorrow,
status: 0,
});
// Create a regular non-recurring task for comparison
const regularTask = await Task.create({
name: 'Review Pull Request',
user_id: user.id,
status: 0,
});
const response = await agent.get(
'/api/tasks?include_instances=true'
);
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
const taskIds = response.body.tasks.map((t) => t.id);
const taskNames = response.body.tasks.map((t) => t.name);
// Should include the recurring template
expect(taskIds).toContain(recurringTemplate.id);
// Should include the recurring instance (this is the key fix - instances should be searchable)
expect(taskIds).toContain(recurringInstance.id);
// Should include the regular task
expect(taskIds).toContain(regularTask.id);
expect(taskNames).toContain('Review Pull Request');
// Verify we have both the template and instance - this proves search will work on both
const allTasks = response.body.tasks;
const templateTask = allTasks.find(
(t) => t.id === recurringTemplate.id
);
const instanceTask = allTasks.find(
(t) => t.id === recurringInstance.id
);
expect(templateTask).toBeDefined();
// The template name gets transformed to show the recurrence type in the API response
expect(templateTask.name).toBe('Daily');
expect(templateTask.recurrence_type).toBe('daily');
expect(templateTask.recurring_parent_id).toBeNull();
// The original name is preserved in original_name field
expect(templateTask.original_name).toBe('RecurringTask');
expect(instanceTask).toBeDefined();
// Instances keep their original name
expect(instanceTask.name).toBe('RecurringTask');
expect(instanceTask.recurring_parent_id).toBe(recurringTemplate.id);
});
it('should not include past recurring instances', async () => {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
// Create a recurring task template
const recurringTemplate = await Task.create({
name: 'Daily Review',
user_id: user.id,
recurrence_type: 'daily',
recurrence_interval: 1,
due_date: yesterday, // Template is in the past but should still be included if it's recurring
status: 0,
});
// Create a past recurring task instance
const twoDaysAgo = new Date();
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
const pastInstance = await Task.create({
name: 'Daily Review',
user_id: user.id,
recurring_parent_id: recurringTemplate.id,
due_date: twoDaysAgo,
status: 0,
});
// Create a future recurring task instance
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
const futureInstance = await Task.create({
name: 'Daily Review',
user_id: user.id,
recurring_parent_id: recurringTemplate.id,
due_date: tomorrow,
status: 0,
});
const response = await agent.get(
'/api/tasks?include_instances=true'
);
expect(response.status).toBe(200);
expect(response.body.tasks).toBeDefined();
const taskIds = response.body.tasks.map((t) => t.id);
// Should not include past instances
expect(taskIds).not.toContain(pastInstance.id);
// Should include future instances
expect(taskIds).toContain(futureInstance.id);
// Template should not be included because it's in the past
expect(taskIds).not.toContain(recurringTemplate.id);
});
});
});