console fill: sign-in only by the top-level URL, the main frame's form or an Apple sign-in frame
Google's account widget and cookie rotation frames (accounts.google.com / ogs.google.com) on a signed-in Play Console no longer count as a sign-in page. Each detection logs its rule and URL; heartbeats name the page; a copied Chrome session that lands on a sign-in URL is said plainly; the sign-in wait gets a watchdog dump, and --debug-dump up to 3 dumps. Fixtures and tests for the chooser and home with the widget frames and a top-level accounts.google.com page; pitfall 16.
This commit is contained in:
parent
b48407115e
commit
3aedf9e137
10 changed files with 294 additions and 44 deletions
|
|
@ -7,11 +7,11 @@
|
|||
// of the engine (navTimeoutMs / navMs, scaled down through the options). The relaunch of storebound's own profile
|
||||
// (session cookies kept) needs a real Chrome: tests/console-persist.test.ts.
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { matchAccount, openAcross, readAccounts, rememberApple, rememberPlay, withAccountIds } from '../cli/console/accounts';
|
||||
import { createAppHere, fillPage, fillSession, navClick, navigate, resumeAt, run, safeClick, signInPage } from '../cli/console/fill';
|
||||
import { createAppHere, fillPage, fillSession, navClick, navigate, resumeAt, run, safeClick, signInPage, signInReason } from '../cli/console/fill';
|
||||
import type { ConsoleBrowser } from '../cli/console/session';
|
||||
import { parseArgs } from '../cli/lib/args';
|
||||
import { type ShipConfig, validateExtras } from '../cli/lib/config';
|
||||
|
|
@ -976,3 +976,86 @@ test('ticks are gated on agreements only: help text with "saved" is fine, Develo
|
|||
expect(forbiddenControl('Save')).toBe(true);
|
||||
expect(forbiddenControl('Publish')).toBe(true);
|
||||
});
|
||||
|
||||
// ---- sign-in detection is precise: Google's account widget / cookie rotation frames never make a page a sign-in page ----
|
||||
// Real run (Play Console, copied Chrome cookies): the chooser carried an accounts.google.com frame and the old
|
||||
// detector (any frame on a sign-in host) waited "for the sign-in" on a page that was signed in.
|
||||
|
||||
/** The Play Console routes of a signed-in owner whose pages carry the Google account widget frames. */
|
||||
const playOgb = (u: URL): string | null => {
|
||||
if (u.hostname === 'accounts.google.com' || u.hostname === 'ogs.google.com') return u.pathname.startsWith('/v3/signin') ? 'google-signin.html' : 'google-account-widget.html';
|
||||
if (u.hostname !== 'play.google.com') return null;
|
||||
if (u.pathname.endsWith('/app-list')) return 'play-home-ogb.html';
|
||||
if (u.pathname === '/play-create-app.html') return 'play-create-app.html';
|
||||
return u.pathname === '/console' || u.pathname === '/console/u/0/developers' ? 'play-chooser-ogb.html' : null;
|
||||
};
|
||||
const frameOn = (page: any, host: string): boolean => page.frames().some((f: any) => { try { return new URL(f.url()).hostname === host; } catch { return false; } });
|
||||
/** Test helper: waits until `pred` holds (at most 10 s). */
|
||||
async function until(pred: () => boolean): Promise<void> {
|
||||
const end = Date.now() + 10_000;
|
||||
while (!pred()) { if (Date.now() > end) throw new Error('until: timed out'); await Bun.sleep(50); }
|
||||
}
|
||||
|
||||
describe.skipIf(!browser)('sign-in detection: only the top-level URL, the main frame\'s form or an Apple sign-in frame', () => {
|
||||
test('the Play account chooser with the account widget (accounts.google.com frames, avatar and email header) is NOT a sign-in page', async () => {
|
||||
const b = await consoleBrowser(playOgb);
|
||||
await b.page.goto('https://play.google.com/console/u/0/developers');
|
||||
await until(() => frameOn(b.page, 'accounts.google.com') && frameOn(b.page, 'ogs.google.com'));
|
||||
expect(await signInReason(b.page)).toBeNull();
|
||||
expect(await signInPage(b.page)).toBe(false);
|
||||
await b.context.close();
|
||||
}, BROWSER_TEST_MS);
|
||||
|
||||
test('Play home with the widget frame is NOT a sign-in page', async () => {
|
||||
const b = await consoleBrowser(playOgb);
|
||||
await b.page.goto('https://play.google.com/console/u/0/developers/7000000001/app-list');
|
||||
await until(() => frameOn(b.page, 'accounts.google.com') && frameOn(b.page, 'ogs.google.com'));
|
||||
expect(await signInReason(b.page)).toBeNull();
|
||||
await b.context.close();
|
||||
}, BROWSER_TEST_MS);
|
||||
|
||||
test('a top-level accounts.google.com page IS a sign-in page, and the rule says why (URL without its query)', async () => {
|
||||
const b = await consoleBrowser(playOgb);
|
||||
await b.page.goto('https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fplay.google.com%2Fconsole');
|
||||
expect(await signInReason(b.page)).toEqual({ rule: 'the top-level URL is a sign-in page', url: 'https://accounts.google.com/v3/signin/identifier' });
|
||||
await b.context.close();
|
||||
// a sign-in form in the main frame on a host storebound does not know: its texts tell
|
||||
const page = await open('signin-passkey.html');
|
||||
expect((await signInReason(page))?.rule).toMatch(/^a sign-in form in the page \("(Email or Phone Number|Sign in with Passkey)"\)$/);
|
||||
await page.close();
|
||||
}, BROWSER_TEST_MS);
|
||||
|
||||
test('fillSession play-app-record: the chooser with the widget frames is passed, the form filled, no sign-in said', async () => {
|
||||
const b = await consoleBrowser(playOgb);
|
||||
const log = logLines(), said = log.lines;
|
||||
const done = scope.run(fillSession(b, { form: 'play-app-record', config, ...fast(log) }));
|
||||
await log.wait('the Create app form filled', l => l.some(m => /^Create app: filled \d+/.test(m)));
|
||||
expect(said).toContain('creating the app in developer account "Example Studio" (7000000001, the only developer account of this login)');
|
||||
expect(said.filter(m => /sign-in detected|sign in in the browser window|still waiting for the sign-in/.test(m))).toEqual([]);
|
||||
expect(await clicks(b.page)).toEqual([]);
|
||||
await b.context.close();
|
||||
await done;
|
||||
}, SLOW_TEST_MS);
|
||||
|
||||
test('a copied Chrome session that lands on accounts.google.com: said plainly with the rule; --debug-dump dumps the sign-in wait (at most 3), the watchdog once', async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sb-signin-debug-'));
|
||||
tmpDirs.push(dir);
|
||||
const b: ConsoleBrowser = { ...await consoleBrowser(u => (u.hostname === 'play.google.com' ? 'play-signed-out.html' : playOgb(u))), mode: 'copy' };
|
||||
const log = logLines(), said = log.lines;
|
||||
const done = scope.run(fillSession(b, { form: 'play-app-record', config, ...fast(log), heartbeatMs: 300, stepTimeoutMs: 1000, debugDir: dir, debugEveryStep: true }));
|
||||
await log.wait('the watchdog of the sign-in wait', l => l.some(m => m.startsWith('the sign-in has not finished after')));
|
||||
await log.wait('four heartbeats', l => l.filter(m => m.startsWith('still waiting for the sign-in')).length >= 4);
|
||||
expect(said).toContain('sign-in detected because: the top-level URL is a sign-in page at https://accounts.google.com/v3/signin/identifier');
|
||||
expect(said.filter(m => m.startsWith('sign-in detected because'))).toHaveLength(1);
|
||||
expect(said).toContain('your Chrome session for Play Console did not carry over (expired or tied to this device); sign in once in this window, or run with --fresh-profile');
|
||||
expect(said.find(m => m.startsWith('still waiting for the sign-in'))).toMatch(/\(https:\/\/accounts\.google\.com\/v3\/signin\/identifier: a sign-in page \(the top-level URL is a sign-in page\)\)$/);
|
||||
expect(said.find(m => m.startsWith('the sign-in has not finished after'))).toContain(`Debug saved to ${dir}`);
|
||||
const names = readdirSync(dir).sort();
|
||||
expect(names.filter(n => /-sign-in-wait$/.test(n))).toHaveLength(3);
|
||||
expect(names.filter(n => /-sign-in-not-finished$/.test(n))).toHaveLength(1);
|
||||
expect(await b.page.locator('#identifier').inputValue()).toBe(''); // nothing typed
|
||||
expect(await clicks(b.page)).toEqual([]); // nothing clicked
|
||||
await b.context.close();
|
||||
await done;
|
||||
}, SLOW_TEST_MS);
|
||||
});
|
||||
|
|
|
|||
16
tests/fixtures/console/google-account-widget.html
vendored
Normal file
16
tests/fixtures/console/google-account-widget.html
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED, not recorded: what Google pages (Play Console included) embed from accounts.google.com / ogs.google.com
|
||||
while the owner IS signed in: the account widget (avatar, name, email, Manage your Google Account, Sign out) and
|
||||
the hidden cookie rotation frame. Its URL is a sign-in host, but it is a third-party frame, never a sign-in page.
|
||||
Placeholder data only. -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>Google Account</title></head>
|
||||
<body>
|
||||
<div aria-label="Google Account: Example Owner (owner@example.com)">
|
||||
<img alt="Example Owner" width="32" height="32" src="data:image/gif;base64,R0lGODlhAQABAAAAACw=">
|
||||
<p>Example Owner</p>
|
||||
<p>owner@example.com</p>
|
||||
<a href="https://myaccount.google.com/">Manage your Google Account</a>
|
||||
<a href="https://accounts.google.com/AddSession">Add another account</a>
|
||||
<a href="https://accounts.google.com/Logout">Sign out</a>
|
||||
</div>
|
||||
</body></html>
|
||||
12
tests/fixtures/console/google-signin.html
vendored
Normal file
12
tests/fixtures/console/google-signin.html
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED, not recorded: Google's sign-in page as a top-level page on accounts.google.com (where Play Console sends
|
||||
a signed-out browser). storebound must detect it by its top-level URL, never type or click there. -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>Sign in - Google Accounts</title><script src="/_common.js"></script></head>
|
||||
<body>
|
||||
<h1>Sign in</h1>
|
||||
<p>to continue to Google Play Console</p>
|
||||
<p><input id="identifier" type="email" aria-label="Email or phone"></p>
|
||||
<a href="https://accounts.google.com/signin/v2/usernamerecovery">Forgot email?</a>
|
||||
<button type="button">Create account</button>
|
||||
<button type="button">Next</button>
|
||||
</body></html>
|
||||
24
tests/fixtures/console/play-chooser-ogb.html
vendored
Normal file
24
tests/fixtures/console/play-chooser-ogb.html
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED, not recorded: Play Console's "Choose developer account" page as a signed-in owner sees it, with what every
|
||||
Google page carries: a header with the avatar, name and email (the account widget, OGB), the widget's frame from
|
||||
ogs.google.com and a hidden accounts.google.com cookie rotation frame. Signed in: NOT a sign-in page, although a
|
||||
frame sits on accounts.google.com. One developer account. Ids and names are placeholders. -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>Choose developer account - Play Console</title><script src="/_common.js"></script>
|
||||
<style>.sub { display: block; color: #666; font-size: 12px; } .row { display: flex; gap: 8px; align-items: center; padding: 6px; }</style></head>
|
||||
<body>
|
||||
<header role="banner">
|
||||
<a href="https://play.google.com/console">Google Play Console</a>
|
||||
<a href="https://accounts.google.com/SignOutOptions" aria-label="Google Account: Example Owner (owner@example.com)"><img alt="Example Owner" width="32" height="32" src="data:image/gif;base64,R0lGODlhAQABAAAAACw="></a>
|
||||
<span>owner@example.com</span>
|
||||
<iframe title="Google Account" src="https://ogs.google.com/widget/account?origin=https%3A%2F%2Fplay.google.com" width="320" height="120"></iframe>
|
||||
</header>
|
||||
<iframe title="" src="https://accounts.google.com/RotateCookiesPage?og_pid=1&rot=3&origin=https%3A%2F%2Fplay.google.com" width="0" height="0" style="display:none"></iframe>
|
||||
<iframe title="Google Account" src="https://accounts.google.com/o/oauth2/iframe" width="1" height="1"></iframe>
|
||||
<main>
|
||||
<h1>Choose developer account</h1>
|
||||
<div role="list">
|
||||
<div role="listitem"><a class="row" href="./developers/7000000001/app-list"><span><span>Example Studio</span><span class="sub">Developer account</span></span></a></div>
|
||||
<div role="listitem"><a class="row" href="/signup"><span>Create a new developer account</span></a></div>
|
||||
</div>
|
||||
</main>
|
||||
</body></html>
|
||||
20
tests/fixtures/console/play-home-ogb.html
vendored
Normal file
20
tests/fixtures/console/play-home-ogb.html
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED, not recorded: Play Console > Home (All apps) of a signed-in owner with the Google account widget (avatar,
|
||||
name, email in the header, its ogs.google.com frame) and the accounts.google.com cookie rotation frame. NOT a
|
||||
sign-in page. "Create app" opens the Create app form. Placeholder data only. -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>All apps - Play Console</title><script src="/_common.js"></script></head>
|
||||
<body>
|
||||
<header role="banner">
|
||||
<a href="https://play.google.com/console">Google Play Console</a>
|
||||
<a href="https://accounts.google.com/SignOutOptions" aria-label="Google Account: Example Owner (owner@example.com)"><img alt="Example Owner" width="32" height="32" src="data:image/gif;base64,R0lGODlhAQABAAAAACw="></a>
|
||||
<iframe title="Google Account" src="https://ogs.google.com/widget/account?origin=https%3A%2F%2Fplay.google.com" width="320" height="120"></iframe>
|
||||
</header>
|
||||
<iframe title="" src="https://accounts.google.com/RotateCookiesPage?og_pid=1&rot=3&origin=https%3A%2F%2Fplay.google.com" width="0" height="0" style="display:none"></iframe>
|
||||
<h1>All apps</h1>
|
||||
<label>Search apps <input type="search"></label>
|
||||
<button type="button" onclick="location.href='/play-create-app.html'">Create app</button>
|
||||
<table>
|
||||
<tr><th>App</th><th>Package</th></tr>
|
||||
<tr><td><a href="/play-dashboard.html">Other App</a></td><td>com.example.other</td></tr>
|
||||
</table>
|
||||
</body></html>
|
||||
7
tests/fixtures/console/play-signed-out.html
vendored
Normal file
7
tests/fixtures/console/play-signed-out.html
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<!doctype html>
|
||||
<!-- MODELED, not recorded: Play Console for a browser whose copied session did not carry over: it sends the window on
|
||||
to Google's sign-in page (a top-level accounts.google.com URL). -->
|
||||
<html lang="en"><head><meta charset="utf-8"><title>Play Console</title></head>
|
||||
<body>
|
||||
<script>location.replace('https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2Fplay.google.com%2Fconsole&service=androiddeveloper');</script>
|
||||
</body></html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue