Ctrl K
ComunidadGuías…

test/devin-connect-login-host-fallback.test.js

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// app.devin.ai second-host login fallback (DEVIN_CONNECT_LOGIN_HOST_FALLBACK).
//
// Study .workflow-results/oauth-relogin-study/FEASIBILITY-BLUEPRINT.md §6:
// point a SECOND host at the SAME Auth1 password/login mechanism so
// windsurf.com (Vercel) 504/503 spikes / endpoint migrations don't take the
// whole re-login chain down. Adds app.devin.ai/api/auth1/{connections,
// password/login} as a COMPLETE, independent fallback, gated behind an opt-in
// env flag, reusing the existing postAuthDualPath exchanger.
//
// TODO(unverified): the app.devin.ai -> OUR-PostAuth token-exchange hop is NOT
// confirmed against a live account (study §1.2/§5.3). These tests prove the
// WIRING (flag gating, host targeting, no-fake-success, endpoint construction)
// with a fully mocked transport. They do NOT prove the cross-host exchange
// works upstream.
//
// Zero network: httpsRequest is routed through __setLoginTransportForTests.
// No real account / no real auth request. Email/password are throwaway literals.

import { afterEach, beforeEach, describe, it } 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';
import {
  windsurfLogin,
  __setLoginTransportForTests,
  _resetEmailLockoutForTests,
} from '../src/dashboard/windsurf-login.js';

const __dirname = dirname(fileURLToPath(import.meta.url));
const LOGIN_SRC = readFileSync(join(__dirname, '..', 'src', 'dashboard', 'windsurf-login.js'), 'utf8');

const EMAIL = 'fallback-test@example.com';
const PASSWORD = 'throwaway-not-real';

function setFlag(on) {
  if (on) process.env.DEVIN_CONNECT_LOGIN_HOST_FALLBACK = '1';
  else delete process.env.DEVIN_CONNECT_LOGIN_HOST_FALLBACK;
}

// First-substring-match route table. raw requests (PostAuth) get a Buffer.
// Avoid 5xx replies so httpsRequestRetrying backoff never fires (fast suite).
function makeTransport(routes) {
  const seen = [];
  const fn = async (url, opts) => {
    seen.push(url);
    for (const [matcher, reply] of routes) {
      if (url.includes(matcher)) {
        if (reply instanceof Error) throw reply;
        if (opts.raw) {
          const buf = Buffer.isBuffer(reply.data) ? reply.data
            : Buffer.from(typeof reply.data === 'string' ? reply.data : JSON.stringify(reply.data));
          return { status: reply.status, data: buf };
        }
        return { status: reply.status, data: reply.data };
      }
    }
    throw new Error(`unexpected request to ${url}`);
  };
  fn.seen = seen;
  return fn;
}

// The authoritative method probe (measured live 2026-10-22). Without it the
// probe would fall through to CheckUserLoginMethod — and, with the fallback
// flag on, to app.devin.ai — which these password-path tests do not exercise.
const CONN_OK = ['windsurf.com/_devin-auth/connections', { status: 200, data: { auth_method: { method: 'auth1', has_password: true, enterprise_sso_domain: false }, connections: [{ id: 'email', type: 'email', enabled: true }] } }];
const PROBE = ['SeatManagementService/CheckUserLoginMethod', { status: 200, data: { userExists: true, hasPassword: true } }];
const PRIMARY_FAIL = ['windsurf.com/_devin-auth/password/login', { status: 400, data: { detail: 'Invalid email or password' } }];
const PRIMARY_OK = ['windsurf.com/_devin-auth/password/login', { status: 200, data: { token: 'auth1-primary' } }];
const POSTAUTH_OK = ['WindsurfPostAuth', { status: 200, data: { sessionToken: 'devin-session-token$NEW', accountId: 'account-abc' } }];
const DEVIN_CONN_OK = ['app.devin.ai/api/auth1/connections', { status: 200, data: { connections: [] } }];
const DEVIN_LOGIN_OK = ['app.devin.ai/api/auth1/password/login', { status: 200, data: { token: 'auth1-devin' } }];

beforeEach(() => { _resetEmailLockoutForTests(); setFlag(false); });
afterEach(() => { __setLoginTransportForTests(null); _resetEmailLockoutForTests(); setFlag(false); });

describe('host fallback — flag OFF (default, behavior unchanged)', () => {
  it('successful primary login only hits windsurf.com', async () => {
    setFlag(false);
    const t = makeTransport([CONN_OK, PROBE, PRIMARY_OK, POSTAUTH_OK]);
    __setLoginTransportForTests(t);
    const result = await windsurfLogin(EMAIL, PASSWORD, null);
    assert.equal(result.apiKey, 'devin-session-token$NEW');
    assert.ok(!t.seen.some(u => u.includes('app.devin.ai')), 'no app.devin.ai when flag off');
  });

  it('primary failure surfaces directly — no second host', async () => {
    setFlag(false);
    const t = makeTransport([CONN_OK, PROBE, PRIMARY_FAIL]);
    __setLoginTransportForTests(t);
    await assert.rejects(() => windsurfLogin(EMAIL, PASSWORD, null));
    assert.ok(!t.seen.some(u => u.includes('app.devin.ai')), 'flag off → no fallback');
  });
});

describe('host fallback — flag ON', () => {
  it('falls back to app.devin.ai after the primary windsurf.com path fails', async () => {
    setFlag(true);
    const t = makeTransport([CONN_OK, PROBE, PRIMARY_FAIL, DEVIN_CONN_OK, DEVIN_LOGIN_OK, POSTAUTH_OK]);
    __setLoginTransportForTests(t);
    const result = await windsurfLogin(EMAIL, PASSWORD, null);
    assert.equal(result.apiKey, 'devin-session-token$NEW');
    assert.equal(result.viaHost, 'app.devin.ai');
    assert.ok(t.seen.includes('https://app.devin.ai/api/auth1/password/login'), 'hits devin login endpoint');
    assert.ok(t.seen.includes('https://app.devin.ai/api/auth1/connections'), 'probes devin connections endpoint');
  });

  it('tolerates a failing app.devin.ai connections probe and still logs in', async () => {
    setFlag(true);
    const t = makeTransport([
      CONN_OK, PROBE, PRIMARY_FAIL,
      ['app.devin.ai/api/auth1/connections', { status: 404, data: { detail: 'not found' } }],
      DEVIN_LOGIN_OK, POSTAUTH_OK,
    ]);
    __setLoginTransportForTests(t);
    const result = await windsurfLogin(EMAIL, PASSWORD, null);
    assert.equal(result.viaHost, 'app.devin.ai', 'connections probe is best-effort');
  });

  it('both hosts fail → rejects with the ORIGINAL primary error (no fake success)', async () => {
    setFlag(true);
    const t = makeTransport([
      CONN_OK, PROBE, PRIMARY_FAIL,
      DEVIN_CONN_OK,
      ['app.devin.ai/api/auth1/password/login', { status: 400, data: { detail: 'Invalid email or password' } }],
    ]);
    __setLoginTransportForTests(t);
    await assert.rejects(() => windsurfLogin(EMAIL, PASSWORD, null), (err) => {
      // primary error code is surfaced, not the fallback's
      assert.ok(/ERR_INVALID_CREDENTIALS|ERR_LOGIN_FAILED|Invalid/i.test(err.code || err.message), 'primary error surfaced');
      return true;
    });
    assert.ok(t.seen.some(u => u.includes('app.devin.ai')), 'fallback was attempted before giving up');
  });

  it('app.devin.ai login OK but its PostAuth has no sessionToken → fail, no fake success', async () => {
    setFlag(true);
    const t = makeTransport([
      CONN_OK, PROBE, PRIMARY_FAIL, DEVIN_CONN_OK, DEVIN_LOGIN_OK,
      ['WindsurfPostAuth', { status: 200, data: { somethingElse: true } }],
    ]);
    __setLoginTransportForTests(t);
    await assert.rejects(() => windsurfLogin(EMAIL, PASSWORD, null), 'missing sessionToken must not be treated as success');
  });

  it('flag ON but primary SUCCEEDS → app.devin.ai never contacted', async () => {
    setFlag(true);
    const t = makeTransport([CONN_OK, PROBE, PRIMARY_OK, POSTAUTH_OK]);
    __setLoginTransportForTests(t);
    const result = await windsurfLogin(EMAIL, PASSWORD, null);
    assert.equal(result.apiKey, 'devin-session-token$NEW');
    assert.notEqual(result.viaHost, 'app.devin.ai');
    assert.ok(!t.seen.some(u => u.includes('app.devin.ai')), 'fallback only on primary failure');
  });
});

describe('source invariants (host/endpoint construction + unverified honesty)', () => {
  it('targets the correct app.devin.ai Auth1 endpoints', () => {
    assert.match(LOGIN_SRC, /https:\/\/app\.devin\.ai/, 'second host constant present');
    assert.match(LOGIN_SRC, /api\/auth1\/connections/, 'connections endpoint present');
    assert.match(LOGIN_SRC, /api\/auth1\/password\/login/, 'password/login endpoint present');
  });

  it('gates the whole fallback behind DEVIN_CONNECT_LOGIN_HOST_FALLBACK', () => {
    assert.match(LOGIN_SRC, /DEVIN_CONNECT_LOGIN_HOST_FALLBACK/, 'env flag referenced');
  });

  it('keeps the unverified cross-host PostAuth hop explicitly flagged', () => {
    assert.match(LOGIN_SRC, /TODO\(unverified[\s\S]*?PostAuth/i,
      'cross-host PostAuth exchange must stay marked unverified');
  });

  it('does NOT touch classifyUpstreamError or reLoginAccount', () => {
    assert.ok(!/classifyUpstreamError/.test(LOGIN_SRC), 'error classifier untouched by this module');
    assert.ok(!/function reLoginAccount/.test(LOGIN_SRC), 'reLoginAccount lives in auth.js, untouched');
  });

  it('reuses postAuthDualPath rather than reimplementing the exchanger', () => {
    const idx = LOGIN_SRC.indexOf('async function windsurfLoginViaDevinHost(');
    assert.ok(idx >= 0, 'fallback chain function exists');
    const open = LOGIN_SRC.indexOf('{', idx);
    let depth = 0, body = '';
    for (let i = open; i < LOGIN_SRC.length; i++) {
      if (LOGIN_SRC[i] === '{') depth++;
      else if (LOGIN_SRC[i] === '}') { depth--; if (depth === 0) { body = LOGIN_SRC.slice(idx, i + 1); break; } }
    }
    assert.match(body, /postAuthDualPath\s*\(/, 'must reuse postAuthDualPath');
  });
});