Ctrl K
ComunidadGuías…

__tests__/account-chooser.test.js

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

Archivo original ↗Preguntar a KiBot
/*
 * "WHICH ACCOUNT?" MUST NOT BE READ AS "SIGNED OUT", AND MUST NEVER BE ANSWERED BY GUESSING.
 *
 * From production: the gsc.audit job sat on accounts.google.com/v3/signin/accountchooser for 31
 * minutes with zero steps while the owner was signed in the entire time. Two Gmail accounts live in
 * that profile and one holds the property, so Google asked which — and because the chooser is served
 * from the same host as the sign-in form, loginWall called it a wall. The audit then recorded "no
 * access" on the property, which reads as a Google penalty rather than an unanswered question. That
 * exact misreading has already produced one phantom penalty in this codebase.
 *
 * The second half matters more than the first: picking the wrong Google account is not a harmless
 * retry, it reads a stranger's Search Console and files the numbers as ours. A wrong answer here is
 * worse than no answer, so these tests pin that an unconfigured chooser REPORTS and never picks.
 */
import { describe, it, expect } from 'vitest';
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
const { isChooser, isSignIn, decide } = require('../src/accountChooser');

const CHOOSER = 'https://accounts.google.com/v3/signin/accountchooser?continue=https://search.google.com/search-console';
const SIGNIN = 'https://accounts.google.com/v3/signin/identifier?continue=https://search.google.com/';

describe('an account chooser is its own answer, not a missing login', () => {
  it('recognises the chooser the audit actually sat on', () => {
    expect(isChooser(CHOOSER)).toBe(true);
    expect(isChooser('https://accounts.google.com/signin/selectaccount?prompt=select_account')).toBe(true);
    expect(isChooser('https://accounts.google.com/AccountChooser?hl=pl')).toBe(true);
  });

  /* The distinction the whole fix rests on: same host, different question. */
  it('does not confuse it with a sign-in form, which IS a missing session', () => {
    expect(isChooser(SIGNIN)).toBe(false);
    expect(isSignIn(SIGNIN)).toBe(true);
    expect(isSignIn(CHOOSER)).toBe(false);
  });

  it('says nothing about pages that are neither', () => {
    for (const u of ['https://search.google.com/search-console', 'https://search.google.com/search-console/about', '', null, undefined]) {
      expect(isChooser(u)).toBe(false);
      expect(isSignIn(u)).toBe(false);
    }
  });

  it('is not fooled by another host that merely mentions the word', () => {
    expect(isChooser('https://example.com/accountchooser')).toBe(false);
  });
});

describe('which account is configured, never inferred', () => {
  it('picks the configured account when the chooser is up', () => {
    const d = decide(CHOOSER, 'owner@example.org');
    expect(d.act).toBe('pick');
    expect(d.email).toBe('owner@example.org');
  });

  /*
   * THE IMPORTANT ONE. With two accounts and nothing configured, a flow that picks the first tile
   * reads the wrong property and files it as ours. Reporting is the correct outcome.
   */
  it('refuses to choose when no account is configured, and says why', () => {
    const d = decide(CHOOSER, '');
    expect(d.act).toBe('report');
    expect(d.why).toMatch(/no account is configured/);
    expect(d.why).toMatch(/wrong account/);
  });

  it('refuses a configured value that is not an address rather than hunting for it', () => {
    const d = decide(CHOOSER, 'the main one');
    expect(d.act).toBe('report');
    expect(d.why).toMatch(/not an address/);
  });

  it('is case and whitespace tolerant about the configured address', () => {
    expect(decide(CHOOSER, '  Owner@Example.ORG ').email).toBe('owner@example.org');
  });

  it('does nothing at all when the page is not a chooser, configured or not', () => {
    expect(decide('https://search.google.com/search-console', 'owner@example.org').act).toBe('none');
    expect(decide(SIGNIN, 'owner@example.org').act).toBe('none');   // a wall is the wall detector's job
  });
});

/*
 * ── A SIGNED-OUT PROFILE IS NOT A GOOGLE PENALTY, AND A RE-AUTH PROMPT IS NOT A STEP ───────────
 *
 * Measured 2026-09-21. The gsc.connect job spent 39 minutes walking Google's re-auth gauntlet,
 * /signin/challenge/pwd then /signin/challenge/pk, holding the `google` profile the owner needed in
 * order to sign in, and reporting nothing usable. The dashboard meanwhile showed "Je hebt geen
 * toegang tot deze property" and raised a MANUAL ACTION flag, which is the phantom-penalty failure
 * this codebase has already produced once: it could not open the manual-actions tab, so "we did not
 * look" was rendered as an alarm and sent the owner hunting for a Google sanction.
 *
 * The cause sat one layer below the account chooser. That profile held 63 google.com cookies and
 * not one auth cookie, so it was simply signed out. The owner's real session was in a different
 * profile entirely.
 *
 * Both guards exist to turn a 39-minute stall into one accurate sentence, and neither may ever try
 * to answer a password or passkey prompt: signing in is the owner's act, by hand, once.
 */
describe('signed out, challenged, or actually in', () => {
  const CH_PWD = 'https://accounts.google.com/v3/signin/challenge/pwd?TL=ACv9tzFibv4YKbZh';
  const CH_PK = 'https://accounts.google.com/v3/signin/challenge/pk?TL=ACv9tzFibv4YKbZh';
  const INSIDE = 'https://search.google.com/search-console?resource_id=sc-domain%3Amy-app.engineer';
  const ck = (names, domain) => names.map((n) => ({ name: n, domain: domain || '.google.com' }));
  const LIVE = ck(['SID', 'HSID', 'SSID', 'APISID', 'SAPISID', '__Secure-1PSID', 'LSID']);

  const { isChallenge, hasSession, googleState } = require('../src/accountChooser');

  it('recognises the exact challenges the job was caught walking', () => {
    expect(isChallenge(CH_PWD)).toBe(true);
    expect(isChallenge(CH_PK)).toBe(true);
    expect(isChallenge('https://accounts.google.com/v3/signin/challenge/totp')).toBe(true);
  });

  it('does not call the console or the chooser a challenge', () => {
    expect(isChallenge(INSIDE)).toBe(false);
    expect(isChallenge('https://accounts.google.com/v3/signin/accountchooser')).toBe(false);
    expect(isChallenge('')).toBe(false);
  });

  /* THE MEASURED CASE: many google cookies, no auth cookie. 63 of them, and signed out. */
  it('calls a jar with cookies but no auth cookie signed out', () => {
    const junk = ck(['NID', 'AEC', '1P_JAR', 'CONSENT', 'OTZ', 'SEARCH_SAMESITE']);
    const r = hasSession(junk);
    expect(r.signedIn).toBe(false);
    expect(r.found).toEqual([]);
  });

  it('calls a real session signed in', () => {
    const r = hasSession(LIVE);
    expect(r.signedIn).toBe(true);
    expect(r.found).toContain('SAPISID');
  });

  it('ignores another site cookies named the same', () => {
    expect(hasSession(ck(['SID', 'SAPISID'], '.example.com')).signedIn).toBe(false);
  });

  it('survives an empty or missing jar instead of throwing mid-run', () => {
    for (const j of [[], null, undefined, [null, {}]]) expect(hasSession(j).signedIn).toBe(false);
  });

  /*
   * THE POINT OF ALL OF IT: the sentence handed back must say a login is missing and must NOT read
   * as a Google penalty, because that misreading already shut the whole search channel once.
   */
  it('blames the missing login, not Google, and says who to sign in as', () => {
    const r = googleState(INSIDE, [], 'owner.example@example.org');
    expect(r.state).toBe('signed-out');
    expect(r.why).toMatch(/no Google session/i);
    expect(r.why).toMatch(/by hand/i);
    expect(r.why).toContain('owner.example@example.org');
    expect(r.why).toMatch(/not a Google permission problem/i);
  });

  it('reports a challenge as a wall to be signed in by hand, never pushed through', () => {
    const r = googleState(CH_PK, LIVE, 'owner.example@example.org');
    expect(r.state).toBe('challenge');
    expect(r.why).toMatch(/re-verify/i);
    expect(r.why).toMatch(/by hand/i);
  });

  /* Signed out beats everything: there is no point naming a chooser we will never reach. */
  it('answers signed-out first, even when the url looks like a chooser', () => {
    expect(googleState('https://accounts.google.com/v3/signin/accountchooser', [], '').state).toBe('signed-out');
  });

  it('says plainly when it is actually inside', () => {
    const r = googleState(INSIDE, LIVE, 'owner.example@example.org');
    expect(r.state).toBe('in');
    expect(r.why).toBe('');
  });

  it('still points at the chooser when signed in and asked which account', () => {
    const r = googleState('https://accounts.google.com/v3/signin/accountchooser', LIVE, '');
    expect(r.state).toBe('chooser');
  });
});