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); }); }); });