tududi/backend/tests/integration/views.test.js
Chris b0b613f7bd
Reorder elements (#687)
* Reorder elements

* Enhance global search
2025-12-09 10:51:51 +02:00

585 lines
20 KiB
JavaScript

const request = require('supertest');
const app = require('../../app');
const { View, Task, Tag, User } = require('../../models');
const { createTestUser } = require('../helpers/testUtils');
describe('Views Routes', () => {
let user, agent;
beforeEach(async () => {
user = await createTestUser({
email: 'views-test@example.com',
});
// Create authenticated agent
agent = request.agent(app);
await agent.post('/api/login').send({
email: 'views-test@example.com',
password: 'password123',
});
});
describe('POST /api/views', () => {
it('should create a view without tags', async () => {
const response = await agent.post('/api/views').send({
name: 'My Tasks',
search_query: 'important',
filters: ['Task'],
priority: 'high',
due: null,
tags: null,
});
expect(response.status).toBe(201);
expect(response.body.name).toBe('My Tasks');
expect(response.body.search_query).toBe('important');
expect(response.body.filters).toEqual(['Task']);
expect(response.body.priority).toBe('high');
expect(response.body.tags).toEqual([]);
expect(response.body.uid).toBeDefined();
});
it('should create a view with single tag', async () => {
const response = await agent.post('/api/views').send({
name: 'Work Tasks',
search_query: null,
filters: ['Task'],
priority: null,
due: null,
tags: ['work'],
});
expect(response.status).toBe(201);
expect(response.body.name).toBe('Work Tasks');
expect(response.body.tags).toEqual(['work']);
});
it('should create a view with multiple tags', async () => {
const response = await agent.post('/api/views').send({
name: 'Urgent Work Tasks',
search_query: null,
filters: ['Task'],
priority: 'high',
due: null,
tags: ['work', 'urgent'],
});
expect(response.status).toBe(201);
expect(response.body.name).toBe('Urgent Work Tasks');
expect(response.body.tags).toEqual(['work', 'urgent']);
});
it('should create a view with all filters including tags', async () => {
const response = await agent.post('/api/views').send({
name: 'Comprehensive View',
search_query: 'meeting',
filters: ['Task', 'Project'],
priority: 'high',
due: 'today',
defer: 'tomorrow',
tags: ['work', 'important'],
extras: ['recurring', 'has_content'],
});
expect(response.status).toBe(201);
expect(response.body.name).toBe('Comprehensive View');
expect(response.body.search_query).toBe('meeting');
expect(response.body.filters).toEqual(['Task', 'Project']);
expect(response.body.priority).toBe('high');
expect(response.body.due).toBe('today');
expect(response.body.defer).toBe('tomorrow');
expect(response.body.tags).toEqual(['work', 'important']);
expect(response.body.extras).toEqual(['recurring', 'has_content']);
});
it('should create a view that persists extras without tags', async () => {
const response = await agent.post('/api/views').send({
name: 'Recurring Tasks',
filters: ['Task'],
extras: ['recurring', 'overdue'],
});
expect(response.status).toBe(201);
expect(response.body.extras).toEqual(['recurring', 'overdue']);
expect(response.body.tags).toEqual([]);
});
it('should require view name', async () => {
const response = await agent.post('/api/views').send({
name: '',
filters: ['Task'],
});
expect(response.status).toBe(400);
expect(response.body.error).toBe('View name is required');
});
it('should handle empty tags array', async () => {
const response = await agent.post('/api/views').send({
name: 'No Tags View',
filters: ['Task'],
tags: [],
});
expect(response.status).toBe(201);
expect(response.body.tags).toEqual([]);
});
});
describe('GET /api/views', () => {
beforeEach(async () => {
await View.create({
user_id: user.id,
name: 'View 1',
filters: ['Task'],
tags: ['work'],
});
await View.create({
user_id: user.id,
name: 'View 2',
filters: ['Project'],
tags: ['personal', 'home'],
});
await View.create({
user_id: user.id,
name: 'Pinned View',
filters: ['Task'],
tags: [],
is_pinned: true,
});
});
it('should retrieve all views for the user', async () => {
const response = await agent.get('/api/views');
expect(response.status).toBe(200);
expect(response.body.length).toBe(3);
});
it('should return views with tags', async () => {
const response = await agent.get('/api/views');
expect(response.status).toBe(200);
const view1 = response.body.find((v) => v.name === 'View 1');
const view2 = response.body.find((v) => v.name === 'View 2');
expect(view1.tags).toEqual(['work']);
expect(view2.tags).toEqual(['personal', 'home']);
});
it('should order pinned views first', async () => {
const response = await agent.get('/api/views');
expect(response.status).toBe(200);
expect(response.body[0].name).toBe('Pinned View');
});
});
describe('GET /api/views/:identifier', () => {
let viewUid;
beforeEach(async () => {
const view = await View.create({
user_id: user.id,
name: 'Tagged View',
filters: ['Task', 'Note'],
priority: 'high',
tags: ['work', 'urgent'],
});
viewUid = view.uid;
});
it('should retrieve a specific view by uid', async () => {
const response = await agent.get(`/api/views/${viewUid}`);
expect(response.status).toBe(200);
expect(response.body.name).toBe('Tagged View');
expect(response.body.filters).toEqual(['Task', 'Note']);
expect(response.body.priority).toBe('high');
expect(response.body.tags).toEqual(['work', 'urgent']);
});
it('should return 404 for non-existent view', async () => {
const response = await agent.get('/api/views/nonexistent-uid');
expect(response.status).toBe(404);
expect(response.body.error).toBe('View not found');
});
});
describe('PATCH /api/views/:identifier', () => {
let viewUid;
beforeEach(async () => {
const view = await View.create({
user_id: user.id,
name: 'Original View',
filters: ['Task'],
tags: ['work'],
});
viewUid = view.uid;
});
it('should update view tags', async () => {
const response = await agent.patch(`/api/views/${viewUid}`).send({
tags: ['work', 'urgent', 'high-priority'],
});
expect(response.status).toBe(200);
expect(response.body.tags).toEqual([
'work',
'urgent',
'high-priority',
]);
});
it('should clear tags when set to empty array', async () => {
const response = await agent.patch(`/api/views/${viewUid}`).send({
tags: [],
});
expect(response.status).toBe(200);
expect(response.body.tags).toEqual([]);
});
it('should update name and tags together', async () => {
const response = await agent.patch(`/api/views/${viewUid}`).send({
name: 'Updated View',
tags: ['personal'],
});
expect(response.status).toBe(200);
expect(response.body.name).toBe('Updated View');
expect(response.body.tags).toEqual(['personal']);
});
it('should update all fields including tags', async () => {
const response = await agent.patch(`/api/views/${viewUid}`).send({
name: 'Fully Updated View',
search_query: 'important',
filters: ['Task', 'Project'],
priority: 'high',
due: 'today',
defer: 'next_week',
tags: ['work', 'urgent'],
extras: ['recurring'],
is_pinned: true,
});
expect(response.status).toBe(200);
expect(response.body.name).toBe('Fully Updated View');
expect(response.body.search_query).toBe('important');
expect(response.body.filters).toEqual(['Task', 'Project']);
expect(response.body.priority).toBe('high');
expect(response.body.due).toBe('today');
expect(response.body.defer).toBe('next_week');
expect(response.body.tags).toEqual(['work', 'urgent']);
expect(response.body.extras).toEqual(['recurring']);
expect(response.body.is_pinned).toBe(true);
});
});
describe('DELETE /api/views/:identifier', () => {
let viewUid;
beforeEach(async () => {
const view = await View.create({
user_id: user.id,
name: 'View to Delete',
filters: ['Task'],
tags: ['work'],
});
viewUid = view.uid;
});
it('should delete a view', async () => {
const response = await agent.delete(`/api/views/${viewUid}`);
expect(response.status).toBe(200);
expect(response.body.message).toBe('View successfully deleted');
// Verify it's gone
const getResponse = await agent.get(`/api/views/${viewUid}`);
expect(getResponse.status).toBe(404);
});
it('should return 404 for non-existent view', async () => {
const response = await agent.delete('/api/views/nonexistent-uid');
expect(response.status).toBe(404);
expect(response.body.error).toBe('View not found');
});
});
describe('Views with Tag Filtering Integration', () => {
let workTag, urgentTag, personalTag;
beforeEach(async () => {
// Create tags
workTag = await Tag.create({
user_id: user.id,
name: 'work',
});
urgentTag = await Tag.create({
user_id: user.id,
name: 'urgent',
});
personalTag = await Tag.create({
user_id: user.id,
name: 'personal',
});
// Create tasks with tags (mix of active and completed)
const task1 = await Task.create({
user_id: user.id,
name: 'Work task 1',
status: 0, // active
});
await task1.addTag(workTag);
const task2 = await Task.create({
user_id: user.id,
name: 'Urgent work task',
status: 0, // active
});
await task2.addTag(workTag);
await task2.addTag(urgentTag);
const task3 = await Task.create({
user_id: user.id,
name: 'Personal task',
status: 0, // active
});
await task3.addTag(personalTag);
// Add completed task with work tag
const task4 = await Task.create({
user_id: user.id,
name: 'Completed work task',
status: 2, // completed (done)
});
await task4.addTag(workTag);
});
it('should create view with tags and retrieve matching results', async () => {
// Create a view with work tag
const createResponse = await agent.post('/api/views').send({
name: 'Work Tasks View',
filters: ['Task'],
tags: ['work'],
});
expect(createResponse.status).toBe(201);
expect(createResponse.body.tags).toEqual(['work']);
// Verify the view is retrievable
const getResponse = await agent.get(
`/api/views/${createResponse.body.uid}`
);
expect(getResponse.status).toBe(200);
expect(getResponse.body.tags).toEqual(['work']);
// Now use search API with the same tags to verify filtering works
const searchResponse = await agent.get('/api/search').query({
tags: 'work',
filters: 'Task',
});
expect(searchResponse.status).toBe(200);
const tasks = searchResponse.body.results.filter(
(r) => r.type === 'Task'
);
// Should now return 3 tasks (2 active + 1 completed)
expect(tasks.length).toBe(3);
// All tasks should have 'work' in their name (case-insensitive)
expect(
tasks.every((t) => t.name.toLowerCase().includes('work'))
).toBe(true);
});
it('should save view with multiple tags and retrieve correct results', async () => {
// Create a view with multiple tags
const createResponse = await agent.post('/api/views').send({
name: 'Urgent Work View',
filters: ['Task'],
tags: ['work', 'urgent'],
});
expect(createResponse.status).toBe(201);
expect(createResponse.body.tags).toEqual(['work', 'urgent']);
// Search with same tags
const searchResponse = await agent.get('/api/search').query({
tags: 'work,urgent',
filters: 'Task',
});
expect(searchResponse.status).toBe(200);
const tasks = searchResponse.body.results.filter(
(r) => r.type === 'Task'
);
// Should find tasks with either work OR urgent tag
expect(tasks.length).toBeGreaterThanOrEqual(1);
});
it('should persist tags correctly after update', async () => {
// Create view with one tag
const createResponse = await agent.post('/api/views').send({
name: 'Initial View',
filters: ['Task'],
tags: ['work'],
});
const viewUid = createResponse.body.uid;
// Update to different tags
const updateResponse = await agent
.patch(`/api/views/${viewUid}`)
.send({
tags: ['personal'],
});
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.tags).toEqual(['personal']);
// Retrieve and verify
const getResponse = await agent.get(`/api/views/${viewUid}`);
expect(getResponse.status).toBe(200);
expect(getResponse.body.tags).toEqual(['personal']);
});
it('should return both active and completed tasks in search results', async () => {
// Create a view with work tag
const createResponse = await agent.post('/api/views').send({
name: 'All Work Tasks View',
filters: ['Task'],
tags: ['work'],
});
expect(createResponse.status).toBe(201);
// Search for work tasks
const searchResponse = await agent.get('/api/search').query({
tags: 'work',
filters: 'Task',
});
expect(searchResponse.status).toBe(200);
const tasks = searchResponse.body.results.filter(
(r) => r.type === 'Task'
);
// Should return 3 tasks: 2 active + 1 completed
expect(tasks.length).toBe(3);
// Verify we have both active and completed tasks
const activeTasks = tasks.filter(
(t) => t.status === 0 || t.status === 'active'
);
const completedTasks = tasks.filter(
(t) => t.status === 2 || t.status === 'done'
);
expect(activeTasks.length).toBe(2);
expect(completedTasks.length).toBe(1);
// Verify the completed task is included
const completedTask = tasks.find(
(t) => t.name === 'Completed work task'
);
expect(completedTask).toBeDefined();
expect(completedTask.status).toBe(2); // done status
});
it('should include completed tasks with correct status values', async () => {
// Create tasks with different completion statuses
const archivedTask = await Task.create({
user_id: user.id,
name: 'Archived work task',
status: 3, // archived
});
await archivedTask.addTag(workTag);
// Search for all work tasks
const searchResponse = await agent.get('/api/search').query({
tags: 'work',
filters: 'Task',
});
expect(searchResponse.status).toBe(200);
const tasks = searchResponse.body.results.filter(
(r) => r.type === 'Task'
);
// Should now have 4 tasks (2 active, 1 done, 1 archived)
expect(tasks.length).toBe(4);
// Verify different status types are present
const statusTypes = tasks.map((t) => t.status);
expect(statusTypes).toContain(0); // active
expect(statusTypes).toContain(2); // done
expect(statusTypes).toContain(3); // archived
// Frontend will filter these out, but backend should provide them all
const nonActiveTasks = tasks.filter((t) => t.status >= 2);
expect(nonActiveTasks.length).toBe(2); // done + archived
});
});
describe('User Isolation', () => {
let otherUser, otherAgent;
beforeEach(async () => {
// Create another user
otherUser = await createTestUser({
email: 'other-views-user@example.com',
});
otherAgent = request.agent(app);
await otherAgent.post('/api/login').send({
email: 'other-views-user@example.com',
password: 'password123',
});
// Create view for first user
await View.create({
user_id: user.id,
name: 'User 1 View',
filters: ['Task'],
tags: ['work'],
});
// Create view for second user
await View.create({
user_id: otherUser.id,
name: 'User 2 View',
filters: ['Task'],
tags: ['personal'],
});
});
it('should only return views for authenticated user', async () => {
const response = await agent.get('/api/views');
expect(response.status).toBe(200);
expect(response.body.length).toBe(1);
expect(response.body[0].name).toBe('User 1 View');
expect(response.body[0].tags).toEqual(['work']);
});
it('should not allow access to other users views', async () => {
const otherUserViews = await View.findAll({
where: { user_id: otherUser.id },
});
const otherViewUid = otherUserViews[0].uid;
const response = await agent.get(`/api/views/${otherViewUid}`);
expect(response.status).toBe(404);
});
});
});