diff --git a/.github/agents/e2e-test-setup.agent.md b/.github/agents/e2e-test-setup.agent.md new file mode 100644 index 000000000..437b7c18c --- /dev/null +++ b/.github/agents/e2e-test-setup.agent.md @@ -0,0 +1,69 @@ +--- +description: 'Drives setting up a self-contained e2e test suite (Testcontainers stack + Playwright/BDD + CI) on a project by following the same patterns used to build a proven reference e2e setup, working phase by phase and delegating to the e2e-* skills.' +tools: ['search/codebase', 'search', 'search/usages', 'vscodeGeneral/usages', 'edit/editFiles', 'execute/getTerminalOutput', 'execute/runInTerminal', 'read/terminalLastCommand', 'read/terminalSelection', 'execute/createAndRunTask', 'execute/runTask', 'read/getTaskOutput', 'vscodeTasks/createAndRunTask', 'vscodeTasks/getTaskOutput', 'vscodeTasks/runTask', 'web/fetch', 'web/githubRepo', 'read/problems','vscodeTasks/problems'] +--- + +# E2E test setup agent + +You set up a self-contained, browser-based end-to-end test suite on a project by +following the same patterns used to build a reference e2e setup: Playwright + +`playwright-bdd` running against a Testcontainers local stack (the app under test +plus its dependencies, each mocked or run for real), plus a GitHub Actions +workflow. Apply the approach and adapt it to the target project — do not clone +the reference stack. + +## Source of truth +Always ground your work in the **`e2e-test-setup` skill** and its +[reference playbook](../skills/e2e-test-setup/reference/e2e-playbook.md). Read the +playbook before acting, and consult the reference implementation under +`e2e-tests/` as a worked example of each pattern rather than inventing your own. +Delegate the detail of +each phase to the matching skill: `e2e-stack-setup`, `e2e-fixtures-and-mocks`, +`e2e-ci-workflow`, and for tests `feature-file-from-templates` + +`feature-file-step-definitions`. Build-speed optimisations are baked into +`e2e-stack-setup` (playbook §6), not a separate phase. + +## How you work +1. **Discover first (Phase 0).** Before writing anything, establish: the app's + framework and how it runs in dev; every dependency (other services, databases, + object storage, each upstream HTTP API); the auth model; and that Docker is + available. For each dependency version-controlled in a Guardian repository + (private or public), ask the user whether to run the real service (repo + checkout, built from source) or mock it with WireMock — default to mocking. Ask + the user for anything you cannot determine from the codebase. Summarise + findings and confirm before scaffolding. +2. **Then proceed phase by phase** (scaffold + stack → fixtures + mocks → tests → + CI), following the playbook's phase table. Bake the build-speed optimisations + (playbook §6) into the stack build rather than as a separate pass. In the tests + phase, author only a **small** set of features to validate the setup — **ask + the user which part of the UI** to cover, don't generate the whole suite. + Complete and verify one phase before starting the next. +3. **Verify each phase** with the skill's verification steps (boot the stack, + run the suite, check teardown). Fix failures before moving on. +4. **Keep the app-code footprint minimal** — ideally a single env-gated switch; + everything else lives under `e2e-tests/`. + +## Guardrails +- Never commit real secrets or personal data into fixtures; use synthetic values. +- Pin CI actions to commit SHAs. +- Preserve the playbook's §4 decisions unless the target genuinely differs; when + it does, follow playbook §8 and record what you changed and why. +- Prefer stock images with bind-mounted fixtures over bespoke Dockerfiles. +- Run the app **natively** in dev; containerise only for `test:ci` / CI, and + bind-mount the code, never copy it (playbook §9–§10, guiding principles). +- Keep `dev` / `dev:local` working with **no browser setup** — provide cookies and + routing server-side, not via forced cookies or browser mocks. +- **Don't run the test suite unless asked.** When you do, use `test`: start + `dev:local` if no local stack is up, and **abort if `dev` (remote infra) is + running** — never test against remote infrastructure. +- Confirm before destructive or shared-system actions (pushing, deleting, + editing CI secrets). + +## Guardian projects +Each phase skill ends with a **Guardian specifics** section covering pan-domain +auth, dev-nginx, the private `guardian/workflow` datastore and the GitHub App +token — read it as part of that phase. + +## Output +At the end of each phase, report what was created/changed (as file links), how +you verified it, and the next phase. Keep prose short. diff --git a/.github/instructions/e2e-stack.instructions.md b/.github/instructions/e2e-stack.instructions.md new file mode 100644 index 000000000..9c51486f3 --- /dev/null +++ b/.github/instructions/e2e-stack.instructions.md @@ -0,0 +1,50 @@ +--- +applyTo: "e2e-tests/setup/**,e2e-tests/images/**" +description: "Conventions for the e2e Testcontainers stack code and its Dockerfiles." +--- + +# E2E stack conventions + +Rules for the local-stack setup code under `e2e-tests/setup/` and the Dockerfiles +under `e2e-tests/images/`. Background and rationale live in the +`e2e-test-setup` skill's [reference playbook](../skills/e2e-test-setup/reference/e2e-playbook.md). + +## Dockerfiles +- Base app/backend images on `debian:bookworm-slim` (glibc). Do **not** use + Alpine for images that run a JDK or use `apt-get`. +- Bake **only the toolchain** (via `mise` from a copied `.tool-versions`); never + `COPY` application/backend source into the image — it is bind-mounted at + runtime. +- Keep the build context tiny: build from a temp dir holding just + `.tool-versions` + the Dockerfile. +- Start every Dockerfile with the `# syntax=docker/dockerfile:1` directive. + +## Container start code +- Create exactly one `Network` per stack run; stop it and every started + container in the failure path and in `stopLocalStack`. +- Prefer stock images with bind-mounted fixtures/config over bespoke images + (WireMock, LocalStack, Postgres, nginx). +- Run all mocks from the single shared WireMock image via `MOCK_WIREMOCK_CONFIGS` + + `startMockWiremock`; add a mock by adding a fixture folder and a table entry, + not a Dockerfile. +- Give each service a network alias matching the real upstream hostname so the + app resolves it inside the Docker network without config overrides. +- Every container must have an explicit `Wait` strategy (log message or HTTP + healthcheck) and a sensible `withStartupTimeout`. +- Bind-mount source read-write only where the toolchain writes (`target/`, + `public/build`); mount fixtures read-only. +- Seed datastores from the host after the container is ready; seed the SQL DB + only after the owning service's migrations have created the schema, parents + before FK children. + +## Safety +- Never commit real secrets, tokens or personal data into `e2e-tests/fixtures/` + — use synthetic values. +- Keep the production app change footprint minimal (env-gated switches only). + +## Guiding principles +- The code lives at the dev-container root; when a Docker image runs the app + (CI / `test:ci`), it **bind-mounts** the code, never copies it in. +- Run the app **natively** in dev; containerise only for CI / `test:ci`. +- Provide anything a browser needs (cookies, routing) **server-side** so `dev` / + `dev:local` need no forced cookies or browser mocks. diff --git a/.github/instructions/feature-files.instructions.md b/.github/instructions/feature-files.instructions.md new file mode 100644 index 000000000..bf7f79352 --- /dev/null +++ b/.github/instructions/feature-files.instructions.md @@ -0,0 +1,39 @@ +--- +applyTo: "e2e-tests/features/**/*.feature" +description: "Conventions for authoring Cucumber/Gherkin .feature files in workflow-frontend." +--- + +# Feature File Conventions + +These rules apply to every `.feature` file under `e2e-tests/features/`. + +## Structure +- Start with a `Feature:` title, followed by 1–3 indented plain-English lines describing intent. +- Use the standard `Background:` for every feature; only the final "opened" line changes to name the relevant page: + ```gherkin + Background: + Given the application stack is running + And I am signed in through pan-domain auth + And I have opened + ``` +- One `Scenario:` per distinct, observable behaviour or branch (default state, open/close, each action, each data/permission variation). + +## Steps +- Write steps as user behaviour (what the user sees or does), not implementation detail. +- Use `Given` for preconditions, `When` for actions, `Then` for expected outcomes, `And` to continue the previous keyword. +- Keep wording consistent across scenarios so step definitions can be reused. + +## Evidence comments +- Immediately after each scenario, cite every source file the scenario relies on: + ```gherkin + # Evidence: public/path/to/template.html + # Evidence: public/path/to/controller.js + ``` +- Use real, workspace-relative paths, and only cite files that were actually inspected. + +## Coverage +- Every interactive element and state branch in the source template must map to at least one scenario. +- Data-driven branches (feature switches, permissions) need both on/off (present/absent) scenarios. +- Do not pad the file with scenarios for purely decorative elements; note such gaps instead. + +See the `feature-file-from-templates` skill for the full authoring workflow, and `feature-file-step-definitions` for wiring scenarios to Playwright tests. diff --git a/.github/prompts/setup-e2e-tests.prompt.md b/.github/prompts/setup-e2e-tests.prompt.md new file mode 100644 index 000000000..f147384d0 --- /dev/null +++ b/.github/prompts/setup-e2e-tests.prompt.md @@ -0,0 +1,47 @@ +--- +mode: 'agent' +description: 'Kick off setting up an e2e test suite (Testcontainers stack + Playwright/BDD + CI) on this project, following the same patterns used to build a proven reference e2e setup.' +tools: ['codebase', 'search', 'usages', 'editFiles', 'runCommands', 'fetch', 'githubRepo', 'problems'] +--- + +# Set up e2e tests + +Set up a self-contained end-to-end test suite on this project by following the +same patterns used to build a reference e2e setup, per the +**`e2e-test-setup` skill** and its +[reference playbook](../skills/e2e-test-setup/reference/e2e-playbook.md). Use the +`e2e-test-setup` agent's phased approach, adapting each pattern to this project's +stack rather than copying the reference stack. + +Before scaffolding anything, run **Phase 0 discovery** and confirm findings with +me. Establish: + +1. **App runtime** — framework/language and how the app is built and run in dev. +2. **Dependencies** — every service and store the app talks to: + - other services, and whether each is version-controlled in a Guardian + repository (private or public); + - databases (SQL, DynamoDB, …); + - object storage (S3); + - each upstream HTTP API. + For each Guardian-repo dependency, I'll confirm with you whether to run the + real service (checked out and built from source) or mock it with WireMock. +3. **Auth model** — how a request is authenticated (cookie/JWT/OIDC/pan-domain). +4. **Environment** — is Docker available in the dev container and in CI? +5. **Scope** — which of these phases to do now: scaffold+stack, fixtures+mocks, + feature tests, CI workflow. (Build-speed optimisations are baked into the + stack build, not a separate phase.) The feature-tests phase only **validates + the setup** — tell me which part of the UI to extract a small set of features + for, rather than covering the whole app. + +Infer as much as possible from the codebase first, then ask me only what's left. +Once I confirm the discovery summary, proceed phase by phase, delegating to the +`e2e-stack-setup`, `e2e-fixtures-and-mocks` and `e2e-ci-workflow` skills, and +verifying each phase before the next. + +Follow the standard command set and guiding principles in the playbook (§9–§11): +the app runs natively in dev and is containerised only in CI; provide anything +the browser needs server-side (no forced cookies or browser mocks for dev). + +Each phase skill ends with a **Guardian specifics** section (pan-domain auth, the +private `guardian/workflow` datastore, the GitHub App token) — read it as part of +that phase. diff --git a/.github/skills/e2e-ci-workflow/SKILL.md b/.github/skills/e2e-ci-workflow/SKILL.md new file mode 100644 index 000000000..04c5e5396 --- /dev/null +++ b/.github/skills/e2e-ci-workflow/SKILL.md @@ -0,0 +1,111 @@ +--- +name: e2e-ci-workflow +description: 'Add a GitHub Actions workflow that runs the Playwright/Testcontainers e2e suite in CI: check out any private Guardian repo (for a dependency you run for real) via a GitHub App token, install the toolchain and the Playwright headless shell only, run the BDD suite, and upload traces on failure. Use when adding e2e tests to CI, creating the GitHub Actions workflow for Playwright, or granting CI access to a private dependency repository.' +argument-hint: '' +--- + +# E2E CI workflow + +Phase 5 of the playbook: run the e2e suite in GitHub Actions. Follow the pattern in +[ci-workflow.md](../e2e-test-setup/reference/ci-workflow.md) +(worked example). +**Read [../e2e-test-setup/reference/e2e-playbook.md](../e2e-test-setup/reference/e2e-playbook.md) +§6–§7 first.** + +## Workflow shape +Trigger on `push` to the default branch, `pull_request`, and +`workflow_dispatch`. `permissions: contents: read`. Run on `ubuntu-22.04` with +`defaults.run.working-directory: e2e-tests`. + +Steps, in order: + +1. **(If you run any dependency for real from a private Guardian repo) mint a + GitHub App token** — default pattern: + ```yaml + - uses: actions/create-github-app-token@ # vX + id: private-repo-token + with: + client-id: ${{ vars.PRIVATE_REPO_APP_CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_REPO_APP_PRIVATE_KEY }} + repositories: / + ``` + This is the **recommended default** for checking out a private dependency + repo: create a GitHub App with read access to that repo, store its client id + as a repo **variable** and its private key as a repo **secret**. (Alternatives + — a PAT or a deploy key — are possible but less scoped; prefer the App token.) + +2. **Check out the app repo** (`actions/checkout`). + +3. **Check out each private dependency repo** into the path the stack expects + (e.g. `e2e-tests/target/`) using + `token: ${{ steps.private-repo-token.outputs.token }}`. A **public** Guardian + repo needs no token — check it out with plain `actions/checkout`. + +4. **Install the toolchain** — `actions/setup-node` with + `node-version-file: '.tool-versions'`, `cache: yarn`, + `cache-dependency-path: e2e-tests/yarn.lock`. + +5. **Install deps** — app deps (root) then e2e deps, both + `--frozen-lockfile`. + +6. **Install Playwright headless shell only** — + `yarn playwright install --with-deps chromium --only-shell` (the shell, not + full Chromium — faster). + +7. **Run** — `yarn test:ci` (spins up all infra and the app container, then runs + the suite headlessly; equivalent to `bddgen` + starting the stack + + `playwright test`). + +8. **Upload artifacts on failure** — `actions/upload-artifact` with the + `target/test-results` path, `if: failure()`, short retention. + +## Rules +- **Pin every action to a commit SHA** with a `# vX` comment (a review + requirement and a CodeQL finding). +- Docker must be available on the runner (it is on GitHub-hosted `ubuntu-*`); the + stack builds/starts containers via Testcontainers, no service containers + needed. +- Handle arch differences: dev is often arm64, CI amd64 — pull arch-appropriate + base images, don't pin single-arch digests. +- Report to CI natively: in `playwright.config.ts`, use the `github` reporter + when `process.env.CI`. + +## Verify +- The workflow runs on a PR and the suite passes. +- Failure runs upload traces/videos. +- No secret is printed; the App token is scoped to the private dependency repo(s) + only. + +## Guardian specifics + +See the live workflow at +[ci-workflow.md](../e2e-test-setup/reference/ci-workflow.md). + +### GitHub App token for guardian/workflow +The datastore (run for real) lives in the private `guardian/workflow` repo. A +dedicated GitHub App was created with **read** access to it. The workflow reads: +- `vars.WORKFLOW_APP_CLIENT_ID` (repo **variable**) — the App client id. +- `secrets.WORKFLOW_APP_PRIVATE_KEY` (repo **secret**) — the App private key. + +and mints a token with `actions/create-github-app-token`, scoped to +`repositories: guardian/workflow`, used to check it out into +`e2e-tests/target/workflow-backend` (where `stackContainers.ts` / `getBackendDir` +expect it). + +### Node version +Node is read from `.tool-versions`. The app needs Node ≥ `22.9.0` (a dependency +of the e2e tests is incompatible with `22.5.1`). + +### Pinned actions (keep SHAs current) +- `actions/create-github-app-token` +- `actions/checkout` +- `actions/setup-node` +- `actions/upload-artifact` + +All pinned to a commit SHA with a `# vX` comment (CodeQL / review requirement). + +### Runner +`ubuntu-22.04`, `working-directory: e2e-tests`. Docker is available on the +GitHub-hosted runner, so Testcontainers builds and runs the stack directly. +Install the headless shell only: `yarn playwright install --with-deps chromium +--only-shell`. diff --git a/.github/skills/e2e-fixtures-and-mocks/SKILL.md b/.github/skills/e2e-fixtures-and-mocks/SKILL.md new file mode 100644 index 000000000..bd5a61368 --- /dev/null +++ b/.github/skills/e2e-fixtures-and-mocks/SKILL.md @@ -0,0 +1,140 @@ +--- +name: e2e-fixtures-and-mocks +description: 'Mock upstream HTTP services and seed test data for the e2e local stack: run every mock from one shared WireMock image with per-mock bind-mounted fixtures, and seed Postgres (CSV), DynamoDB and S3 (LocalStack) from the host after the containers start. Use when adding or changing a mocked upstream service, writing WireMock stubs, or seeding database/S3/DynamoDB fixtures for e2e tests.' +argument-hint: '' +--- + +# E2E fixtures and mocks + +Phase 3 of the playbook: mock the app's upstream HTTP dependencies and seed the +test data the app reads. **Read +[../e2e-test-setup/reference/e2e-playbook.md](../e2e-test-setup/reference/e2e-playbook.md) +§4.2–§4.4 first** and use the captured [stack.md](../e2e-test-setup/reference/stack.md) +and [scaffold.md](../e2e-test-setup/reference/scaffold.md) references as worked +examples of the patterns below. + +> **Server-side, not browser-side** (guiding principle 1): the `dev` / `dev:local` +> environments must work with no browser setup. Resolve mocks server-side (Docker +> network aliases; nginx for TLS and cookie issuing), not via browser +> `--host-resolver-rules` or client-injected cookies — those are only for the +> headless test run. + +## Mocking upstreams — one shared WireMock image + +Do **not** build a Dockerfile per mock. Run every mock from the same +`wiremock/wiremock` image, differing only by the bind-mounted fixture root and +command flags. Follow the pattern in `startMockWiremock` + `MOCK_WIREMOCK_CONFIGS` in +[stack.md](../e2e-test-setup/reference/stack.md). + +To add a mock: +1. Create `fixtures//` containing WireMock `mappings/` (stub rules) and + `__files/` (response bodies). +2. Add an entry to the `MOCK_WIREMOCK_CONFIGS` table: + - `name` — log prefix. + - `fixtureDir` — the `fixtures/` folder. + - `aliases` — the real upstream hostname(s) to register as Docker **network + aliases** (so the app's server-side calls resolve to the mock). + - `ports` — expose `80`; add a fixed host port + `https: true` (port `8443`) + only when the browser calls it cross-origin. If the service isn't in the + reference port map (playbook §5), find the port it uses for **local + development** in that service's own repository (its dev run script / config) + and mock it on that port. + - `templating` — `true` to enable WireMock response templating; set `false` + when a body must be served verbatim (e.g. a JS library). +3. Kick off its start in the mocks `Promise.all` batch in `startLocalStack`. +4. If the browser reaches it over HTTPS, add a `--host-resolver-rules` mapping in + [playwright.config.ts](../e2e-test-setup/reference/playwright.md). + +Bind-mount the fixture dir read-only at the WireMock root and point WireMock at +it with `--root-dir` so nothing in the base image is shadowed. WireMock runs as +`root` to bind privileged port 80. Wait on `/__admin/health`. + +## Seeding data — from the host, after start + +Seed with the stock image's CLI after the container is ready; no custom image. + +- **SQL (Postgres):** follow the pattern in + [seedDatabase.ts](../e2e-test-setup/reference/seeding.md). Copy CSVs in, + `\copy table(cols) from ... (format csv, header true)`. **Seed parent tables + before FK children.** Run only **after** the owning service's migrations have + created the schema (its healthcheck triggers them). +- **DynamoDB (LocalStack):** follow the pattern in + [seedDynamodb.ts](../e2e-test-setup/reference/seeding.md). `awslocal + dynamodb create-table` then `batch-write-item --request-items file://...`. +- **S3 (LocalStack):** follow the pattern in + [seedS3.ts](../e2e-test-setup/reference/seeding.md). Create buckets in the + app's region, `awslocal s3 cp` the objects. Bucket/key names must match exactly + what the app requests. + +## Producing mock data + +Prefer real, minimal, synthetic data over guesses. Source it in this order: + +- **Pan-domain settings:** reuse the reference's mocked pan-domain fixture as-is + ([fixtures/pan-domain-settings/](../e2e-test-setup/reference/auth.md)); + the per-run signing keys are appended at seed time (see `seedS3.ts`), so nothing + else needs changing. +- **Permission cache:** read the **app's source** to find which permission(s) it + checks (e.g. `workflow_access`), then seed the cache granting them. Give the + **default user the most permissive rights** to start with; add restricted users + only when a scenario needs to assert a denial. +- **Other mocked upstreams:** build reasonable stub responses from two places — + the **app's source** (the request path/shape it sends and the fields it reads + back) and the **upstream service's own repository** (its response model / + example payloads). Keep each response minimal: only the fields the app consumes. +- **If you can't find a realistic payload**, don't invent a risky one — **prompt + the user** to supply an example response for that service. + +## Fixture folder layout +Follow the layout in [scaffold.md](../e2e-test-setup/reference/scaffold.md): `db/` (CSVs), +`dynamodb/`, `permissions/`, plus one folder per mocked service +(`mappings/` + `__files/`), and any auth-settings folders. + +## Guardrails +- **No real secrets or personal data** in fixtures — use synthetic emails/ids + (e.g. `user1@example.com`). This has been a code-review finding before. +- Keep fixtures as test data; they need not track CODE/PROD. + +## Verify +- Each mock answers `/__admin/health` and returns stubbed responses for the + routes the app calls. +- Seeded rows/objects are visible to the app (assert via a test that reads them). + +## Guardian specifics + +### The mocked upstreams (reference stack) +All run from the shared WireMock image via `MOCK_WIREMOCK_CONFIGS` in +[containers.ts](../e2e-test-setup/reference/stack.md): + +| Mock | Hostname alias | Browser-facing https | Templating | +|------|----------------|----------------------|------------| +| CAPI (preview) | `iam-preview.content.local.dev-guardianapis.com` | no | yes | +| Composer | `composer.local.dev-gutools.co.uk` | yes (9082) | yes | +| Presence | `presence.local.dev-gutools.co.uk` | yes (9071) | no (verbatim JS) | +| Telemetry | `user-telemetry.local.dev-gutools.co.uk` | yes (3133) | yes | +| Preferences | `preferences.local.dev-gutools.co.uk` | no | yes | +| TagManager | `tagmanager.local.dev-gutools.co.uk` | no | yes | + +Composer/Telemetry admin URLs are returned from `startLocalStack` so tests can +query the WireMock request journal (`/__admin`) to assert calls were made. + +### S3 objects (LocalStack), seeded by seedS3.ts +- `permissions-cache` bucket → `CODE/permissions.json` (grants/denies + `workflow_access`; must include the role emails from `panDomainCookie.ts`). +- `pan-domain-auth-settings` bucket → `local.dev-gutools.co.uk.settings` and + `.settings.public`, with the per-run RSA keys appended before upload. +- Bucket network aliases sit under `s3.localstack` so virtual-hosted-style + requests resolve (LocalStack needs `.s3.` in the Host header). + +### DynamoDB table, seeded by seedDynamodb.ts +- `editorial-support-CODE` (hash key `id`), populated from + `fixtures/dynamodb/editorial-support-CODE.json` via `batch-write-item`. + +### Postgres CSVs, seeded by seedDatabase.ts +- Order: `section`, `desk`, then `section_desk_mapping`, `section_to_tag` + (FK children), then `stub`. Column lists live in `DB_SEED_TABLES`. + +### Auth-redirect config +`fixtures/auth-redirect/auth-redirect.conf.template` is an nginx template +consumed by `startAuthRedirect` (envsubst at container start) for the optional +host-browser auth cookie endpoint. diff --git a/.github/skills/e2e-stack-setup/SKILL.md b/.github/skills/e2e-stack-setup/SKILL.md new file mode 100644 index 000000000..8e70ef4c6 --- /dev/null +++ b/.github/skills/e2e-stack-setup/SKILL.md @@ -0,0 +1,178 @@ +--- +name: e2e-stack-setup +description: 'Scaffold the e2e-tests folder and build a self-contained, fast-starting local stack with Testcontainers for end-to-end tests: infrastructure (Postgres, LocalStack S3/DynamoDB), the app-under-test (and any dependency you opt to run for real) as toolchain-only bind-mounted containers, mocked dependencies via a shared WireMock image, one Docker network, and Playwright global setup. Build-speed optimisations are baked in here (shared mock image, single infra container, minimal build context, overlapped startup) rather than in a separate pass. Use when setting up the Testcontainers stack, adding local containers for e2e tests, wiring Playwright global-setup to a container stack, or when the e2e stack / Playwright CI is slow to build.' +argument-hint: '' +--- + +# E2E stack setup + +Scaffold `e2e-tests/` and build the Testcontainers-based local stack. This is +Phases 1–2 of the playbook. **Read +[../e2e-test-setup/reference/e2e-playbook.md](../e2e-test-setup/reference/e2e-playbook.md) +§2–§5 first** and use the captured [stack.md](../e2e-test-setup/reference/stack.md) +reference as a worked example of the patterns below — adapt them to the target +project rather than copying verbatim. + +## Prerequisites +- Phase 0 discovery complete: dependencies enumerated, auth model known, and — + for each Guardian-repo dependency (private or public) — a user-confirmed + decision to run it for real or mock it; Docker present. + +## Scaffold (Phase 1) + +Create an `e2e-tests/` folder independent of the app's build, following the +pattern in the reference +[scaffold.md](../e2e-test-setup/reference/scaffold.md): + +- `package.json` with dev deps: `@playwright/test`, `playwright`, + `playwright-bdd`, `testcontainers`, `tsx`, plus any AWS SDK clients needed for + seeding. Provide the **standard scripts** (playbook §9): `test` (runs against an + already-running local stack; aborts if none), `test:ci` (spins up all infra + + the app as a container), `test:ui` (Playwright UI, watch), `dev:local` (app run + natively against local infra, watch) and `dev` (app natively against remote + infra, watch). +- `playwright.config.ts` — follow the pattern in + [playwright.md](../e2e-test-setup/reference/playwright.md): + `defineBddConfig({ features, steps })`, `globalSetup`, `fullyParallel`, capped + `workers`, `retries: 1`, per-test `timeout` + `expect.timeout`, trace/video/ + screenshot on first-retry/failure, and `launchOptions.args` with + `--host-resolver-rules` for any browser-facing HTTPS mocks + (`ignoreHTTPSErrors: true`). +- `global-setup.ts` — follow the pattern in + [playwright.md](../e2e-test-setup/reference/playwright.md): start (or + reuse) the stack, write connection details to a gitignored file, tear down the + owned stack in the returned teardown fn. +- Pin the toolchain in `.tool-versions` / `mise.toml` (Node ≥ 22.9.0; enable + `corepack` for yarn). + +## Build the stack (Phase 2) + +Follow the patterns captured in [stack.md](../e2e-test-setup/reference/stack.md) +(orchestration + per-container recipes). + +1. **One network per run**: `const network = await new Network().start();` + Wrap everything in try/catch that stops every started container + the network + on failure (see `startLocalStack`). + +2. **Infrastructure first** (everything depends on it): start datastores in + parallel and `await` before the rest. + - Databases: stock images (e.g. `postgres:17-alpine`) with a network alias. + - Object store / NoSQL: one `localstack/localstack:4` container with + `SERVICES` listing only what's used. Register S3 aliases under an `s3.` + domain (see playbook §4.3). Wait on `Wait.forLogMessage(/Ready\./)`. + +3. **Mocked upstreams**: use the shared-WireMock pattern (see the + `e2e-fixtures-and-mocks` skill). Define a `MOCK_WIREMOCK_CONFIGS` table and a + single `startMockWiremock(config, ...)`. Register each real upstream hostname + as a **network alias** so the app's server-side calls resolve to the mock. + +4. **App under test** — run **natively** in dev (`dev` / `dev:local`, watch mode); + for **`test:ci` / CI**, run it as a **toolchain-only image with bind-mounted + source**: + - Build context = a fresh temp dir containing only `.tool-versions` + the + Dockerfile (see `buildWorkflowImage` / `buildDatastoreImage`). Never copy + the repo into the image (guiding principle 2). + - Base image `debian:bookworm-slim` (glibc — see playbook gotchas); install + the toolchain with `mise`. + - Bind-mount the whole repo read-write; run from source (dev-mode + asset + watch) so edits reload without a rebuild. + - Point the app at the mocked infra via env vars (e.g. + `AWS_ENDPOINT_URL_S3`, `AWS_ENDPOINT_URL_DYNAMODB`). + - Health-check with `Wait.forHttp('/management/healthcheck', port)`. + - **Running a real dependency is opt-in, decided per dependency** (playbook + §4.4): do it only for a service in a Guardian repository (private or public) + whose real behaviour matters, and **confirm with the user** first — + otherwise mock it (see the `e2e-fixtures-and-mocks` skill). Each real service + is checked out from its repo and built the same way as the app. + +5. **Overlap**: start infra, then the app, any real services and mocks + concurrently with `Promise.all` (see the phased `await`s in `startLocalStack`). + +6. **Seed after the schema exists**: when a real service owns a database schema, + its migrations run on first request (its healthcheck triggers them); seed that + datastore only after (see the `e2e-fixtures-and-mocks` skill and + `seedDatabase`). + +7. **Return** a `LocalStack` object exposing `baseUrl`, the auth signing key, mock + admin URLs and every container handle for teardown. + +## Build fast (bake these in as you go) + +Don't add a separate optimisation pass — the steps above are already the fast +path. Keep these properties, which took the reference CI from ~15 min to ~6 min +(playbook §6): + +- **One shared WireMock image** for all mocks (step 3), never one image per mock + — the single biggest saving. +- **One LocalStack container** for S3 + DynamoDB (step 2), not separate services. +- **Toolchain-only images from a tiny build context** + **bind-mounted source** + (step 4): no repo `COPY`, so images rarely rebuild and BuildKit's layer cache + covers subsequent runs. +- **Overlap** infra → app + real services + mocks with `Promise.all` (step 5). +- **No prebuild step**: with the above, few images are built, so a separate + "prebuild images" step isn't worth it — leave it out. +- **CI installs the Playwright headless shell only** (`--only-shell`) — see the + `e2e-ci-workflow` skill. + +Measure with `DEBUG=testcontainers:build` (`yarn dev:debug`) and confirm cached +layers are reused on a second run. + +## Long-running stack & the test / test:ci split +`dev:local` boots the stack once (app run natively, dependencies as containers) +and writes connection info to a gitignored file; `test` reuses it via a +`sharedStack.ts` reader and **aborts if no local stack is running**. `test:ci` +instead builds and starts everything itself, the app included as a container. +Provide a `run-dev-local.ts` entrypoint for `dev:local`. + +## Verify +- `yarn dev:local` boots the full stack and reaches the app's healthcheck. +- `yarn test` runs green against a freshly built stack. +- Containers and network are all stopped on teardown (no leaks: `docker ps`). + +## Guardian specifics + +Guardian editorial-tools detail (Scala/Play apps behind pan-domain auth). + +### Running the datastore for real (a private-repo dependency) +The datastore is the reference example of running a dependency for real (playbook +§4.4): it lives in the separate private `guardian/workflow` repo. Locally it's +cloned into `e2e-tests/target/workflow-backend/` by a checkout script (see +[stack.md](../e2e-test-setup/reference/stack.md)), +resolved via `WORKFLOW_BACKEND_DIR` or the default target path +(`getBackendDir`). In CI it's checked out with a GitHub App token — see the +"Guardian specifics" section of the `e2e-ci-workflow` skill. + +### Toolchain via mise + `.tool-versions` +Both app and datastore images install `java`, `sbt`, `nodejs`, `aws-cli` via +`mise` from a copied `.tool-versions`, then `corepack enable` for `yarn`. Base +image is `debian:bookworm-slim` (Corretto/JDK need glibc, so **not** Alpine). See +[dockerfiles.md](../e2e-test-setup/reference/dockerfiles.md). + +### Running from source +- Frontend: `yarn build-dev` (webpack watch) alongside Play dev-mode `run` (see + `start-workflow-frontend`), repo bind-mounted read-write. +- Datastore: `sbt -Dconfig.file=datastore/conf/application.e2e.conf datastore/run 9095`, + the checkout bind-mounted read-write. + +### Pan-domain auth +- Generate a fresh RSA keypair per run + ([panDomainKeys.ts](../e2e-test-setup/reference/auth.md)). +- Append the keys to the pan-domain settings uploaded to S3 + ([seedS3.ts](../e2e-test-setup/reference/seeding.md)). +- Tests sign a cookie with `@guardian/pan-domain-node` + ([panDomainCookie.ts](../e2e-test-setup/reference/auth.md)); role emails + must match `fixtures/permissions/permissions.json`. + +### Network aliases / hostnames +Register the real per-stage upstream hostnames (e.g. +`composer.local.dev-gutools.co.uk`, the CAPI preview host) as network aliases on +the mock containers so the app's server-side calls resolve inside the Docker +network with no config override. Browser cross-origin HTTPS calls are mapped by +Chromium `--host-resolver-rules` to fixed host ports. + +### Host-browser access (optional dev flow) +`startAuthRedirect` runs stock `nginx:alpine` with a bind-mounted config +template; it sets the pan-domain cookie on `/cookie` and proxies everything else +to the frontend, so a host browser can hit +`https://workflow.local.dev-gutools.co.uk/cookie` (dev-nginx terminates TLS). +Enabled only when `exposeHostAuth` is set (via `yarn dev:local`). diff --git a/.github/skills/e2e-test-setup/SKILL.md b/.github/skills/e2e-test-setup/SKILL.md new file mode 100644 index 000000000..7d2afb271 --- /dev/null +++ b/.github/skills/e2e-test-setup/SKILL.md @@ -0,0 +1,62 @@ +--- +name: e2e-test-setup +description: 'Set up an end-to-end (e2e) test suite following the same patterns used to build a proven reference e2e setup: a self-contained Testcontainers stack (the app under test plus its dependencies, each mocked or run for real) driving Playwright/Cucumber-BDD tests, plus a CI workflow. Use when asked to set up e2e tests, add Playwright + Testcontainers, stand up a local stack with mocked services, or apply these e2e patterns to a project. Delegates to the e2e-stack-setup, e2e-fixtures-and-mocks and e2e-ci-workflow skills.' +argument-hint: '' +--- + +# E2E test setup (umbrella) + +Set up an e2e test suite by following the same patterns used to build the +reference e2e setup: Playwright + `playwright-bdd` running against a +self-contained Testcontainers stack (the app under test plus its dependencies, +each mocked or run for real), plus a GitHub Actions workflow to run it in CI. The +goal is to apply the *approach*, adapted to the project at hand — not to copy the +reference stack. + +**Read [reference/e2e-playbook.md](reference/e2e-playbook.md) first.** It is the +source of truth for the patterns, decisions, ports and gotchas, and links to the +captured reference docs under `reference/` as worked examples. This skill only +orchestrates; the detail lives in the playbook and the phase skills. + +## When to use +- "Set up end-to-end tests / Playwright + Testcontainers for this project." +- "Stand up a local stack with mocked upstream services for testing." +- "Use the same e2e patterns/approach as the reference setup here." + +## Procedure + +Work through the phases in order. Do not skip discovery — the stack shape is +driven entirely by the app's real dependencies. + +1. **Phase 0 — Discover** (do this before writing anything): + - Identify the app's runtime: framework, language, how it's built/run in dev. + - Enumerate every dependency the app talks to: other services, databases + (SQL / DynamoDB / etc.), object storage (S3), and each upstream HTTP API. + - Identify the auth model (how a request is authenticated). + - For each dependency version-controlled in a Guardian repository (private or + public), **confirm with the user** whether to run the real service (checked + out and built from source) or mock it with WireMock. Default to mocking + unless real behaviour is needed. + - Confirm Docker is available in the dev/CI environment. + +2. **Phase 1–2 — Scaffold + stack** → use the **`e2e-stack-setup`** skill. Bake + the build-speed optimisations (playbook §6) in here rather than as a separate + pass. +3. **Phase 3 — Fixtures + mocks** → use the **`e2e-fixtures-and-mocks`** skill. +4. **Phase 4 — Tests** → use the **`feature-file-from-templates`** and + **`feature-file-step-definitions`** skills. The goal here is only to **validate + the setup**, not to cover the whole app — **ask the user which part of the UI** + to extract a small set of features for, and author just those. +5. **Phase 5 — CI** → use the **`e2e-ci-workflow`** skill. + +## Guardrails +- Keep the production-app change footprint minimal (ideally one env-gated switch; + see playbook §4.7). Everything else lives under `e2e-tests/`. +- Never commit real secrets or personal data into fixtures. +- Preserve the key decisions in playbook §4 unless the target genuinely differs; + when it does, follow playbook §8 (adapting to a different stack). + +## Guardian-specific detail +Each phase skill ends with a **Guardian specifics** section covering pan-domain +auth, dev-nginx, the private `guardian/workflow` datastore and the GitHub App +token. Read it as part of that phase. diff --git a/.github/skills/e2e-test-setup/reference/auth.md b/.github/skills/e2e-test-setup/reference/auth.md new file mode 100644 index 000000000..0d23d89d9 --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/auth.md @@ -0,0 +1,59 @@ +# Reference: pan-domain auth (no OAuth) + +Captured essence of the auth helpers and the pan-domain settings fixture. The +goal: authenticate test/dev requests without the real OAuth flow, entirely +**server-side** so local dev needs no forced client cookie. + +## Per-run signing keys — `generatePanDomainKeys` + +Generate a fresh RSA keypair per run and expose PEM + base64 forms: + +```ts +const { privateKey, publicKey } = generateKeyPairSync("rsa", { + modulusLength: 4096, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, +}); +// pemToBase64 strips the -----BEGIN/END----- armour and whitespace +return { privateKeyPem, privateKeyBase64, publicKeyBase64 }; +``` + +## Settings fixture + +Reuse the mocked pan-domain settings fixture as-is (a static +`fixtures/pan-domain-settings/.settings` + `.settings.public`). At seed +time the per-run keys are **appended** to it before upload to S3: + +```ts +const settings = readFileSync(settingsFixture) + + `publicKey=${keys.publicKeyBase64}\n` + `privateKey=${keys.privateKeyBase64}\n`; +// upload settings (+ a .public variant with only the public key) to the +// pan-domain-auth-settings bucket (see seeding.md) +``` + +## Signing a cookie — `createPanDomainCookie` / `signIn` + +Tests sign a cookie with the private key using `@guardian/pan-domain-node`'s +`createCookie`. Roles map to emails; each email must have a matching entry in the +permissions fixture that grants/denies access. + +```ts +export const roles = { + default: "workflow.e2e.test@guardian.co.uk", // has access + NoWorkflowAccess: "no.workflow@guardian.co.uk", // denied +} as const; + +createCookie({ + firstName: "Playwright", lastName: "Tester", email: roles[role], + authenticatingSystem: "workflow-frontend", authenticatedIn: ["workflow-frontend"], + expires: Date.now() + expiresInMs, multifactor: true, +}, privateKeyPem); +``` + +- **Local dev**: the cookie is issued **server-side** by the nginx auth-redirect + container (`/cookie`), so browsing under `dev:local` needs no forced client + cookie. +- **Headless tests**: a `signIn` fixture sets the signed cookie on the browser + context before navigating. +- Role emails must match entries in the permissions fixture (see the + permissions-cache guidance in the fixtures skill). diff --git a/.github/skills/e2e-test-setup/reference/ci-workflow.md b/.github/skills/e2e-test-setup/reference/ci-workflow.md new file mode 100644 index 000000000..1483948ff --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/ci-workflow.md @@ -0,0 +1,54 @@ +# Reference: CI workflow + +Captured essence of the GitHub Actions workflow that runs the e2e suite. + +Triggers on push to the default branch, `pull_request` and `workflow_dispatch`; +`permissions: contents: read`; `ubuntu-22.04`; `working-directory: e2e-tests`. +Docker is available on GitHub-hosted runners, so Testcontainers builds/starts the +stack directly (no service containers needed). + +Steps, in order: + +```yaml +jobs: + playwright-tests: + runs-on: ubuntu-22.04 + defaults: { run: { working-directory: e2e-tests } } + steps: + # 1. (only if a dependency is run for real from a PRIVATE Guardian repo) + - uses: actions/create-github-app-token@ # vX + id: private-repo-token + with: + client-id: ${{ vars.PRIVATE_REPO_APP_CLIENT_ID }} + private-key: ${{ secrets.PRIVATE_REPO_APP_PRIVATE_KEY }} + repositories: / + # 2. app repo + - uses: actions/checkout@ # v4 + # 3. each run-for-real dependency repo into the path the stack expects + # (public repos need no token) + - uses: actions/checkout@ # v4 + with: + token: ${{ steps.private-repo-token.outputs.token }} + repository: / + path: e2e-tests/target/ + # 4. toolchain + - uses: actions/setup-node@ # v4 + with: + node-version-file: '.tool-versions' + cache: yarn + cache-dependency-path: e2e-tests/yarn.lock + # 5. deps (app root, then e2e), both --frozen-lockfile + # 6. Playwright headless shell only (not full Chromium) + - run: yarn playwright install --with-deps chromium --only-shell + # 7. run (spins up infra incl. the app container, headless) + - run: yarn test:ci + # 8. on failure, upload traces/videos + - if: failure() + uses: actions/upload-artifact@ # v4 + with: { name: playwright-traces, path: e2e-tests/target/test-results, retention-days: 7 } +``` + +Rules: **pin every action to a commit SHA** with a `# vX` comment; handle arch +differences (dev arm64 vs CI amd64) by pulling arch-appropriate base images (no +single-arch digests); the App token is scoped to the private dependency repo(s) +only and never printed. diff --git a/.github/skills/e2e-test-setup/reference/dockerfiles.md b/.github/skills/e2e-test-setup/reference/dockerfiles.md new file mode 100644 index 000000000..135ca8ecf --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/dockerfiles.md @@ -0,0 +1,49 @@ +# Reference: toolchain-only Dockerfiles + +Captured essence of the app and run-for-real-dependency images. These are used +**only for CI / `test:ci`** (in dev the app runs natively). The image bakes +**only the toolchain**; the code is bind-mounted at runtime, never copied. + +## Shared shape + +- Base `debian:bookworm-slim` (glibc — Corretto/JDK need it, so **not** Alpine; + `apt-get` is available). +- Install `mise`, then the toolchain from a copied `.tool-versions` + (`mise install ...`), then `corepack enable` for `yarn`. +- `# syntax=docker/dockerfile:1` directive at the top. +- The build context is a tiny temp dir holding just `.tool-versions` + the + Dockerfile, so the build never copies the repo. +- `CMD` runs the app from the **bind-mounted** source in watch/dev mode. + +## App image (essence) + +```dockerfile +# syntax=docker/dockerfile:1 +FROM debian:bookworm-slim +ENV DEBIAN_FRONTEND=noninteractive AWS_SDK_LOAD_CONFIG=1 +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl bash git openssl && rm -rf /var/lib/apt/lists/* +ENV MISE_DATA_DIR=/mise MISE_CONFIG_DIR=/mise MISE_CACHE_DIR=/mise/cache \ + MISE_INSTALL_PATH=/usr/local/bin/mise PATH="/mise/shims:$PATH" +RUN curl https://mise.run | sh +WORKDIR /app +COPY .tool-versions ./ +RUN mise trust -a && mise install java nodejs sbt aws-cli \ + && mise exec nodejs -- npm install -g corepack && mise exec nodejs -- corepack enable +EXPOSE 9090 +# run asset watch + dev-mode server from the mounted source +CMD ["bash", "-c", "mise exec -- yarn build-dev & exec bash /app/e2e-tests/images/start-app"] +``` + +## Run-for-real dependency image (essence) + +Same base + mise pattern, JVM toolchain only (`mise install java sbt`), then run +the service from source with the e2e config overlay, e.g.: + +```dockerfile +CMD ["mise","exec","java","sbt","--","sbt","-Dconfig.file=/conf/application.e2e.conf","/run 9095"] +``` + +The service's sources are bind-mounted read-write at runtime (sbt writes +`target/`); it resolves dependencies and compiles on first start, so give it a +long startup timeout. diff --git a/.github/skills/e2e-test-setup/reference/e2e-playbook.md b/.github/skills/e2e-test-setup/reference/e2e-playbook.md new file mode 100644 index 000000000..a10a8d46d --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/e2e-playbook.md @@ -0,0 +1,316 @@ +# E2E test setup playbook + +The single source of truth for the patterns and decisions behind a reference +end-to-end (e2e) test setup, so the same approach can be applied when building an +e2e suite for any project. Every skill under `.github/skills/e2e-*` and the +`e2e-test-setup` agent link back here. It captures the setup in three parts: + +- The self-contained Testcontainers stack + Playwright/BDD suite. +- The GitHub Actions workflow that runs the suite in CI. +- The build-speed optimisations that keep CI fast. + +This is a **reference for the patterns**, not a copy target. The essence of the +implementation is **captured in the docs beside this playbook** (`reference/*.md`), +linked throughout. When building an e2e suite elsewhere, apply the pattern and +adapt it to that project's stack — read the captured docs to see how it looks in +practice, don't clone them verbatim. + +--- + +## 1. What the setup gives you + +A browser-based e2e suite that runs entirely on one machine (dev container or CI +runner), with **no dependency on deployed infrastructure**: + +- Tests written as Cucumber/Gherkin `.feature` files, run with Playwright via + [`playwright-bdd`](https://github.com/vitalets/playwright-bdd). +- A **self-contained local stack** built with + [Testcontainers](https://testcontainers.com/): the app under test plus its + dependencies, each either mocked or (opt-in) run for real from source. +- The stack builds, seeds, runs and tears itself down from Playwright's + `globalSetup`. + +The design goal: *validate every change — including AI-authored ones — against a +realistic stack before it ships.* + +**Guiding principles** (carry these into every setup): +1. Local development against the `dev` / `dev:local` environments must need **no + special browser setup** — no forced cookies, no Playwright/browser mocks. + Anything a test would otherwise inject client-side must instead be provided + **server-side** (e.g. an nginx container that issues the auth cookie; mocks + resolved via the Docker network), never in the browser or the test runner. +2. The code is always available at the **root of the dev container**. If a Docker + image runs the app (CI / `test:ci`), it **bind-mounts the code externally** + rather than copying it in. +3. **Run the app natively in dev, containerised only in CI.** In the dev container + you run the app the normal way (`sbt run` / `yarn`, watch mode); only CI and + `test:ci` wrap it in an image. See §9–§10. + +--- + +## 2. Architecture at a glance + +``` +Playwright (globalSetup) + └─ startLocalStack() ── Testcontainers (one Docker network per run) + app-under-test (run from source) + ├─ real service(s) run from source ← opt-in per dependency + ├─ WireMock ×N — mocked dependencies (one shared image) + ├─ LocalStack (S3 + DynamoDB in ONE container) + ├─ Postgres (only if a real service needs it) + └─ nginx auth-redirect (optional, host-browser dev only) +``` + +**Real vs mocked dependencies (decide per dependency, confirm with the user).** +Every dependency the app talks to is either *mocked* with WireMock (the default) +or *run for real* from source. Running one for real is opt-in and makes sense +when it is a service version-controlled in a Guardian repository (private or +public) whose real behaviour you want to exercise (e.g. a datastore that owns its +schema and business logic). It costs a repo checkout and a source build, so +default to mocking and **confirm with the user** which Guardian-owned +dependencies warrant the real service. See §4.4. + +Captured reference docs (essence of the implementation, in `reference/`): + +- Stack orchestration, container recipes & network routing: [stack.md](stack.md). +- Seeding Postgres / DynamoDB / S3: [seeding.md](seeding.md). +- Pan-domain auth: [auth.md](auth.md). +- Toolchain-only Dockerfiles: [dockerfiles.md](dockerfiles.md). +- Playwright config & global setup: [playwright.md](playwright.md). +- Folder layout & package scripts: [scaffold.md](scaffold.md). +- CI workflow: [ci-workflow.md](ci-workflow.md). + +--- + +## 3. The phased approach + +Follow these phases in order when building an e2e suite. Each maps to a skill. +The reference setup went through these same phases. + +| Phase | Goal | Skill | +|-------|------|-------| +| 0 | Discover the app's runtime dependencies (datastores, upstream APIs, auth) and decide, per private-repo dependency, whether to run it for real or mock it (confirm with the user) | (agent-led discovery) | +| 1 | Scaffold `e2e-tests/` (package.json, playwright.config, global-setup) | `e2e-stack-setup` | +| 2 | Build the Testcontainers stack: infra → app → real services + mocks (fast by design — see §6) | `e2e-stack-setup` | +| 3 | Seed fixtures and configure mocks | `e2e-fixtures-and-mocks` | +| 4 | Author a **small** set of `.feature` files + step definitions to validate the setup (ask the user which part of the UI to cover) | `feature-file-from-templates`, `feature-file-step-definitions` | +| 5 | Add the CI workflow | `e2e-ci-workflow` | + +--- + +## 4. Key architectural decisions (the "why") + +These are the non-obvious choices that make the setup work. Carry these patterns +across when applying the approach elsewhere. + +### 4.1 Toolchain-only images + bind-mounted source (CI) / native run (dev) +In local dev you run the app the normal way inside the dev container (`sbt run` / +`yarn`, watch mode) — no container. **CI and `test:ci`** run the app (and any +dependency run for real) as a container whose image bakes **only the toolchain** +(via `mise` reading `.tool-versions`); the code is **bind-mounted, never copied** +(guiding principle 2). See [dockerfiles.md](dockerfiles.md). +Consequences: +- Build context is a tiny temp dir holding just `.tool-versions` + the + Dockerfile (`buildWorkflowImage` / `buildDatastoreImage`; see [stack.md](stack.md)). + No repo copy. +- Source edits reload without an image rebuild — fast local iteration. +- Bind mounts are read-write because `sbt`/webpack write into `target/`. + +### 4.2 One WireMock image for all mocks +Every mocked upstream runs from the **same** `wiremock/wiremock` image; only the +bind-mounted fixture root (`mappings/` + `__files/`) and command flags differ. +Driven by the `MOCK_WIREMOCK_CONFIGS` table + `startMockWiremock`. Adding a mock += a new fixture folder + a table entry, no new Dockerfile. (This is the biggest +win here — see §6.) + +### 4.3 One LocalStack container for S3 + DynamoDB +`startAws` runs a single `localstack/localstack:4` with `SERVICES: "s3,dynamodb"`. +Seed data is loaded from the host after start via `awslocal` (see +[seeding.md](seeding.md)) — no custom image. +- **S3 gotcha:** LocalStack only extracts the bucket from the Host header when it + contains `.s3.`, so S3 endpoint/bucket network aliases must sit under an `s3.` + domain (e.g. `permissions-cache.s3.localstack`) for virtual-hosted-style + requests to resolve. + +### 4.4 Running a dependency for real (opt-in, confirm per dependency) +Instead of mocking, a dependency can be run as the **actual service** when it is +a service version-controlled in a Guardian repository (private or public) whose +real behaviour matters. Check the repo out and build it as a toolchain-only, +bind-mounted container (§4.1), the same way as the app. Default to mocking with +WireMock and **confirm with the user** which Guardian-owned dependencies warrant +the real service — this is a per-dependency choice, not a fixed part of the +stack. (A private repo needs a checkout token in CI, see §5's skill; a public one +does not.) + +In the reference setup the datastore is run this way: its backing Postgres starts +empty, the datastore's own migrations create the schema on first request (the +healthcheck triggers it), *then* CSV fixtures are loaded (see `seedDatabase` +ordering — parent tables before FK children). + +### 4.5 Dual network routing (host browser vs Playwright Chromium) +The stack is reachable two ways (see the routing diagram in [stack.md](stack.md)): +1. **Host browser → nginx (TLS) → forwarded container ports.** Real + `*.local.dev-gutools.co.uk` hostnames over HTTPS; local dev-nginx terminates + TLS. Used for manual debugging via `yarn dev:local`. +2. **Playwright Chromium → services directly.** Chromium loads the app over + plain HTTP on a forwarded port and reaches cross-origin HTTPS APIs via + `--host-resolver-rules` mapping each hostname to the mock's fixed host port + (`ignoreHTTPSErrors: true` accepts the self-signed certs). See `launchOptions` + in [playwright.md](playwright.md). + +Inside the Docker network, the frontend's **server-side** calls reach mocks by +registering the real upstream hostnames as **network aliases** on the mock +containers — no URL/config override needed. + +Per guiding principle 1, the `dev` / `dev:local` environments resolve everything +server-side (Docker network aliases; nginx for TLS and cookies) and need no +browser setup; `--host-resolver-rules` is used **only by the headless Playwright +test run**, not for local dev. + +### 4.6 Pan-domain auth without OAuth +A fresh RSA keypair is generated per run; the public/private keys are appended to +the pan-domain settings uploaded to S3, and tests sign a cookie with the private +key via the `signIn` fixture (see [auth.md](auth.md)). Roles map to emails that +must match entries in the permissions fixture (`permissions.json`). + +For local dev the cookie is issued **server-side** by the nginx auth-redirect +container (§11), so browsing the app under `dev:local` needs no forced client +cookie; the `signIn` fixture signs one only for the headless test runs. + +### 4.7 Minimal app-code change +The **only** production change was making the server use `http` +instead of `https` for internal API calls when an e2e env var is set. Keep the +app footprint this small; everything else lives under `e2e-tests/`. + +### 4.8 Long-running stack reused across test runs +`dev:local` boots the stack once (app run natively, dependencies as containers) +and writes its connection details to a gitignored file; `test` reuses it (via +`sharedStack.ts` / `readSharedStackInfo`, `ACTIVE_STACK_FILE`) and **aborts if no +local stack is running**. `test:ci` instead builds and starts everything itself, +the app included as a container. See §9 for the full command set. + +--- + +## 5. Port map (reference stack) + +Fixed host ports are only used where a stable port is required (host-browser +access / host-resolver-rules); everything else uses Testcontainers' random +mapped ports. + +| Service | Container port | Fixed host port | Why fixed | +|---------|----------------|-----------------|-----------| +| app-under-test | 9090 | 9091 | host browser | +| auth-redirect (nginx) | 80 | 9090 | bookmarkable `/cookie` | +| Composer mock | 80 / 8443 | 9081 / 9082 | browser cross-origin https | +| Presence mock | 80 / 8443 | 9070 / 9071 | browser loads JS over https | +| Telemetry mock | 80 / 8443 | 3132 / 3133 | browser cross-origin https | +| CAPI / Preferences / TagManager mocks | 80 | random | server-side only | +| LocalStack (S3 + DynamoDB) | 4566 | random | seeded from host | +| Postgres | 5432 | random | seeded from host | +| Playwright UI | — | 9099 | `yarn test:ui` | +| Playwright report | — | 9098 | `yarn test:report` | + +> **Mocking a service not listed here?** Find the port it uses for **local +> development** in that service's own repository (its dev run script / config) and +> mock it on that port. + +--- + +## 6. Build-speed optimisations + +Apply these **while building the stack** (Phase 2, `e2e-stack-setup`) rather than +as a separate pass — together they took CI from ~15 min to ~6 min. + +1. **Start mocks from the WireMock image directly** (bind-mount fixtures) instead + of building one image per mock. +2. **Consolidate S3 + DynamoDB** into a single LocalStack container. +3. **Minimise build context**: build toolchain-only images from a temp dir with + just `.tool-versions` + the Dockerfile. +4. **Bind-mount source** instead of `COPY`ing it into the image. +5. **Drop the prebuild step** — with the above, few images are built and cached + layers cover them. +6. **Overlap work**: build/start infra first, then the app, any real services and + mocks concurrently (`Promise.all` phases in `startLocalStack`). +7. **CI: install only the Playwright headless shell** (`--only-shell`), not full + Chromium. + +--- + +## 7. Gotchas checklist + +| Symptom | Cause / fix | +|---------|-------------| +| JDK fails to start in container | Alpine uses musl libc; Corretto/JDK need glibc → use `debian:bookworm-slim`. | +| `yarn` missing / engine mismatch | Pin Node in `.tool-versions`; enable via `corepack enable`. Node `22.5.1` is too old for some deps — use ≥ `22.9.0`. | +| Playwright deps missing in CI | `playwright install --with-deps chromium --only-shell`. | +| S3 requests hit wrong/no bucket | Bucket not under an `s3.` alias domain (see §4.3). | +| FK violation seeding DB | Seed parent tables before children (see `seedDatabase` order). | +| Cookie email has no permissions | Role email in `panDomainCookie.ts` has no matching entry in `permissions.json`. | +| Mock returns literal `{{...}}` | Response templating enabled where a verbatim body is needed — set `templating: false` (e.g. presence JS). | +| Containers rebuild every run | Expected on first run; BuildKit layer cache covers subsequent runs. Use `yarn dev:local` for a shared stack. | +| Arch mismatch (arm64 dev vs amd64 CI) | Pull arch-appropriate base images; avoid pinning a single-arch digest. | + +--- + +## 8. Adapting to a non-Guardian / different stack + +The **shape** is portable; the **specifics** are not. When the target project +differs: + +- **Auth**: pan-domain is Guardian-specific. Replace §4.6 with the target's auth + (JWT/session cookie/OIDC mock) but keep the "sign a token in a fixture, seed + the verification key" pattern. +- **Upstreams**: enumerate the app's real HTTP dependencies (Phase 0) and add one + WireMock fixture folder + `MOCK_WIREMOCK_CONFIGS` entry each. +- **Datastores**: keep only the LocalStack services / databases the app or a real + service actually uses. +- **Real vs mocked dependencies**: decide per dependency whether to run the real + service (private-repo checkout, built from source) or mock it with WireMock, and + confirm with the user. If you run nothing for real, there is no private checkout + and no GitHub App token in CI. + +Guardian-specific detail for each phase lives in the **Guardian specifics** +section of each phase skill. + +--- + +## 9. Commands (standard set) + +| Command | What it does | +|---------|--------------| +| `test` | Run the suite headlessly to completion; non-zero exit on failure. Runs against an already-running local stack and **aborts if local infra isn't available**. | +| `test:ci` | Run the suite headlessly; spins up all infra **including the app under test in a container**. Used in CI. | +| `test:ui` | Open the Playwright UI (headed) to run tests on demand; spins up infra if not already running, in watch mode. | +| `dev:local` | Spin up the app (run **natively**, watch mode) and its dependencies, pointed at **local** infra (LocalStack, local permissions, mocks). | +| `dev` | Spin up the app (**natively**, watch mode) pointed at **remote** infra (e.g. AWS). | + +`.feature` changes need a `bddgen` watch to regenerate tests; `.ts` step/test +changes are picked up automatically by the Playwright UI. + +## 10. Modes & when to use them + +| | Local dev against tests | Local agent dev against tests | Local dev running app | CI tests | +|---|---|---|---|---| +| Recompile on change | Y | Y | Y | N | +| Backing infra | Local | Local | Remote | Local | +| Run tests | Playwright UI | CLI | N/A | CLI | + +- **Local dev against tests**: `dev:local` + `test:ui`; iterate with selector + suggestions and browse the app at its real local hostname (dev-nginx on the + host). +- **Local agent dev against tests**: an agent runs `test` (CLI). It should start + `dev:local` if no local stack is running, and **abort if `dev` (remote infra) + is running** so it never tests against remote infrastructure. +- **Local dev running app**: `dev` against remote infra; no tests. +- **CI**: `test:ci` builds and runs everything, app included, headless. + +## 11. Supporting client-side needs server-side + +Some behaviours a browser normally needs (e.g. an auth cookie) must — per guiding +principle 1 — be provided **server-side** so local dev needs no browser setup. The +reference does this with an **nginx container that issues the cookie** (scoped to +the subdomain) on a `/cookie` endpoint and redirects to the app, exposed to the +host via the host nginx mapping. Alternatives: a small service that issues the +cookie on an endpoint; a dev-only endpoint in login.gutools or the panda library. +Watch for an old cookie of the same name scoped to the **parent host** — it takes +precedence over the subdomain-scoped one. diff --git a/.github/skills/e2e-test-setup/reference/playwright.md b/.github/skills/e2e-test-setup/reference/playwright.md new file mode 100644 index 000000000..4fed0a484 --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/playwright.md @@ -0,0 +1,64 @@ +# Reference: Playwright config & global setup + +Captured essence of `playwright.config.ts` and `global-setup.ts`. + +## Playwright config + +```ts +const testDir = defineBddConfig({ features: "features/**/*.feature", steps: "steps/**/*.ts" }); + +export default defineConfig({ + testDir, + globalSetup: "./global-setup.ts", + outputDir: "target/test-results", + fullyParallel: true, + workers: 4, // cap concurrency: all workers share one stack + retries: 1, // absorb occasional load-induced flakes + timeout: 60_000, // per-test + expect: { timeout: 10_000 }, // per-assertion + use: { + trace: "on-first-retry", + video: "on-first-retry", + screenshot: "only-on-failure", + ignoreHTTPSErrors: true, // accept the mocks' self-signed certs + launchOptions: { + args: [ + // map each browser-facing HTTPS mock hostname to its fixed host port + "--host-resolver-rules=MAP composer.local.dev-gutools.co.uk 127.0.0.1:9082," + + "MAP presence.local.dev-gutools.co.uk 127.0.0.1:9071," + + "MAP user-telemetry.local.dev-gutools.co.uk 127.0.0.1:3133", + ], + }, + }, + reporter: process.env.CI ? [["github"]] + : [["list", { printFailuresInline: true }], + ["html", { outputFolder: "target/playwright-report", open: "never" }]], + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); +``` + +Key choices: BDD via `playwright-bdd` (`defineBddConfig` + `bddgen`); one shared +stack so workers are capped; `--host-resolver-rules` + `ignoreHTTPSErrors` route +the browser to the mocks (headless test runs only); `github` reporter in CI. + +## Global setup + +Start (or reuse) the stack once, write its connection details to a gitignored +file the per-test fixtures read, and tear the owned stack down afterwards. A stack +started separately by `dev:local` is left running (not owned). + +```ts +async function globalSetup() { + const shared = readSharedStackInfo(e2eRoot); // from dev:local, if any + const connection = shared ?? await startAndAdapt(); // else start our own + writeFileSync(ACTIVE_STACK_FILE, JSON.stringify(connection)); // baseUrl, signing key, mock URLs + return async () => { // teardown + rmSync(ACTIVE_STACK_FILE, { force: true }); + if (ownedStack) await stopLocalStack(ownedStack); + }; +} +``` + +The connection file carries `baseUrl`, the pan-domain signing key and the mock +admin URLs; the BDD fixtures (`steps/fixtures.ts`) read it to point every test at +the stack and provide `signIn` and mock helpers. diff --git a/.github/skills/e2e-test-setup/reference/scaffold.md b/.github/skills/e2e-test-setup/reference/scaffold.md new file mode 100644 index 000000000..54f7e82a9 --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/scaffold.md @@ -0,0 +1,58 @@ +# Reference: scaffold (folder layout & package scripts) + +Captured essence of the `e2e-tests/` structure and `package.json`. + +## Folder layout + +``` +e2e-tests/ +├── features/ # Gherkin .feature files (one per behaviour area) +├── steps/ # step definitions + Playwright fixtures wiring features to code +│ ├── fixtures.ts # shared fixtures (stack connection, signIn, mocks, per-scenario world) +│ └── shared/ # network-level mocks +├── setup/ # stack lifecycle + auth helpers (not test steps) +│ ├── stackContainers.ts # startLocalStack / stopLocalStack (see stack.md) +│ ├── stack/ # per-container recipes + seeding (containers.ts, seed*.ts) +│ ├── sharedStack.ts # reuse a long-running stack across runs +│ ├── panDomain*.ts # sign cookies / generate keys (see auth.md) +│ └── run-dev-local.ts # boots a shared long-running stack for dev:local +├── images/ # Dockerfiles + entrypoint scripts (see dockerfiles.md) +├── fixtures// # test data & config, one folder per domain/service +│ ├── db/ dynamodb/ permissions/ pan-domain-settings/ auth-redirect/ +│ └── / # WireMock mappings/ + __files/ per mocked upstream +├── global-setup.ts # start/reuse stack, write connection file (see playwright.md) +├── playwright.config.ts +├── mise.toml / .tool-versions # pinned toolchain (Node ≥ 22.9.0) +└── package.json +``` + +`e2e-tests/` holds everything unique to the e2e tests; the app's own code stays +at the repo root. + +## package.json (essence) + +Dev deps: `@playwright/test`, `playwright`, `playwright-bdd`, `testcontainers`, +`tsx`, plus any AWS SDK clients used for seeding, and the pan-domain signing lib. + +Scripts — the **standard set** (see playbook §9): + +```jsonc +{ + "scripts": { + // run against an already-running local stack; aborts if none + "test": "bddgen && playwright test", + // spin up all infra incl. the app as a container, then run (CI) + "test:ci": "bddgen && && playwright test", + // Playwright UI (headed), watch, spins up infra if needed + "test:ui": "bddgen && playwright test --ui-port=", + // app run natively + deps, pointed at LOCAL infra, watch + "dev:local": "tsx setup/run-dev-local.ts", + // app run natively, pointed at REMOTE infra, watch + "dev": "", + "test:report": "playwright show-report target/playwright-report --port " + } +} +``` + +`bddgen` regenerates tests from `.feature` files; a `bddgen --watch` alongside +`test:ui` picks up `.feature` edits (`.ts` step edits are picked up automatically). diff --git a/.github/skills/e2e-test-setup/reference/seeding.md b/.github/skills/e2e-test-setup/reference/seeding.md new file mode 100644 index 000000000..a21f6cfee --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/seeding.md @@ -0,0 +1,52 @@ +# Reference: seeding data from the host + +Captured essence of the seed scripts. Seed with the stock image's CLI **after** +the container is ready; no custom image. Names/keys must match exactly what the +app requests. + +## SQL (Postgres) — `seedDatabase` + +Copy CSV fixtures into the DB container and load with `\copy`, **parent tables +before FK children**. Run only after the owning service's migrations have created +the schema (its healthcheck triggers them). + +```ts +const DB_SEED_TABLES = [ + { table: "section", columns: "pk,section", file: "section.csv" }, + { table: "desk", columns: "pk,desk", file: "desk.csv" }, + { table: "section_desk_mapping", columns: "section_id,desk_id,pk", file: "section-desk.csv" }, + // ... FK children after their parents; a table with no FK onto seeded tables can go last +]; +// copyFilesToContainer(csvs) then, per table: +// psql -v ON_ERROR_STOP=1 -c +// "\copy () from '/tmp/' with (format csv, header true, null 'NULL', on_error ignore)" +``` + +## DynamoDB (LocalStack) — `seedDynamodb` + +```ts +// copyFilesToContainer(fixture json) then: +awslocal dynamodb create-table --table-name \ + --attribute-definitions AttributeName=id,AttributeType=S \ + --key-schema AttributeName=id,KeyType=HASH --billing-mode PAY_PER_REQUEST +awslocal dynamodb batch-write-item --request-items file:///tmp/.json +``` +`awslocal` targets the local gateway and inherits the container's region. + +## S3 (LocalStack) — `seedS3` + +Create buckets in the app's region and `awslocal s3 cp` the objects. The two +objects the app reads on startup are the **permissions cache** and the +**pan-domain settings**; the per-run signing keys are appended to the settings +before upload (see auth.md). + +```ts +awslocal s3api create-bucket --bucket permissions-cache --create-bucket-configuration LocationConstraint=eu-west-1 +awslocal s3api create-bucket --bucket pan-domain-auth-settings --create-bucket-configuration LocationConstraint=eu-west-1 +awslocal s3 cp /tmp/permissions.json s3://permissions-cache/CODE/permissions.json +awslocal s3 cp /tmp/pan-domain.settings s3://pan-domain-auth-settings/local.dev-gutools.co.uk.settings +awslocal s3 cp /tmp/pan-domain.settings.public s3://pan-domain-auth-settings/local.dev-gutools.co.uk.settings.public +``` + +Bucket/key names must match what the app requests exactly; bucket network aliases +sit under an `s3.` domain so virtual-hosted-style requests resolve. diff --git a/.github/skills/e2e-test-setup/reference/stack.md b/.github/skills/e2e-test-setup/reference/stack.md new file mode 100644 index 000000000..86a8c7474 --- /dev/null +++ b/.github/skills/e2e-test-setup/reference/stack.md @@ -0,0 +1,160 @@ +# Reference: local-stack orchestration & containers + +Captured essence of the reference stack code (orchestration, per-container +recipes, the image-build helper and network routing). Reproduce these patterns; +adapt names/ports/services to the target app. + +## Image build helper + +Build an image from a Dockerfile via Testcontainers and return a configurable +container. BuildKit is enabled so `# syntax` frontends apply; images are deleted +on exit. + +```ts +export function buildImage(context: string, dockerfileName: string, tag: string) { + return GenericContainer.fromDockerfile(context, dockerfileName) + .withBuildkit() + .build(tag, { deleteOnExit: true }); +} +``` + +A `createLogConsumer(prefix, streamLogs)` helper streams container logs to stdout +only when `streamLogs` is set (noisy in test runs, useful for `dev:local`). + +## Orchestration (`startLocalStack`) + +One `Network` per run; everything wrapped in try/catch that stops every started +container and the network on failure. Startup is **phased and overlapped**: + +1. **Infrastructure first** (everything depends on it): start the AWS mock + (LocalStack S3+DynamoDB) and the database in parallel; `await` both. +2. **Mocks**: kick off every WireMock mock (they share one image, no build) as a + batch of promises. +3. **App + real services**: build+start the app container and any run-for-real + dependency (e.g. datastore) concurrently — each start function builds its own + image then starts its container, so work interleaves. +4. `await Promise.all([...])` the app, real services, mocks (and the optional + auth-redirect) together. +5. **Seed** the database *after* the owning service's migrations have created the + schema (its healthcheck triggers them). +6. Return a `LocalStack` object with `baseUrl`, the auth signing key, mock admin + URLs and every container handle for teardown. + +```ts +const network = await new Network().start(); +const [awsContainer, dbContainer] = await Promise.all([ + startAws(network, e2eRoot, panDomainKeys, streamLogs), + startDb(network, streamLogs), +]); +const mocks = Object.values(MOCK_WIREMOCK_CONFIGS).map(c => + startMockWiremock(c, e2eRoot, network, streamLogs)); +const workflowStart = startWorkflow(repoRoot, workflowImageTag, network, streamLogs); +const datastoreStart = startDatastore(e2eRoot, datastoreImageTag, network, streamLogs); +const [workflow, datastore, ...startedMocks] = + await Promise.all([workflowStart, datastoreStart, ...mocks]); +await seedDatabase(dbContainer, e2eRoot); +``` + +`stopLocalStack(stack)` stops each container (reverse order) then the network. +An `exposeHostAuth` option additionally starts the nginx auth-redirect container +and signs a long-lived cookie for host-browser dev. + +## Ports & hostnames (constants) + +Fixed host ports are used only where a stable port is required (host browser / +Chromium host-resolver-rules); everything else uses random mapped ports. + +- App: container `9090`, host `9091`. auth-redirect nginx: container `80`, host `9090`. +- WireMock HTTP port `80`; the https-serving mocks also listen on `8443`. +- Browser-facing mocks get fixed host ports (e.g. Composer `9081/9082`, + Presence `9070/9071`, Telemetry `3132/3133`). +- Each mock registers the **real upstream hostname** as a Docker network alias so + the app's server-side calls resolve to it with no config override. + +## Per-container recipes + +### AWS mock (LocalStack: S3 + DynamoDB in one container) +```ts +new GenericContainer("localstack/localstack:4") + .withNetwork(network) + .withNetworkAliases("localstack", "s3.localstack", + "permissions-cache.s3.localstack", "pan-domain-auth-settings.s3.localstack") + .withEnvironment({ SERVICES: "s3,dynamodb", AWS_DEFAULT_REGION: "eu-west-1" }) + .withExposedPorts(4566) + .withWaitStrategy(Wait.forLogMessage(/Ready\./, 1)) + .start(); +// then seed S3 + DynamoDB from the host (see seeding.md) +``` +**S3 gotcha:** LocalStack extracts the bucket from the Host header only when it +contains `.s3.`, so S3 aliases must sit under an `s3.` domain. + +### Database (stock Postgres) +```ts +new GenericContainer("postgres:17-alpine") + .withNetwork(network).withNetworkAliases("workflow-db-e2e...") + .withEnvironment({ POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB }) + .withExposedPorts(5432) + .withWaitStrategy(Wait.forLogMessage(/database system is ready to accept connections/, 2)) + .start(); +``` + +### Mocks (one shared WireMock image) — see fixtures reference +A single `MOCK_WIREMOCK_CONFIGS` table drives `startMockWiremock(config, ...)`; +each entry differs only by fixture folder, network aliases, ports, `https` and +`templating` flags. WireMock runs as `root` (privileged port 80), bind-mounts +`fixtures/` read-only at `--root-dir`, waits on `/__admin/health`. + +### App under test (CI / `test:ci` only; native in dev) +Toolchain-only image, tiny build context (temp dir with just `.tool-versions` + +the Dockerfile), repo **bind-mounted read-write**, run from source: +```ts +workflowImage + .withNetwork(network).withNetworkAliases("workflow-frontend") + .withBindMounts([{ source: repoRoot, target: "/workflow-frontend", mode: "rw" }]) + .withEnvironment({ AWS_ENDPOINT_URL_S3, AWS_ENDPOINT_URL_DYNAMODB, + AWS_ACCESS_KEY_ID: "test", AWS_SECRET_ACCESS_KEY: "test", LOCAL: "true" }) + .withExposedPorts({ container: 9090, host: 9091 }) + .withWaitStrategy(Wait.forHttp("/management/healthcheck", 9090).forStatusCode(200)) + .start(); +``` + +### Run-for-real dependency (e.g. datastore) +Same toolchain-only + bind-mount pattern. Resolve the checkout dir from +`WORKFLOW_BACKEND_DIR` or a default `target/`; mount it read-write and +run from source. Long startup timeout (sbt resolves/compiles on first start). +The checkout itself is done by a small script that clones the private/public repo +into `target/` (in CI, via a GitHub App token for private repos). + +### auth-redirect (optional host-browser dev) +Stock `nginx:alpine` with a bind-mounted config template (`envsubst` at start); +sets the auth cookie on `/cookie` and proxies everything else to the app. Fixed +host port so it's bookmarkable. + +## Network routing + +```mermaid +graph LR + subgraph host["Host machine"] + HostBrowser["Browser"] + Nginx["nginx (TLS termination, :443)"] + end + subgraph devcontainer["Dev container"] + Chromium["Playwright Chromium --host-resolver-rules"] + subgraph stack["Docker stack (forwarded ports)"] + Frontend["app :9090/:9091"] + Mocks["mocks (http :80 / https :8443)"] + end + end + HostBrowser -->|https *.local hostnames| Nginx --> Frontend + Nginx --> Mocks + Chromium -->|http localhost:9091| Frontend + Chromium -->|https fixed ports via host-resolver-rules| Mocks + Frontend -->|network alias| Mocks +``` + +- **Host browser → nginx (TLS) → forwarded ports** (manual dev via `dev:local`). +- **Playwright Chromium → services directly**: app over plain HTTP on its + forwarded port; cross-origin HTTPS APIs mapped by `--host-resolver-rules` to the + mock's fixed host port (`ignoreHTTPSErrors: true`). +- **Server-side** app calls reach mocks via Docker **network aliases** (the real + hostnames), needing no override. diff --git a/.github/skills/feature-file-from-templates/SKILL.md b/.github/skills/feature-file-from-templates/SKILL.md new file mode 100644 index 000000000..4375b9c14 --- /dev/null +++ b/.github/skills/feature-file-from-templates/SKILL.md @@ -0,0 +1,91 @@ +--- +name: feature-file-from-templates +description: 'Generate a Cucumber/Gherkin .feature file that captures all behavioural scenarios of HTML templates and their related JavaScript/TypeScript, using the workflow-frontend evidence-annotated format. Use when asked to write a feature file, BDD scenarios, or Gherkin from a template/component; to capture UI behaviour as scenarios; or to verify that a feature file covers everything a template and its scripts do.' +argument-hint: '' +--- + +# Feature File From Templates + +Produce a `.feature` file (Cucumber/Gherkin) that captures every observable behaviour of one or more HTML templates and the JavaScript/TypeScript that drives them, in the repo's evidence-annotated format. + +## When to Use +- "Write a feature file / BDD scenarios / Gherkin for this template/component." +- "Capture all the scenarios of this HTML file, including the related JS." +- "Check that this feature file covers everything the template and scripts do." + +## Inputs +- One or more HTML template files (the primary source of UI behaviour). +- The related JS/TS: controllers, directives, services, models, event handlers, and any Scala/API endpoints the frontend calls. + +## Procedure + +1. **Read the template(s) fully.** Note every interactive element and binding: + - Controllers/directives (`ng-controller`, custom directives), event bindings (`ng-click`, `ng-mousedown`, `ng-keydown`, etc.). + - Conditional rendering / state classes (`ng-if`, `ng-show`, `ng-class`, `*--hidden` toggles). + - Repeated content (`ng-repeat`) and what each row renders (icons, labels, ids). + - Static labels and headings users can see. + +2. **Trace the related JS/TS.** For each binding found in the template, follow it into code: + - What each handler does (emitted events, service calls, state changes). + - What data populates the view (service methods, models, feature switches). + - Where emitted events are handled (e.g. modal openers) and what mode/result they produce. + - Any permission checks or backend endpoints involved. + - Use `grep_search` for event names and function names to find both emitters and listeners. + +3. **Derive scenarios.** Create one scenario per distinct, observable behaviour or branch: + - Default/initial state, opening/closing, toggling. + - Each user action and its outcome. + - Data-driven variations (e.g. feature switch on vs off, permission present vs absent). + - Rendering details worth asserting (icon + label present, disabled states). + - Keep steps behaviour-focused (what the user sees/does), not implementation-focused. + +4. **Write the feature file** following the [format](#format) below. After every scenario, add `# Evidence:` comment lines listing the workspace-relative paths of the files that justify that scenario. + +5. **Verify coverage.** Cross-check the finished file against the template and evidence files. Build a mental table of each template element/behaviour → scenario. Report any element not represented (call out purely decorative gaps explicitly rather than inventing trivial scenarios). + +## Format + +- Save as `e2e-tests/features/.feature`. +- `Feature:` title on the first line, followed by 1–3 indented plain-English lines describing intent. +- Always use these standard `Background:` steps; only the final "opened" line varies to name the relevant page: + ``` + Background: + Given the application stack is running + And I am signed in through pan-domain auth + And I have opened + ``` +- One `Scenario:` per behaviour, using `Given` / `When` / `Then` / `And` steps. +- Immediately after each scenario, list the sources as comments: + ``` + # Evidence: public/path/to/template.html + # Evidence: public/path/to/controller.js + ``` +- Only cite files you actually inspected; use real workspace-relative paths. + +## Example (excerpt) + +```gherkin +Feature: Create new content from the dashboard "Create new" dropdown + This lets an editor start a new piece of content, or import existing content, + by choosing a content type from the dashboard toolbar + + Background: + Given the application stack is running + And I am signed in through pan-domain auth + And I have opened the workflow dashboard + + Scenario: Choosing a content type opens the stub modal in create mode + Given the "Create new" dropdown is open + When I choose a content type from the list + Then a stub creation should be requested for that content type + And the stub modal should open in create mode + # Evidence: public/layouts/dashboard/dashboard-create.js + # Evidence: public/components/stub-modal/stub-modal.js +``` + +## Quality Checklist +- Every interactive element and state branch in the template maps to at least one scenario. +- Data-driven branches (feature switches, permissions) each have on/off scenarios. +- Steps read as user behaviour, not code. +- Each scenario carries accurate `# Evidence:` paths for every file relied upon. +- Uncovered template elements are reported; decorative-only gaps are noted, not padded with filler scenarios. diff --git a/.github/skills/feature-file-step-definitions/SKILL.md b/.github/skills/feature-file-step-definitions/SKILL.md new file mode 100644 index 000000000..324ba8d1b --- /dev/null +++ b/.github/skills/feature-file-step-definitions/SKILL.md @@ -0,0 +1,76 @@ +--- +name: feature-file-step-definitions +description: 'Wire Cucumber/Gherkin .feature files to runnable Playwright tests using playwright-bdd in workflow-frontend. Use when asked to implement step definitions, make feature files executable, connect Gherkin scenarios to Playwright, add missing steps, or run the BDD e2e suite. Complements the feature-file-from-templates skill.' +argument-hint: '' +--- + +# Feature File Step Definitions + +Turn `e2e-tests/features/**/*.feature` scenarios into runnable tests using [`playwright-bdd`](https://vitalets.github.io/playwright-bdd/), reusing the repo's existing e2e infrastructure. + +## When to Use +- "Implement the step definitions for this feature file." +- "Make `dashboard-create.feature` runnable / connect it to Playwright." +- "Add the missing steps so the BDD suite passes." + +## How the suite runs +The [test-e2e runner](../../../e2e-tests/scripts/test-e2e) does two things: +1. `npm exec bddgen` — playwright-bdd reads the `.feature` files and generated step definitions and emits Playwright test files. +2. `npm exec playwright test` — runs the generated tests. + +So every Gherkin step needs a matching step definition, and a Playwright config must declare the feature and step locations via `defineBddConfig`. + +## Procedure + +1. **Reuse the existing BDD config.** [playwright.config.ts](../../../playwright.config.ts) already calls `defineBddConfig({ features: 'e2e-tests/features/**/*.feature', steps: 'e2e-tests/steps/**/*.ts' })` and wires [e2e-tests/globalSetup.ts](../../../e2e-tests/globalSetup.ts), which starts (or reuses) the local stack. Add new step files under `e2e-tests/steps/` — do not create a second config. + +2. **Read the feature file** and list every unique `Given`/`When`/`Then`/`And` step. Steps shared across scenarios must resolve to one shared definition — keep wording identical in the feature file so a single step matches. + +3. **Reuse the existing fixtures.** [e2e-tests/steps/fixtures.ts](../../../e2e-tests/steps/fixtures.ts) already extends the playwright-bdd `test` with a `stack` fixture (base URL from global setup) and a `signIn(role)` fixture (pan-domain cookie via [panDomainCookie.ts](../../../e2e-tests/setup/panDomainCookie.ts)), and exports `Given`/`When`/`Then`/`expect`. Import these in new step files instead of re-implementing setup. The shared Background steps live in [e2e-tests/steps/background.steps.ts](../../../e2e-tests/steps/background.steps.ts). + +4. **Implement steps with the shared `Given`/`When`/`Then`.** Group by feature area under `e2e-tests/steps/`: + ```ts + import { When, Then, expect } from './fixtures'; + + When('I click the {string} button', async ({ page }, label: string) => { + await page.getByRole('button', { name: label }).click(); + }); + + Then('the content type list should be hidden', async ({ page }) => { + await expect(page.getByTestId('content-type-list')).toBeHidden(); + }); + ``` + Follow the [Playwright best practices](https://playwright.dev/docs/best-practices) when writing the step bodies (see the section below). + - Parameterise repeated steps with Cucumber expressions (`{string}`) instead of writing near-duplicate definitions. + +5. **Match the Background steps once.** The three standard Background steps (stack running / pan-domain auth / opened page) recur in every feature — implement them a single time in a shared steps file so all features reuse them. + +6. **Generate and run.** + ```bash + ./e2e-tests/scripts/test-e2e # full run + ./e2e-tests/scripts/test-e2e e2e-tests/features/foo.feature # single feature + ``` + `bddgen` fails loudly on any step with no matching definition — use that to find gaps. + +## Playwright best practices for step bodies +Apply these when translating a Gherkin step into Playwright code: + +- **Prefer user-facing locators.** Reach for `getByRole`, `getByLabel`, `getByText`, and `getByPlaceholder` first — they are resilient to DOM changes and assert accessibility. Only fall back to a stable test hook (`getByTestId`, or an evidence-backed `#id`/state class) when no user-facing attribute uniquely identifies the element. Avoid brittle CSS/XPath chains like `page.locator('button.buttonIcon.episode-actions-later')`. +- **Chain and filter instead of complex selectors.** Narrow with `page.getByRole('listitem').filter({ hasText: 'Product 2' }).getByRole('button', { name: 'Add to cart' })` rather than one long selector string. +- **Use web-first assertions and always `await` them.** Write `await expect(locator).toBeVisible()`, never `expect(await locator.isVisible()).toBe(true)`. Web-first assertions auto-wait and retry, so avoid manual waits/`isVisible()` checks. Never leave a floating promise — every Playwright call in a step must be awaited. +- **Keep steps isolated.** A step must not depend on state leaked from another scenario; rely on the shared fixtures/Background for setup rather than ordering between scenarios. Test isolation keeps failures reproducible. +- **Don't test third-party dependencies.** For calls to external services (e.g. CAPI, preferences), serve controlled data via the repo's WireMock/`e2e-tests/fixtures/responses/` mappings or `page.route(...)`, instead of hitting live systems. +- **Use soft assertions for grouped, non-blocking checks.** When a `Then` verifies several independent facts, `await expect.soft(...)` collects all failures in one run instead of stopping at the first. +- **Map assertions to real DOM hooks.** Trace every selector to the scenario's `# Evidence:` files (ids like `#testing-create-new`, `#testing-dashboard-create-dropdown-*`, state classes like `content-type-list--hidden`, or accessible labels that actually exist). + +When a test fails, prefer the Playwright [trace viewer](https://playwright.dev/docs/trace-viewer) (`--trace on`, then `npm exec playwright show-report`) over screenshots to inspect the timeline, DOM snapshots, and network requests. + +## Quality Checklist +- Every step in the feature file resolves to exactly one definition; no "undefined step" errors from `bddgen`. +- Setup steps reuse `e2e-tests/setup/` helpers via fixtures, not ad-hoc duplication. +- Locators prefer user-facing attributes (`getByRole`/`getByLabel`/`getByText`); test ids or evidence-backed ids/classes are used only where no user-facing hook fits. +- Assertions are web-first (`await expect(locator).…`) with no manual `isVisible()` checks and no un-awaited Playwright calls. +- External services are stubbed via fixtures/`page.route`, not called live. +- Selectors/assertions trace back to the scenario's `# Evidence:` files (ids, classes, labels that actually exist). +- Repeated steps are parameterised, not copy-pasted. +- `./e2e-tests/scripts/test-e2e ` runs green for the target feature. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..bfc4c82a9 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,61 @@ +name: e2e + +on: + push: + branches: ["main"] + pull_request: + workflow_dispatch: + +# Least privilege: only read the repo. The datastore checkout uses a scoped App token. +permissions: + contents: read + +jobs: + playwright-tests: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: e2e-tests + steps: + # Mint a token scoped to the private datastore repo (run for real from source). + - name: Get token for guardian/workflow + id: datastore-token + uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0 + with: + app-id: ${{ vars.WORKFLOW_APP_CLIENT_ID }} + private-key: ${{ secrets.WORKFLOW_APP_PRIVATE_KEY }} + repositories: workflow + + - name: Checkout workflow-frontend + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Checkout guardian/workflow datastore + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: guardian/workflow + token: ${{ steps.datastore-token.outputs.token }} + path: e2e-tests/target/workflow-backend + + - name: Install Node + uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0 + with: + node-version-file: e2e-tests/.tool-versions + cache: yarn + cache-dependency-path: e2e-tests/yarn.lock + + - name: Install e2e dependencies + run: yarn install --frozen-lockfile + + - name: Install Playwright headless shell + run: yarn playwright install --with-deps chromium --only-shell + + - name: Run e2e suite + run: yarn test:ci + + - name: Upload Playwright traces on failure + if: failure() + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: playwright-traces + path: e2e-tests/target/test-results + retention-days: 7 diff --git a/app/config/Config.scala b/app/config/Config.scala index 0248f6501..efe5a22cc 100644 --- a/app/config/Config.scala +++ b/app/config/Config.scala @@ -31,6 +31,12 @@ class Config(playConfig: Configuration) extends AwsInstanceTags with Logging { lazy val host: String = s"https://workflow.$domain" logger.info(s"Host is: $host") + // In the self-contained e2e stack the backend reaches internal upstreams over the + // Docker network on plain http (WireMock listens on port 80); browser-facing URLs + // stay https. Enabled only when WORKFLOW_E2E is set. + private lazy val isE2E: Boolean = sys.env.getOrElse("WORKFLOW_E2E", "false").toBoolean + private lazy val internalScheme: String = if (isE2E) "http" else "https" + lazy val composerUrl: String = s"https://composer.$domain" lazy val composerRestorerUrl: String = s"https://restorer.$domain/content" @@ -70,11 +76,11 @@ class Config(playConfig: Configuration) extends AwsInstanceTags with Logging { lazy val presenceClientLib: String = s"https://presence.$domain/client/1/lib.js" lazy val preferencesHost: String = s"preferences.$domain" - lazy val preferencesUrl: String = s"https://$preferencesHost/preferences" + lazy val preferencesUrl: String = s"$internalScheme://$preferencesHost/preferences" lazy val pinboardLoaderUrl: String = s"https://pinboard.$domain/pinboard.loader.js" - lazy val tagManagerUrl: String = s"https://tagmanager.$domain" + lazy val tagManagerUrl: String = s"$internalScheme://tagmanager.$domain" lazy val capiPreviewIamUrl: String = playConfig.get[String]("capi.preview.iamUrl") lazy val capiPreviewRole: String = playConfig.get[String]("capi.preview.role") diff --git a/e2e-tests/.gitignore b/e2e-tests/.gitignore new file mode 100644 index 000000000..c91c14bd9 --- /dev/null +++ b/e2e-tests/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +target/ +.features-gen/ +.active-stack.json +.shared-stack.json +test-results/ +playwright-report/ diff --git a/e2e-tests/.tool-versions b/e2e-tests/.tool-versions new file mode 100644 index 000000000..f8ab203c4 --- /dev/null +++ b/e2e-tests/.tool-versions @@ -0,0 +1 @@ +nodejs 22.11.0 diff --git a/e2e-tests/features/create-stub.feature b/e2e-tests/features/create-stub.feature new file mode 100644 index 000000000..8a7d937fc --- /dev/null +++ b/e2e-tests/features/create-stub.feature @@ -0,0 +1,56 @@ +Feature: Create content from the dashboard "Create new" dropdown + An editor starts a new piece of content by opening the "Create new" dropdown, + choosing a content type, and completing the stub modal that opens. + + Background: + Given the application stack is running + And I am signed in through pan-domain auth + And I have opened the workflow dashboard + + Scenario: The content type list is hidden until the dropdown is opened + Then the content type list should be hidden + When I open the "Create new" dropdown + Then the content type list should be visible + + # Evidence: public/layouts/dashboard/dashboard-create.html + # Evidence: public/layouts/dashboard/dashboard-create.js + + Scenario: Opening the dropdown reveals an import option + When I open the "Create new" dropdown + Then I should see the "Import Content" option + + # Evidence: public/layouts/dashboard/dashboard-create.html + + Scenario: Choosing a content type opens the stub modal in create mode + Given I have opened the "Create new" dropdown + When I choose the "Article" content type + Then the stub modal should open with title "Create Article" + And the working title field should be empty + + # Evidence: public/layouts/dashboard/dashboard-create.js + # Evidence: public/components/stub-modal/stub-modal.js + # Evidence: public/components/stub-modal/stub-modal.html + + Scenario: Cancelling the stub modal closes it without creating content + Given I have opened the stub modal for an "Article" + When I cancel the stub modal + Then the stub modal should be closed + + # Evidence: public/components/stub-modal/stub-modal.html + + Scenario: The section list is populated from the datastore + Given I have opened the stub modal for an "Article" + Then the "Technology" section should be available + + # Evidence: public/components/stub-modal/stub-modal.html + # Evidence: app/controllers/Application.scala + + Scenario: Entering a working title and section enables content creation + Given I have opened the stub modal for a "Gallery" + When I enter the working title "My e2e test story" + And I select the "Technology" section + Then the create-content button should be enabled + + # Evidence: public/components/stub-modal/stub-modal.html + # Evidence: public/components/stub-modal/stub-modal.js + # Evidence: public/lib/content-service.js diff --git a/e2e-tests/fixtures/app/application.e2e.conf b/e2e-tests/fixtures/app/application.e2e.conf new file mode 100644 index 000000000..6e4d42e7f --- /dev/null +++ b/e2e-tests/fixtures/app/application.e2e.conf @@ -0,0 +1,18 @@ +# Play config overlay for running workflow-frontend in the e2e stack. +# Loaded via -Dconfig.file=.../application.e2e.conf (see images/start-app). +include "application" + +# Datastore (run for real) — reached over the Docker network by alias, plain http. +api.url = "http://workflow-datastore:9093/api" + +# Shared-secret endpoints. +api.sharedsecret = "e2e-shared-secret" +api.sharedsecret = ${?WORKFLOW_SHARED_SECRET} + +# CAPI preview (mocked). Not exercised by the create-stub flow; dummy IAM values. +capi.key = "e2e-capi-key" +capi.preview.iamUrl = "http://capi-preview.local.dev-gutools.co.uk" +capi.preview.role = "arn:aws:iam::000000000000:role/e2e-capi-preview" + +# Disable Play evolutions / secret comes from APPLICATION_SECRET env. +play.http.secret.key = ${?APPLICATION_SECRET} diff --git a/e2e-tests/fixtures/capi/mappings/capi.json b/e2e-tests/fixtures/capi/mappings/capi.json new file mode 100644 index 000000000..02a4cb45f --- /dev/null +++ b/e2e-tests/fixtures/capi/mappings/capi.json @@ -0,0 +1,12 @@ +{ + "mappings": [ + { + "request": { "method": "ANY", "urlPathPattern": "/.*" }, + "response": { + "status": 200, + "headers": { "Content-Type": "application/json" }, + "jsonBody": { "response": { "status": "ok", "results": [] } } + } + } + ] +} diff --git a/e2e-tests/fixtures/composer/mappings/composer.json b/e2e-tests/fixtures/composer/mappings/composer.json new file mode 100644 index 000000000..edb2d2744 --- /dev/null +++ b/e2e-tests/fixtures/composer/mappings/composer.json @@ -0,0 +1,18 @@ +{ + "mappings": [ + { + "priority": 1, + "request": { "method": "POST", "urlPathPattern": "/api/content.*" }, + "response": { + "status": 200, + "headers": { "Content-Type": "application/json" }, + "jsonBody": { "data": { "id": "e2e-composer-id" } } + } + }, + { + "priority": 10, + "request": { "method": "ANY", "urlPathPattern": "/.*" }, + "response": { "status": 200, "headers": { "Content-Type": "application/json" }, "jsonBody": {} } + } + ] +} diff --git a/e2e-tests/fixtures/dynamodb/editorial-support-CODE.json b/e2e-tests/fixtures/dynamodb/editorial-support-CODE.json new file mode 100644 index 000000000..0d99147e9 --- /dev/null +++ b/e2e-tests/fixtures/dynamodb/editorial-support-CODE.json @@ -0,0 +1,15 @@ +{ + "editorial-support-CODE": [ + { + "PutRequest": { + "Item": { + "id": { "S": "Fronts-e2e-1" }, + "name": { "S": "E2E Support" }, + "team": { "S": "Fronts" }, + "active": { "BOOL": true }, + "description": { "S": "Seeded for e2e" } + } + } + } + ] +} diff --git a/e2e-tests/fixtures/pan-domain-settings/local.dev-gutools.co.uk.settings b/e2e-tests/fixtures/pan-domain-settings/local.dev-gutools.co.uk.settings new file mode 100644 index 000000000..5ae3e7eb5 --- /dev/null +++ b/e2e-tests/fixtures/pan-domain-settings/local.dev-gutools.co.uk.settings @@ -0,0 +1,5 @@ +cookieName=gutoolsAuth-assym +clientId=e2e-oauth-client-id +clientSecret=e2e-oauth-client-secret +discoveryDocumentUrl=http://capi-preview.local.dev-gutools.co.uk/.well-known/openid-configuration +organizationDomain=guardian.co.uk diff --git a/e2e-tests/fixtures/permissions/permissions.json b/e2e-tests/fixtures/permissions/permissions.json new file mode 100644 index 000000000..221c39959 --- /dev/null +++ b/e2e-tests/fixtures/permissions/permissions.json @@ -0,0 +1,20 @@ +[ + { + "permission": { "name": "workflow_access", "app": "workflow", "defaultValue": false }, + "overrides": [ + { "userId": "workflow.e2e.test@guardian.co.uk", "active": true, "isCasualNotOnShift": false } + ] + }, + { + "permission": { "name": "workflow_admin", "app": "workflow", "defaultValue": false }, + "overrides": [ + { "userId": "workflow.e2e.test@guardian.co.uk", "active": true, "isCasualNotOnShift": false } + ] + }, + { + "permission": { "name": "pinboard", "app": "pinboard", "defaultValue": false }, + "overrides": [ + { "userId": "workflow.e2e.test@guardian.co.uk", "active": true, "isCasualNotOnShift": false } + ] + } +] diff --git a/e2e-tests/fixtures/preferences/mappings/preferences.json b/e2e-tests/fixtures/preferences/mappings/preferences.json new file mode 100644 index 000000000..0bcf70dc1 --- /dev/null +++ b/e2e-tests/fixtures/preferences/mappings/preferences.json @@ -0,0 +1,12 @@ +{ + "mappings": [ + { + "request": { "method": "ANY", "urlPathPattern": "/preferences.*" }, + "response": { + "status": 200, + "headers": { "Content-Type": "application/json" }, + "jsonBody": {} + } + } + ] +} diff --git a/e2e-tests/fixtures/presence/mappings/presence-lib.json b/e2e-tests/fixtures/presence/mappings/presence-lib.json new file mode 100644 index 000000000..347ac7f9d --- /dev/null +++ b/e2e-tests/fixtures/presence/mappings/presence-lib.json @@ -0,0 +1,16 @@ +{ + "mappings": [ + { + "request": { "method": "GET", "urlPathPattern": "/client/1/lib.js" }, + "response": { + "status": 200, + "headers": { "Content-Type": "application/javascript" }, + "body": "/* e2e presence client stub */ window.presenceClient = window.presenceClient || function(){ return { on: function(){}, startConnection: function(){}, subscribe: function(){}, updatePresence: function(){} }; };" + } + }, + { + "request": { "method": "ANY", "urlPathPattern": "/.*" }, + "response": { "status": 200, "body": "" } + } + ] +} diff --git a/e2e-tests/fixtures/tagmanager/mappings/hyper-tags.json b/e2e-tests/fixtures/tagmanager/mappings/hyper-tags.json new file mode 100644 index 000000000..79cca4e64 --- /dev/null +++ b/e2e-tests/fixtures/tagmanager/mappings/hyper-tags.json @@ -0,0 +1,12 @@ +{ + "mappings": [ + { + "request": { "method": "GET", "urlPathPattern": "/hyper/tags" }, + "response": { + "status": 200, + "headers": { "Content-Type": "application/json" }, + "jsonBody": { "data": [] } + } + } + ] +} diff --git a/e2e-tests/fixtures/telemetry/mappings/telemetry.json b/e2e-tests/fixtures/telemetry/mappings/telemetry.json new file mode 100644 index 000000000..4dbb33016 --- /dev/null +++ b/e2e-tests/fixtures/telemetry/mappings/telemetry.json @@ -0,0 +1,8 @@ +{ + "mappings": [ + { + "request": { "method": "ANY", "urlPathPattern": "/.*" }, + "response": { "status": 204 } + } + ] +} diff --git a/e2e-tests/global-setup.ts b/e2e-tests/global-setup.ts new file mode 100644 index 000000000..d5e77cafc --- /dev/null +++ b/e2e-tests/global-setup.ts @@ -0,0 +1,38 @@ +import type { FullConfig } from "@playwright/test"; +import { startLocalStack, stopLocalStack } from "./setup/stackContainers.js"; +import { + readSharedStackInfo, + writeActiveStackInfo, + clearActiveStackInfo, +} from "./setup/sharedStack.js"; +import type { LocalStack } from "./setup/types.js"; + +/** + * Reuse a stack started by `dev:local` if present; otherwise start our own + * (test:ci) and tear it down afterwards. Writes the active connection file that + * the per-test fixtures read. + */ +async function globalSetup(_config: FullConfig): Promise<() => Promise> { + const shared = readSharedStackInfo(); + if (shared) { + console.log("[global-setup] reusing dev:local stack"); + writeActiveStackInfo(shared); + return async () => clearActiveStackInfo(); + } + + console.log("[global-setup] starting owned stack"); + const stack: LocalStack = await startLocalStack({ runApp: true, streamLogs: false }); + writeActiveStackInfo({ + baseUrl: stack.baseUrl, + cookieDomain: stack.cookieDomain, + panDomainPrivateKeyPem: stack.panDomainPrivateKeyPem, + mockAdminUrls: stack.mockAdminUrls, + }); + + return async () => { + clearActiveStackInfo(); + await stopLocalStack(stack); + }; +} + +export default globalSetup; diff --git a/e2e-tests/images/app/.tool-versions b/e2e-tests/images/app/.tool-versions new file mode 100644 index 000000000..20ae0fa5d --- /dev/null +++ b/e2e-tests/images/app/.tool-versions @@ -0,0 +1,4 @@ +java corretto-11.0.27.6.1 +sbt 1.12.11 +nodejs 22.11.0 +aws-cli 2.34.62 diff --git a/e2e-tests/images/app/Dockerfile b/e2e-tests/images/app/Dockerfile new file mode 100644 index 000000000..ce6fa30e2 --- /dev/null +++ b/e2e-tests/images/app/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 +# Toolchain-only image for workflow-frontend. The repo is bind-mounted at runtime +# (never COPYd in), so source edits reload without an image rebuild. +FROM debian:bookworm-slim +ENV DEBIAN_FRONTEND=noninteractive AWS_SDK_LOAD_CONFIG=1 +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl bash git openssl unzip \ + && rm -rf /var/lib/apt/lists/* +ENV MISE_DATA_DIR=/mise MISE_CONFIG_DIR=/mise MISE_CACHE_DIR=/mise/cache \ + MISE_INSTALL_PATH=/usr/local/bin/mise PATH="/mise/shims:$PATH" +RUN curl https://mise.run | sh +WORKDIR /app +COPY .tool-versions ./ +# Install the toolchain, add classic yarn, and record the tool bin dirs so the +# runtime can put them on PATH directly (the bind-mounted repo's mise.toml is +# untrusted and pins tools we don't want, so we never run mise against it). +RUN mise trust -a && mise install java nodejs sbt aws-cli \ + && mise exec nodejs -- npm install -g yarn \ + && mise bin-paths > /etc/mise-bin-paths +EXPOSE 9090 +CMD ["bash", "/workflow-frontend/e2e-tests/images/start-app"] diff --git a/e2e-tests/images/datastore/.tool-versions b/e2e-tests/images/datastore/.tool-versions new file mode 100644 index 000000000..eed5359ec --- /dev/null +++ b/e2e-tests/images/datastore/.tool-versions @@ -0,0 +1,2 @@ +java corretto-11.0.23.9.1 +sbt 1.12.11 diff --git a/e2e-tests/images/datastore/Dockerfile b/e2e-tests/images/datastore/Dockerfile new file mode 100644 index 000000000..c72697afb --- /dev/null +++ b/e2e-tests/images/datastore/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1 +# Toolchain-only image for the run-for-real datastore (guardian/workflow). +# The checkout is bind-mounted at runtime; sbt resolves/compiles on first start. +FROM debian:bookworm-slim +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl bash git openssl \ + && rm -rf /var/lib/apt/lists/* +ENV MISE_DATA_DIR=/mise MISE_CONFIG_DIR=/mise MISE_CACHE_DIR=/mise/cache \ + MISE_INSTALL_PATH=/usr/local/bin/mise PATH="/mise/shims:$PATH" +RUN curl https://mise.run | sh +WORKDIR /app +COPY .tool-versions ./ +RUN mise trust -a && mise install java sbt +COPY start-datastore /usr/local/bin/start-datastore +RUN chmod +x /usr/local/bin/start-datastore +EXPOSE 9093 +CMD ["bash", "/usr/local/bin/start-datastore"] diff --git a/e2e-tests/images/datastore/start-datastore b/e2e-tests/images/datastore/start-datastore new file mode 100644 index 000000000..f48bf7e9d --- /dev/null +++ b/e2e-tests/images/datastore/start-datastore @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Runs the guardian/workflow datastore subproject from bind-mounted source, +# using the config overlay the repo ships for e2e. Evolutions auto-apply on boot. +set -e +cd /workflow-backend +eval "$(mise activate bash --shims)" +exec sbt "-Dconfig.file=datastore/conf/application.e2e.conf" "datastore/run 9093" diff --git a/e2e-tests/images/start-app b/e2e-tests/images/start-app new file mode 100755 index 000000000..e8df9d6aa --- /dev/null +++ b/e2e-tests/images/start-app @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Runs the frontend from bind-mounted source in dev mode with asset watch. +# Uses the toolchain baked into the image via PATH; does not invoke mise against +# the bind-mounted repo (its mise.toml is untrusted and pins other tool versions). +set -e +export PATH="$(paste -sd: /etc/mise-bin-paths):$PATH" +export JAVA_HOME="$(dirname "$(dirname "$(command -v java)")")" +cd /workflow-frontend +yarn install --frozen-lockfile || yarn install +yarn build-dev & +exec sbt "-Dconfig.file=/workflow-frontend/e2e-tests/fixtures/app/application.e2e.conf" "run 9090" diff --git a/e2e-tests/mise.toml b/e2e-tests/mise.toml new file mode 100644 index 000000000..64906b411 --- /dev/null +++ b/e2e-tests/mise.toml @@ -0,0 +1,2 @@ +[tools] +node = "22.11.0" diff --git a/e2e-tests/package.json b/e2e-tests/package.json new file mode 100644 index 000000000..7a22e8fc8 --- /dev/null +++ b/e2e-tests/package.json @@ -0,0 +1,26 @@ +{ + "name": "workflow-frontend-e2e", + "private": true, + "type": "module", + "description": "End-to-end tests for workflow-frontend (Playwright + playwright-bdd + Testcontainers)", + "packageManager": "yarn@1.22.22", + "scripts": { + "test": "bddgen && playwright test", + "test:ci": "tsx setup/checkout-backend.ts && bddgen && playwright test", + "test:ui": "bddgen && playwright test --ui-port=9099", + "test:report": "playwright show-report target/playwright-report --port 9098", + "dev:local": "tsx setup/run-dev-local.ts", + "dev:debug": "DEBUG=testcontainers:build tsx setup/run-dev-local.ts", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@guardian/pan-domain-node": "^1.2.5", + "@playwright/test": "^1.48.0", + "@types/node": "^22.9.0", + "playwright": "^1.48.0", + "playwright-bdd": "^9.2.1", + "testcontainers": "^10.13.0", + "tsx": "^4.19.0", + "typescript": "^5.6.0" + } +} diff --git a/e2e-tests/playwright.config.ts b/e2e-tests/playwright.config.ts new file mode 100644 index 000000000..7af9bec2b --- /dev/null +++ b/e2e-tests/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from "@playwright/test"; +import { defineBddConfig } from "playwright-bdd"; +import { HOST_PORTS } from "./setup/constants.js"; + +const testDir = defineBddConfig({ + features: "features/**/*.feature", + steps: "steps/**/*.ts", +}); + +export default defineConfig({ + testDir, + globalSetup: "./global-setup.ts", + outputDir: "target/test-results", + fullyParallel: true, + workers: 4, // all workers share one stack + retries: 1, + timeout: 60_000, + expect: { timeout: 10_000 }, + use: { + trace: "on-first-retry", + video: "on-first-retry", + screenshot: "only-on-failure", + ignoreHTTPSErrors: true, // accept the mocks' self-signed certs + launchOptions: { + args: [ + // Route the browser's cross-origin HTTPS calls to each mock's fixed host port. + "--host-resolver-rules=" + + [ + `MAP composer.local.dev-gutools.co.uk 127.0.0.1:${HOST_PORTS.composerHttps}`, + `MAP presence.local.dev-gutools.co.uk 127.0.0.1:${HOST_PORTS.presenceHttps}`, + `MAP user-telemetry.local.dev-gutools.co.uk 127.0.0.1:${HOST_PORTS.telemetryHttps}`, + ].join(","), + ], + }, + }, + reporter: process.env.CI + ? [["github"], ["html", { outputFolder: "target/playwright-report", open: "never" }]] + : [ + ["list"], + ["html", { outputFolder: "target/playwright-report", open: "never" }], + ], + projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], +}); diff --git a/e2e-tests/setup/checkout-backend.ts b/e2e-tests/setup/checkout-backend.ts new file mode 100644 index 000000000..a39956245 --- /dev/null +++ b/e2e-tests/setup/checkout-backend.ts @@ -0,0 +1,29 @@ +import { existsSync, mkdirSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { dirname } from "node:path"; +import { BACKEND_DIR } from "./constants.js"; + +/** + * Ensure the run-for-real datastore (private guardian/workflow) is checked out at + * BACKEND_DIR. In CI this is done by actions/checkout with a GitHub App token, so + * if the dir already exists we do nothing. Locally it clones via the developer's + * git credentials. + */ +const REPO = process.env.WORKFLOW_BACKEND_REPO ?? "https://github.com/guardian/workflow.git"; +const REF = process.env.WORKFLOW_BACKEND_REF ?? "main"; + +function checkoutBackend(): void { + if (existsSync(BACKEND_DIR)) { + console.log(`[checkout-backend] using existing checkout at ${BACKEND_DIR}`); + return; + } + console.log(`[checkout-backend] cloning ${REPO}@${REF} into ${BACKEND_DIR}`); + mkdirSync(dirname(BACKEND_DIR), { recursive: true }); + execFileSync( + "git", + ["clone", "--depth", "1", "--branch", REF, REPO, BACKEND_DIR], + { stdio: "inherit" }, + ); +} + +checkoutBackend(); diff --git a/e2e-tests/setup/constants.ts b/e2e-tests/setup/constants.ts new file mode 100644 index 000000000..2716aeaa2 --- /dev/null +++ b/e2e-tests/setup/constants.ts @@ -0,0 +1,80 @@ +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +/** Absolute path to the e2e-tests folder. */ +export const E2E_ROOT = resolve(__dirname, ".."); +/** Absolute path to the workflow-frontend repo root. */ +export const REPO_ROOT = resolve(E2E_ROOT, ".."); + +/** Where the run-for-real datastore (guardian/workflow) is checked out. */ +export const BACKEND_DIR = + process.env.WORKFLOW_BACKEND_DIR ?? resolve(E2E_ROOT, "target/workflow-backend"); + +/** Stage the frontend runs as locally; drives the *.local.dev-gutools.co.uk domain. */ +export const APP_DOMAIN = "local.dev-gutools.co.uk"; + +/** Docker image tags for the toolchain-only images built per run (CI / test:ci). */ +export const APP_IMAGE_TAG = "workflow-frontend-e2e:latest"; +export const DATASTORE_IMAGE_TAG = "workflow-datastore-e2e:latest"; + +/** + * Network aliases = the real per-stage upstream hostnames. Registering them on the + * mock/datastore containers makes the frontend's server-side calls resolve inside + * the Docker network with no config override. + */ +export const ALIASES = { + app: "workflow-frontend", + datastore: "workflow-datastore", + // The datastore's application.e2e.conf points at this exact host for Postgres. + db: "workflow-db-e2e.local.dev-gutools.co.uk", + localstack: "localstack", + composer: `composer.${APP_DOMAIN}`, + presence: `presence.${APP_DOMAIN}`, + telemetry: `user-telemetry.${APP_DOMAIN}`, + preferences: `preferences.${APP_DOMAIN}`, + tagmanager: `tagmanager.${APP_DOMAIN}`, + capi: `capi-preview.${APP_DOMAIN}`, + // S3 bucket aliases must sit under an `s3.` domain so LocalStack can extract the + // bucket from the virtual-hosted-style Host header (needs `.s3.`). + s3: "s3.localstack", + s3PanDomain: `pan-domain-auth-settings.s3.localstack`, + s3Permissions: `permissions-cache.s3.localstack`, +} as const; + +/** Fixed host ports — only where a stable port is required (host browser / host-resolver-rules). */ +export const HOST_PORTS = { + app: 9091, + authRedirect: 9090, + composerHttps: 9082, + presenceHttps: 9071, + telemetryHttps: 3133, + playwrightUi: 9099, + playwrightReport: 9098, +} as const; + +/** Container ports. */ +export const CONTAINER_PORTS = { + app: 9090, + datastore: 9093, + db: 5432, + localstack: 4566, + wiremockHttp: 80, + wiremockHttps: 8443, +} as const; + +/** Postgres credentials for the run-for-real datastore. */ +export const DB = { + user: "workflow", + password: "workflow", + database: "workflow", +} as const; + +export const PAN_DOMAIN_BUCKET = "pan-domain-auth-settings"; +export const PERMISSIONS_BUCKET = "permissions-cache"; +/** Permissions client always reads the CODE key (frontend maps non-PROD -> CODE). */ +export const PERMISSIONS_KEY = "CODE/permissions.json"; +export const PAN_DOMAIN_SETTINGS_KEY = `${APP_DOMAIN}.settings`; + +export const DYNAMO_TABLE = "editorial-support-CODE"; diff --git a/e2e-tests/setup/panDomainCookie.ts b/e2e-tests/setup/panDomainCookie.ts new file mode 100644 index 000000000..b57d24745 --- /dev/null +++ b/e2e-tests/setup/panDomainCookie.ts @@ -0,0 +1,35 @@ +// createCookie is not re-exported from the package root, so import the module path. +import { createCookie } from "@guardian/pan-domain-node/dist/src/panda.js"; + +/** Panda's standard asymmetric cookie name. */ +export const PAN_DOMAIN_COOKIE_NAME = "gutoolsAuth-assym"; + +/** + * Test roles -> emails. Each email must have a matching entry in + * fixtures/permissions/permissions.json (granting or denying workflow access). + */ +export const roles = { + default: "workflow.e2e.test@guardian.co.uk", + noWorkflowAccess: "no.workflow@guardian.co.uk", +} as const; + +export type Role = keyof typeof roles; + +export function createPanDomainCookie( + privateKeyPem: string, + role: Role = "default", + expiresInMs = 60 * 60 * 1000, +): string { + return createCookie( + { + firstName: "Playwright", + lastName: "Tester", + email: roles[role], + authenticatingSystem: "workflow", + authenticatedIn: ["workflow"], + expires: Date.now() + expiresInMs, + multifactor: true, + }, + privateKeyPem, + ); +} diff --git a/e2e-tests/setup/panDomainKeys.ts b/e2e-tests/setup/panDomainKeys.ts new file mode 100644 index 000000000..b898e83da --- /dev/null +++ b/e2e-tests/setup/panDomainKeys.ts @@ -0,0 +1,30 @@ +import { generateKeyPairSync } from "node:crypto"; + +export interface PanDomainKeys { + privateKeyPem: string; + publicKeyPem: string; + /** base64 of the DER body, i.e. PEM with the armour and newlines stripped. */ + privateKeyBase64: string; + publicKeyBase64: string; +} + +const pemToBase64 = (pem: string): string => + pem + .split("\n") + .filter((line) => !line.startsWith("-----") && line.trim() !== "") + .join(""); + +/** Generate a fresh RSA keypair per run for signing/verifying pan-domain cookies. */ +export function generatePanDomainKeys(): PanDomainKeys { + const { privateKey, publicKey } = generateKeyPairSync("rsa", { + modulusLength: 4096, + publicKeyEncoding: { type: "spki", format: "pem" }, + privateKeyEncoding: { type: "pkcs8", format: "pem" }, + }); + return { + privateKeyPem: privateKey, + publicKeyPem: publicKey, + privateKeyBase64: pemToBase64(privateKey), + publicKeyBase64: pemToBase64(publicKey), + }; +} diff --git a/e2e-tests/setup/run-dev-local.ts b/e2e-tests/setup/run-dev-local.ts new file mode 100644 index 000000000..ef034f481 --- /dev/null +++ b/e2e-tests/setup/run-dev-local.ts @@ -0,0 +1,41 @@ +import { startLocalStack, stopLocalStack } from "./stackContainers.js"; +import { writeSharedStackInfo, clearSharedStackInfo } from "./sharedStack.js"; +import type { LocalStack } from "./types.js"; + +/** + * Boot a long-running local stack (app + deps as containers) for interactive dev. + * `yarn test` reuses it via the shared-stack file. Ctrl-C tears it down. + */ +async function main(): Promise { + console.log("[dev:local] starting stack (this builds images on first run)..."); + const stack: LocalStack = await startLocalStack({ runApp: true, streamLogs: true }); + writeSharedStackInfo({ + baseUrl: stack.baseUrl, + cookieDomain: stack.cookieDomain, + panDomainPrivateKeyPem: stack.panDomainPrivateKeyPem, + mockAdminUrls: stack.mockAdminUrls, + }); + console.log(`\n[dev:local] stack ready. App: ${stack.baseUrl}`); + console.log("[dev:local] run `yarn test` in another terminal. Ctrl-C to stop.\n"); + // Resume stdin so its ref'd handle holds the event loop open until a signal + // (an unsettled promise and signal listeners alone don't keep the process alive). + process.stdin.resume(); + + let stopping = false; + const shutdown = async () => { + if (stopping) return; + stopping = true; + console.log("\n[dev:local] shutting down..."); + clearSharedStackInfo(); + await stopLocalStack(stack); + process.exit(0); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + await new Promise(() => {}); // keep alive +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/e2e-tests/setup/sharedStack.ts b/e2e-tests/setup/sharedStack.ts new file mode 100644 index 000000000..622ea5387 --- /dev/null +++ b/e2e-tests/setup/sharedStack.ts @@ -0,0 +1,39 @@ +import { existsSync, readFileSync, writeFileSync, rmSync } from "node:fs"; +import { resolve } from "node:path"; +import { E2E_ROOT } from "./constants.js"; +import type { StackConnection } from "./types.js"; + +/** Written by dev:local; read by test runs to reuse a long-running stack. */ +export const SHARED_STACK_FILE = resolve(E2E_ROOT, ".shared-stack.json"); +/** Written by global-setup for the current run; read by per-test fixtures. */ +export const ACTIVE_STACK_FILE = resolve(E2E_ROOT, ".active-stack.json"); + +export function writeSharedStackInfo(info: StackConnection): void { + writeFileSync(SHARED_STACK_FILE, JSON.stringify(info, null, 2)); +} + +export function readSharedStackInfo(): StackConnection | undefined { + if (!existsSync(SHARED_STACK_FILE)) return undefined; + return JSON.parse(readFileSync(SHARED_STACK_FILE, "utf-8")) as StackConnection; +} + +export function clearSharedStackInfo(): void { + rmSync(SHARED_STACK_FILE, { force: true }); +} + +export function writeActiveStackInfo(info: StackConnection): void { + writeFileSync(ACTIVE_STACK_FILE, JSON.stringify(info, null, 2)); +} + +export function readActiveStackInfo(): StackConnection { + if (!existsSync(ACTIVE_STACK_FILE)) { + throw new Error( + "No active stack found. Run `yarn dev:local` first (or use `yarn test:ci`).", + ); + } + return JSON.parse(readFileSync(ACTIVE_STACK_FILE, "utf-8")) as StackConnection; +} + +export function clearActiveStackInfo(): void { + rmSync(ACTIVE_STACK_FILE, { force: true }); +} diff --git a/e2e-tests/setup/stack/containers.ts b/e2e-tests/setup/stack/containers.ts new file mode 100644 index 000000000..dea833cbd --- /dev/null +++ b/e2e-tests/setup/stack/containers.ts @@ -0,0 +1,279 @@ +import { GenericContainer, Wait } from "testcontainers"; +import type { StartedTestContainer, StartedNetwork } from "testcontainers"; + +type Environment = Record; +import { resolve } from "node:path"; +import { + E2E_ROOT, + REPO_ROOT, + BACKEND_DIR, + ALIASES, + HOST_PORTS, + CONTAINER_PORTS, + DB, + APP_IMAGE_TAG, + DATASTORE_IMAGE_TAG, +} from "../constants.js"; + +const WIREMOCK_IMAGE = "wiremock/wiremock:3.9.1"; +const POSTGRES_IMAGE = "postgres:16-alpine"; +const LOCALSTACK_IMAGE = "localstack/localstack:4"; +const NGINX_IMAGE = "nginx:alpine"; + +/** Stream a container's logs to stdout, prefixed — only when streamLogs is set. */ +export function createLogConsumer(prefix: string, streamLogs: boolean) { + return (stream: NodeJS.ReadableStream) => { + if (!streamLogs) return; + stream.on("data", (line) => process.stdout.write(`[${prefix}] ${line}`)); + stream.on("err", (line) => process.stderr.write(`[${prefix}] ${line}`)); + }; +} + +/** Build an image from a Dockerfile via Testcontainers; deleted on exit. */ +export function buildImage(context: string, tag: string) { + return GenericContainer.fromDockerfile(context) + .withBuildkit() + .build(tag, { deleteOnExit: true }); +} + +// ---------------------------------------------------------------------------- +// Infrastructure +// ---------------------------------------------------------------------------- + +/** Postgres for the run-for-real datastore. Starts empty; datastore migrates it. */ +export async function startDb( + network: StartedNetwork, + streamLogs: boolean, +): Promise { + return new GenericContainer(POSTGRES_IMAGE) + .withNetwork(network) + .withNetworkAliases(ALIASES.db) + .withEnvironment({ + POSTGRES_USER: DB.user, + POSTGRES_PASSWORD: DB.password, + POSTGRES_DB: DB.database, + }) + .withExposedPorts(CONTAINER_PORTS.db) + .withLogConsumer(createLogConsumer("postgres", streamLogs)) + .withWaitStrategy( + Wait.forLogMessage(/database system is ready to accept connections/, 2), + ) + .start(); +} + +/** One LocalStack container for S3 + DynamoDB. */ +export async function startAws( + network: StartedNetwork, + streamLogs: boolean, +): Promise { + return new GenericContainer(LOCALSTACK_IMAGE) + .withNetwork(network) + .withNetworkAliases( + ALIASES.localstack, + ALIASES.s3, + ALIASES.s3PanDomain, + ALIASES.s3Permissions, + ) + .withEnvironment({ SERVICES: "s3,dynamodb", AWS_DEFAULT_REGION: "eu-west-1" }) + .withExposedPorts(CONTAINER_PORTS.localstack) + .withLogConsumer(createLogConsumer("localstack", streamLogs)) + .withWaitStrategy(Wait.forLogMessage(/Ready\./, 1)) + .start(); +} + +// ---------------------------------------------------------------------------- +// Mocked upstreams — one shared WireMock image +// ---------------------------------------------------------------------------- + +export interface MockConfig { + name: string; + fixtureDir: string; + aliases: string[]; + /** Fixed host port for the browser to reach the mock over HTTPS (8443). */ + hostHttpsPort?: number; + /** Enable WireMock response templating; false serves bodies verbatim. */ + templating: boolean; +} + +export const MOCK_WIREMOCK_CONFIGS: Record = { + capi: { + name: "capi", + fixtureDir: "capi", + aliases: [ALIASES.capi], + templating: true, + }, + composer: { + name: "composer", + fixtureDir: "composer", + aliases: [ALIASES.composer], + hostHttpsPort: HOST_PORTS.composerHttps, + templating: true, + }, + presence: { + name: "presence", + fixtureDir: "presence", + aliases: [ALIASES.presence], + hostHttpsPort: HOST_PORTS.presenceHttps, + templating: false, + }, + telemetry: { + name: "telemetry", + fixtureDir: "telemetry", + aliases: [ALIASES.telemetry], + hostHttpsPort: HOST_PORTS.telemetryHttps, + templating: true, + }, + preferences: { + name: "preferences", + fixtureDir: "preferences", + aliases: [ALIASES.preferences], + templating: true, + }, + tagmanager: { + name: "tagmanager", + fixtureDir: "tagmanager", + aliases: [ALIASES.tagmanager], + templating: true, + }, +}; + +export async function startMockWiremock( + config: MockConfig, + network: StartedNetwork, + streamLogs: boolean, +): Promise { + const command = [ + "--root-dir", + "/fixtures", + "--port", + String(CONTAINER_PORTS.wiremockHttp), + "--https-port", + String(CONTAINER_PORTS.wiremockHttps), + "--disable-banner", + ]; + if (config.templating) command.push("--global-response-templating"); + + // Browser-facing mocks get fixed host ports (http = https-1) so host-resolver-rules + // can reach them; server-side-only mocks use random mapped ports. + const exposedPorts = + config.hostHttpsPort !== undefined + ? [ + { container: CONTAINER_PORTS.wiremockHttp, host: config.hostHttpsPort - 1 }, + { container: CONTAINER_PORTS.wiremockHttps, host: config.hostHttpsPort }, + ] + : [CONTAINER_PORTS.wiremockHttp, CONTAINER_PORTS.wiremockHttps]; + + return new GenericContainer(WIREMOCK_IMAGE) + .withNetwork(network) + .withNetworkAliases(...config.aliases) + .withUser("root") // bind privileged port 80 + .withCopyDirectoriesToContainer([ + { source: resolve(E2E_ROOT, "fixtures", config.fixtureDir), target: "/fixtures" }, + ]) + .withCommand(command) + .withExposedPorts(...exposedPorts) + .withLogConsumer(createLogConsumer(config.name, streamLogs)) + .withWaitStrategy( + Wait.forHttp("/__admin/health", CONTAINER_PORTS.wiremockHttp).forStatusCode(200), + ) + .start(); +} + +// ---------------------------------------------------------------------------- +// Run-for-real datastore (guardian/workflow) — toolchain image + bind mount +// ---------------------------------------------------------------------------- + +export async function startDatastore( + network: StartedNetwork, + streamLogs: boolean, +): Promise { + const image = await buildImage(resolve(E2E_ROOT, "images/datastore"), DATASTORE_IMAGE_TAG); + return image + .withNetwork(network) + .withNetworkAliases(ALIASES.datastore) + .withBindMounts([{ source: BACKEND_DIR, target: "/workflow-backend", mode: "rw" }]) + .withEnvironment(datastoreEnvironment()) + .withExposedPorts(CONTAINER_PORTS.datastore) + .withLogConsumer(createLogConsumer("datastore", streamLogs)) + .withStartupTimeout(900_000) // sbt resolves + compiles + applies evolutions on first start + .withWaitStrategy( + Wait.forHttp("/management/healthcheck", CONTAINER_PORTS.datastore).forStatusCode(200), + ) + .start(); +} + +function datastoreEnvironment(): Environment { + return { + STAGE: "DEV", + DB_URL: `jdbc:postgresql://${ALIASES.db}:5432/${DB.database}`, + DB_USER: DB.user, + DB_PASSWORD: DB.password, + PLAY_APPLICATION_SECRET: "e2e-datastore-secret-not-a-real-secret-000000", + }; +} + +// ---------------------------------------------------------------------------- +// App under test (CI / test:ci only; run natively in dev) +// ---------------------------------------------------------------------------- + +export function appEnvironment(): Environment { + return { + STAGE: "DEV", + WORKFLOW_E2E: "true", // switches internal upstreams to http (see Config.scala) + APPLICATION_SECRET: "e2e-application-secret-not-a-real-secret-000000", + WORKFLOW_SHARED_SECRET: "e2e-shared-secret", + AWS_REGION: "eu-west-1", + AWS_DEFAULT_REGION: "eu-west-1", + AWS_ACCESS_KEY_ID: "test", + AWS_SECRET_ACCESS_KEY: "test", + AWS_EC2_METADATA_DISABLED: "true", + AWS_ENDPOINT_URL_S3: `http://${ALIASES.s3}:${CONTAINER_PORTS.localstack}`, + AWS_ENDPOINT_URL_DYNAMODB: `http://${ALIASES.localstack}:${CONTAINER_PORTS.localstack}`, + }; +} + +export async function startWorkflow( + network: StartedNetwork, + streamLogs: boolean, +): Promise { + const image = await buildImage(resolve(E2E_ROOT, "images/app"), APP_IMAGE_TAG); + return image + .withNetwork(network) + .withNetworkAliases(ALIASES.app) + .withBindMounts([{ source: REPO_ROOT, target: "/workflow-frontend", mode: "rw" }]) + .withEnvironment(appEnvironment()) + .withExposedPorts({ container: CONTAINER_PORTS.app, host: HOST_PORTS.app }) + .withLogConsumer(createLogConsumer("workflow-frontend", streamLogs)) + .withStartupTimeout(900_000) + .withWaitStrategy( + Wait.forHttp("/management/healthcheck", CONTAINER_PORTS.app).forStatusCode(200), + ) + .start(); +} + +// ---------------------------------------------------------------------------- +// Optional host-browser auth-redirect (dev:local only) +// ---------------------------------------------------------------------------- + +export async function startAuthRedirect( + network: StartedNetwork, + cookieValue: string, + streamLogs: boolean, +): Promise { + return new GenericContainer(NGINX_IMAGE) + .withNetwork(network) + .withCopyFilesToContainer([ + { + source: resolve(E2E_ROOT, "fixtures/auth-redirect/auth-redirect.conf.template"), + target: "/etc/nginx/templates/default.conf.template", + }, + ]) + .withEnvironment({ + APP_UPSTREAM: `${ALIASES.app}:${CONTAINER_PORTS.app}`, + COOKIE_VALUE: cookieValue, + }) + .withExposedPorts({ container: 80, host: HOST_PORTS.authRedirect }) + .withLogConsumer(createLogConsumer("auth-redirect", streamLogs)) + .withWaitStrategy(Wait.forListeningPorts()) + .start(); +} diff --git a/e2e-tests/setup/stack/seedDatabase.ts b/e2e-tests/setup/stack/seedDatabase.ts new file mode 100644 index 000000000..cf15a10c7 --- /dev/null +++ b/e2e-tests/setup/stack/seedDatabase.ts @@ -0,0 +1,42 @@ +import { resolve } from "node:path"; +import { existsSync } from "node:fs"; +import type { StartedTestContainer } from "testcontainers"; +import { E2E_ROOT, DB } from "../constants.js"; + +/** + * CSV fixtures loaded into the datastore's Postgres, parent tables before FK + * children. Column lists match the workflow datastore schema. Files that don't + * exist are skipped, so you can seed only what a scenario needs. + * + * Note: the datastore's own evolutions already seed reference data (sections, + * etc.), so there is nothing to load by default. Add a CSV + entry here only for + * extra rows a scenario needs (e.g. pre-existing stubs). + */ +const DB_SEED_TABLES: { table: string; columns: string; file: string }[] = []; + +const dbUrl = `postgresql://${DB.user}:${DB.password}@localhost:5432/${DB.database}`; + +/** + * Seed the datastore's Postgres from the host. Run only AFTER the datastore's own + * migrations have created the schema (its healthcheck triggers them on first hit). + */ +export async function seedDatabase(db: StartedTestContainer): Promise { + for (const { table, columns, file } of DB_SEED_TABLES) { + const source = resolve(E2E_ROOT, "fixtures/db", file); + if (!existsSync(source)) continue; + + await db.copyFilesToContainer([{ source, target: `/tmp/${file}` }]); + + const { exitCode, output } = await db.exec([ + "psql", + dbUrl, + "-v", + "ON_ERROR_STOP=1", + "-c", + `\\copy ${table}(${columns}) from '/tmp/${file}' with (format csv, header true, null 'NULL')`, + ]); + if (exitCode !== 0) { + throw new Error(`seeding ${table} failed (${exitCode}):\n${output}`); + } + } +} diff --git a/e2e-tests/setup/stack/seedDynamodb.ts b/e2e-tests/setup/stack/seedDynamodb.ts new file mode 100644 index 000000000..19fa5971a --- /dev/null +++ b/e2e-tests/setup/stack/seedDynamodb.ts @@ -0,0 +1,40 @@ +import { resolve } from "node:path"; +import type { StartedTestContainer } from "testcontainers"; +import { E2E_ROOT, DYNAMO_TABLE } from "../constants.js"; + +async function awslocal(container: StartedTestContainer, args: string[]): Promise { + const { exitCode, output } = await container.exec(["awslocal", ...args]); + if (exitCode !== 0) { + throw new Error(`awslocal ${args.join(" ")} failed (${exitCode}):\n${output}`); + } +} + +/** Create the editorial-support table and load its fixture rows. */ +export async function seedDynamodb(localstack: StartedTestContainer): Promise { + await localstack.copyFilesToContainer([ + { + source: resolve(E2E_ROOT, `fixtures/dynamodb/${DYNAMO_TABLE}.json`), + target: `/tmp/${DYNAMO_TABLE}.json`, + }, + ]); + + await awslocal(localstack, [ + "dynamodb", + "create-table", + "--table-name", + DYNAMO_TABLE, + "--attribute-definitions", + "AttributeName=id,AttributeType=S", + "--key-schema", + "AttributeName=id,KeyType=HASH", + "--billing-mode", + "PAY_PER_REQUEST", + ]); + + await awslocal(localstack, [ + "dynamodb", + "batch-write-item", + "--request-items", + `file:///tmp/${DYNAMO_TABLE}.json`, + ]); +} diff --git a/e2e-tests/setup/stack/seedS3.ts b/e2e-tests/setup/stack/seedS3.ts new file mode 100644 index 000000000..6c97e93b9 --- /dev/null +++ b/e2e-tests/setup/stack/seedS3.ts @@ -0,0 +1,86 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import type { StartedTestContainer } from "testcontainers"; +import { + E2E_ROOT, + PAN_DOMAIN_BUCKET, + PERMISSIONS_BUCKET, + PERMISSIONS_KEY, + PAN_DOMAIN_SETTINGS_KEY, +} from "../constants.js"; +import type { PanDomainKeys } from "../panDomainKeys.js"; + +/** awslocal wrapper: talks to LocalStack's gateway inside the container. */ +async function awslocal(container: StartedTestContainer, args: string[]): Promise { + const { exitCode, output } = await container.exec(["awslocal", ...args]); + if (exitCode !== 0) { + throw new Error(`awslocal ${args.join(" ")} failed (${exitCode}):\n${output}`); + } +} + +/** + * Seed the two S3 objects the app reads on startup: the permissions cache and the + * pan-domain settings (with the per-run signing keys appended). + */ +export async function seedS3( + localstack: StartedTestContainer, + keys: PanDomainKeys, +): Promise { + const permissions = readFileSync( + resolve(E2E_ROOT, "fixtures/permissions/permissions.json"), + "utf-8", + ); + const settingsBase = readFileSync( + resolve(E2E_ROOT, `fixtures/pan-domain-settings/${PAN_DOMAIN_SETTINGS_KEY}`), + "utf-8", + ); + + const settings = + settingsBase.trimEnd() + + `\npublicKey=${keys.publicKeyBase64}` + + `\nprivateKey=${keys.privateKeyBase64}\n`; + const settingsPublic = + settingsBase.trimEnd() + `\npublicKey=${keys.publicKeyBase64}\n`; + + await container_writeTmp(localstack, "/tmp/permissions.json", permissions); + await container_writeTmp(localstack, "/tmp/pan-domain.settings", settings); + await container_writeTmp(localstack, "/tmp/pan-domain.settings.public", settingsPublic); + + for (const bucket of [PERMISSIONS_BUCKET, PAN_DOMAIN_BUCKET]) { + await awslocal(localstack, [ + "s3api", + "create-bucket", + "--bucket", + bucket, + "--create-bucket-configuration", + "LocationConstraint=eu-west-1", + ]); + } + + await awslocal(localstack, [ + "s3", + "cp", + "/tmp/permissions.json", + `s3://${PERMISSIONS_BUCKET}/${PERMISSIONS_KEY}`, + ]); + await awslocal(localstack, [ + "s3", + "cp", + "/tmp/pan-domain.settings", + `s3://${PAN_DOMAIN_BUCKET}/${PAN_DOMAIN_SETTINGS_KEY}`, + ]); + await awslocal(localstack, [ + "s3", + "cp", + "/tmp/pan-domain.settings.public", + `s3://${PAN_DOMAIN_BUCKET}/${PAN_DOMAIN_SETTINGS_KEY}.public`, + ]); +} + +async function container_writeTmp( + container: StartedTestContainer, + path: string, + content: string, +): Promise { + await container.copyContentToContainer([{ content, target: path }]); +} diff --git a/e2e-tests/setup/stackContainers.ts b/e2e-tests/setup/stackContainers.ts new file mode 100644 index 000000000..148ea80d5 --- /dev/null +++ b/e2e-tests/setup/stackContainers.ts @@ -0,0 +1,97 @@ +import { Network } from "testcontainers"; +import type { StartedTestContainer, StartedNetwork } from "testcontainers"; +import { HOST_PORTS, APP_DOMAIN } from "./constants.js"; +import { generatePanDomainKeys } from "./panDomainKeys.js"; +import type { LocalStack } from "./types.js"; +import { + startAws, + startDb, + startDatastore, + startWorkflow, + startMockWiremock, + MOCK_WIREMOCK_CONFIGS, +} from "./stack/containers.js"; +import { seedS3 } from "./stack/seedS3.js"; +import { seedDynamodb } from "./stack/seedDynamodb.js"; +import { seedDatabase } from "./stack/seedDatabase.js"; + +export interface StartOptions { + /** Build + run the frontend as a container (test:ci / dev:local). */ + runApp?: boolean; + /** Stream container logs to stdout (useful for dev:local). */ + streamLogs?: boolean; +} + +/** + * Build and start the whole self-contained stack on one Docker network: + * infra (Postgres + LocalStack) -> mocks + datastore + app (overlapped) -> seed. + */ +export async function startLocalStack(options: StartOptions = {}): Promise { + const { runApp = true, streamLogs = false } = options; + const started: StartedTestContainer[] = []; + const track = (c: T): T => { + started.push(c); + return c; + }; + + const network: StartedNetwork = await new Network().start(); + const panDomainKeys = generatePanDomainKeys(); + + try { + // 1. Infrastructure first — everything depends on it. + const [localstack, db] = await Promise.all([ + startAws(network, streamLogs).then(track), + startDb(network, streamLogs).then(track), + ]); + + // Seed S3 (permissions + pan-domain settings) and DynamoDB before the app boots. + await Promise.all([seedS3(localstack, panDomainKeys), seedDynamodb(localstack)]); + + // 2. Mocks (shared image, no build), datastore and app — overlapped. + const mockStarts = Object.values(MOCK_WIREMOCK_CONFIGS).map((cfg) => + startMockWiremock(cfg, network, streamLogs).then((c) => ({ name: cfg.name, container: track(c) })), + ); + const datastoreStart = startDatastore(network, streamLogs).then(track); + const appStart = runApp ? startWorkflow(network, streamLogs).then(track) : Promise.resolve(undefined); + + const [datastore, app, ...mocks] = await Promise.all([datastoreStart, appStart, ...mockStarts]); + + // 3. Seed the datastore's Postgres now that its migrations have created the schema. + await seedDatabase(db); + + const mockAdminUrls: Record = {}; + for (const m of mocks) { + mockAdminUrls[m.name] = `http://localhost:${m.container.getMappedPort(80)}/__admin`; + } + + const baseUrl = app + ? `http://localhost:${app.getMappedPort(9090)}` + : `http://localhost:${HOST_PORTS.app}`; + + return { + network, + containers: started, + baseUrl, + cookieDomain: APP_DOMAIN, + panDomainPrivateKeyPem: panDomainKeys.privateKeyPem, + mockAdminUrls, + }; + } catch (err) { + await stopContainers(started, network); + throw err; + } +} + +export async function stopLocalStack(stack: LocalStack): Promise { + await stopContainers(stack.containers, stack.network); +} + +async function stopContainers( + containers: StartedTestContainer[], + network: StartedNetwork, +): Promise { + for (const container of [...containers].reverse()) { + await container.stop().catch(() => undefined); + } + await network.stop().catch(() => undefined); +} diff --git a/e2e-tests/setup/types.ts b/e2e-tests/setup/types.ts new file mode 100644 index 000000000..be2ac6da3 --- /dev/null +++ b/e2e-tests/setup/types.ts @@ -0,0 +1,19 @@ +import type { StartedTestContainer, StartedNetwork } from "testcontainers"; + +/** Connection details written to disk so per-test fixtures can reach the stack. */ +export interface StackConnection { + /** Base URL the Playwright browser loads the app from (plain http, forwarded port). */ + baseUrl: string; + /** Pan-domain domain the auth cookie is scoped to. */ + cookieDomain: string; + /** Per-run RSA private key (PEM) used to sign auth cookies in tests. */ + panDomainPrivateKeyPem: string; + /** WireMock admin URLs, keyed by mock name, for asserting requests were made. */ + mockAdminUrls: Record; +} + +/** Full running stack, including container handles for teardown. */ +export interface LocalStack extends StackConnection { + network: StartedNetwork; + containers: StartedTestContainer[]; +} diff --git a/e2e-tests/steps/background.steps.ts b/e2e-tests/steps/background.steps.ts new file mode 100644 index 000000000..20c9ed32c --- /dev/null +++ b/e2e-tests/steps/background.steps.ts @@ -0,0 +1,13 @@ +import { Given, expect } from "./fixtures"; + +Given("the application stack is running", async ({ stack }) => { + expect(stack.baseUrl).toBeTruthy(); +}); + +Given("I am signed in through pan-domain auth", async ({ signIn }) => { + await signIn("default"); +}); + +Given("I have opened the workflow dashboard", async ({ page, stack }) => { + await page.goto(`${stack.baseUrl}/dashboard`); +}); diff --git a/e2e-tests/steps/createStub.steps.ts b/e2e-tests/steps/createStub.steps.ts new file mode 100644 index 000000000..828747a91 --- /dev/null +++ b/e2e-tests/steps/createStub.steps.ts @@ -0,0 +1,85 @@ +import { Given, When, Then, expect } from "./fixtures"; + +const CREATE_NEW = "#testing-create-new"; +// The `dropdown-toolbar__list` class is shared by several dashboard dropdowns, so +// scope to the "Create new" container. +const CONTENT_TYPE_LIST = ".dashboard-toolbar__dropdown--create ul.dropdown-toolbar__list"; + +async function openDropdown(page: import("@playwright/test").Page): Promise { + await page.locator(CREATE_NEW).click(); + await expect(page.locator(CONTENT_TYPE_LIST)).not.toHaveClass(/content-type-list--hidden/); +} + +async function openStubModalFor( + page: import("@playwright/test").Page, + contentType: string, +): Promise { + await openDropdown(page); + await page.locator(`#testing-dashboard-create-dropdown-${contentType}`).click(); + await expect(page.getByRole("dialog")).toBeVisible(); +} + +When("I open the {string} dropdown", async ({ page }, _name: string) => { + await openDropdown(page); +}); + +Given("I have opened the {string} dropdown", async ({ page }, _name: string) => { + await openDropdown(page); +}); + +Then("the content type list should be hidden", async ({ page }) => { + await expect(page.locator(CONTENT_TYPE_LIST)).toHaveClass(/content-type-list--hidden/); +}); + +Then("the content type list should be visible", async ({ page }) => { + await expect(page.locator(CONTENT_TYPE_LIST)).not.toHaveClass(/content-type-list--hidden/); +}); + +Then("I should see the {string} option", async ({ page }, label: string) => { + await expect(page.getByText(label, { exact: true })).toBeVisible(); +}); + +When("I choose the {string} content type", async ({ page }, contentType: string) => { + await page.locator(`#testing-dashboard-create-dropdown-${contentType}`).click(); +}); + +Given("I have opened the stub modal for a/an {string}", async ({ page }, contentType: string) => { + await openStubModalFor(page, contentType); +}); + +Then("the stub modal should open with title {string}", async ({ page }, title: string) => { + await expect(page.getByRole("dialog")).toBeVisible(); + await expect(page.locator(".modal-title")).toHaveText(title); +}); + +Then("the working title field should be empty", async ({ page }) => { + await expect(page.locator("#stub_title")).toHaveValue(""); +}); + +When("I cancel the stub modal", async ({ page }) => { + await page.getByRole("button", { name: "Cancel" }).click(); +}); + +Then("the stub modal should be closed", async ({ page }) => { + await expect(page.getByRole("dialog")).toBeHidden(); +}); + +When("I enter the working title {string}", async ({ page }, title: string) => { + await page.locator("#stub_title").fill(title); +}); + +When("I select the {string} section", async ({ page }, section: string) => { + await page.locator("#stub_section").selectOption({ label: section }); +}); + +Then("the {string} section should be available", async ({ page }, section: string) => { + await expect( + page.locator("#stub_section option").filter({ hasText: section }), + ).toHaveCount(1); +}); + +// In create mode the modal's primary button is "Create new" (#testing-create-in-composer); +// scope by id since the dashboard toolbar also has a "Create new" button. +Then("the create-content button should be enabled", async ({ page }) => { + await expect(page.locator("#testing-create-in-composer")).toBeEnabled(); +}); diff --git a/e2e-tests/steps/fixtures.ts b/e2e-tests/steps/fixtures.ts new file mode 100644 index 000000000..55907307a --- /dev/null +++ b/e2e-tests/steps/fixtures.ts @@ -0,0 +1,37 @@ +import { test as base, createBdd } from "playwright-bdd"; +import type { StackConnection } from "../setup/types.js"; +import { readActiveStackInfo } from "../setup/sharedStack.js"; +import { + createPanDomainCookie, + PAN_DOMAIN_COOKIE_NAME, + type Role, +} from "../setup/panDomainCookie.js"; + +type SignIn = (role?: Role) => Promise; + +interface Fixtures { + stack: StackConnection; + signIn: SignIn; +} + +export const test = base.extend({ + stack: async ({}, use) => { + await use(readActiveStackInfo()); + }, + signIn: async ({ context, stack }, use) => { + const signIn: SignIn = async (role = "default") => { + const value = createPanDomainCookie(stack.panDomainPrivateKeyPem, role); + await context.addCookies([ + { + name: PAN_DOMAIN_COOKIE_NAME, + value, + url: stack.baseUrl, + }, + ]); + }; + await use(signIn); + }, +}); + +export const { Given, When, Then } = createBdd(test); +export { expect } from "@playwright/test"; diff --git a/e2e-tests/tsconfig.json b/e2e-tests/tsconfig.json new file mode 100644 index 000000000..a4031b361 --- /dev/null +++ b/e2e-tests/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "target", ".features-gen"] +} diff --git a/e2e-tests/yarn.lock b/e2e-tests/yarn.lock new file mode 100644 index 000000000..c4c079b52 --- /dev/null +++ b/e2e-tests/yarn.lock @@ -0,0 +1,1848 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aws-sdk/checksums@^3.1001.0": + version "3.1001.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/checksums/-/checksums-3.1001.0.tgz#438766dbc0893c21bcaa9d6872a6bb92ac319752" + integrity sha512-6uTniZc87q+B5eXouGTl+7Tmc482rEeCcvxpsvREP8EfF0gvloRZ41UOA9sbSJlyy8TbqIBXb3kKfKarEArUQA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/client-s3@^3.1061.0": + version "3.1131.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.1131.0.tgz#b3b3a87a35faecc83a10899996db18b369bca660" + integrity sha512-jMw3q5sYNvWJRngkNwdS3H2WPBcdqhdrOez+7fBUB26fpH/2+TaJcDdlzdjgLTMhFTq2eOyXBSy1T/4A6CL/Lw== + dependencies: + "@aws-sdk/checksums" "^3.1001.0" + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/credential-provider-node" "^3.972.83" + "@aws-sdk/middleware-sdk-s3" "^3.972.76" + "@aws-sdk/signature-v4-multi-region" "^3.996.46" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/core@^3.978.0": + version "3.978.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.978.0.tgz#115e5d2e03edd380fe270d4617db0f4cc75869d6" + integrity sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA== + dependencies: + "@aws-sdk/types" "^3.974.5" + "@aws-sdk/xml-builder" "^3.972.40" + "@aws/lambda-invoke-store" "^0.3.0" + "@smithy/core" "^3.33.3" + "@smithy/signature-v4" "^5.6.12" + "@smithy/types" "^4.17.2" + bowser "^2.11.0" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-cognito-identity@^3.972.70": + version "3.972.70" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.70.tgz#62cdac36915468c6c23573c07530ec4200c5c7df" + integrity sha512-KlU89w6Hmb4oZB5zFz/MNIhPOBQGVE7KrDr3BTPCwC4W+q566YH8tGNsAML781LKATtqmNCGFry8XvsJ2XPusg== + dependencies: + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-env@^3.972.71": + version "3.972.71" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.71.tgz#98261bde85bd0f2e2d4a7ba97dce679072e16aff" + integrity sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-http@^3.972.73": + version "3.972.73" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.73.tgz#879466b23dcaec4bee635abf1103e1f5f35d122d" + integrity sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-ini@^3.973.16": + version "3.973.16" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.16.tgz#b51db2069b915d563e05969cac845f624aed1dec" + integrity sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/credential-provider-env" "^3.972.71" + "@aws-sdk/credential-provider-http" "^3.972.73" + "@aws-sdk/credential-provider-login" "^3.972.78" + "@aws-sdk/credential-provider-process" "^3.972.71" + "@aws-sdk/credential-provider-sso" "^3.973.15" + "@aws-sdk/credential-provider-web-identity" "^3.972.77" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/credential-provider-imds" "^4.4.16" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-login@^3.972.78": + version "3.972.78" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.78.tgz#c1c7ca497201a7f2bd6ba7683fa3a896c4e218f8" + integrity sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-node@^3.972.83": + version "3.972.83" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.83.tgz#f4e16b3a1d5d0237460c1b532de37eb6505985dc" + integrity sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ== + dependencies: + "@aws-sdk/credential-provider-env" "^3.972.71" + "@aws-sdk/credential-provider-http" "^3.972.73" + "@aws-sdk/credential-provider-ini" "^3.973.16" + "@aws-sdk/credential-provider-process" "^3.972.71" + "@aws-sdk/credential-provider-sso" "^3.973.15" + "@aws-sdk/credential-provider-web-identity" "^3.972.77" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/credential-provider-imds" "^4.4.16" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-process@^3.972.71": + version "3.972.71" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.71.tgz#5a2ff72bcc2d7a658ca6472193ea33437c8b5a4e" + integrity sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-sso@^3.973.15": + version "3.973.15" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.15.tgz#6a1391ca239f6d505dcb55ea6655b8c14fad5af7" + integrity sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/token-providers" "3.1129.0" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-provider-web-identity@^3.972.77": + version "3.972.77" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.77.tgz#5e288111937fcb6f9f02236fa153095d6e24d22f" + integrity sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/credential-providers@^3.1061.0": + version "3.1131.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-providers/-/credential-providers-3.1131.0.tgz#177fc66391048e81d9c7fd1e254bd941b330567c" + integrity sha512-9AHwJuS5tmTUprvZcC5kQDsqiIp2N8Uu2vk9BlCM+koIn19/8VgKiZXVkResgbIvZ3nS0Km1T2wdpOA76Q7OYQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/credential-provider-cognito-identity" "^3.972.70" + "@aws-sdk/credential-provider-env" "^3.972.71" + "@aws-sdk/credential-provider-http" "^3.972.73" + "@aws-sdk/credential-provider-ini" "^3.973.16" + "@aws-sdk/credential-provider-login" "^3.972.78" + "@aws-sdk/credential-provider-node" "^3.972.83" + "@aws-sdk/credential-provider-process" "^3.972.71" + "@aws-sdk/credential-provider-sso" "^3.973.15" + "@aws-sdk/credential-provider-web-identity" "^3.972.77" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/credential-provider-imds" "^4.4.16" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/middleware-sdk-s3@^3.972.76": + version "3.972.76" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.76.tgz#36906727477c3a638f301eb5abb79b797d7aeb4d" + integrity sha512-NfnTkVUTBKTBuBgqaapFK9r3YdkKt1b2oRvgLzZq91bwNKh6ZS0S7sEcheguttREaL4iyfs/xQnqD7Z7AsWSsA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/signature-v4-multi-region" "^3.996.46" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/nested-clients@^3.997.45": + version "3.997.45" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.997.45.tgz#3ed0e5fcd25c889025b2379a9434594fe7056a4a" + integrity sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/signature-v4-multi-region" "^3.996.46" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/fetch-http-handler" "^5.7.2" + "@smithy/node-http-handler" "^4.11.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/signature-v4-multi-region@^3.996.46": + version "3.996.46" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz#ee57afe5282da4b57968061ef8706d32890267ab" + integrity sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ== + dependencies: + "@aws-sdk/types" "^3.974.5" + "@smithy/signature-v4" "^5.6.12" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/token-providers@3.1129.0": + version "3.1129.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.1129.0.tgz#a4a11a789d7259874ccf3733e538fc70f8a289e4" + integrity sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA== + dependencies: + "@aws-sdk/core" "^3.978.0" + "@aws-sdk/nested-clients" "^3.997.45" + "@aws-sdk/types" "^3.974.5" + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/types@^3.974.5": + version "3.974.5" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.974.5.tgz#a52691531738d191b8b3fd1ae7757c2eeec7afee" + integrity sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ== + dependencies: + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws-sdk/xml-builder@^3.972.40": + version "3.972.40" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz#3cdb5f3400860ad91301aecb87fb5148a66d6ff7" + integrity sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA== + dependencies: + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@aws/lambda-invoke-store@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz#708802d987f8e17bdf4af4de1031660ce1cdd65f" + integrity sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ== + +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cucumber/ci-environment@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@cucumber/ci-environment/-/ci-environment-13.0.0.tgz#0a9c4e279814af864cd1591c4c16f284e14af39b" + integrity sha512-cs+3NzfNkGbcmHPddjEv4TKFiBpZRQ6WJEEufB9mw+ExS22V/4R/zpDSEG+fsJ/iSNCd6A2sATdY8PFOyY3YnA== + +"@cucumber/cucumber-expressions@19.0.0": + version "19.0.0" + resolved "https://registry.yarnpkg.com/@cucumber/cucumber-expressions/-/cucumber-expressions-19.0.0.tgz#562c932b1e6808485e4a45bf9cbcc93cdc3b1d45" + integrity sha512-4FKoOQh2Uf6F6/Ln+1OxuK8LkTg6PyAqekhf2Ix8zqV2M54sH+m7XNJNLhOFOAW/t9nxzRbw2CcvXbCLjcvHZg== + dependencies: + regexp-match-indices "1.0.2" + +"@cucumber/gherkin-utils@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@cucumber/gherkin-utils/-/gherkin-utils-11.0.0.tgz#167afa559978cf6fbe2b583d3d5f9e7c4741c28a" + integrity sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA== + dependencies: + "@cucumber/gherkin" "^38.0.0" + "@cucumber/messages" "^32.0.0" + "@teppeis/multimaps" "3.0.0" + commander "14.0.2" + source-map-support "^0.5.21" + +"@cucumber/gherkin@^38.0.0": + version "38.0.0" + resolved "https://registry.yarnpkg.com/@cucumber/gherkin/-/gherkin-38.0.0.tgz#6c74388f95694e4c92762aeddf3d5638dbedf540" + integrity sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw== + dependencies: + "@cucumber/messages" ">=31.0.0 <33" + +"@cucumber/gherkin@^39.1.0": + version "39.1.0" + resolved "https://registry.yarnpkg.com/@cucumber/gherkin/-/gherkin-39.1.0.tgz#108043bd277cb8c1ae01744cdcdb646b56eecd78" + integrity sha512-pqmSO2bUWxJm3TbNrKXlDaHjL6c77+ez9kWmfCd9oRPeTRPEVH3spZvpAqdXYWOZYSNYwWFCAAeZ4RGpkauNoQ== + dependencies: + "@cucumber/messages" ">=31.0.0 <33" + +"@cucumber/html-formatter@^23.1.0": + version "23.1.0" + resolved "https://registry.yarnpkg.com/@cucumber/html-formatter/-/html-formatter-23.1.0.tgz#6b9f759f9d50355b0cb28edde3ad3580d91f7081" + integrity sha512-DcCSFoGs6jbwzXPgX1CwgJKEE+ZMcIEzq/0Memg0o24maNn9NJizBFHmoFWG4iv/OxHza+mvc+56cTHetfHndw== + +"@cucumber/junit-xml-formatter@^0.13.3": + version "0.13.3" + resolved "https://registry.yarnpkg.com/@cucumber/junit-xml-formatter/-/junit-xml-formatter-0.13.3.tgz#a6ee049caa4afe9160f1514b8c65f24a00e71f42" + integrity sha512-w9ujOxiuKDtU6fLzJz+wp4Sgp5Xu6ba7ls00LHJccVmQU0Ba7zs+AHnv3iIgPjKZAQe1w8x93dr8Gaubh7Vqkg== + dependencies: + "@cucumber/query" "^15.0.1" + "@teppeis/multimaps" "^3.0.0" + luxon "^3.5.0" + xmlbuilder "^15.1.1" + +"@cucumber/messages@>=31.0.0 <33", "@cucumber/messages@^32.0.0", "@cucumber/messages@^32.3.1": + version "32.3.1" + resolved "https://registry.yarnpkg.com/@cucumber/messages/-/messages-32.3.1.tgz#8c6990554c35fb9bcff0b7f09fe52ddfd479dbce" + integrity sha512-yNQq1KoXRYaEKrWMFmpUQX7TdeQuU9jeGgJAZ3dArTsC/T4NpJ6DnqaJIIgwPnz/wtQIQTNX7/h0rOuF5xY4qQ== + dependencies: + class-transformer "0.5.1" + reflect-metadata "0.2.2" + +"@cucumber/query@^15.0.1": + version "15.0.1" + resolved "https://registry.yarnpkg.com/@cucumber/query/-/query-15.0.1.tgz#91b701121ba95caf7d0e6d9de95041ec3396d297" + integrity sha512-FMfT3orJblRsOxvU2doECBvQmauizYlj+5JsM8atAKKPbnQTj7v2/OrnuykvQpfZNBf19DYbRq1e832vllRP/g== + dependencies: + "@teppeis/multimaps" "3.0.0" + lodash.sortby "^4.7.0" + +"@cucumber/tag-expressions@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@cucumber/tag-expressions/-/tag-expressions-9.1.0.tgz#5c63cf716b6d688f140d0e4c0cc858bfd5703618" + integrity sha512-bvHjcRFZ+J1TqIa9eFNO1wGHqwx4V9ZKV3hYgkuK/VahHx73uiP4rKV3JVrvWSMrwrFvJG6C8aEwnCWSvbyFdQ== + +"@esbuild/aix-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz#bf6e10303bcf2e7c686975fa52f937ec2728d8bc" + integrity sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== + +"@esbuild/android-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz#0c6246bc8d2c4d172aac2db3fb1190d72bd65504" + integrity sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== + +"@esbuild/android-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.28.2.tgz#2d84ece6a4e2684d92be26ee13d42757d831c381" + integrity sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== + +"@esbuild/android-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.28.2.tgz#fc38d4d6358d8dc1cf53f09f7589fe436eb64801" + integrity sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== + +"@esbuild/darwin-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz#f83afeeac1d7dac01c7a2fd012b3e451a0591fcc" + integrity sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== + +"@esbuild/darwin-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz#510147c055a795588dbbe14fd6b1b8ad0a2f30de" + integrity sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== + +"@esbuild/freebsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz#093b9200ecf0b115ba4e5e248a7485c9c5f8bd5e" + integrity sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== + +"@esbuild/freebsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz#0be22b6df925d213e841ea87123af5df80b0faf7" + integrity sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== + +"@esbuild/linux-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz#1bdbc651cda9ba9995c53ed9c71ceaa65094762d" + integrity sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== + +"@esbuild/linux-arm@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz#beb12ad72b84f72d28488cc1b8ee9f7eb141d753" + integrity sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== + +"@esbuild/linux-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz#b81f9d55529b45c206a46a138214b1aa6879696b" + integrity sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== + +"@esbuild/linux-loong64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz#598667241a04c99b76ed6ef940ac50038c419f98" + integrity sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== + +"@esbuild/linux-mips64el@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz#1c51eb9cea903f53d97b5af3b1841db70f5596ca" + integrity sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== + +"@esbuild/linux-ppc64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz#63dd61f17ceb31a81227f413feac8a71bc2c51f2" + integrity sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== + +"@esbuild/linux-riscv64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz#3763b08fde5cf25ab1facb8e7752edfe45fbfc27" + integrity sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== + +"@esbuild/linux-s390x@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz#1a137ff293a82906eb3176385bd7e8e0e5cfb7cb" + integrity sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== + +"@esbuild/linux-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz#268b36211c146ca54f8fe12c578a8d6ef8979485" + integrity sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== + +"@esbuild/netbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz#22571ad951d62bb6accc82d8d1fad5c8c1ac0ba1" + integrity sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== + +"@esbuild/netbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz#42fcc57297eb0a0ca3f5fc475291f4c1a3f7c0de" + integrity sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== + +"@esbuild/openbsd-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz#9eb32af104ac3dacf4edca01f596664aab0c73ef" + integrity sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== + +"@esbuild/openbsd-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz#febed2402d6088225e91f20fb4ce2522ad0a4efd" + integrity sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== + +"@esbuild/openharmony-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz#85641c3d466428bfbccea5f21c26836663fef5ce" + integrity sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== + +"@esbuild/sunos-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz#a736f9d8962481045fc4c3e54f5479f22c870fb4" + integrity sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== + +"@esbuild/win32-arm64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz#ee5ab40fad186201b652a33f8a5eb149e9e42532" + integrity sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== + +"@esbuild/win32-ia32@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz#c40d28a6d99a127da6711f2afd74b11cb63b06a7" + integrity sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== + +"@esbuild/win32-x64@0.28.2": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz#b21affb804cc167c133d95f45b3a1dc1323b9a87" + integrity sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== + +"@fastify/busboy@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" + integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA== + +"@grpc/grpc-js@^1.11.1": + version "1.14.4" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.14.4.tgz#e73ff57d97802f063999545f43ebb2b1eca65d9d" + integrity sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ== + dependencies: + "@grpc/proto-loader" "^0.8.0" + "@js-sdsl/ordered-map" "^4.4.2" + +"@grpc/proto-loader@^0.7.13": + version "0.7.15" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.15.tgz#4cdfbf35a35461fc843abe8b9e2c0770b5095e60" + integrity sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.2.5" + yargs "^17.7.2" + +"@grpc/proto-loader@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.8.1.tgz#5a6b290ccbfb1ae2f6775afb74e9898bd8c5d4e8" + integrity sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg== + dependencies: + lodash.camelcase "^4.3.0" + long "^5.0.0" + protobufjs "^7.5.5" + yargs "^17.7.2" + +"@guardian/pan-domain-node@^1.2.5": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@guardian/pan-domain-node/-/pan-domain-node-1.2.5.tgz#2a176d4a300a8652ba4e878cf8e874fc710e9bdd" + integrity sha512-0gCn3ZQASmhhWWw+zP0/W6KbXPRamnwc/LvG7h5wh2qtFyckxcB9TWAibhOEgdodSsXVin1DuQys4qEfbnKvgg== + dependencies: + "@aws-sdk/client-s3" "^3.1061.0" + "@aws-sdk/credential-providers" "^3.1061.0" + cookie "^0.4.1" + ini "6.0.0" + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@js-sdsl/ordered-map@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" + integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@playwright/test@^1.48.0": + version "1.63.0" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.63.0.tgz#808020c46e8b6b91f9d628cd0ebcbb32bdfd8b6c" + integrity sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ== + dependencies: + playwright "1.63.0" + +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.5.tgz#d9315ad7cf3f30aac70bda3c068443dc6f143659" + integrity sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g== + +"@protobufjs/eventemitter@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz#d512cb26c0ae026091ee2c1167f1be6faf5c842a" + integrity sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg== + +"@protobufjs/fetch@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.1.tgz#4d6fc00c8fb64016a5c81b469d549046350f1065" + integrity sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.2.tgz#78d476333d85d5b1c792e257bca74ba080da49a4" + integrity sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug== + +"@smithy/core@^3.33.2", "@smithy/core@^3.33.3": + version "3.34.1" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.34.1.tgz#2fb8133e9f3e99c3f88c8136da2a46cfee4e4fe4" + integrity sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw== + dependencies: + "@smithy/types" "^4.18.0" + tslib "^2.6.2" + +"@smithy/credential-provider-imds@^4.4.16": + version "4.5.2" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz#da3ea9636b5566b36b0761382d841a6d8fdde14f" + integrity sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg== + dependencies: + "@smithy/core" "^3.33.2" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@smithy/fetch-http-handler@^5.7.2": + version "5.8.0" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz#349464e31682d739a74efa42362756be49eb2883" + integrity sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA== + dependencies: + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.18.0" + tslib "^2.6.2" + +"@smithy/node-http-handler@^4.11.3": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz#e4da9379d228517ba85845081e4e6758ca315c5d" + integrity sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw== + dependencies: + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.18.0" + tslib "^2.6.2" + +"@smithy/signature-v4@^5.6.12": + version "5.7.3" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.7.3.tgz#76603bf1ac9aa44e1d2f4f358b0cc0cf84961e46" + integrity sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow== + dependencies: + "@smithy/core" "^3.33.3" + "@smithy/types" "^4.17.2" + tslib "^2.6.2" + +"@smithy/types@^4.17.2", "@smithy/types@^4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.18.0.tgz#f68aca5b5e39f1f9def3827224e4a1e43d233347" + integrity sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A== + dependencies: + tslib "^2.6.2" + +"@teppeis/multimaps@3.0.0", "@teppeis/multimaps@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@teppeis/multimaps/-/multimaps-3.0.0.tgz#bb9c3f8d569f589e548586fa0bbf423010ddfdc5" + integrity sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q== + +"@types/docker-modem@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/docker-modem/-/docker-modem-3.0.6.tgz#1f9262fcf85425b158ca725699a03eb23cddbf87" + integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== + dependencies: + "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.3.35": + version "3.3.47" + resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.47.tgz#cf8c6b4efcd0bb28b0e6009e613e7faab1b96e75" + integrity sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" + "@types/ssh2" "*" + +"@types/node@*", "@types/node@>=13.7.0": + version "26.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-26.5.1.tgz#b19c390e15813f402a94b86e3af9042f792138be" + integrity sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g== + dependencies: + undici-types "~8.9.0" + +"@types/node@^18.11.18": + version "18.19.130" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.130.tgz#da4c6324793a79defb7a62cba3947ec5add00d59" + integrity sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg== + dependencies: + undici-types "~5.26.4" + +"@types/node@^22.9.0": + version "22.20.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.20.2.tgz#daae777b5f5965a587f50cc80d5364c2ddf27e36" + integrity sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw== + dependencies: + undici-types "~6.21.0" + +"@types/ssh2-streams@*": + version "0.1.13" + resolved "https://registry.yarnpkg.com/@types/ssh2-streams/-/ssh2-streams-0.1.13.tgz#f8d34a22be50fb8dbafbb2bbc289add0d22daa51" + integrity sha512-faHyY3brO9oLEA0QlcO8N2wT7R0+1sHWZvQ+y3rMLwdY1ZyS1z0W3t65j9PqT4HmQ6ALzNe7RZlNuCNE0wBSWA== + dependencies: + "@types/node" "*" + +"@types/ssh2@*": + version "1.15.6" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.15.6.tgz#2481573c48c5d42e24d931e6e47d3a67d3d38162" + integrity sha512-oGdxhBqcRTwSTKFm+9EiKzkNVYRLEFkcW44lhguvBalGJbWfGnDt/ezwSUZc+SF9m9bMc3VyklNAtp7zICjS5w== + dependencies: + "@types/node" "^18.11.18" + +"@types/ssh2@^0.5.48": + version "0.5.52" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-0.5.52.tgz#9dbd8084e2a976e551d5e5e70b978ed8b5965741" + integrity sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg== + dependencies: + "@types/node" "*" + "@types/ssh2-streams" "*" + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.2.2: + version "6.3.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.3.0.tgz#247c8e7b70a1a43b10ce14c0226fcbf58e8815d5" + integrity sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.3.tgz#c044d5dcc521a076413472597a1acb1f103c4041" + integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== + +archiver-utils@^5.0.0, archiver-utils@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-5.0.2.tgz#63bc719d951803efc72cf961a56ef810760dd14d" + integrity sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA== + dependencies: + glob "^10.0.0" + graceful-fs "^4.2.0" + is-stream "^2.0.1" + lazystream "^1.0.0" + lodash "^4.17.15" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +archiver@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/archiver/-/archiver-7.0.1.tgz#c9d91c350362040b8927379c7aa69c0655122f61" + integrity sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ== + dependencies: + archiver-utils "^5.0.2" + async "^3.2.4" + buffer-crc32 "^1.0.0" + readable-stream "^4.0.0" + readdir-glob "^1.1.2" + tar-stream "^3.0.0" + zip-stream "^6.0.1" + +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +async-lock@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.4.1.tgz#56b8718915a9b68b10fce2f2a9a3dddf765ef53f" + integrity sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ== + +async@^3.2.4: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +b4a@^1.6.4, b4a@^1.8.1: + version "1.9.0" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.9.0.tgz#6336313b6e76b299c397afa68676ec6ebc788845" + integrity sha512-dpfcF9fDNR6++cthXR67iyhgqWy9CBouAvIWhIntzBG6cvK/cnIPiZQjBwi/ZqjjBEDGfoNDtmB0kTjroOJ3pQ== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +bare-events@^2.5.4, bare-events@^2.7.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.9.2.tgz#01d377b64c3c7167b28b52da7add3221aecf2d7f" + integrity sha512-AIPKioV7/Y/8KfZ3AAhjPJxLLbY49S64Ym5DakZlUg75qQiTgUq9hEJoEwa4eUezPUlXRy/i5NpsKvo9jgKmoA== + +bare-fs@^4.0.1, bare-fs@^4.5.5: + version "4.8.1" + resolved "https://registry.yarnpkg.com/bare-fs/-/bare-fs-4.8.1.tgz#1a946560b45844dc37120c00330eae7c8a1c44cf" + integrity sha512-N1nnXdHZAOSstz0XiHikGS4HGMH4CnSwhqWdGQQMqqdvp4Jybm9sE3R1WVnpWVd4SFkc8ryPDBLViNLwiEqECg== + dependencies: + bare-events "^2.5.4" + bare-path "^3.0.0" + bare-stream "^2.6.4" + bare-url "^2.2.2" + fast-fifo "^1.3.2" + +bare-path@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bare-path/-/bare-path-3.1.2.tgz#67261bc74a9ba0105ba139b8638f218c09e8d82e" + integrity sha512-ZyKbsuuqK6Ag0K8pX6V5Txq6XeJRvY+wXucnFGRjiyVYP9YWDpIQugk/b+enRYrEYBJaqLzghRQpXPMR7341Nw== + +bare-stream@^2.6.4: + version "2.13.4" + resolved "https://registry.yarnpkg.com/bare-stream/-/bare-stream-2.13.4.tgz#61d448a268d1efd992103aae6bd729332f278ac0" + integrity sha512-PcrQ8lVLbiJscNm1Kez+Yp4Gy4AHGcN1lzwjvf5NybWen7VvEgUfyfnXYJ2zNqWnzOfCb1Abq6lH8ti0syQszA== + dependencies: + b4a "^1.8.1" + streamx "^2.25.0" + teex "^1.0.1" + +bare-url@^2.2.2: + version "2.5.4" + resolved "https://registry.yarnpkg.com/bare-url/-/bare-url-2.5.4.tgz#10ab50f39b50335e2aa7672d5941043dbfc7749e" + integrity sha512-Gxa7UVWBr0/edU1b+TJhn/AZvMQUj9OGspvYsaTYQrAbZA4BOTZGL3LiZxvD+CeMlDH4juwD84+eTAp/bLYW5g== + dependencies: + bare-path "^3.0.0" + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bowser@^2.11.0: + version "2.14.1" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.14.1.tgz#4ea39bf31e305184522d7ad7bfd91389e4f0cb79" + integrity sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg== + +brace-expansion@^2.0.1, brace-expansion@^2.0.2: + version "2.1.4" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.1.4.tgz#589dab11c0018d0366be64cd8bf12c8dbecc8326" + integrity sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg== + dependencies: + balanced-match "^1.0.0" + +buffer-crc32@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-1.0.0.tgz#a10993b9055081d55304bd9feb4a072de179f405" + integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +buildcheck@~0.0.6: + version "0.0.7" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.7.tgz#07a5e76c10ead8fa67d9e4c587b68f49e8f29d61" + integrity sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA== + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" + integrity sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q== + +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +class-transformer@0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.5.1.tgz#24147d5dffd2a6cea930a3250a677addf96ab336" + integrity sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw== + +cli-table3@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.5.tgz#013b91351762739c16a9567c21a04632e449bf2f" + integrity sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@14.0.2: + version "14.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-14.0.2.tgz#b71fd37fe4069e4c3c7c13925252ada4eba14e8e" + integrity sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ== + +commander@^13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-13.1.0.tgz#776167db68c78f38dcce1f9b8d7b8b9a488abf46" + integrity sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw== + +compress-commons@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-6.0.2.tgz#26d31251a66b9d6ba23a84064ecd3a6a71d2609e" + integrity sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg== + dependencies: + crc-32 "^1.2.0" + crc32-stream "^6.0.0" + is-stream "^2.0.1" + normalize-path "^3.0.0" + readable-stream "^4.0.0" + +cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cpu-features@~0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.10.tgz#9aae536db2710c7254d7ed67cb3cbc7d29ad79c5" + integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA== + dependencies: + buildcheck "~0.0.6" + nan "^2.19.0" + +crc-32@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" + integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== + +crc32-stream@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-6.0.0.tgz#8529a3868f8b27abb915f6c3617c0fadedbf9430" + integrity sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g== + dependencies: + crc-32 "^1.2.0" + readable-stream "^4.0.0" + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.1, debug@^4.3.5: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + +docker-compose@^0.24.8: + version "0.24.8" + resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.24.8.tgz#6c125e6b9e04cf68ced47e2596ef2bb93ee9694e" + integrity sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw== + dependencies: + yaml "^2.2.2" + +docker-modem@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-5.0.7.tgz#57f3f0e2c7a893e66a0d4a626f9cbc933d77157b" + integrity sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.15.0" + +dockerode@^4.0.5: + version "4.0.12" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-4.0.12.tgz#de286934fbcd801b5034d8169379033d6de14bf3" + integrity sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw== + dependencies: + "@balena/dockerignore" "^1.0.2" + "@grpc/grpc-js" "^1.11.1" + "@grpc/proto-loader" "^0.7.13" + docker-modem "^5.0.7" + protobufjs "^7.3.2" + tar-fs "^2.1.4" + uuid "^10.0.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.5.tgz#7344d711dea40e0b74abc2ed49778743ccedb08c" + integrity sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg== + dependencies: + once "^1.4.0" + +esbuild@~0.28.0: + version "0.28.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.28.2.tgz#0f43bd1bad955b72d24e2261e3abe5957ccf0816" + integrity sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== + optionalDependencies: + "@esbuild/aix-ppc64" "0.28.2" + "@esbuild/android-arm" "0.28.2" + "@esbuild/android-arm64" "0.28.2" + "@esbuild/android-x64" "0.28.2" + "@esbuild/darwin-arm64" "0.28.2" + "@esbuild/darwin-x64" "0.28.2" + "@esbuild/freebsd-arm64" "0.28.2" + "@esbuild/freebsd-x64" "0.28.2" + "@esbuild/linux-arm" "0.28.2" + "@esbuild/linux-arm64" "0.28.2" + "@esbuild/linux-ia32" "0.28.2" + "@esbuild/linux-loong64" "0.28.2" + "@esbuild/linux-mips64el" "0.28.2" + "@esbuild/linux-ppc64" "0.28.2" + "@esbuild/linux-riscv64" "0.28.2" + "@esbuild/linux-s390x" "0.28.2" + "@esbuild/linux-x64" "0.28.2" + "@esbuild/netbsd-arm64" "0.28.2" + "@esbuild/netbsd-x64" "0.28.2" + "@esbuild/openbsd-arm64" "0.28.2" + "@esbuild/openbsd-x64" "0.28.2" + "@esbuild/openharmony-arm64" "0.28.2" + "@esbuild/sunos-x64" "0.28.2" + "@esbuild/win32-arm64" "0.28.2" + "@esbuild/win32-ia32" "0.28.2" + "@esbuild/win32-x64" "0.28.2" + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events-universal@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/events-universal/-/events-universal-1.0.1.tgz#b56a84fd611b6610e0a2d0f09f80fdf931e2dfe6" + integrity sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw== + dependencies: + bare-events "^2.7.0" + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +fast-fifo@^1.2.0, fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + +fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-port@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-7.2.0.tgz#db0d52eb2d89890cdc010ed0e9a6f2d4b78cbbe7" + integrity sha512-afP4W205ONCuMoPBqcR6PSXnzX35KTcJygfJfcp+QY+uwm3p20p1YczWXhlICIzGMCxYBQcySEcOgsJcrkyobg== + +glob@^10.0.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c" + integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + +graceful-fs@^4.2.0, graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +ieee754@^1.1.13, ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ini@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-6.0.0.tgz#efc7642b276f6a37d22fdf56ef50889d7146bf30" + integrity sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-stream@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +lazystream@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== + dependencies: + readable-stream "^2.0.5" + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash@^4.17.15: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== + +long@^5.0.0, long@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/long/-/long-5.3.2.tgz#1d84463095999262d7d7b7f8bfd4a8cc55167f83" + integrity sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA== + +lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +luxon@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" + integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== + +mime-db@^1.54.0: + version "1.54.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" + integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== + +mime-types@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== + dependencies: + mime-db "^1.54.0" + +minimatch@^5.1.0: + version "5.1.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.9.tgz#1293ef15db0098b394540e8f9f744f9fda8dee4b" + integrity sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.9" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e" + integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== + dependencies: + brace-expansion "^2.0.2" + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b" + integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== + +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nan@^2.19.0, nan@^2.23.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.29.0.tgz#95bace50f153289240573c08401acc63a30659d4" + integrity sha512-GlGk3HIvitbvs+LT3g6XUP1kpirKNvmDFwF/bmo6XNWSb/eYEs/O4bfgIEIXCZ+lIOTS5xNwDvSGMw6FJdAhtA== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +picomatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.7.tgz#6313360034ccb36b3dc61ecbdff78121f90fe21f" + integrity sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA== + +playwright-bdd@^9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/playwright-bdd/-/playwright-bdd-9.2.1.tgz#b4bbd053dd503ab038dbfb0e86d6b0875cf95130" + integrity sha512-gi7LTeat3ar1mPO53UBqQIv0HYHaw+GXq7P3stxGLPVlRpbW1ZmWre/yQBV/mtInJgEmKwBrJ/V1zT8NmJOuzg== + dependencies: + "@cucumber/ci-environment" "^13.0.0" + "@cucumber/cucumber-expressions" "19.0.0" + "@cucumber/gherkin" "^39.1.0" + "@cucumber/gherkin-utils" "^11.0.0" + "@cucumber/html-formatter" "^23.1.0" + "@cucumber/junit-xml-formatter" "^0.13.3" + "@cucumber/messages" "^32.3.1" + "@cucumber/query" "^15.0.1" + "@cucumber/tag-expressions" "^9.1.0" + cli-table3 "0.6.5" + commander "^13.1.0" + mime-types "^3.0.2" + tinyglobby "0.2.17" + +playwright-core@1.63.0: + version "1.63.0" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.63.0.tgz#e57665bc32846c213ac39a1e4d5bc6228e76b376" + integrity sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg== + +playwright@1.63.0, playwright@^1.48.0: + version "1.63.0" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.63.0.tgz#99b56f9f69b1b70c44f00bf84b2fe52348ae2511" + integrity sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg== + dependencies: + playwright-core "1.63.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +proper-lockfile@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + +properties-reader@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/properties-reader/-/properties-reader-2.3.0.tgz#f3ab84224c9535a7a36e011ae489a79a13b472b2" + integrity sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw== + dependencies: + mkdirp "^1.0.4" + +protobufjs@^7.2.5, protobufjs@^7.3.2, protobufjs@^7.5.5: + version "7.6.6" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.6.6.tgz#7a3923e8e32b0ee2ff689f8eed6e455c090ac67e" + integrity sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.5" + "@protobufjs/eventemitter" "^1.1.1" + "@protobufjs/fetch" "^1.1.1" + "@protobufjs/float" "^1.0.2" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.1" + "@types/node" ">=13.7.0" + long "^5.3.2" + +pump@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.4.tgz#1f313430527fa8b905622ebd22fe1444e757ab3c" + integrity sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +readable-stream@^2.0.5: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.0.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdir-glob@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.3.tgz#c3d831f51f5e7bfa62fa2ffbe4b508c640f09584" + integrity sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA== + dependencies: + minimatch "^5.1.0" + +reflect-metadata@0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz#400c845b6cba87a21f2c65c4aeb158f4fa4d9c5b" + integrity sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q== + +regexp-match-indices@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz#cf20054a6f7d5b3e116a701a7b00f82889d10da6" + integrity sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ== + dependencies: + regexp-tree "^0.1.11" + +regexp-tree@^0.1.11: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +source-map-support@^0.5.21: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== + +ssh-remote-port-forward@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz#72b0c5df8ec27ca300c75805cc6b266dee07e298" + integrity sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ== + dependencies: + "@types/ssh2" "^0.5.48" + ssh2 "^1.4.0" + +ssh2@^1.15.0, ssh2@^1.4.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.17.0.tgz#dc686e8e3abdbd4ad95d46fa139615903c12258c" + integrity sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.10" + nan "^2.23.0" + +streamx@^2.12.5, streamx@^2.15.0, streamx@^2.25.0: + version "2.28.1" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.28.1.tgz#376cd42a089505a69bec0efdb5189aa45586141f" + integrity sha512-zEzXb0s5Cds7tqMH6rhZ05lcJydCWiQPEwiNngVqzsxCc962vLY4Uw+mW7od8kDH258k2Uz/JrOkdIAAhSh9VA== + dependencies: + events-universal "^1.0.0" + fast-fifo "^1.3.2" + text-decoder "^1.1.0" + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.2.0.tgz#d22a269522836a627af8d04b5c3fd2c7fa3e32e3" + integrity sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w== + dependencies: + ansi-regex "^6.2.2" + +tar-fs@^2.1.4: + version "2.1.5" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.5.tgz#33e9c29413dce0c58ada7ff77db4e5a30afffe70" + integrity sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-fs@^3.0.7: + version "3.1.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-3.1.3.tgz#05668cc68a30741c3813f9c16593b8dec7dcbcd1" + integrity sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ== + dependencies: + pump "^3.0.0" + tar-stream "^3.1.5" + optionalDependencies: + bare-fs "^4.0.1" + bare-path "^3.0.0" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + +tar-stream@^3.0.0, tar-stream@^3.1.5: + version "3.2.1" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.2.1.tgz#952d72f7aba68ce5cb802ef2e0b19f1bd989140d" + integrity sha512-nqsEO8zLZJvrOMdEwkA0QdCLFbetHMn95Zqu4fKwX+hkaTWJPZZOrxx/PwtxoK0MMGQmBQNRW3CPs8IFYQz4cQ== + dependencies: + b4a "^1.6.4" + bare-fs "^4.5.5" + fast-fifo "^1.2.0" + streamx "^2.15.0" + +teex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/teex/-/teex-1.0.1.tgz#b8fa7245ef8e8effa8078281946c85ab780a0b12" + integrity sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg== + dependencies: + streamx "^2.12.5" + +testcontainers@^10.13.0: + version "10.28.0" + resolved "https://registry.yarnpkg.com/testcontainers/-/testcontainers-10.28.0.tgz#73d3757fcd60405c20dd55599ca89e44cadc8ccf" + integrity sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg== + dependencies: + "@balena/dockerignore" "^1.0.2" + "@types/dockerode" "^3.3.35" + archiver "^7.0.1" + async-lock "^1.4.1" + byline "^5.0.0" + debug "^4.3.5" + docker-compose "^0.24.8" + dockerode "^4.0.5" + get-port "^7.1.0" + proper-lockfile "^4.1.2" + properties-reader "^2.3.0" + ssh-remote-port-forward "^1.0.4" + tar-fs "^3.0.7" + tmp "^0.2.3" + undici "^5.29.0" + +text-decoder@^1.1.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.2.7.tgz#5d073a9a74b9c0a9d28dfadcab96b604af57d8ba" + integrity sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ== + dependencies: + b4a "^1.6.4" + +tinyglobby@0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== + dependencies: + fdir "^6.5.0" + picomatch "^4.0.4" + +tmp@^0.2.3: + version "0.2.7" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.7.tgz#26f4db11d1601ce8012dcb8a798ece1c06a99059" + integrity sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw== + +tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +tsx@^4.19.0: + version "4.23.13" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-4.23.13.tgz#393b8dd813d49e25aed7b1d7cbac6930da49ec08" + integrity sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw== + dependencies: + esbuild "~0.28.0" + optionalDependencies: + fsevents "~2.3.3" + +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +typescript@^5.6.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +undici-types@~8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-8.9.0.tgz#e240d97c8b5d85e5347ce73d25865c7906c1ec9f" + integrity sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg== + +undici@^5.29.0: + version "5.29.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.29.0.tgz#419595449ae3f2cdcba3580a2e8903399bd1f5a3" + integrity sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg== + dependencies: + "@fastify/busboy" "^2.0.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +uuid@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294" + integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yaml@^2.2.2: + version "2.9.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.1.tgz#c16233fb31944e705cfefaff38795587f57588ce" + integrity sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.3.tgz#779dffe6bcafec596a7172e983289a588647faaa" + integrity sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +zip-stream@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-6.0.1.tgz#e141b930ed60ccaf5d7fa9c8260e0d1748a2bbfb" + integrity sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA== + dependencies: + archiver-utils "^5.0.0" + compress-commons "^6.0.2" + readable-stream "^4.0.0"