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(); }); const PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==', 'base64' ); /** * Nothing exists, so every id below is absent. 999999 is well-formed and * missing; 'abc' is not a number at all. Before #207 the first answered 200 * with an empty body and the second answered 500, because the raw string * reached Postgres and raised 22P02. */ const ABSENT = 999999; describe('admin item routes for an id that does not exist', () => { it('PUT answers 404 rather than 200 with an empty body', async () => { const res = await request(app) .put(`/api/admin/items/${ABSENT}`) .field('name', 'renamed') .field('price', '10.00'); expect(res.status).toBe(404); expect(res.body).toEqual({ error: 'not found' }); }); it('mark-sold answers 404', async () => { const res = await request(app).post(`/api/admin/items/${ABSENT}/mark-sold`); expect(res.status).toBe(404); }); it('mark-available answers 404', async () => { const res = await request(app).post(`/api/admin/items/${ABSENT}/mark-available`); expect(res.status).toBe(404); }); // A garbage id used to reach Postgres and raise 22P02, which the catch turned // into a 500. From the caller's side "/items/abc" identifies no item, exactly // like "/items/999999" does. it.each(['abc', '1.5', '-1', ''])('PUT answers 404 for the id %p', async (id) => { const res = await request(app) .put(`/api/admin/items/${id}`) .field('name', 'renamed') .field('price', '10.00'); expect(res.status).toBe(404); }); it('mark-available answers 404 for a non-numeric id', async () => { const res = await request(app).post('/api/admin/items/abc/mark-available'); expect(res.status).toBe(404); }); }); describe('admin item routes for an id that does exist', () => { async function makeItem(): Promise { const res = await request(app) .post('/api/admin/items') .field('name', 'a real item') .field('price', '12.00') .attach('images', PNG, 'a.png'); return res.body.id; } // The point of the change is to stop lying about misses, not to start // refusing hits. it('PUT still updates and answers with the item', async () => { const id = await makeItem(); const res = await request(app) .put(`/api/admin/items/${id}`) .field('name', 'renamed') .field('price', '15.00'); expect(res.status).toBe(200); expect(res.body.name).toBe('renamed'); expect(res.body.price_cents).toBe(1500); }); it('mark-available still publishes', async () => { const id = await makeItem(); const res = await request(app).post(`/api/admin/items/${id}/mark-available`); expect(res.status).toBe(200); expect(res.body.status).toBe('available'); }); }); describe('a non-numeric id on every admin item route', () => { // Each of these used to reach Postgres, raise 22P02 and surface as a 500. // The block was named "every" while covering two; it now covers every POST // route that takes an id, which is what makes the name true (#207). it.each([ ['mark-sold', '/api/admin/items/abc/mark-sold'], ['mark-available', '/api/admin/items/abc/mark-available'], ['unpublish', '/api/admin/items/abc/unpublish'], ['regenerate', '/api/admin/item-drafts/abc/regenerate'], ['discard', '/api/admin/item-drafts/abc/discard'], ['restore', '/api/admin/item-drafts/abc/restore'], ['remove-background', '/api/admin/item-drafts/abc/images/1/remove-background'], ['restore-original', '/api/admin/item-drafts/abc/images/1/restore-original'], // The image id, not the item id — a route with two of them can guard one // and forget the other, and only a case each way would notice. ['remove-background by image', '/api/admin/item-drafts/1/images/abc/remove-background'], ['restore-original by image', '/api/admin/item-drafts/1/images/abc/restore-original'] ])('%s answers 404', async (_name, path) => { expect((await request(app).post(path)).status).toBe(404); }); it('PUT answers 404', async () => { const res = await request(app) .put('/api/admin/items/abc') .field('name', 'renamed') .field('price', '10.00'); expect(res.status).toBe(404); }); // Publish validates its body before it looks at the id, so this sends a // valid one — otherwise the 400 would mask whether the id was ever handled. it('publish answers 404', async () => { const res = await request(app) .post('/api/admin/item-drafts/abc/publish') .send({ name: 'a name', description: '', priceCents: 1000 }); expect(res.status).toBe(404); }); it.each([ ['DELETE an item', '/api/admin/items/abc'], ['DELETE an image by item id', '/api/admin/items/abc/images/1'], ['DELETE an image by image id', '/api/admin/items/1/images/abc'] ])('%s answers 404', async (_name, path) => { expect((await request(app).delete(path)).status).toBe(404); }); }); /** * A well-formed id that is simply absent. * * Deliberately not the same question as a malformed one. DELETE stays 204 for * an item that is not there: the method is idempotent and the caller's intent — * that the item should not exist — is satisfied either way. What must not * happen is a 500, which is what a malformed id used to produce. */ describe('a well-formed but absent id on the delete routes', () => { it('DELETE answers 204 rather than failing', async () => { expect((await request(app).delete(`/api/admin/items/${ABSENT}`)).status).toBe(204); }); });