← back to Costa Rica
test/apple-route.test.js
71 lines
'use strict';
// Route-level regression test for the c2 account-takeover guard (commit 0689909).
// The verifier-layer test (apple.test.js) proves email_verified is a real boolean;
// THIS proves the /auth/apple route only auto-links an Apple identity to an existing
// account when that boolean is true. No DB / no network: pool.query + apple verifier
// are mocked via their shared module objects. Run: node --test
const { test, before, after } = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const express = require('express');
const db = require('../lib/db');
const apple = require('../lib/apple');
let sqls = []; // every SQL the route runs, in order
let appleClaims = null; // what the mocked verifier returns
// Mock pool.query: record SQL, return sensible rows for the /auth/apple flow.
db.pool.query = async (sql) => {
sqls.push(sql);
if (/SELECT \* FROM app_users WHERE apple_sub/.test(sql)) return { rows: [] }; // no existing apple account
if (/UPDATE app_users SET apple_sub/.test(sql)) return { rows: [{ id: 1, email: 'a@b.com', role: 'guest', is_host: false }] };
if (/INSERT INTO app_users \(apple_sub/.test(sql)) return { rows: [{ id: 2, email: 'a@b.com', role: 'guest', is_host: false }] };
return { rows: [] };
};
apple.verifyIdentityToken = async () => appleClaims;
const { router } = require('../routes/app');
let server, base;
before(async () => {
const app = express();
app.use(express.json());
app.use('/api/app', router);
await new Promise(r => { server = app.listen(0, r); });
base = `http://127.0.0.1:${server.address().port}`;
});
after(() => server && server.close());
function post(path, body) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const req = http.request(base + path, { method: 'POST', headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) } },
res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ status: res.statusCode, json: JSON.parse(b || '{}') })); });
req.on('error', reject); req.end(data);
});
}
const linked = () => sqls.some(s => /UPDATE app_users SET apple_sub/.test(s));
test('SECURITY: email_verified=false → does NOT auto-link, creates a fresh Apple account instead', async () => {
sqls = []; appleClaims = { sub: 'apple-x', email: 'a@b.com', email_verified: false };
const r = await post('/api/app/auth/apple', { identity_token: 'stub' });
assert.equal(r.status, 200);
assert.equal(linked(), false, 'unverified email must NOT link to the existing account');
assert.ok(sqls.some(s => /INSERT INTO app_users \(apple_sub/.test(s)), 'a new Apple-owned account should be created');
});
test('email_verified=true → DOES auto-link the Apple identity to the existing account', async () => {
sqls = []; appleClaims = { sub: 'apple-x', email: 'a@b.com', email_verified: true };
const r = await post('/api/app/auth/apple', { identity_token: 'stub' });
assert.equal(r.status, 200);
assert.equal(linked(), true, 'verified email should link');
assert.ok(!sqls.some(s => /INSERT INTO app_users \(apple_sub/.test(s)), 'link path should skip the INSERT');
});
test('no email present → no link even if email_verified is somehow true', async () => {
sqls = []; appleClaims = { sub: 'apple-x', email: null, email_verified: true };
const r = await post('/api/app/auth/apple', { identity_token: 'stub' });
assert.equal(r.status, 200);
assert.equal(linked(), false, 'no email → nothing to link against');
});