Ctrl K
ComunidadGuías…

__tests__/console-auth.test.js

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

Archivo original ↗Preguntar a KiBot
import { describe, it, expect, vi } from 'vitest';
import { createRequire } from 'node:module';
import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { JSDOM } from 'jsdom';
const require = createRequire(import.meta.url);
const { authenticate, boot, PLATFORM_SIGN_IN, appLink } = require('../public/js/console-auth');
const { prepareConsole } = require('../scripts/prepare-console');
const html = readFileSync(new URL('../public/compose-index.html', import.meta.url), 'utf8');
const url = (envelope) => 'https://ghost-browser.ben.my-app.engineer/#leadflow=' + encodeURIComponent(JSON.stringify(envelope));
const reply = (body, ok = true) => ({ ok, json: async () => body });

describe('the shipped console authenticates before Compose starts', () => {
  it('exchanges the fragment first, strips it from history and sends cookies', async () => {
    const dom = new JSDOM(html, { url: url({ auth: 'signed-token' }) });
    const calls = [];
    const fetch = vi.fn(async (path, options) => { calls.push({ path, options, hash: dom.window.location.hash }); return reply(path.endsWith('/sso') ? {} : { signedIn: true }); });
    const state = await authenticate({ location: dom.window.location, history: dom.window.history, fetch });
    expect(state.signedIn).toBe(true);
    expect(calls.map(c => c.path)).toEqual(['/api/auth/sso', '/api/auth/state']);
    expect(calls[0].hash).toBe('');
    expect(JSON.parse(calls[0].options.body)).toEqual({ token: 'signed-token' });
    expect(calls.every(c => c.options.credentials === 'include')).toBe(true);
  });
  it('shows a direct platform sign-in and no dead password form on a fresh browser', async () => {
    const dom = new JSDOM(html, { url: 'https://ghost-browser.ben.my-app.engineer/' });
    dom.window.fetch = vi.fn(async () => reply({ signedIn: false, sso: true, ssoOnly: true, hasPassword: false }));
    await boot(dom.window);
    expect(dom.window.document.getElementById('platform-sign-in').href).toBe(PLATFORM_SIGN_IN);
    expect(dom.window.document.getElementById('password-option').hidden).toBe(true);
    expect(dom.window.document.querySelector('script[src="/console/gbweb.js"]')).toBeNull();
  });
  it('starts the real console only after the cookie is confirmed', async () => {
    const dom = new JSDOM(html, { url: url({ auth: 'signed-token' }) });
    dom.window.fetch = vi.fn(async path => reply(path.endsWith('/state') ? { signedIn: true } : {}));
    await boot(dom.window);
    expect(dom.window.document.getElementById('auth-gate').hidden).toBe(true);
    expect(dom.window.document.querySelector('script[src="/console/gbweb.js"]')).not.toBeNull();
    expect(dom.window.document.getElementById('ComposeTarget').hidden).toBe(false);
    expect(dom.window.document.querySelector('.mcp-link')).toBeNull();
  });
  it('expired handoffs show an actionable retry instead of a password form', async () => {
    const dom = new JSDOM(html, { url: url({ auth: 'expired' }) });
    dom.window.fetch = vi.fn(async () => reply({}, false));
    await boot(dom.window);
    expect(dom.window.location.hash).toBe('');
    expect(dom.window.document.getElementById('auth-status').textContent).toMatch(/expired/);
    expect(dom.window.document.getElementById('password-option').hidden).toBe(true);
  });
  it('rejects malformed handoffs and a blocked session cookie', async () => {
    for (const hash of ['#leadflow=not-json', '#leadflow=%7B%7D']) {
      const dom = new JSDOM(html, { url: 'https://host.test/' + hash });
      const fetch = vi.fn();
      await expect(authenticate({ location: dom.window.location, history: dom.window.history, fetch })).rejects.toThrow();
      expect(fetch).not.toHaveBeenCalled();
      expect(dom.window.location.hash).toBe('');
    }
    const dom = new JSDOM(html, { url: url({ auth: 'valid' }) });
    await expect(authenticate({ location: dom.window.location, history: dom.window.history, fetch: async () => reply({ signedIn: false }) })).rejects.toThrow(/cookies/);
  });
  it('keeps a separately configured password optional', async () => {
    const dom = new JSDOM(html, { url: 'https://host.test/' });
    dom.window.fetch = vi.fn(async () => reply({ signedIn: false, sso: true, hasPassword: true }));
    await boot(dom.window);
    expect(dom.window.document.getElementById('password-option').hidden).toBe(false);
    expect(dom.window.document.getElementById('password-option').open).toBe(false);
  });
  it('returns a mobile login with only a one-time code, not a bearer key', async () => {
    const device = { challenge: 'c'.repeat(43), state: 's'.repeat(43) };
    const dom = new JSDOM(html, { url: url({ auth: 'valid', device }) });
    const assign = vi.fn();
    const win = { document: dom.window.document, history: dom.window.history, location: { hash: dom.window.location.hash, pathname: '/', search: '', origin: dom.window.location.origin, assign },
      fetch: vi.fn(async path => reply(path.endsWith('/state') ? { signedIn: true } : path.endsWith('/device-code') ? { code: 'k'.repeat(43) } : {})) };
    await boot(win);
    expect(assign).toHaveBeenCalledTimes(1);
    const callback = new URL(assign.mock.calls[0][0]);
    expect(callback.protocol).toBe('ghostbrowser:');
    expect(callback.searchParams.get('code')).toBe('k'.repeat(43));
    expect(callback.searchParams.get('state')).toBe(device.state);
    expect(callback.searchParams.has('token')).toBe(false);
  });
  it('on Android opens the app by name, so the system cannot offer Chrome for the way back', async () => {
    const device = { challenge: 'c'.repeat(43), state: 's'.repeat(43) };
    const dom = new JSDOM(html, { url: url({ auth: 'valid', device }) });
    const assign = vi.fn();
    const win = { document: dom.window.document, history: dom.window.history,
      navigator: { userAgent: 'Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 Chrome/130.0 Mobile Safari/537.36' },
      location: { hash: dom.window.location.hash, pathname: '/', search: '', origin: dom.window.location.origin, assign },
      fetch: vi.fn(async path => reply(path.endsWith('/state') ? { signedIn: true } : path.endsWith('/device-code') ? { code: 'k'.repeat(43) } : {})) };
    await boot(win);
    const link = assign.mock.calls[0][0];
    expect(link).toMatch(/^intent:\/\/connect\?/);
    expect(link).toMatch(/#Intent;scheme=ghostbrowser;package=engineer\.myapp\.gbmobile;end$/);
    const query = new URLSearchParams(link.slice('intent://connect?'.length, link.indexOf('#')));
    expect(query.get('code')).toBe('k'.repeat(43));
    expect(query.get('state')).toBe(device.state);
    expect(dom.window.document.getElementById('platform-sign-in').getAttribute('href')).toBe(link);
  });
  it('keeps the plain app link off Android', () => {
    expect(appLink({ navigator: { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)' } }, 'a=1')).toBe('ghostbrowser://connect?a=1');
  });
});

describe('console build artifact preparation', () => {
  it('updates both entry points even when the Wasm artifact was preserved from an older release', () => {
    const root = mkdtempSync(join(tmpdir(), 'gb-loader-'));
    try {
      mkdirSync(join(root, 'console')); writeFileSync(join(root, 'console', 'gbweb.js'), 'old-wasm');
      writeFileSync(join(root, 'console', 'index.html'), 'old-login'); writeFileSync(join(root, 'compose-index.html'), html);
      expect(prepareConsole(root)).toBe(true);
      expect(readFileSync(join(root, 'console', 'index.html'), 'utf8')).toBe(html);
      expect(readFileSync(join(root, 'index.html'), 'utf8')).toBe(html);
      expect(readFileSync(join(root, 'console', 'gbweb.js'), 'utf8')).toBe('old-wasm');
    } finally { rmSync(root, { recursive: true, force: true }); }
  });
});