Ctrl K
ComunidadGuías…

__tests__/console-signin.e2e.test.js

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

Archivo original ↗Preguntar a KiBot
// Explicit browser test: GHOST_BROWSER_E2E=1 npm test -- __tests__/console-signin.e2e.test.js
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createRequire } from 'node:module';
import { readFileSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import crypto from 'node:crypto';
import { chromium, devices } from 'playwright';
const require = createRequire(import.meta.url);
const express = require('express');
const { makeMobileAuth } = require('../src/mobileAuth');
const sso = require('../src/sso');
const enabled = process.env.GHOST_BROWSER_E2E === '1';
const suite = enabled ? describe : describe.skip;
suite('fresh browser authentication and native connection', () => {
  let server, browser, base, accounts, dir;
  const secret = 'browser-e2e-secret', owner = 'browser-test@example.test';
  const mint = (claims = {}) => {
    const h = Buffer.from(JSON.stringify({ alg: 'HS256' })).toString('base64url');
    const b = Buffer.from(JSON.stringify({ email: owner, exp: Math.floor(Date.now() / 1000) + 120, ...claims })).toString('base64url');
    return h + '.' + b + '.' + crypto.createHmac('sha256', secret).update(h + '.' + b).digest('base64url');
  };
  const handoff = (device, expired = false) => base + '/#leadflow=' + encodeURIComponent(JSON.stringify({ auth: mint(expired ? { exp: 1 } : {}), ...(device ? { device } : {}) }));
  beforeAll(async () => {
    dir = mkdtempSync(join(tmpdir(), 'gb-browser-auth-'));
    process.env.PROFILE_DIR = dir;
    delete require.cache[require.resolve('../src/accounts')]; accounts = require('../src/accounts');
    const record = accounts.signup(owner, 'test-only-owner-password');
    const app = express(); app.use(express.json());
    const mobile = makeMobileAuth({ mintDevice: input => ({ ...input, token: 'test-device-token' }) });
    const signedIn = req => accounts.verifyToken(accounts.readCookie(req));
    app.get('/', (_req, res) => res.type('html').send(readFileSync(new URL('../public/compose-index.html', import.meta.url))));
    app.get('/js/console-auth.js', (_req, res) => res.type('js').send(readFileSync(new URL('../public/js/console-auth.js', import.meta.url))));
    app.get('/console/live-overlay.js', (_req, res) => res.type('js').send(''));
    app.get('/console/gbweb.js', (_req, res) => res.type('js').send('document.body.dataset.consoleStarted="true"'));
    app.get('/api/auth/state', (req, res) => res.json({ signedIn: !!signedIn(req), sso: true, ssoOnly: true, hasPassword: false }));
    app.post('/api/auth/sso', (req, res) => {
      try { sso.verifyLeadflowToken(req.body.token, secret); const session = accounts.issue(record); accounts.setCookie(res, session.token, session.exp); res.json({ username: owner }); }
      catch { res.status(401).json({ error: 'expired' }); }
    });
    app.post('/api/auth/logout', (_req, res) => { accounts.clearCookie(res); res.json({ ok: true }); });
    app.post('/api/auth/device-code', (req, res) => {
      if (!signedIn(req)) return res.sendStatus(401);
      res.json(mobile.issue(owner, req.body.challenge));
    });
    app.post('/api/auth/device-token', (req, res) => { try { res.json(mobile.redeem(req.body)); } catch { res.sendStatus(401); } });
    server = await new Promise(resolve => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
    base = 'http://127.0.0.1:' + server.address().port;
    browser = await chromium.launch({ headless: true, ...(process.env.GHOST_TEST_CHROMIUM ? { executablePath: process.env.GHOST_TEST_CHROMIUM } : {}) });
  }, 30000);
  afterAll(async () => { await browser?.close(); await new Promise(resolve => server?.close(resolve)); if (dir) rmSync(dir, { recursive: true, force: true }); });
  for (const [label, options] of [['desktop', {}], ['Android viewport', devices['Pixel 5']]]) {
    it(`${label}: fresh install -> sign-in button -> handoff -> session -> reload -> sign-out`, async () => {
      const context = await browser.newContext(options); const page = await context.newPage();
      page.setDefaultTimeout(5000);
      let step = 'fresh sign-in screen';
      try {
        await page.goto(base);
        await page.getByRole('link', { name: 'Sign in with MyApp.Engineer' }).waitFor();
        expect(await page.locator('#password-option').isVisible()).toBe(false);
        expect(await page.locator('#ComposeTarget').isVisible()).toBe(false);
        step = 'exchange handoff and start console'; await page.goto(handoff());
        await page.waitForFunction(() => document.body.dataset.consoleStarted === 'true');
        expect(new URL(page.url()).hash).toBe('');
        expect((await page.evaluate(() => fetch('/api/auth/state', { credentials: 'include' }).then(r => r.json()))).signedIn).toBe(true);
        step = 'reload signed-in console'; await page.reload(); await page.waitForFunction(() => document.body.dataset.consoleStarted === 'true');
        step = 'sign out and reload';
        await page.evaluate(() => fetch('/api/auth/logout', { method: 'POST', credentials: 'include' })); await page.reload();
        await page.getByRole('link', { name: 'Sign in with MyApp.Engineer' }).waitFor();
      } catch (error) { throw new Error(step + ': ' + error.message); }
      finally { await context.close(); }
    }, 30000);
  }
  it('an expired handoff stays on a useful retry screen', async () => {
    const context = await browser.newContext(); const page = await context.newPage();
    try {
      await page.goto(handoff(null, true));
      await page.getByText('This sign-in link expired or could not be verified. Please sign in again.').waitFor();
      expect(await page.locator('#password-option').isVisible()).toBe(false);
    } finally { await context.close(); }
  });
  it('a mobile return exchanges its code once using the app verifier', async () => {
    const context = await browser.newContext(devices['Pixel 5']); const page = await context.newPage();
    const verifier = 'v'.repeat(43), challenge = crypto.createHash('sha256').update(verifier).digest('base64url');
    try {
      await page.goto(handoff({ challenge, state: 's'.repeat(43) }));
      const open = page.getByRole('link', { name: 'Open Ghost Browser Mobile' }); await open.waitFor();
      const callback = new URL(await open.getAttribute('href'));
      expect(callback.protocol).toBe('ghostbrowser:');
      const code = callback.searchParams.get('code');
      const exchange = await context.request.post(base + '/api/auth/device-token', { data: { code, verifier, deviceId: 'test-phone' } });
      expect(exchange.status()).toBe(200);
      expect((await exchange.json()).owner).toBe(owner);
      expect((await context.request.post(base + '/api/auth/device-token', { data: { code, verifier, deviceId: 'test-phone' } })).status()).toBe(401);
    } finally { await context.close(); }
  }, 30000);
});