Skip to main content

Test DXP End-to-End

This guide walks through manual test scenarios for the DXP auth flow, covering the happy path and key failure cases. It is intended for developers verifying a local or deployed environment, and for QA engineers writing test plans.


Prerequisites checklist

Before running any scenario, confirm all of the following:

  • Provider is running with dxp.mode = "provider" in stack9.config.json
  • Client is running with dxp.mode = "client" in stack9.config.json
  • Both instances have APP_DXP_CODE_EXCHANGE_SECRET set to the same value
  • Provider has SERVER_BASE_URL set to its public-facing URL
  • Client has APP_DXP_PROVIDER_EXCHANGE_URL set to the Provider's URL reachable from the Client server (especially important in Docker — see the Client configuration guide)
  • Client bootstrap has run: node main.js bootstrap (look for bootstrap.dxp.ready in logs)
  • The DXP app keys configured on the Provider (mkt, email, pages) are also listed in the Client's dxp.apps
  • Access to server logs for both Provider and Client (tail the logs during testing)
  • Browser dev tools open (Network tab)

Test 1: Happy path — tile click → auto sign-in

Goal: Verify the complete flow from tile click to authenticated session on the Client.

Steps:

  1. Log in to the Provider instance as a user with Privileged (3) role or above.
  2. Open the navigation bar — confirm DXP tiles appear (e.g. "Marketing", "Email", "Pages").
  3. Click the Marketing tile.
  4. Observe the browser URL — it should briefly pass through {clientUrl}/api/auth/dxp/callback?code=....
  5. The browser should land at {clientUrl}/mkt (or the configured redirect path).

Expected result:

  • No login prompt on the Client.
  • The Marketing module loads with data scoped to the tenant.
  • Session cookie api-token is present in the browser (visible in DevTools → Application → Cookies). It should be httpOnly (not readable by JavaScript) in non-local environments.

Provider logs to look for:

dxp.provider.code.issued

Client logs to look for:

dxp.client.callback.received
dxp.client.callback.exchange_host ← only if DXP_PROVIDER_EXCHANGE_URL is set
dxp.client.session.created

Test 2: First login — user provisioning

Goal: Verify that a user logging in via DXP for the first time is automatically provisioned on the Client.

Steps:

  1. Use an email address that does not exist in the Client's database.
  2. Log in to the Provider with that user account.
  3. Click a DXP tile.

Expected result:

  • The user arrives at the Client and is signed in (no error).
  • In the Client database: a new user row exists with the email, is_active = true, is_administrator = false, and security_role set to "DXP User".
  • The user is a member of the dxp-users user group.

Client logs to look for:

dxp.client.user.created
dxp.client.user.backfilled_to_dxp_group
dxp.client.session.created

Test 3: Existing user — group backfill

Goal: Verify that a user who already exists on the Client but is not yet in dxp-users is backfilled into the group on their first DXP login.

Steps:

  1. Create a user on the Client directly (via the admin UI or DB) with any security role.
  2. Confirm the user is not a member of dxp-users.
  3. Use the same email to log in to the Provider and click a DXP tile.

Expected result:

  • The user arrives at the Client and is signed in.
  • The user is now a member of dxp-users.

Client logs to look for:

dxp.client.user.backfilled_to_dxp_group
dxp.client.session.created

Test 4: Expired code

Goal: Verify that codes older than the TTL (default: 30 seconds) are rejected.

Steps:

  1. Click a DXP tile on the Provider.
  2. Copy the code query parameter from the redirect URL before the browser follows it (you may need to slow down the redirect — e.g. use browser network throttling or temporarily break the Client).
  3. Wait 35+ seconds.
  4. Manually navigate to {clientUrl}/api/auth/dxp/callback?code={code}&origin={providerUrl}&redirect=/.

Expected result:

  • Browser is redirected back to the Provider with ?dxp_error=session_expired.
  • No session is created on the Client.

Client logs to look for:

dxp.client.callback: provider rejected the code exchange

Test 5: Invalid exchange secret

Goal: Verify that a mismatched APP_DXP_CODE_EXCHANGE_SECRET is rejected.

Steps:

  1. Temporarily change APP_DXP_CODE_EXCHANGE_SECRET on the Client to any different value (without restarting the Provider).
  2. Restart the Client webserver.
  3. Click a DXP tile on the Provider.

Expected result:

  • Browser is redirected back to the Provider with ?dxp_error=session_expired.
  • No session is created on the Client.

Provider logs to look for:

dxp.provider.code.exchange.rejected  {reason: "invalid_secret"}

Client logs to look for:

dxp.client.callback: provider rejected the code exchange  {status: 401}

Restore the correct secret and restart the Client before continuing.


Test 6: Blocked or inactive user

Goal: Verify that blocked or inactive users cannot gain a session on the Client.

Steps:

  1. Create (or find) a user on the Client whose email matches a Provider user.
  2. Set is_blocked = true on that Client user (via admin UI or DB).
  3. Log in to the Provider as that user and click a DXP tile.

Expected result:

  • Browser is redirected back to the Provider with ?dxp_error=access_denied.
  • No session is created on the Client.

Client logs to look for:

dxp.client.callback: user account is blocked

Repeat with is_active = false — the result should be the same (access_denied).


Test 7: Code reuse (single-use enforcement)

Goal: Verify that a code cannot be used twice.

Steps:

  1. Intercept the redirect URL from a tile click (before the browser follows it).
  2. Let the browser follow the redirect normally — the session is created.
  3. In a new browser tab or via curl, navigate to the same callback URL with the same code.

Expected result:

  • The second request is rejected with a redirect to the Provider including ?dxp_error=session_expired.
  • No second session is created.

Test 8: API key never visible in browser

Goal: Verify the tenant API key is never exposed client-side at any point.

Steps:

  1. Open Browser DevTools → Network tab before clicking a DXP tile.
  2. Click any DXP tile and let the flow complete.
  3. After landing on the Client, inspect all network requests made during the flow.
  4. Search all request URLs, response bodies, cookies, and local storage for the value of APP_S9_APIS_API_KEY from the Provider.

Expected result:

  • The API key is not present in any browser-observable location — not in URLs, response bodies, cookies accessible to JavaScript, or local storage.
  • The api-token session cookie is present and marked httpOnly (and secure in non-local environments).

Diagnosing common issues

ECONNREFUSED connecting to localhost:3333

Symptom: Client logs show ECONNREFUSED 127.0.0.1:3333 when the browser-supplied origin is http://localhost:3333.

Cause: APP_DXP_PROVIDER_EXCHANGE_URL is not set on the Client. The Client is trying to reach the Provider at the browser-visible localhost:3333, which resolves to the Client container itself in a containerised deployment.

Fix: Set APP_DXP_PROVIDER_EXCHANGE_URL to the Provider's internal service URL and restart the Client.

Look for this warning in Client logs (emitted when APP_DXP_PROVIDER_EXCHANGE_URL is unset):

dxp.client.callback: DXP_PROVIDER_EXCHANGE_URL not set — falling back to browser-supplied origin...

DXP user group not found — misconfigured

Symptom: Client callback returns misconfigured error and redirects the browser back to the Provider.

Client logs:

dxp.client.callback: DXP user group not found — bootstrap may not have run for this instance

Cause: The dxp-users user group doesn't exist in the Client database. Bootstrap has not run for this instance (or ran against a different database).

Fix: Run node main.js bootstrap against the Client instance and verify the log shows bootstrap.dxp.ready.


403: You have no access to run {queryName}

Symptom: After signing in to the Client, navigating to a screen returns a 403 error on POST /api/execute-query.

Cause: Screen query roles are above Privileged (3). This happens when bootstrap ran before the ensureScreenQueryPermissions step existed, or when new screens were added after the last bootstrap run.

Fix: Re-run node main.js bootstrap. Look for bootstrap.dxp.screen_permission.capped log entries confirming query roles were capped.

Diagnostic: Also check the Client webserver logs for:

[appPermission] No role found for app "mkt" in user roles. Available role keys: ...

This indicates the JWT was issued without a role for the mkt app key — the DXP user group may not have been correctly associated with the user.


Blank page after successful sign-in

Symptom: The user lands on a DXP module page (e.g. /mkt/marketing-campaign) but the page is blank — no error, no data, no components visible.

Cause: The screen definition's component tree is empty (nodes: [] in the screen JSON). This is a content configuration issue, not a backend error — no queries are triggered when there are no UI components.

Fix: Add UI components to the screen via the Stack9 visual editor. Verify by checking the response to GET /api/screen/by-route/{appKey}/{screenRoute} — if components.ROOT.nodes is an empty array, the screen has no content.


Key structured log events reference

Log eventWhereMeaning
dxp.provider.code.issuedProviderCode generated and stored in Redis
dxp.provider.code.exchange.rejectedProviderExchange request rejected (bad secret, expired/used code)
dxp.client.callback.receivedClientCallback endpoint invoked with code and origin
dxp.client.callback.exchange_hostClientExchange host resolved (DXP_PROVIDER_EXCHANGE_URL set)
dxp.client.callback: DXP_PROVIDER_EXCHANGE_URL not setClientWarning — falling back to browser origin for exchange
dxp.client.user.createdClientNew user provisioned on first DXP login
dxp.client.user.backfilled_to_dxp_groupClientExisting user added to dxp-users group
dxp.client.session.createdClientJWT signed and session cookie set
bootstrap.dxp.readyClient (bootstrap)Bootstrap completed successfully
bootstrap.dxp.user_group.createdClient (bootstrap)dxp-users group created
bootstrap.dxp.security_role.createdClient (bootstrap)DXP User role created
bootstrap.dxp.app_permission.updatedClient (bootstrap)Existing app_permissions patched for DXP group
bootstrap.dxp.app_permission.createdClient (bootstrap)New app_permissions row created
bootstrap.dxp.screen_permission.cappedClient (bootstrap)Screen query roles capped from Admin to Privileged