fix-backend-review-findings (ornith-35): oauth empty-token guard, session-secret startup assertion, OAuth email-link test
Fixes from ornith's backend code review, implemented by ornith-35 (cross-review by qwopus35b pending). 129/129 tests green.
This commit is contained in:
@@ -173,6 +173,51 @@ test('oauth: oauthStart returns a redirect URL + CSRF state cookie; oauthCallbac
|
||||
}
|
||||
});
|
||||
|
||||
// --- Slice 2c: OAuth — link existing account by email ---
|
||||
test('oauthCallback links an OAuth identity to an existing user when emails match', async () => {
|
||||
const { db, cache } = setup();
|
||||
process.env.GITHUB_CLIENT_ID = 'gh_id'; process.env.GITHUB_CLIENT_SECRET = 'gh_secret';
|
||||
type Ctx = { db: typeof db; cache: typeof cache; resHeaders: Headers; userId: string | null; cookies: Record<string, string> };
|
||||
|
||||
// Step 1: sign up with email "link-me@example.com" (creates a password-based account).
|
||||
const signupCtx: Ctx = { db, cache, resHeaders: new Headers(), userId: null, cookies: {} };
|
||||
const signup = await appRouter.createCaller(signupCtx).auth.signup({ email: 'link-me@example.com', password: 'password123' });
|
||||
assert.ok(signup.userId);
|
||||
|
||||
// Step 2: oauthStart to get a valid CSRF state + redirect URL.
|
||||
const start = await appRouter.createCaller(signupCtx).auth.oauthStart({ provider: 'github', redirectUri: 'http://localhost/cb' });
|
||||
const setCookie = signupCtx.resHeaders.get('set-cookie') ?? '';
|
||||
// The oauth state cookie may be the 2nd Set-Cookie header; find it by prefix.
|
||||
const stateMatch = setCookie.match(/iflow_oauth_state=([^;]+)/);
|
||||
assert.ok(stateMatch, 'oauth state cookie should be set');
|
||||
const stateVal = stateMatch[1];
|
||||
|
||||
// Step 3: oauthCallback with the SAME email from the provider -> should link, NOT create a new row.
|
||||
const origFetch = global.fetch;
|
||||
let calls = 0;
|
||||
global.fetch = (async (url: unknown) => {
|
||||
calls++;
|
||||
const u = String(url);
|
||||
if (u.includes('/access_token')) return new Response(JSON.stringify({ access_token: 'tok' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
||||
return new Response(JSON.stringify({ id: 999, email: 'link-me@example.com', name: 'Link User' }), { status: 200, headers: { 'content-type': 'application/json' } });
|
||||
}) as typeof fetch;
|
||||
try {
|
||||
const cbCtx: Ctx = { db, cache, resHeaders: new Headers(), userId: null, cookies: { iflow_oauth_state: stateVal } };
|
||||
const res = await appRouter.createCaller(cbCtx).auth.oauthCallback({ provider: 'github', code: 'abc', state: start.state, redirectUri: 'http://localhost/cb' });
|
||||
assert.equal(res.userId, signup.userId, 'should return the EXISTING user id');
|
||||
const u = db.prepare('SELECT oauth_subject,oauth_provider,pw_hash FROM users WHERE id=?').get(signup.userId) as { oauth_subject: string; oauth_provider: string; pw_hash: string };
|
||||
assert.equal(u.oauth_subject, '999', 'oauth_subject updated');
|
||||
assert.equal(u.oauth_provider, 'github', 'oauth_provider updated');
|
||||
assert.ok(u.pw_hash.startsWith('scrypt$'), 'original pw_hash preserved as scrypt hash (linked account)');
|
||||
assert.ok(cbCtx.resHeaders.get('set-cookie'), 'session cookie set');
|
||||
// No duplicate rows: exactly 1 user.
|
||||
assert.equal((db.prepare('SELECT COUNT(*) AS c FROM users').get() as { c: number }).c, 1, 'no duplicate user created');
|
||||
} finally {
|
||||
global.fetch = origFetch;
|
||||
delete process.env.GITHUB_CLIENT_ID; delete process.env.GITHUB_CLIENT_SECRET;
|
||||
}
|
||||
});
|
||||
|
||||
// --- Slice 3: onboarding ---
|
||||
test('onboarding.complete writes complexity/risk/drawdown + starter watchlist + subscribes demand', async () => {
|
||||
const { db, cache, freshCtx } = setup();
|
||||
|
||||
Reference in New Issue
Block a user