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(); }); describe('POST /api/customers/register', () => { // Both names are required from anyone new, so the greeting in every email has // something to use. Refused individually rather than as one "name required", // so a form that filled one and not the other is told which. See #106. it('refuses a registration with no first name', async () => { const res = await request(app).post('/api/customers/register').send({ email: 'nofirst@example.com', password: 'supersecret123', lastName: 'Customer' }); expect(res.status).toBe(400); expect(res.body.error).toBe('first name is required'); }); it('refuses a registration with no last name', async () => { const res = await request(app).post('/api/customers/register').send({ email: 'nolast@example.com', password: 'supersecret123', firstName: 'Test' }); expect(res.status).toBe(400); expect(res.body.error).toBe('last name is required'); }); it('refuses names that are only whitespace', async () => { const res = await request(app).post('/api/customers/register').send({ email: 'blank@example.com', password: 'supersecret123', firstName: ' ', lastName: 'Customer' }); expect(res.status).toBe(400); expect(res.body.error).toBe('first name is required'); }); it('stores both names, trimmed, and returns them', async () => { const res = await request(app).post('/api/customers/register').send({ email: 'named@example.com', password: 'supersecret123', firstName: ' Thom ', lastName: ' Lamb ' }); expect(res.status).toBe(200); expect(res.body.first_name).toBe('Thom'); expect(res.body.last_name).toBe('Lamb'); const { rows } = await pool.query( `SELECT first_name, last_name FROM customers WHERE email = $1`, ['named@example.com'] ); expect(rows[0]).toEqual({ first_name: 'Thom', last_name: 'Lamb' }); }); // The account is the customer's own, but the shape returned to them should // not quietly grow — a password hash or a token appearing here is the kind of // thing that goes unnoticed. it('does not return anything beyond the public customer shape', async () => { const res = await request(app).post('/api/customers/register').send({ email: 'shape@example.com', password: 'supersecret123', firstName: 'Test', lastName: 'Customer' }); expect(Object.keys(res.body).sort()).toEqual([ 'analytics_consent', 'created_at', 'email', 'email_verified', 'favorite_alerts', // Whether, never what. Added in #344 so the account page can offer to set // a first password rather than to change one that does not exist; the // hash itself must never appear in this list. 'first_name', 'has_password', 'id', 'last_name', 'marketing_consent' ]); }); it('creates an account with marketing consent unchecked by default', async () => { const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'jane@example.com', password: 'supersecret123' }); expect(res.status).toBe(200); expect(res.body.email).toBe('jane@example.com'); expect(res.body.marketing_consent).toBe(false); expect(res.headers['set-cookie']).toBeDefined(); }); it('respects an explicit marketing opt-in', async () => { const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'opt-in@example.com', password: 'supersecret123', marketingConsent: true }); expect(res.body.marketing_consent).toBe(true); }); // Quebec's Law 25 s.8.1 requires profiling to be off until the person turns // it on, so this is a compliance property rather than a default worth // debating. Asserted end to end because the column default, the register // route and the stored wording all have to agree for it to hold. it('creates an account with analytics consent off by default', async () => { const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'analytics-default@example.com', password: 'supersecret123' }); expect(res.body.analytics_consent).toBe(false); }); // The two consents are separate purposes and must be separately refusable. // Taking the emails must not opt anybody into being tracked — that bundling // is what GDPR treats as invalid consent, and it is the mistake this branch // made once before it was caught. it('opting in to marketing alone does not opt in to analytics', async () => { const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'marketing-only@example.com', password: 'supersecret123', marketingConsent: true }); expect(res.body.marketing_consent).toBe(true); expect(res.body.analytics_consent).toBe(false); }); it('respects an explicit analytics opt-in, independently of marketing', async () => { const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'analytics-only@example.com', password: 'supersecret123', analyticsConsent: true }); expect(res.body.analytics_consent).toBe(true); // Refusing the emails while accepting the tracking has to be possible too, // or the consent is not granular in both directions. expect(res.body.marketing_consent).toBe(false); }); it('rejects a duplicate email', async () => { await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'dupe@example.com', password: 'supersecret123' }); const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'dupe@example.com', password: 'anotherpassword' }); expect(res.status).toBe(409); }); it('rejects a password under 8 characters', async () => { const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'short@example.com', password: '123' }); expect(res.status).toBe(400); }); it('rejects a malformed email', async () => { const res = await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'not-an-email', password: 'supersecret123' }); expect(res.status).toBe(400); }); }); describe('session-gated routes', () => { it('logs in and can access /me with the returned session cookie', async () => { const agent = request.agent(app); await agent.post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'login-test@example.com', password: 'supersecret123' }); const me = await agent.get('/api/customers/me'); expect(me.status).toBe(200); expect(me.body.email).toBe('login-test@example.com'); }); it('rejects /me with no session', async () => { const res = await request(app).get('/api/customers/me'); expect(res.status).toBe(401); }); it('rejects login with the wrong password', async () => { await request(app).post('/api/customers/register').send({ firstName: 'Test', lastName: 'Customer', email: 'wrongpass@example.com', password: 'supersecret123' }); const res = await request(app).post('/api/customers/login').send({ email: 'wrongpass@example.com', password: 'incorrect-password' }); expect(res.status).toBe(401); }); });