'use strict'; const request = require('supertest'); const app = require('../../src/main/app'); const { setDb } = require('../../src/db/knex'); const { createTestDb } = require('../helpers/testDb'); const { issueToken } = require('../../src/utils/jwt'); const { v4: uuidv4 } = require('uuid'); function makeUser(overrides = {}) { return { id: uuidv4(), email: 'jane@example.com', name: 'Jane Doe', profile_picture_url: null, email_verified: true, deleted_at: null, deletion_scheduled_at: null, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), ...overrides, }; } describe('Sync Route', () => { let db; let user; let token; beforeEach(() => { user = makeUser(); db = createTestDb({ users: [user], pantry_items: [], shopping_lists: [], shopping_list_items: [], deleted_records: [], }); setDb(db); token = issueToken(user).token; }); describe('GET /v1/sync', () => { it('returns full sync when no since param', async () => { const res = await request(app) .get('/v1/sync') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.full_sync).toBe(true); expect(res.body.server_timestamp).toBeDefined(); expect(res.body.pantry).toBeDefined(); expect(res.body.shopping_lists).toBeDefined(); }); it('returns delta sync when since param provided', async () => { const since = new Date(Date.now() - 60000).toISOString(); // 1 minute ago const res = await request(app) .get(`/v1/sync?since=${encodeURIComponent(since)}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.full_sync).toBe(false); }); it('includes pantry items in full sync', async () => { db.seedTable('pantry_items', [ { id: uuidv4(), user_id: user.id, item_name: 'Flour', item_name_lower: 'flour', quantity: 5, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }, ]); const res = await request(app) .get('/v1/sync') .set('Authorization', `Bearer ${token}`); expect(res.body.pantry.items).toHaveLength(1); expect(res.body.pantry.items[0].item_name).toBe('Flour'); expect(res.body.pantry.items[0].deleted).toBe(false); }); it('includes shopping lists in full sync', async () => { const listId = uuidv4(); db.seedTable('shopping_lists', [ { id: listId, user_id: user.id, list_name: 'Weekly', last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }, ]); const res = await request(app) .get('/v1/sync') .set('Authorization', `Bearer ${token}`); expect(res.body.shopping_lists.lists).toHaveLength(1); expect(res.body.shopping_lists.lists[0].list_name).toBe('Weekly'); }); it('includes deleted_ids for tombstoned pantry items', async () => { const deletedId = uuidv4(); const since = new Date(Date.now() - 60000).toISOString(); db.seedTable('deleted_records', [ { id: uuidv4(), user_id: user.id, record_type: 'pantry_item', record_id: deletedId, deleted_at: new Date().toISOString(), }, ]); const res = await request(app) .get(`/v1/sync?since=${encodeURIComponent(since)}`) .set('Authorization', `Bearer ${token}`); expect(res.body.pantry.deleted_ids).toContain(deletedId); }); it('returns 401 without token', async () => { const res = await request(app).get('/v1/sync'); expect(res.status).toBe(401); }); it('returns 400 for invalid since timestamp', async () => { const res = await request(app) .get('/v1/sync?since=not-a-date') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(400); }); it('does not return data belonging to other users', async () => { const otherUser = makeUser({ id: uuidv4(), email: 'other@example.com' }); db.seedTable('pantry_items', [ { id: uuidv4(), user_id: otherUser.id, item_name: 'Sugar', item_name_lower: 'sugar', quantity: 3, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }, ]); const res = await request(app) .get('/v1/sync') .set('Authorization', `Bearer ${token}`); expect(res.body.pantry.items).toHaveLength(0); }); }); });