'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'); function makeUser(overrides = {}) { return { id: require('uuid').v4(), 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('Pantry Routes', () => { let db; let user; let token; beforeEach(() => { user = makeUser(); db = createTestDb({ users: [user] }); setDb(db); token = issueToken(user).token; }); // ── GET /v1/pantry ────────────────────────────────────────────────────────── describe('GET /v1/pantry', () => { it('returns empty items array for new user', async () => { const res = await request(app) .get('/v1/pantry') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.items).toEqual([]); expect(res.body.synced_at).toBeDefined(); }); it('returns pantry items for authenticated user', async () => { db.seedTable('pantry_items', [ { id: 'item-1', 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/pantry') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.items).toHaveLength(1); expect(res.body.items[0].item_name).toBe('Flour'); }); it('returns 401 without token', async () => { const res = await request(app).get('/v1/pantry'); expect(res.status).toBe(401); }); it('does not return items belonging to other users', async () => { const otherUser = makeUser({ id: 'other-user-id', email: 'other@example.com' }); db.seedTable('pantry_items', [ { id: 'item-other', user_id: otherUser.id, item_name: 'Sugar', item_name_lower: 'sugar', quantity: 2, last_modified: new Date().toISOString(), created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }, ]); const res = await request(app) .get('/v1/pantry') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(200); expect(res.body.items).toHaveLength(0); }); }); // ── POST /v1/pantry ───────────────────────────────────────────────────────── describe('POST /v1/pantry', () => { it('adds a new pantry item', async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Flour', quantity: 5 }); expect(res.status).toBe(201); expect(res.body.item.item_name).toBe('Flour'); expect(res.body.item.quantity).toBe(5); expect(res.body.item.id).toBeDefined(); }); it('returns 409 for duplicate item (case-insensitive)', async () => { await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Flour', quantity: 5 }); const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'flour', quantity: 3 }); expect(res.status).toBe(409); expect(res.body.code).toBe('DUPLICATE_ITEM'); }); it('returns 409 for duplicate with different capitalisation', async () => { await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Protein Powder', quantity: 1 }); const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'PROTEIN POWDER', quantity: 1 }); expect(res.status).toBe(409); }); it('returns 400 for zero quantity', async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Flour', quantity: 0 }); expect(res.status).toBe(400); }); it('returns 400 for negative quantity', async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Flour', quantity: -1 }); expect(res.status).toBe(400); }); it('returns 400 for fractional quantity', async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Flour', quantity: 1.5 }); expect(res.status).toBe(400); }); it('returns 400 for missing item_name', async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ quantity: 5 }); expect(res.status).toBe(400); }); it('returns 400 for blank item_name', async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: ' ', quantity: 5 }); expect(res.status).toBe(400); }); it('returns 401 without token', async () => { const res = await request(app) .post('/v1/pantry') .send({ item_name: 'Flour', quantity: 5 }); expect(res.status).toBe(401); }); }); // ── PUT /v1/pantry/:item_id ───────────────────────────────────────────────── describe('PUT /v1/pantry/:item_id', () => { let itemId; beforeEach(async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Flour', quantity: 5 }); itemId = res.body.item.id; }); it('updates quantity of existing item', async () => { const res = await request(app) .put(`/v1/pantry/${itemId}`) .set('Authorization', `Bearer ${token}`) .send({ quantity: 10 }); expect(res.status).toBe(200); expect(res.body.item.quantity).toBe(10); }); it('returns 404 for non-existent item', async () => { const res = await request(app) .put('/v1/pantry/00000000-0000-0000-0000-000000000000') .set('Authorization', `Bearer ${token}`) .send({ quantity: 10 }); expect(res.status).toBe(404); }); it('returns 404 for item belonging to another user', async () => { const otherUser = makeUser({ id: 'other-id', email: 'other@example.com' }); const otherToken = issueToken(otherUser).token; const res = await request(app) .put(`/v1/pantry/${itemId}`) .set('Authorization', `Bearer ${otherToken}`) .send({ quantity: 10 }); expect(res.status).toBe(404); }); it('returns 400 for invalid quantity', async () => { const res = await request(app) .put(`/v1/pantry/${itemId}`) .set('Authorization', `Bearer ${token}`) .send({ quantity: -5 }); expect(res.status).toBe(400); }); }); // ── DELETE /v1/pantry/:item_id ────────────────────────────────────────────── describe('DELETE /v1/pantry/:item_id', () => { let itemId; beforeEach(async () => { const res = await request(app) .post('/v1/pantry') .set('Authorization', `Bearer ${token}`) .send({ item_name: 'Butter', quantity: 2 }); itemId = res.body.item.id; }); it('deletes an existing pantry item', async () => { const res = await request(app) .delete(`/v1/pantry/${itemId}`) .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(204); // Verify item is gone const getRes = await request(app) .get('/v1/pantry') .set('Authorization', `Bearer ${token}`); expect(getRes.body.items).toHaveLength(0); }); it('records a tombstone for sync', async () => { await request(app) .delete(`/v1/pantry/${itemId}`) .set('Authorization', `Bearer ${token}`); const tombstones = db.getTable('deleted_records'); expect(tombstones.some((t) => t.record_id === itemId)).toBe(true); }); it('returns 404 for non-existent item', async () => { const res = await request(app) .delete('/v1/pantry/00000000-0000-0000-0000-000000000000') .set('Authorization', `Bearer ${token}`); expect(res.status).toBe(404); }); it('returns 404 for item belonging to another user', async () => { const otherUser = makeUser({ id: 'other-id', email: 'other@example.com' }); const otherToken = issueToken(otherUser).token; const res = await request(app) .delete(`/v1/pantry/${itemId}`) .set('Authorization', `Bearer ${otherToken}`); expect(res.status).toBe(404); }); }); });