Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
fb3d0a7
feat(bind): an app that binds a GitHub identity on a local chain
SupremaLex Oct 1, 2026
0f213b8
test(bind): bind a real GitHub identity end to end in Playwright
SupremaLex Oct 1, 2026
71c4ddc
fix(bind): pass the workspace lint and format checks
SupremaLex Oct 1, 2026
717f9f0
ci(bind): run the live GitHub binding nightly and on demand
SupremaLex Oct 2, 2026
31f3b8f
test(bind): bind a real X identity end to end
SupremaLex Oct 2, 2026
5e75e12
test(bind): bind a real Google identity end to end
SupremaLex Oct 2, 2026
eb0abbb
ci(bind): run the live bindings on pull requests
SupremaLex Oct 2, 2026
15df634
fix(ci): set the stack log path on the step
SupremaLex Oct 2, 2026
7d360db
chore: merge main into feat/e2e-bind
SupremaLex Oct 5, 2026
8d04f8b
fix(bind): follow main's CCDP client API
SupremaLex Oct 5, 2026
4b2f2c9
build(bind): deploy with libid-deploy 0.15.0
SupremaLex Oct 5, 2026
d8bee4a
test(bind): fail fast on a failed ceremony and a Google challenge
SupremaLex Oct 2, 2026
f002fb4
ci(bind): leave the Google binding out of the live run
SupremaLex Oct 5, 2026
2a0cc70
fix(ci): never publish the test accounts' secrets from a failed live run
SupremaLex Oct 5, 2026
5886746
refactor(bind): one popup driver, one constants module, a page outcome
SupremaLex Oct 5, 2026
96d47c8
fix(bind): bind through the verifier slot, with the wallet the run st…
SupremaLex Oct 5, 2026
77f1c37
fix(bind): start the stack fresh, and never over another one
SupremaLex Oct 5, 2026
5b2b0a7
fix(bind): finish the GitHub authorization on GitHub's redirect
SupremaLex Oct 5, 2026
29510f4
ci(bind): nest the live run's budgets, and skip runs without secrets
SupremaLex Oct 5, 2026
69a2e72
fix(bind): the popup returns on Bridge's callback, inside one budget
SupremaLex Oct 5, 2026
62363f5
fix(bind): report "bound" only for a binding the registry and indexer…
SupremaLex Oct 5, 2026
ff1c242
fix(bind): one stack per checkout, its output kept off the job log
SupremaLex Oct 5, 2026
86166bf
ci(bind): per-PR concurrency, and scan the log the run finished with
SupremaLex Oct 5, 2026
ececb6d
refactor(bind): one stop signal for the stack, and overlap its startup
SupremaLex Oct 5, 2026
fce3132
refactor(bind): share saved sessions and click marking across drivers
SupremaLex Oct 5, 2026
be7190f
refactor(ceremony-live): rename the bind app to ceremony-live
SupremaLex Oct 6, 2026
cf159a7
refactor(ceremony-live): run the CCDP ceremony live, without a chain
SupremaLex Oct 6, 2026
8afefa4
ci(ceremony-live): run the live ceremonies without a chain
SupremaLex Oct 6, 2026
7bf9e06
chore: merge main into feat/e2e-bind
SupremaLex Oct 19, 2026
c70dad5
chore: merge feat/e2e-bind, with main, into the ceremony-live branch
SupremaLex Oct 19, 2026
b802eea
ci(live-e2e): choose the platforms with a repository variable
SupremaLex Oct 19, 2026
cdc025a
ci(live-e2e): prove the Google identity through grndd-testnet's proxy
SupremaLex Oct 19, 2026
34e3291
ci(live-e2e): port-forward to the Flux-managed google-proxy in namesp…
SupremaLex Oct 20, 2026
0989127
ci(live-e2e): run the Google job on this repository's pull requests too
SupremaLex Oct 20, 2026
5e2027f
ci(live-e2e): an opt-in Google job on a self-hosted runner
SupremaLex Oct 20, 2026
2167b26
ci(live-e2e): the cluster Google job runs while LIBID_GOOGLE_RUNNER i…
SupremaLex Oct 20, 2026
77d89d8
ci(live-e2e): the self-hosted Google job runs only on dispatch or sch…
SupremaLex Oct 21, 2026
5bd1a7c
ci(live-e2e): the cluster Google job runs on main only, through names…
SupremaLex Oct 21, 2026
ceb4061
ci(live-e2e): a Google job through a proxy reached over SSM
SupremaLex Oct 22, 2026
35109ea
ci(live-e2e): TEMPORARY run the SSM Google job on this repository's p…
SupremaLex Oct 22, 2026
1d4d599
chore: merge main into feat/e2e-bind
SupremaLex Oct 22, 2026
d3d0f35
fix(ceremony-live): the operation for a chain-free digest
SupremaLex Oct 22, 2026
1a5ceeb
test(dev): run the live ceremony specs against the dev application
SupremaLex Oct 22, 2026
b98899e
ci(live-e2e): run from @libid/dev and keep only the SSM Google job
SupremaLex Oct 22, 2026
c357daf
fix(dev): skip GitHub's 2FA checkup in the live GitHub driver
SupremaLex Oct 22, 2026
b7e81a7
build(ts): keep oxlint off the dev live tests' stealth script
SupremaLex Oct 22, 2026
b664d85
docs(dev): how to run the live tests
SupremaLex Oct 22, 2026
043ac98
refactor(dev): one module for the app and Bridge origins
SupremaLex Oct 22, 2026
35b50f1
fix(dev): refuse to start services beside another stack
SupremaLex Oct 22, 2026
0be2895
fix(dev): bound and clarify the live platform drivers
SupremaLex Oct 22, 2026
5392ac3
test(dev): check the live selection, ceremony version and expiry
SupremaLex Oct 22, 2026
9132043
ci(live-e2e): share the failure-output steps; fit the Google step tim…
SupremaLex Oct 22, 2026
291ae1b
docs(dev): list the live tests' variables; name local.ts
SupremaLex Oct 22, 2026
55f4b51
test(dev): read the live tests' settings through one typed table
SupremaLex Oct 22, 2026
8e74979
test(dev): check the settings table against the workflow and README
SupremaLex Oct 22, 2026
38fc71b
fix(dev): watch the popup's return to Bridge from the moment it appears
SupremaLex Oct 22, 2026
e674d8b
fix(dev): restore saved sessions as the Rust export defines them
SupremaLex Oct 22, 2026
5dc6798
fix(dev): scan for a session's secrets even when it does not decode
SupremaLex Oct 22, 2026
58f9cf3
fix(dev): start the live run's own stack, with bounded steps
SupremaLex Oct 22, 2026
f16c689
refactor(dev): take the notary origin and fixture config URL from loc…
SupremaLex Oct 22, 2026
3411bb9
ci(live-e2e): fit the step timeout to every platform; exclude Dependabot
SupremaLex Oct 22, 2026
6e96c38
fix(dev): fail fast on a bad TOTP key or a refused GitHub sign-in
SupremaLex Oct 22, 2026
72c6ed9
fix(dev): bound every popup driver step by the driver's budget
SupremaLex Oct 22, 2026
3d31225
test(dev): check the proof by field name; fit the live test timeout
SupremaLex Oct 22, 2026
cfa8936
fix(dev): keep a host-only cookie's exact path; name the partition loss
SupremaLex Oct 22, 2026
3cf76eb
fix(dev): check local.ts against compose.yaml and bridge-config.toml
SupremaLex Oct 22, 2026
b5a16f6
test(dev): list HEADED and CI as settings; read job platforms from CI
SupremaLex Oct 22, 2026
ace7549
ci(live-e2e): fit step timeouts to 15-minute tests; one stack log path
SupremaLex Oct 22, 2026
a5d3144
fix(dev): keep only the platform's unexpired cookies of a saved session
SupremaLex Oct 22, 2026
b8ce6df
fix(dev): resolve cookie files like the env file; strict switches
SupremaLex Oct 22, 2026
e37e8fa
fix(dev): scan for encoded secrets and storage-state localStorage values
SupremaLex Oct 22, 2026
1d68425
fix(dev): check that local.ts' notary URL reaches the notary
SupremaLex Oct 22, 2026
aeecb8b
fix(dev): cancel evaluateWithin's timer once the evaluation settles
SupremaLex Oct 22, 2026
406aeaf
test(dev): one budgets module; check CI timeouts and coverage against it
SupremaLex Oct 22, 2026
745fb13
docs(dev): point to local.ts for the origins and check the README's
SupremaLex Oct 22, 2026
e44eaa2
fix(dev): stop after two refused TOTP codes; test the TOTP vector
SupremaLex Oct 22, 2026
a5b747e
fix(dev): read export entries as cookies.rs does, never a TypeError
SupremaLex Oct 22, 2026
ea5ee7a
fix(dev): fail when Google holds the popup on a host no step drives
SupremaLex Oct 22, 2026
391c105
fix(dev): an empty LIBID_LIVE_PLATFORMS selects no platform
SupremaLex Oct 22, 2026
8fc476c
fix(dev): bound the live stack's teardown; keep its log out of CI's
SupremaLex Oct 22, 2026
e34cc94
fix(dev): fail the secret scan closed; test it with fake markers
SupremaLex Oct 22, 2026
a177f18
docs(dev): ignore .env.test repo-wide; document the stack log and CI …
SupremaLex Oct 22, 2026
6f8d88d
fix(dev): one path rule and raw lookups for the live settings
SupremaLex Oct 23, 2026
6b1df26
fix(dev): scan sessions as session.ts reads them; distinct exit codes
SupremaLex Oct 23, 2026
7099fb3
fix(dev): settle TOTP refusals; route GitHub by path first; one X hos…
SupremaLex Oct 23, 2026
173aa67
fix(dev): stop the popup driver on the app's own failure; reuse fixtures
SupremaLex Oct 23, 2026
814d8d7
fix(dev): check sessions and the TOTP key before the stack starts
SupremaLex Oct 23, 2026
ec82158
test(dev): check each live job's timeout covers its step and setup
SupremaLex Oct 23, 2026
411fc08
ci(live-e2e): serialise the sign-in jobs; narrow the pull_request paths
SupremaLex Oct 23, 2026
44dedd1
docs(dev): state the path rule, preflight and CI log rows
SupremaLex Oct 23, 2026
a91033d
fix(dev): keep the test secrets from the stack; start it only for a test
SupremaLex Oct 23, 2026
4eaad1f
fix(dev): fail the scan closed on an unreadable session file
SupremaLex Oct 23, 2026
2f6db1c
ci(live-e2e): best-effort stack cleanup; its project from services.ts
SupremaLex Oct 23, 2026
7d403d9
fix(dev): refuse to start services when the app's port is taken
SupremaLex Oct 23, 2026
e2d55ac
fix(dev): a saved Google session needs the sign-in google.rs checks
SupremaLex Oct 23, 2026
db52115
fix(dev): fail fast on X's error page and a held account
SupremaLex Oct 23, 2026
76771eb
fix(dev): keep GitHub's TOTP refusal reason; submit each code once
SupremaLex Oct 23, 2026
b6e50f5
fix(dev): bound the app-failure check; budget every bounded test step
SupremaLex Oct 23, 2026
f9a0086
refactor(dev)!: simplify the live tests to their core
SupremaLex Oct 23, 2026
a9adc8c
ci(live-e2e): simplify the live jobs
SupremaLex Oct 23, 2026
807e3bf
docs(dev): shorten the live tests section
SupremaLex Oct 23, 2026
b0c3363
refactor(dev): click with Playwright locators and one throttled helper
SupremaLex Oct 23, 2026
e925a0d
refactor(dev)!: one platform switch; trust the ceremony's validation
SupremaLex Oct 23, 2026
d696565
refactor(dev): the live config owns the stack log; local.ts constants
SupremaLex Oct 23, 2026
d690d0a
ci(live-e2e): show the stack log on failure; one platform switch
SupremaLex Oct 23, 2026
435e82a
chore: merge main into feat/e2e-bind
SupremaLex Oct 23, 2026
61cd9e0
ci(live-e2e): the Google job runs on main only
SupremaLex Oct 23, 2026
08373db
docs(dev): Google's session is signed in by hand through the proxy
SupremaLex Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .github/workflows/live-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Live end-to-end

# Proves real platform identities from the dev app's UI (ts/apps/dev/README.md).
# Not a required check: it depends on the platforms, their accounts and the
# network. Third-party actions are pinned by commit SHA, the tag in a comment.

on:
pull_request:
# What a live ceremony runs: the dev app, its packages and their build.
paths:
- "ts/apps/dev/**"
- "ts/packages/ceremony/**"
- "ts/packages/popup/**"
- "ts/packages/ledger/**"
- "ts/scripts/build-package.ts"
- "ts/package.json"
- "ts/pnpm-lock.yaml"
- "ts/pnpm-workspace.yaml"
- "ts/.node-version"
- "ts/tsconfig.base.json"
- "brand/**"
- ".github/workflows/live-e2e.yml"
- ".github/actions/setup-ts/**"
workflow_dispatch:
schedule:
- cron: "23 3 * * *"

permissions:
contents: read

env:
# Where the stack's output goes; a failed job prints its end.
LIBID_STACK_LOGS: /tmp/libid-live-stack.log

jobs:
ceremony:
name: GitHub and X ceremonies (live)
# A pull request from a fork or from Dependabot gets no secrets.
if: >-
github.event_name != 'pull_request' ||
(github.event.pull_request.head.repo.full_name == github.repository &&
github.actor != 'dependabot[bot]')
runs-on: ubuntu-latest
# One run at a time per account: two could submit one TOTP code twice.
concurrency:
group: live-e2e-github-x
cancel-in-progress: false
timeout-minutes: 60 # test step 45: stack 10 + 2 tests × 15 + stop 2
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: ./.github/actions/setup-ts

- name: Install Chromium
run: pnpm -C ts --filter @libid/dev exec playwright install --with-deps chromium

- name: Prove GitHub and X identities end to end
id: test
timeout-minutes: 45
env:
LIBID_LIVE_PLATFORMS: github,x
GH_TEST_ALICE_USERNAME: ${{ secrets.GH_TEST_ALICE_USERNAME }}
GH_TEST_ALICE_PASSWORD: ${{ secrets.GH_TEST_ALICE_PASSWORD }}
GH_TEST_ALICE_TOTP_SECRET: ${{ secrets.GH_TEST_ALICE_TOTP_SECRET }}
X_TEST_ALICE_USERNAME: ${{ secrets.X_TEST_ALICE_USERNAME }}
X_TEST_ALICE_COOKIES: ${{ secrets.X_TEST_ALICE_COOKIES }}
run: pnpm -C ts/apps/dev test:live

# The stack gets no secret (playwright.live.config.ts); GitHub masks them anyway.
- name: Show the stack log
if: ${{ !cancelled() && steps.test.outcome != 'success' }}
run: tail -n 300 "$LIBID_STACK_LOGS" || true

- name: Keep the screenshots
if: ${{ !cancelled() && steps.test.outcome != 'success' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: live-e2e-github-x
path: ts/apps/dev/test-results/**/*.png
retention-days: 7
if-no-files-found: ignore

google-ssm:
name: Google ceremony through an SSM-reached proxy (live)
# Google challenges a session from a runner's address, so the browser goes
# through an EC2 proxy LIBID_GOOGLE_PROXY_ROLE reaches only by SSM.
# Runs while LIBID_GOOGLE_RUNNER is `ssm`, on main only: the role trusts
# this workflow on main and nothing else.
if: >-
vars.LIBID_GOOGLE_RUNNER == 'ssm' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
concurrency:
group: live-e2e-google
cancel-in-progress: false
timeout-minutes: 45 # test step 30: stack 10 + 1 test × 15 + stop 2
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: ./.github/actions/setup-ts

- name: Install Chromium
run: pnpm -C ts --filter @libid/dev exec playwright install --with-deps chromium

- uses: aws-actions/configure-aws-credentials@a03048d87541d1d9fcf2ecf528a4a65ba9bd7838 # v5.0.0
with:
role-to-assume: ${{ vars.LIBID_GOOGLE_PROXY_ROLE }}
aws-region: eu-central-1

# The port-forward runs for the rest of the job; the step fails unless the proxy answers.
- name: Reach the Google proxy over SSM
env:
INSTANCE: ${{ vars.LIBID_GOOGLE_PROXY_INSTANCE }}
run: |
aws ssm start-session --target "$INSTANCE" \
--document-name AWS-StartPortForwardingSession \
--parameters portNumber=3128,localPortNumber=3128 >"$RUNNER_TEMP/port-forward.log" 2>&1 &
for _ in $(seq 1 30); do
ip=$(curl -fs --max-time 5 -x http://127.0.0.1:3128 https://api.ipify.org) && break
sleep 2
done
echo "Google sees ${ip:?the proxy did not answer; see $RUNNER_TEMP/port-forward.log}"

- name: Prove a Google identity end to end
id: test
timeout-minutes: 30
env:
LIBID_LIVE_PLATFORMS: google
LIBID_TEST_PROXY: http://127.0.0.1:3128
GOOGLE_TEST_ALICE_EMAIL: ${{ secrets.GOOGLE_TEST_ALICE_EMAIL }}
GOOGLE_TEST_ALICE_COOKIES: ${{ secrets.GOOGLE_TEST_ALICE_COOKIES }}
run: pnpm -C ts/apps/dev test:live

- name: Show the stack log
if: ${{ !cancelled() && steps.test.outcome != 'success' }}
run: tail -n 300 "$LIBID_STACK_LOGS" || true

- name: Keep the screenshots
if: ${{ !cancelled() && steps.test.outcome != 'success' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: live-e2e-google
path: ts/apps/dev/test-results/**/*.png
retention-days: 7
if-no-files-found: ignore
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ ts/packages/*/node_modules/
ts/apps/*/node_modules/
ts/packages/*/dist/
ts/apps/*/dist/
# Local development environment files
# Local development environment files and test secrets
.env
.env.local
.env.*.local
*.env.test

# @libid/popup e2e bundles and Playwright output
ts/packages/popup/e2e/dist/
Expand Down
1 change: 1 addition & 0 deletions ts/.oxlintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"ignorePatterns": ["apps/dev/e2e/stealth.js"],
"plugins": ["typescript"],
"categories": {
"correctness": "off"
Expand Down
57 changes: 55 additions & 2 deletions ts/apps/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Edit configuration directly where it is used:

- [compose.yaml](compose.yaml): services.
- [bridge-config.toml](bridge-config.toml): Bridge configuration and development OAuth credentials.
- [src/app.ts](src/app.ts): Bridge URL and the local ledger/notary fixture; Bridge configuration selects CCDP.
- [vite.config.ts](vite.config.ts): frontend port.
- [local.ts](local.ts): the app origin (frontend port), and the Bridge and notary URLs.
- [src/app.ts](src/app.ts): the local ledger fixture; Bridge configuration selects CCDP.

The setup includes shared development OAuth credentials and a public development
notary signing key; no environment file is needed. Bridge publishes the GitHub
Expand Down Expand Up @@ -105,3 +105,56 @@ When isolation fallback occurs, **Prover fallback** shows navigation through Pro
readiness, using `prover-fallback` and `prover.started` occurrence timestamps. It
excludes work before navigation and does not measure the difference from a run
without fallback. Direct runs have no fallback row.

## Live tests

The live suite proves real GitHub, X and Google accounts from this app's UI.
It starts its own `pnpm dev` and stops it after, so the ports above must be
free.

```sh
LIBID_TEST_ENV_FILE=path/to/.env.test pnpm --filter @libid/dev test:live
```

The variables carry the names libid-server-rs' ceremony-tests uses, so one
`.env.test` serves both suites; keep it outside the repository, or name it
`.env.test`, which git ignores. Unless `LIBID_LIVE_PLATFORMS` lists it, a
platform whose variables are missing is skipped.

| Variable | Platform | What |
|---|---|---|
| `GH_TEST_ALICE_USERNAME`, `GH_TEST_ALICE_PASSWORD` | GitHub | The test account. |
| `GH_TEST_ALICE_TOTP_SECRET` | GitHub | The base32 key of its authenticator app. |
| `X_TEST_ALICE_USERNAME` | X | The test account's handle. |
| `X_TEST_ALICE_COOKIES` or `X_TEST_ALICE_COOKIES_FILE` | X | A saved session: base64 of the JSON cookie list from `ceremony export x`, or a path to that JSON, relative to the env file. |
| `GOOGLE_TEST_ALICE_EMAIL` | Google | The test account. |
| `GOOGLE_TEST_ALICE_COOKIES` or `GOOGLE_TEST_ALICE_COOKIES_FILE` | Google | A saved session, made by hand: see [Google's session](#googles-session). |
| `LIBID_TEST_ENV_FILE` | — | A dotenv file with the variables above; exported values win. |
| `LIBID_LIVE_PLATFORMS` | — | Comma-separated platforms to prove, such as `github,x`; each one listed must have its variables. Unset, every platform with its variables runs. |
| `LIBID_TEST_PROXY` | — | A proxy for the browser's traffic, loopback aside. |
| `LIBID_STACK_LOGS` | — | A file all of the stack's output, the build's included, is appended to. |
| `HEADED` | — | Set: show the browser. |

In CI, [live-e2e.yml](../../../.github/workflows/live-e2e.yml) runs GitHub and X
daily in one job, and Google in another through a proxy whose address Google
accepts. Each job gets only its own platforms' secrets. On a failure it prints
the end of the stack log to the job log, where GitHub masks the secrets, and
uploads the screenshots.

### Google's session

Google challenges a saved session used from an address other than the one it
was signed in from, and refuses sign-in in an automated browser. So the session
is made once by hand, through the same proxy CI uses:

1. Open the proxy locally:
`aws ssm start-session --target <LIBID_GOOGLE_PROXY_INSTANCE> --document-name AWS-StartPortForwardingSession --parameters portNumber=3128,localPortNumber=3128`.
2. In plain Chrome with a fresh profile, sign in to the test account through it:
`google-chrome --user-data-dir=/tmp/google-session --password-store=basic --proxy-server=http://127.0.0.1:3128 https://myaccount.google.com/`.
Then close Chrome.
3. Export and store it: `node e2e/export-session.ts /tmp/google-session google.json`,
then `base64 -w0 google.json | gh secret set GOOGLE_TEST_ALICE_COOKIES`, and
delete `google.json` and the profile.

Do this again when the Google job reports an expired session, or when the
proxy's address changes.
92 changes: 92 additions & 0 deletions ts/apps/dev/e2e/ceremony.live.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { PlatformId } from '@libid/ceremony'
import { type BrowserContext, expect, type Page, test } from '@playwright/test'
import { expectReady, launch, newestRun } from '../src/app.fixtures.ts'
import { authorizeOnGitHub } from './github.ts'
import { authorizeOnGoogle } from './google.ts'
import { presentAsPerson } from './person.ts'
import { watchAppFailure, watchReturn } from './popup.ts'
import { googleSession, restoreSession, xSession } from './session.ts'
import { env, missing, runnablePlatforms } from './settings.ts'
import { authorizeOnX } from './x.ts'

/**
* Start `name`'s ceremony from the app, let `authorize` drive the popup, then
* check the run's result: accepted, for `user` on `platform`. The ceremony
* itself validates the proof, the nodes and the ids.
*/
async function proveThroughTheUi(
page: Page,
context: BrowserContext,
name: string,
platform: PlatformId,
user: string,
authorize: (popup: Page) => Promise<void>,
) {
await page.goto('/')
await expectReady(page, 60_000)
// Watched as it appears: a platform may return it at once.
context.on('page', watchReturn)
const popup = await launch(page, name)
const row = newestRun(page)
const id = (await row.getAttribute('data-ceremony-id'))!
const result = () =>
page.evaluate((id) => {
const result = window.results.get(id)
const identity = result && 'identity' in result ? result.identity : undefined
return (
result && {
status: result.status,
platform: identity?.platformId,
user: identity?.userName,
}
)
}, id)
const message = async () => (await row.locator('.run-status').textContent()) ?? ''
watchAppFailure(popup, async () => {
const status = (await result())?.status
return status && status !== 'accepted' ? await message() : undefined
})
await authorize(popup)

await expect.poll(result, { timeout: 8 * 60_000 }).toBeDefined()
const outcome = (await result())!
if (outcome.status !== 'accepted') expect(outcome.status, await message()).toBe('accepted')
expect(outcome.platform).toBe(platform)
expect(outcome.user?.toLowerCase()).toBe(user.toLowerCase())
}

/** Skip `platform`'s test unless it runs: listed, or with its variables set. */
const skipUnlessLive = (platform: PlatformId) =>
test.skip(
!runnablePlatforms().includes(platform),
`not listed in LIBID_LIVE_PLATFORMS, or ${missing(platform).join(', ')} not set`,
)

test('a GitHub identity is proved in the browser', async ({ page, context }) => {
skipUnlessLive('github')
const account = {
username: env('GH_TEST_ALICE_USERNAME')!,
password: env('GH_TEST_ALICE_PASSWORD')!,
totpSecret: env('GH_TEST_ALICE_TOTP_SECRET')!,
}
await proveThroughTheUi(page, context, 'GitHub', 'github', account.username, (popup) =>
authorizeOnGitHub(popup, account),
)
})

test('an X identity is proved in the browser', async ({ page, context }) => {
skipUnlessLive('x')
await presentAsPerson(context)
await restoreSession(context, xSession()!)
await proveThroughTheUi(page, context, 'X', 'x', env('X_TEST_ALICE_USERNAME')!, authorizeOnX)
})

test('a Google identity is proved in the browser', async ({ page, context }) => {
skipUnlessLive('google')
const email = env('GOOGLE_TEST_ALICE_EMAIL')!
await presentAsPerson(context)
await restoreSession(context, googleSession()!)
await proveThroughTheUi(page, context, 'Google', 'google', email, (popup) =>
authorizeOnGoogle(popup, email),
)
})
24 changes: 24 additions & 0 deletions ts/apps/dev/e2e/export-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Saves a Chrome profile's session as the JSON GOOGLE_TEST_ALICE_COOKIES holds
// (base64-encoded). Google refuses sign-in in an automated browser, so a person
// signs in first, in plain Chrome, through the proxy CI uses:
//
// google-chrome --user-data-dir=/tmp/google-session --password-store=basic \
// --proxy-server=http://127.0.0.1:3128 https://myaccount.google.com/
//
// Close that Chrome, then: node e2e/export-session.ts /tmp/google-session out.json
import { chromium } from '@playwright/test'

const [profile, out] = process.argv.slice(2)
if (!profile || !out) throw new Error('Usage: node e2e/export-session.ts <profile-dir> <out.json>')

// The same Chrome and password store that wrote the profile, so its cookies decrypt.
const context = await chromium.launchPersistentContext(profile, {
channel: 'chrome',
headless: true,
args: ['--password-store=basic'],
})
await context.storageState({ path: out })
await context.close()
console.log(
`Saved ${out}. It is a credential: keep it out of the repository and delete it after use.`,
)
9 changes: 9 additions & 0 deletions ts/apps/dev/e2e/github.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// The TOTP code against the reference vector libid-server-rs' github.rs checks.
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { totp } from './github.ts'

test('the TOTP code matches the reference vector, spaces and case aside', () => {
assert.equal(totp('JBSWY3DPEHPK3PXP', 1_700_000_000_000), '324550')
assert.equal(totp('jbsw y3dp ehpk 3pxp', 1_700_000_000_000), '324550')
})
Loading
Loading