Multi-step Auth (OTP)
Mock a login flow that requires OTP confirmation before it succeeds — no code changes, four scenarios
Multi-step Auth (OTP)
A common real flow: the app calls POST /login, the backend answers "OTP required", the user types the code, the app calls POST /verify-otp, and only then does login succeed.
Mock Alpha can serve that whole flow today, with match rules and no new feature. What makes it work is one observation:
The second
/loginrequest is not the same request as the first. It carries the token that/verify-otphanded out. That difference is what the rules key off.
This mirrors how real login+OTP backends behave — the pre-auth token exists precisely so the server can tell the two calls apart.
The four scenarios
Two endpoints, four scenarios:
| Endpoint | Scenario | Match rule | Priority | Response |
|---|---|---|---|---|
POST /login | need-otp | body.otpToken not_exists | 1 | 200 { "requiresOtp": true, "otpToken": "..." } |
POST /login | success | body.otpToken exists | 2 | 200 { "accessToken": "..." } |
POST /verify-otp | otp-ok | body.otp eq 123456 | 1 | 200 { "verified": true, "otpToken": "..." } |
POST /verify-otp | otp-wrong | (default scenario) | — | 400 { "error": "Invalid OTP" } |
need-otp and success are mutually exclusive by construction: a request either carries otpToken or it does not.
Walkthrough
1. First login — no token yet, so OTP is demanded
curl -X POST https://mock-alpha.aprix.site/api/mock/login \
-H "X-Api-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{ "email": "tester@example.com", "password": "hunter2" }'body.otpToken is absent → need-otp wins:
{ "requiresOtp": true, "otpToken": "pre-auth-abc123" }2. Verify the code
curl -X POST https://mock-alpha.aprix.site/api/mock/verify-otp \
-H "X-Api-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{ "otp": "123456", "otpToken": "pre-auth-abc123" }'otp equals 123456 → otp-ok. Any other code falls through to the default scenario and gets 400, which is exactly the wrong-code path you want to test.
3. Second login — now it carries the token
curl -X POST https://mock-alpha.aprix.site/api/mock/login \
-H "X-Api-Key: <your-api-key>" \
-H "Content-Type: application/json" \
-d '{ "email": "tester@example.com", "password": "hunter2", "otpToken": "pre-auth-abc123" }'body.otpToken exists → success:
{ "accessToken": "eyJhbGciOi..." }Echoing request data back
Use response templates so the mock answers with the data it was given instead of a fixed string:
{
"accessToken": "mock-token-for-{{request.body.email}}",
"user": { "email": "{{request.body.email}}" }
}Checking which scenario was served
Every mock response carries the scenario that produced it:
X-Mock-Scenario: need-otpWhen the flow does something you did not expect, read that header first — it tells you whether a rule failed to match or the response body itself is wrong.
Gotchas
| Situation | What happens |
|---|---|
Client sends X-Mock-Scenario or ?scenario= | Match rules are skipped entirely — an explicit key always wins. Remove it while testing the flow. |
Request has no Content-Type: application/json | The body is never parsed, so every source=body rule fails and you always get the default scenario. |
OTP sent as a number ("otp": 123456) | Still matches eq 123456 — values are compared as strings. |
| Two scenarios could both match | Lower priority number wins. Give need-otp and success different priorities even though their rules are exclusive. |
| Scenario disabled | Skipped during rule evaluation, silently. Check the toggle before debugging the rule. |
Limitation: the mock has no memory
The mock engine is stateless per request. It decides purely from the request in front of it — an identical request always produces an identical response.
So if your app sends a second /login that is byte-for-byte identical to the first (no otpToken, no header, nothing), no match rule can tell the two apart. Two ways forward:
-
Have the client carry the token — what real backends do anyway, and what makes the recipe above work.
-
Flip the scenario between the calls — from the endpoint page in the dashboard, or programmatically:
POST /api/projects/{projectId}/overrides Content-Type: application/json { "endpointId": "<uuid>", "sessionId": "<your-session-id>", "scenarioKey": "success" }The override is scoped to the
X-Mock-Sessionheader your client sends, so parallel testers do not collide. Note this endpoint authenticates with a logged-in dashboard session, not the project API key — it is meant for the dashboard and for test harnesses that can log in, not for the app under test.