test/addaccount-by-email.test.js
Rama master · réplica de lectura del repositorio original.
// addAccountByEmail wiring + CheckUserLoginMethod empty-body fallback.
//
// Two regressions that surfaced together when email login was tested on
// a lab host against three real accounts (2026-04-29):
//
// 1. src/auth.js `addAccountByEmail` had been stubbed to throw
// `"Direct email/password login is not supported"`. The dashboard had
// a fully-working windsurfLogin pipeline next door, but the
// /auth/login HTTP path called the stub instead. Result: every
// {email,password} POST to /auth/login was a hard 401 even though
// Windsurf upstream still served the credentials fine.
//
// 2. The new Connect-RPC probe `CheckUserLoginMethod` (added 2026-04-26
// when Windsurf moved off /_devin-auth/connections as primary)
// sometimes returns an empty `{}` for valid emails on cold-start
// Vercel edges. The original code defaulted absent fields to false
// and reported `{method:'auth1', hasPassword:false}`, which surfaced
// as `ERR_NO_PASSWORD_SET` for accounts that DO have passwords.
// Fix: when both `userExists` and `hasPassword` are missing, return
// null so the caller does not guess.
// Update (2026-10-22): the probe ORDER was inverted — `/_devin-auth/
// connections` is now primary (its `auth_method.has_password` is what
// windsurf.com itself branches on) and CheckUserLoginMethod is enrichment
// only. An empty `{}` is still "unknown", never "password-capable".
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const AUTH_JS = readFileSync(join(__dirname, '..', 'src/auth.js'), 'utf8');
const LOGIN_JS = readFileSync(join(__dirname, '..', 'src/dashboard/windsurf-login.js'), 'utf8');
describe('addAccountByEmail wiring (#follow-up)', () => {
test('addAccountByEmail no longer throws the stub error', () => {
// The stub used to be the entire body. If it's still there as the
// first statement, the function is broken regardless of what else
// ends up in the file.
const stubLine = 'throw new Error(\'Direct email/password login is not supported';
const m = AUTH_JS.match(/export async function addAccountByEmail\(email, password\)\s*\{([\s\S]+?)\n\}/);
assert.ok(m, 'addAccountByEmail signature/body not found');
const body = m[1];
assert.ok(!body.trim().startsWith(stubLine),
'addAccountByEmail still throws the unsupported-stub error as its first statement');
});
test('addAccountByEmail delegates to windsurfLogin from the dashboard module', () => {
const m = AUTH_JS.match(/export async function addAccountByEmail[\s\S]+?\n\}/);
assert.ok(m);
const body = m[0];
assert.match(body, /windsurfLogin/,
'must call windsurfLogin (the live email→Codeium pipeline)');
assert.match(body, /addAccountByKey\(/,
'must persist the resulting apiKey via addAccountByKey');
assert.match(body, /setAccountTokens\(/,
'must persist Firebase refreshToken via setAccountTokens so background renewal works');
});
test('addAccountByEmail rejects empty email or password before hitting upstream', () => {
const m = AUTH_JS.match(/export async function addAccountByEmail[\s\S]+?\n\}/);
assert.ok(m);
const body = m[0];
assert.match(body, /if \(!email \|\| !password\)/,
'must short-circuit on missing credentials so we never POST {email:"",password:""} to Windsurf');
});
test('addAccountByEmail updates an existing email instead of duplicating the account', () => {
const m = AUTH_JS.match(/export async function addAccountByEmail[\s\S]+?\n\}/);
assert.ok(m);
const body = m[0];
assert.match(body, /existingByEmail/,
'must look up an existing account by normalized email before adding a new one');
assert.match(body, /existingByEmail \|\| addAccountByKey\(/,
'must reuse the existing email account instead of always appending');
assert.match(body, /account\.apiKey = result\.apiKey/,
'must refresh the existing account apiKey after re-login');
});
});
describe('CheckUserLoginMethod empty-body fallback (#follow-up)', () => {
test('fetchCheckUserLoginMethod returns null when both userExists and hasPassword are missing', () => {
// Static-validate the guard. The bug was: `{}` body → `userExists`
// is undefined → not strictly === false → fell through to
// {method:'auth1', hasPassword:!!undefined} = {auth1, false}.
const m = LOGIN_JS.match(/async function fetchCheckUserLoginMethod[\s\S]+?\n\}/);
assert.ok(m, 'fetchCheckUserLoginMethod not found');
const body = m[0];
assert.match(body, /hasOwnProperty\.call\(res\.data, 'userExists'\)/,
'must explicitly check whether userExists field is present (not just truthy)');
assert.match(body, /hasOwnProperty\.call\(res\.data, 'hasPassword'\)/,
'must explicitly check whether hasPassword field is present');
assert.match(body, /if \(!hasUserField && !hasPwField\)[\s\S]{0,200}return null/,
'when neither field is present, must return null so caller falls back to /_devin-auth/connections');
});
test('falsy-but-present has_password:false is a terminal, non-lockout "no password" answer', () => {
// The fix must not accidentally silence the legitimate "no password, sign
// in with an emailed code / OAuth" case. windsurfLogin still stops on a
// positive hasPassword:false — before any password attempt.
const m = LOGIN_JS.match(/if \(probe\.hasPassword === false\)[\s\S]+?\n \}/);
assert.ok(m, 'hasPassword:false terminal branch removed or refactored');
// The error must still make clear this is a no-password / OAuth account
// (it now points users at the email code / OAuth / token instead of the
// old wording).
assert.match(m[0], /noPasswordError\(probe\)/,
'must keep the dedicated no-password error helper');
assert.match(LOGIN_JS, /code\s*=\s*'ERR_NO_PASSWORD_OAUTH_ACCOUNT'/,
'the helper must keep the dedicated no-password error code');
assert.match(m[0], /no password|OAuth|email/i,
'must still surface a clear "this account has no password" friendly error');
assert.match(LOGIN_JS, /method\s*=\s*probe\.emailCode \? 'email_code' : 'oauth'/,
'must tell the caller which method actually works');
// v3: a no-password result is a WRONG-METHOD signal, not a brute-force
// guess — it must NOT feed the email lockout (that locked users for using
// the wrong login tab). Assert the branch does not record a failure.
assert.doesNotMatch(m[0], /recordEmailFailure/,
'no_password must not count toward the email lockout');
// And it must never reach the password endpoints.
assert.doesNotMatch(m[0], /windsurfLoginViaAuth1\(|windsurfLoginViaFirebase\(/,
'no_password must not attempt a password login');
});
});