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"instack9.config.json - Client is running with
dxp.mode = "client"instack9.config.json - Both instances have
APP_DXP_CODE_EXCHANGE_SECRETset to the same value - Provider has
SERVER_BASE_URLset to its public-facing URL - Client has
APP_DXP_PROVIDER_EXCHANGE_URLset 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 forbootstrap.dxp.readyin logs) - The DXP app keys configured on the Provider (
mkt,email,pages) are also listed in the Client'sdxp.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:
- Log in to the Provider instance as a user with Privileged (3) role or above.
- Open the navigation bar — confirm DXP tiles appear (e.g. "Marketing", "Email", "Pages").
- Click the Marketing tile.
- Observe the browser URL — it should briefly pass through
{clientUrl}/api/auth/dxp/callback?code=.... - 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-tokenis present in the browser (visible in DevTools → Application → Cookies). It should behttpOnly(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:
- Use an email address that does not exist in the Client's database.
- Log in to the Provider with that user account.
- Click a DXP tile.
Expected result:
- The user arrives at the Client and is signed in (no error).
- In the Client database: a new
userrow exists with the email,is_active = true,is_administrator = false, andsecurity_roleset to "DXP User". - The user is a member of the
dxp-usersuser 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:
- Create a user on the Client directly (via the admin UI or DB) with any security role.
- Confirm the user is not a member of
dxp-users. - 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:
- Click a DXP tile on the Provider.
- Copy the
codequery 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). - Wait 35+ seconds.
- 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:
- Temporarily change
APP_DXP_CODE_EXCHANGE_SECRETon the Client to any different value (without restarting the Provider). - Restart the Client webserver.
- 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:
- Create (or find) a user on the Client whose email matches a Provider user.
- Set
is_blocked = trueon that Client user (via admin UI or DB). - 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:
- Intercept the redirect URL from a tile click (before the browser follows it).
- Let the browser follow the redirect normally — the session is created.
- In a new browser tab or via
curl, navigate to the same callback URL with the samecode.
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:
- Open Browser DevTools → Network tab before clicking a DXP tile.
- Click any DXP tile and let the flow complete.
- After landing on the Client, inspect all network requests made during the flow.
- Search all request URLs, response bodies, cookies, and local storage for the value of
APP_S9_APIS_API_KEYfrom 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-tokensession cookie is present and markedhttpOnly(andsecurein 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 event | Where | Meaning |
|---|---|---|
dxp.provider.code.issued | Provider | Code generated and stored in Redis |
dxp.provider.code.exchange.rejected | Provider | Exchange request rejected (bad secret, expired/used code) |
dxp.client.callback.received | Client | Callback endpoint invoked with code and origin |
dxp.client.callback.exchange_host | Client | Exchange host resolved (DXP_PROVIDER_EXCHANGE_URL set) |
dxp.client.callback: DXP_PROVIDER_EXCHANGE_URL not set | Client | Warning — falling back to browser origin for exchange |
dxp.client.user.created | Client | New user provisioned on first DXP login |
dxp.client.user.backfilled_to_dxp_group | Client | Existing user added to dxp-users group |
dxp.client.session.created | Client | JWT signed and session cookie set |
bootstrap.dxp.ready | Client (bootstrap) | Bootstrap completed successfully |
bootstrap.dxp.user_group.created | Client (bootstrap) | dxp-users group created |
bootstrap.dxp.security_role.created | Client (bootstrap) | DXP User role created |
bootstrap.dxp.app_permission.updated | Client (bootstrap) | Existing app_permissions patched for DXP group |
bootstrap.dxp.app_permission.created | Client (bootstrap) | New app_permissions row created |
bootstrap.dxp.screen_permission.capped | Client (bootstrap) | Screen query roles capped from Admin to Privileged |