Skip to content

test(serenity): API-level e2e against the Semrush vendor mocks#2709

Open
rainer-friederich wants to merge 9 commits into
mainfrom
tests
Open

test(serenity): API-level e2e against the Semrush vendor mocks#2709
rainer-friederich wants to merge 9 commits into
mainfrom
tests

Conversation

@rainer-friederich

Copy link
Copy Markdown
Contributor

1. Abstract

Adds API-level end-to-end tests for the /serenity/* routes that drive the real controller through the public Semrush vendor mocks (Project Engine + User Manager) in the postgres integration-test harness, replacing the prior 400/401-only contract suite.

2. Reasoning

The /serenity/* surface sits behind IMS-only authentication and a live Semrush gateway, so the existing integration tests could only assert the route gate (400) and the IMS-only contract (401) — never the actual handler behaviour. Now that the Semrush Project Engine and User Manager APIs are published as stateful Counterfact mock Docker images, the routes can be exercised end to end, both locally and in CI, without a real IMS account or a real vendor.

3. High-level overview of the changes

  • The integration-test stack now boots the two Semrush mocks alongside Postgres/PostgREST and MinIO, and the serenity suite drives the controller through them over HTTPS.
  • Auth (test-only): requireImsBearer honours a SERENITY_ALLOW_NON_IMS_AUTH flag that skips the IMS-type gate so the harness's locally-signed JWT can reach the handlers. This is sound only against the mocks, which do not validate the forwarded bearer (the token value never matters). The flag is never written to Vault / any deployed environment — it mirrors the existing SERENITY_ALLOW_WORKSPACE_DELETE opt-in.
  • Base-URL split (issue 2656): the User Manager gateway origin is resolved from SEMRUSH_USERS_BASE_URL, falling back to SEMRUSH_PROJECTS_BASE_URL when unset. This lets the two mocks live on separate hosts with no path-routing reverse proxy. Production/stage/dev (a single shared host today) are unchanged via the fallback.
  • Version-alignment convention: the mock image tag is derived from the installed client dependency version — the harness reads @adobe/spacecat-shared-project-engine-client / -user-manager-client from node_modules and exports SERENITY_PE_MOCK_TAG / SERENITY_UM_MOCK_TAG, which the compose interpolates. The mock is published from the same package as the typed client, so this guarantees the integration test exercises the exact contract version we ship; it can never silently drift. If a client is bumped to a version whose mock image is not yet published, the image pull fails loudly — by design, forcing the mock to be released in lockstep.
  • Dependency bump: the two Semrush clients are moved to 1.3.0, the version for which the mock images are published (and the version whose typed surface the transport already targets).
  • First test increment: route-gate validation, the brand-independent model/language catalog read live through the Project Engine mock, and brand resolution after the relaxed auth. The mutating sub-workspace flows (activate/deactivate, market create/delete) are the next increment; resetSemrushMocks is already wired for them.

4. Required information

5. Affected / used mysticat-workspace projects

  • spacecat-shared — consumed. Provides both the typed Semrush clients and the mock Docker images they are published alongside. This PR bumps the two clients to 1.3.0 and pins the mock images to the same version. No change to spacecat-shared is required (the 1.3.0 clients and mocks are already published).

6. Additional information outside the code

  • The serenity integration suite was run end to end through the harness against the real mock containers (pulled anonymously from GHCR): the route gate, the org-level model and language catalogs returning live data through the Project Engine mock, and brand resolution proceeding past the relaxed auth. It was also run alongside a sibling integration suite to confirm the always-on mocks and the global integration-test env do not regress other integration tests.
  • The Project Engine mock's catalog endpoints were smoke-tested directly over HTTPS to confirm the served shapes match what the controller reads.

7. Test plan

  • Local: bring up the integration stack and run the serenity suite — npx mocha --require test/it/postgres/harness.js --timeout 60000 test/it/postgres/serenity.test.js — which boots both mocks (tag derived from the installed client version), points the transport at them, and drives the routes through to the live mock responses. Verified green this session.
  • CI: runs automatically through the mysticat-ci reusable workflow's existing it-postgres target — it brings up the same compose (the mock images are public, so no registry credentials are needed) with no workflow change.
  • dev / stage / prod: nothing to verify — this is test-only infrastructure. The only production-path code is the base-URL split, which is a no-op while SEMRUSH_USERS_BASE_URL is unset (fallback to the existing host); the auth flag and the split target are never set in any deployed environment.

🤖 Generated with Claude Code

rainer-friederich and others added 4 commits June 26, 2026 14:29
Aligns the typed Semrush clients with the published Counterfact mock Docker
images (only 1.3.0 is published for both), so the serenity IT exercises the
SAME contract version we ship. The bump also resolves the `init_status`
typed-path that was missing from the prior 1.2.0/1.1.0 client surface.

Both are minor, backward-compatible bumps within 1.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add usersBaseUrl(env) reading SEMRUSH_USERS_BASE_URL, falling back to
SEMRUSH_PROJECTS_BASE_URL when unset, and route the typed User Manager
client through it. Factors the trim/https/origin-normalize validation into
a shared normalizeBaseUrl() reused by both resolvers (https kept).

Lets the User Manager calls point at a separate (mock) host independently of
Project Engine — no path-routing reverse proxy — which unblocks local +
automated E2E of the serenity sub-workspace flows. Prod/stage/dev Vault
config (single host today) keeps working untouched via the fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive the /serenity/* controller end to end through the public GHCR
Counterfact mocks (project-engine + user-manager) in the postgres IT
harness, replacing the prior 400/401-only contract suite.

- Boot both mocks via the IT docker-compose (own HTTPS port each); the image
  tag is the INSTALLED client version (setup.js exports SERENITY_*_MOCK_TAG)
  so the mock can never drift from the client we ship. env.js points
  SEMRUSH_PROJECTS_BASE_URL / SEMRUSH_USERS_BASE_URL at them with
  NODE_TLS_REJECT_UNAUTHORIZED=0 for their self-signed certs.
- requireImsBearer honours a test-only SERENITY_ALLOW_NON_IMS_AUTH flag so
  the harness JWT reaches the handlers; sound only vs the mocks, which ignore
  the forwarded bearer. No deployed environment sets the flag (mirrors
  SERENITY_ALLOW_WORKSPACE_DELETE).
- setup.js waits on the mock control routes and exposes resetSemrushMocks;
  the factory seeds via resetPostgres.
- First increment: route-gate 400s, org-level model/language catalog live via
  the Project Engine mock, and brand-resolution after the relaxed auth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…topContainers

The IT harness now boots the full stack (Postgres + PostgREST, MinIO, and the
two Semrush mocks), so the postgres-specific name was misleading.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.96970% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/controllers/serenity.js 90.32% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

# Conflicts:
#	package-lock.json
#	package.json
@github-actions

Copy link
Copy Markdown

This PR will trigger a patch release when merged.

rainer-friederich and others added 2 commits June 26, 2026 14:44
…missing mock tag

- requireImsBearer: document that the forwarded IMS bearer is validated AGAIN by
  the real Semrush gateway on every upstream call — this proxy is a fail-fast
  shape guard, not the auth boundary. Makes explicit why the test-only IMS-type
  relaxation never weakens production auth.
- Remove the hardcoded `:-1.3.0` fallback on the mock image tags. The compose now
  uses `${...:?}` and fails hard when the tag is unset, and setup.js throws if the
  installed client version cannot be resolved. A silent default would test a
  different version than the client we ship.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nfig

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rainer-friederich rainer-friederich marked this pull request as ready for review June 26, 2026 12:50

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rainer-friederich,

Verdict: Approve - well-structured test infrastructure with sound production changes; only non-blocking notes below.
Complexity: HIGH - large diff; dependency bump + API surface change.
Changes: Adds API-level e2e tests for /serenity/* routes against Semrush vendor mocks, splits the User Manager base URL, and bumps Semrush clients to 1.3.0 (12 files).
Note: CI checks are currently failing - resolve before merge.

Non-blocking (5): minor issues and suggestions
  • nit: resetSemrushMocks() swallows all errors with .catch(() => {}) - when the mutating-lifecycle tests land (next increment), a silently failed reset will produce flaky order-dependent tests. Consider checking response.ok and throwing on non-2xx. - test/it/postgres/setup.js:141
  • suggestion: The SERENITY_ALLOW_NON_IMS_AUTH env flag bypasses the IMS-type gate at runtime with no deploy-time structural guard. The defense-in-depth model (Semrush validates upstream) makes this acceptable today, but a debug-level log when the flag is active would make accidental enablement visible in production logs without adding a hard gate. - src/controllers/serenity.js:199
  • nit: The old IT suite tested 401 on each endpoint individually (markets, prompts, tags, models, activate, deactivate, DELETE). The new suite proves auth bypass via a single 404 assertion. Unit tests still cover per-endpoint enforcement, so this is a noted tradeoff rather than a gap - consider adding one more brand-level endpoint (e.g. GET prompts) to the e2e suite for breadth. - test/it/shared/tests/serenity.js
  • nit: GET /serenity/languages asserts only expect(res.body).to.be.an('object') - passes for any JSON object including error responses. Adding expect(res.body).to.have.property('items') would catch schema drift. - test/it/shared/tests/serenity.js:72
  • nit: usersBaseUrl silently falls back to SEMRUSH_PROJECTS_BASE_URL when SEMRUSH_USERS_BASE_URL is unset. A debug-level log on fallback would help distinguish "intentionally shared host" from "forgot to set the new var" during troubleshooting. - src/support/serenity/rest-transport.js:209

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 59s | Cost: $9.16 | Commit: a467c9c37c5378c132b6fc998089e47f30bd2674
If this code review was useful, please react with 👍. Otherwise, react with 👎.

rainer-friederich and others added 2 commits June 26, 2026 15:16
Add a "Serenity E2E" section explaining how the two Counterfact mock containers
are wired here (version-pinned image tags, base-URL split, the test-only auth
flag, statefulness/reset, seed alignment) and where they are developed — the
upstream spacecat-shared mock docs and source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- resetSemrushMocks: throw on a non-2xx reset instead of swallowing it, so the
  mutating-lifecycle increment can't produce flaky order-dependent tests.
- requireImsBearer flag: log once (warn) when SERENITY_ALLOW_NON_IMS_AUTH is
  enabled, so an accidental deployed-env enablement is visible in the logs.
- IT: strengthen the /serenity/languages assertion to check the { items: [...] }
  shape, and add a second brand-level e2e (GET prompts) for route breadth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rainer-friederich

Copy link
Copy Markdown
Contributor Author

Thanks @MysticatBot — addressed the non-blocking notes in 40366566 (all 7 serenity IT cases green against the mocks). Per-item:

  • resetSemrushMocks swallowed errors — fixed. It now throws on a non-2xx reset instead of .catch(() => {}), so a failed reset surfaces immediately rather than producing flaky order-dependent tests when the mutating-lifecycle increment lands.
  • SERENITY_ALLOW_NON_IMS_AUTH visibility — fixed. Logs once per process when the flag is active. Used warn rather than debug on purpose: the whole point is to surface an accidental enablement in a deployed env, where the default log level would drop debug. Gated to one line per process so it isn't per-request noise.
  • More brand-level e2e breadth — fixed. Added a second brand-level case (GET /serenity/prompts with query params) alongside markets, both proving the relaxed-auth path reaches the handler (404 on an unknown brand, not 401/500).
  • /serenity/languages weak assertion — fixed. Now asserts the { items: [...] } envelope (non-empty), matching the models check, so an error body slipping through as 200 would fail.
  • usersBaseUrl silent fallback log — declined. The fallback to SEMRUSH_PROJECTS_BASE_URL is the intended default and the production path today (single shared host), so it fires on every request; logging it even at debug is per-request noise for the normal case. usersBaseUrl is also a pure typed helper with no logger injected (kept clean per this dir's type-check convention), and the fallback is already documented in its JSDoc. A "forgot to set it" mistake is harmless here — it resolves to the same host that works today.

@MysticatBot MysticatBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rainer-friederich,

Verdict: Approve - all prior non-blocking findings addressed; no new issues found on re-review.
Complexity: HIGH - large diff; dependency bump + API surface + infrastructure signals.
Changes: Addresses prior review nits - adds warn log for auth flag, strengthens test assertions, adds GET prompts breadth test, and fixes resetSemrushMocks error propagation (12 files).
Note: CI checks are currently pending - resolve before merge.

Previously flagged, now resolved

  • resetSemrushMocks now throws on non-2xx instead of swallowing errors
  • SERENITY_ALLOW_NON_IMS_AUTH logs once at warn level when active (surfaces accidental enablement)
  • Added GET /serenity/prompts brand-level e2e test for breadth
  • /serenity/languages assertion strengthened to check { items: [...] } envelope
  • usersBaseUrl fallback log declined (sound reasoning: normal production path, typed helper with no logger, documented in JSDoc)
Non-blocking (3): minor issues and suggestions
  • nit: CLAUDE.md documents --timeout 30000 for the full IT glob, but the serenity suite was run with --timeout 60000 in the test plan. The harness hook timeouts are independently managed, so the documented command likely works, but consider adding a note for the serenity-specific timeout or updating the value. - CLAUDE.md
  • nit: NODE_TLS_REJECT_UNAUTHORIZED save/restore in waitForSemrushMocks() and resetSemrushMocks() is redundant given buildEnv() already sets it to '0' for the entire IT process. The save/restore implies scoped mutation that is not actually needed. - test/it/postgres/setup.js:100
  • suggestion: The "relaxed auth" 404 tests only assert status code. Asserting the body shape (e.g., res.body.error === 'notFound') would distinguish "handler ran and returned a clean 404" from a generic middleware 404 for an unmatched route. - test/it/shared/tests/serenity.js:85

Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 41s | Cost: $7.90 | Commit: 40366566cd629331394dd9d0591914237d158317
If this code review was useful, please react with 👍. Otherwise, react with 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-reviewed Reviewed by AI complexity:high AI-assessed PR complexity: HIGH

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants