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 PASSWORD = 'supersecret123'; async function register(email: string) { const agent = request.agent(app); const res = await agent.post('/api/customers/register').send({ email, password: PASSWORD }); expect(res.status).toBe(200); const { rows } = await pool.query(`SELECT id FROM customers WHERE email = $1`, [email]); return { agent, id: rows[0].id as number }; } async function createItem(name: string) { const { rows } = await pool.query( `INSERT INTO items (name, price_cents) VALUES ($1, 1000) RETURNING id`, [name] ); return rows[0].id as number; } describe('disabling a customer', () => { it('is reported in the admin customer list', async () => { const { id } = await register('flag@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); const res = await request(app).get('/api/admin/customers'); const customer = res.body.find((c: { id: number }) => c.id === id); expect(customer.disabled_at).toBeTruthy(); }); it('refuses the sign-in with an explicit reason, not a credential failure', async () => { const { id } = await register('told@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); const res = await request(app) .post('/api/customers/login') .send({ email: 'told@example.com', password: PASSWORD }); // A generic 401 would send them round the password-reset loop forever. expect(res.status).toBe(403); expect(res.body.error).toMatch(/disabled/i); }); it('still refuses a sign-in with the wrong password, without revealing the disable', async () => { const { id } = await register('wrongpw@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); const res = await request(app) .post('/api/customers/login') .send({ email: 'wrongpw@example.com', password: 'not-the-password' }); expect(res.status).toBe(401); }); it('kills sessions that already existed', async () => { const { agent, id } = await register('evicted@example.com'); expect((await agent.get('/api/customers/me')).status).toBe(200); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); // The cookie is still held by the agent; it must stop working immediately // rather than at its 30-day expiry. expect((await agent.get('/api/customers/me')).status).toBe(401); }); it('releases items the customer was holding', async () => { const itemId = await createItem('Held item'); const { agent, id } = await register('holder@example.com'); expect((await agent.post(`/api/cart/items/${itemId}`)).status).toBe(201); await request(app).post(`/api/admin/customers/${id}/disable`).expect(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('leaves a released item purchasable by someone else', async () => { const itemId = await createItem('Freed item'); const { agent: first, id } = await register('first@example.com'); await first.post(`/api/cart/items/${itemId}`); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); const { agent: second } = await register('second@example.com'); expect((await second.post(`/api/cart/items/${itemId}`)).status).toBe(201); }); it('does not resurrect an item that was already sold', async () => { const itemId = await createItem('Sold item'); const { agent, id } = await register('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}/disable`).expect(204); const { rows } = await pool.query(`SELECT status FROM items WHERE id = $1`, [itemId]); expect(rows[0].status).toBe('sold'); }); it('blocks the self-service data export and account deletion', async () => { const { agent, id } = await register('gdpr@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); expect((await agent.get('/api/customers/me/export')).status).toBe(401); expect((await agent.delete('/api/customers/me')).status).toBe(401); }); it('sends no reset mail and issues no token for a disabled account', async () => { const { id } = await register('noreset@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); // Still 200, so the endpoint stays non-enumerating. const res = await request(app) .post('/api/customers/request-password-reset') .send({ email: 'noreset@example.com' }); expect(res.status).toBe(200); const { rows } = await pool.query( `SELECT COUNT(*)::int AS n FROM customer_tokens WHERE customer_id = $1 AND kind = 'password_reset'`, [id] ); expect(rows[0].n).toBe(0); }); it('refuses to complete a reset whose token predates the disable', async () => { const { id } = await register('midreset@example.com'); await request(app).post('/api/customers/request-password-reset').send({ email: 'midreset@example.com' }); const { rows } = await pool.query( `SELECT token FROM customer_tokens WHERE customer_id = $1 AND kind = 'password_reset'`, [id] ); const token = rows[0].token; await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); const res = await request(app) .post('/api/customers/reset-password') .send({ token, password: 'a-brand-new-password' }); expect(res.status).toBe(403); }); it('rejects registering again with the same address', async () => { const { id } = await register('reregister@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); // Otherwise disabling is trivially undone by signing up again. const res = await request(app) .post('/api/customers/register') .send({ email: 'reregister@example.com', password: PASSWORD }); expect(res.status).toBe(409); }); }); describe('re-enabling a customer', () => { it('restores sign-in', async () => { const { id } = await register('back@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); await request(app).post(`/api/admin/customers/${id}/enable`).expect(204); const res = await request(app) .post('/api/customers/login') .send({ email: 'back@example.com', password: PASSWORD }); expect(res.status).toBe(200); }); it('clears the disabled timestamp', async () => { const { id } = await register('cleared@example.com'); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); await request(app).post(`/api/admin/customers/${id}/enable`).expect(204); const res = await request(app).get('/api/admin/customers'); const customer = res.body.find((c: { id: number }) => c.id === id); expect(customer.disabled_at).toBeNull(); }); it('does not give back the items that were released', async () => { const itemId = await createItem('Not returned'); const { agent, id } = await register('norestore@example.com'); await agent.post(`/api/cart/items/${itemId}`); await request(app).post(`/api/admin/customers/${id}/disable`).expect(204); await request(app).post(`/api/admin/customers/${id}/enable`).expect(204); const item = await request(app).get(`/api/items/${itemId}`); expect(item.body.status).toBe('available'); }); it('returns 404 for a customer that does not exist', async () => { expect((await request(app).post('/api/admin/customers/999999/disable')).status).toBe(404); expect((await request(app).post('/api/admin/customers/999999/enable')).status).toBe(404); }); });