import request from 'supertest'; import app from '../../src/app'; import { pool } from '../../src/db'; import { resetDb, closeDb } from './setup/testDb'; beforeEach(async () => { await resetDb(); }); afterAll(async () => { await pool.end(); await closeDb(); }); async function createCategory(name: string, parentId: number | null = null): Promise { const res = await request(app).post('/api/admin/categories').send({ name, parent_id: parentId }); expect(res.status).toBe(201); return res.body.id; } async function createTag(name: string): Promise { const res = await request(app).post('/api/admin/tags').send({ name }); expect(res.status).toBe(201); return res.body.id; } async function createItem( name: string, priceCents: number, options: { categoryId?: number | null; tagIds?: number[]; status?: string } = {} ): Promise { const { rows } = await pool.query( `INSERT INTO items (name, price_cents, category_id, status) VALUES ($1, $2, $3, $4) RETURNING id`, [name, priceCents, options.categoryId ?? null, options.status ?? 'available'] ); const itemId = rows[0].id; for (const tagId of options.tagIds ?? []) { await pool.query(`INSERT INTO item_tags (item_id, tag_id) VALUES ($1, $2)`, [itemId, tagId]); } return itemId; } async function registerCustomer(email: string) { const agent = request.agent(app); await agent.post('/api/customers/register').send({ email, password: 'supersecret123' }); const { rows } = await pool.query(`SELECT id FROM customers WHERE email = $1`, [email]); return { agent, id: rows[0].id as number }; } const names = (body: { name: string }[]) => body.map(item => item.name).sort(); describe('GET /api/admin/items filtering', () => { it('returns every item when nothing is filtered', async () => { await createItem('A', 1000); await createItem('B', 2000, { status: 'sold' }); const res = await request(app).get('/api/admin/items'); expect(res.status).toBe(200); expect(res.body).toHaveLength(2); }); it('filters by status, which is how Reserved is surfaced', async () => { await createItem('Free', 1000, { status: 'available' }); await createItem('Held', 1000, { status: 'reserved' }); await createItem('Gone', 1000, { status: 'sold' }); const res = await request(app).get('/api/admin/items?status=reserved'); expect(names(res.body)).toEqual(['Held']); }); it('matches a category and all of its descendants', async () => { const furniture = await createCategory('Furniture'); const tables = await createCategory('Tables', furniture); const decor = await createCategory('Decor'); await createItem('Nested', 1000, { categoryId: tables }); await createItem('Top', 1000, { categoryId: furniture }); await createItem('Elsewhere', 1000, { categoryId: decor }); const res = await request(app).get(`/api/admin/items?category=${furniture}`); expect(names(res.body)).toEqual(['Nested', 'Top']); }); it('requires every listed tag rather than any of them', async () => { const vintage = await createTag('vintage'); const oak = await createTag('oak'); await createItem('Both', 1000, { tagIds: [vintage, oak] }); await createItem('One', 1000, { tagIds: [vintage] }); const res = await request(app).get(`/api/admin/items?tags=${vintage},${oak}`); expect(names(res.body)).toEqual(['Both']); }); it('bounds the price range inclusively', async () => { await createItem('Under', 900); await createItem('Edge', 1000); await createItem('Over', 5100); const res = await request(app).get('/api/admin/items?min_price=1000&max_price=5000'); expect(names(res.body)).toEqual(['Edge']); }); it('combines every filter with AND', async () => { const furniture = await createCategory('Furniture'); const tables = await createCategory('Tables', furniture); const vintage = await createTag('vintage'); await createItem('Match', 3000, { categoryId: tables, tagIds: [vintage], status: 'reserved' }); await createItem('Wrong status', 3000, { categoryId: tables, tagIds: [vintage], status: 'available' }); await createItem('Wrong category', 3000, { tagIds: [vintage], status: 'reserved' }); await createItem('Wrong price', 9000, { categoryId: tables, tagIds: [vintage], status: 'reserved' }); const res = await request(app).get( `/api/admin/items?category=${furniture}&tags=${vintage}&min_price=1000&max_price=5000&status=reserved` ); expect(names(res.body)).toEqual(['Match']); }); it('rejects an unknown status rather than returning everything', async () => { await createItem('A', 1000); // Not 'pending': that is a real status now, and deliberately valid on the // admin route — filtering for staged items is the point of it. const res = await request(app).get('/api/admin/items?status=archived'); expect(res.status).toBe(400); }); it('still returns admin-only columns alongside the filters', async () => { await createItem('A', 1000, { status: 'reserved' }); const res = await request(app).get('/api/admin/items?status=reserved'); expect(res.body[0]).toHaveProperty('reserved_until'); expect(res.body[0]).toHaveProperty('tags'); }); }); describe('admin customer reservations', () => { it('reports how many items each customer is holding', async () => { const itemId = await createItem('Held', 1000); const { agent, id } = await registerCustomer('holder@example.com'); await agent.post(`/api/cart/items/${itemId}`); const res = await request(app).get('/api/admin/customers'); const customer = res.body.find((c: { id: number }) => c.id === id); expect(Number(customer.reserved_count)).toBe(1); }); it('reports zero for a customer holding nothing', async () => { const { id } = await registerCustomer('empty@example.com'); const res = await request(app).get('/api/admin/customers'); const customer = res.body.find((c: { id: number }) => c.id === id); expect(Number(customer.reserved_count)).toBe(0); }); it('lists the items a customer is holding', async () => { const itemId = await createItem('Oak table', 34000); const { agent, id } = await registerCustomer('lister@example.com'); await agent.post(`/api/cart/items/${itemId}`); const res = await request(app).get(`/api/admin/customers/${id}/reserved`); expect(res.status).toBe(200); expect(res.body).toHaveLength(1); expect(res.body[0].name).toBe('Oak table'); expect(res.body[0].item_id).toBe(itemId); expect(res.body[0].price_cents).toBe(34000); expect(res.body[0].expires_at).toBeTruthy(); }); it('returns an empty list for a customer holding nothing', async () => { const { id } = await registerCustomer('nothing@example.com'); const res = await request(app).get(`/api/admin/customers/${id}/reserved`); expect(res.status).toBe(200); expect(res.body).toEqual([]); }); it('releasing an item returns it to available and empties the cart row', async () => { const itemId = await createItem('Oak table', 34000); const { agent, id } = await registerCustomer('release@example.com'); await agent.post(`/api/cart/items/${itemId}`); const res = await request(app).post(`/api/admin/customers/${id}/reserved/${itemId}/release`); expect(res.status).toBe(204); const item = await request(app).get(`/api/items/${itemId}`); expect(item.body.status).toBe('available'); const { rows } = await pool.query(`SELECT COUNT(*)::int AS n FROM cart_items WHERE item_id = $1`, [itemId]); expect(rows[0].n).toBe(0); }); it('a released item stops counting against the customer', async () => { const itemId = await createItem('Oak table', 34000); const { agent, id } = await registerCustomer('recount@example.com'); await agent.post(`/api/cart/items/${itemId}`); await request(app).post(`/api/admin/customers/${id}/reserved/${itemId}/release`); const res = await request(app).get('/api/admin/customers'); const customer = res.body.find((c: { id: number }) => c.id === id); expect(Number(customer.reserved_count)).toBe(0); }); it('a released item can be reserved again by someone else', async () => { const itemId = await createItem('Oak table', 34000); const { agent: first, id } = await registerCustomer('first@example.com'); await first.post(`/api/cart/items/${itemId}`); await request(app).post(`/api/admin/customers/${id}/reserved/${itemId}/release`); const { agent: second } = await registerCustomer('second@example.com'); const res = await second.post(`/api/cart/items/${itemId}`); expect(res.status).toBe(201); }); it('refuses to release an item the customer is not holding', async () => { const itemId = await createItem('Not theirs', 1000); const { id } = await registerCustomer('other@example.com'); const res = await request(app).post(`/api/admin/customers/${id}/reserved/${itemId}/release`); expect(res.status).toBe(404); }); it('does not touch a sold item when releasing', async () => { const itemId = await createItem('Sold out', 1000); const { agent, id } = await registerCustomer('sold@example.com'); await agent.post(`/api/cart/items/${itemId}`); await pool.query(`UPDATE items SET status = 'sold' WHERE id = $1`, [itemId]); await request(app).post(`/api/admin/customers/${id}/reserved/${itemId}/release`); const { rows } = await pool.query(`SELECT status FROM items WHERE id = $1`, [itemId]); expect(rows[0].status).toBe('sold'); }); });