Files
redefined-designs/backend/tests/integration/customers.integration.test.ts
T
synAdminandClaude Opus 5 e7196f440b
Linting / lint (pull_request) Successful in 2m43s
SonarQube Analysis / sonarqube (pull_request) Failing after 25m24s
test(customers): let the public shape guard see analytics_consent (#56)
The register route now returns analytics_consent, and customers.integration.test.ts asserts the exact key set the public customer shape may contain. That test failed in CI, which is the guard doing its job rather than a problem with it: its whole point is that the shape cannot quietly grow, and a field appearing without someone deciding it belongs there is what it exists to catch. This field does belong there, so the expected set gains it.

Three cases added while here, all of them properties the compliance work depends on and none of them observable from a unit test. Analytics consent is off for a registration that does not mention it, which is what Quebec's Law 25 s.8.1 requires and needs the column default, the register route and the stored wording to agree. Opting in to marketing alone leaves analytics off, which is the bundling GDPR treats as invalid and the mistake this branch already made once. And an analytics-only opt-in works with marketing left off, so the granularity holds in both directions rather than only the convenient one.

Found by CI rather than locally: the integration suite needs a database this machine has no Docker to run, which was called out as unverified when the change went up. Typechecked, linted and the 478 unit tests still pass, but the assertion itself is only proven by the next CI run.

Refs #56

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:28:17 -05:00

205 lines
7.5 KiB
TypeScript
Executable File

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',
'first_name', '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);
});
});