SHIFTLEFT STORE API — FUNCTIONAL REQUIREMENTS, BUSINESS RULES AND END-TO-END SCENARIOS Version 3.0 · 2026-10-13 · Target: https://demo.totalshiftleft.ai Companion document: ShiftLeft-Auth-Testing-Requirements.txt (authentication scheme catalogue) ====================================================================== 1. PURPOSE AND SCOPE ====================================================================== This document specifies the ShiftLeft Store API, a simulated online store used for API testing training and for functional, performance and security tool demonstrations. It is the contract for the store: every statement below is executed against the live API before each release, so a mismatch between this document and the API is treated as a defect in one of them. It contains: - Section 2: system-wide behaviour that applies to every request - Section 3: functional requirements (FR-nnn) with acceptance criteria - Section 4: use cases (UC-nnn), the main business journeys in narrative form - Section 5: field constraints and business rules (RULE X-nn) - Section 6: functional test data (TD-X-nn), one request per row - Section 7: end-to-end scenarios (E2E-nn), multi-step journeys that integrate the endpoints - Section 8: behaviours that tests must not treat as defects The store manages three entities: users (customers), products (catalogue) and orders. They are exposed through four interfaces at the same time: REST, HTTP QUERY, GraphQL and SOAP 1.1. All four read and write the same session-scoped data store. A second, protected copy of the store requires authentication and enforces roles and ownership (FR-022 to FR-024). Actors: - Shopper: registers, browses products, places and views orders - Store Manager: maintains the catalogue and processes orders - Authenticated User or Backend Service: obtains and presents credentials - API Tester: verifies protocol consistency, error handling, performance and security behaviour ====================================================================== 2. SYSTEM-WIDE BEHAVIOUR ====================================================================== ## 2.1 Endpoints Store, REST (entity is users, products or orders): - POST /api/v1/users, POST /api/v1/products, POST /api/v1/orders: create a record - GET /api/v1/users, GET /api/v1/products, GET /api/v1/orders: list with pagination, sorting and filters - GET /api/v1/users/{id}, GET /api/v1/products/{id}, GET /api/v1/orders/{id}: read one record - PUT /api/v1/users/{id}, PUT /api/v1/products/{id}, PUT /api/v1/orders/{id}: replace a record - PATCH /api/v1/users/{id}, PATCH /api/v1/products/{id}, PATCH /api/v1/orders/{id}: partially update a record - DELETE /api/v1/users/{id}, DELETE /api/v1/products/{id}, DELETE /api/v1/orders/{id}: delete a record Store, other interfaces: - QUERY /api/v1/users, QUERY /api/v1/products, QUERY /api/v1/orders: search with a JSON body (HTTP QUERY method) - QUERY /api/v1/echo: reflects a QUERY request - POST /graphql: GraphQL queries and mutations, body { "query": "..." } - POST /soap: SOAP 1.1, Content-Type text/xml, SOAPAction header names the operation, namespace http://demo.totalshiftleft.ai/soap Protected store (authentication required): - POST /api/v1/secure/users, GET /api/v1/secure/users, GET /api/v1/secure/users/{id}, PUT /api/v1/secure/users/{id}, PATCH /api/v1/secure/users/{id}, DELETE /api/v1/secure/users/{id} - POST /api/v1/secure/products, GET /api/v1/secure/products, GET /api/v1/secure/products/{id}, PUT /api/v1/secure/products/{id}, PATCH /api/v1/secure/products/{id}, DELETE /api/v1/secure/products/{id} - POST /api/v1/secure/orders, GET /api/v1/secure/orders, GET /api/v1/secure/orders/{id}, PUT /api/v1/secure/orders/{id}, PATCH /api/v1/secure/orders/{id}, DELETE /api/v1/secure/orders/{id} Authentication: - POST /auth/token: password login, returns a JWT - POST /auth/verify: verifies a JWT and returns its claims - GET /auth/me: reports the identity behind the request's credentials - POST /auth/oauth: OAuth 2.0 client credentials grant - GET /auth/apikey-info: lists the demo API keys - The authentication-testing catalogue (/auth/methods, /auth/protected/{scheme}, /auth/oauth/*, /auth/cookie-login/*, /auth/steps/*, /auth/cookie-session/issue) is specified in the companion document. Sessions, demo data and flows: - GET /session: current session; POST /session/new: start an isolated session; DELETE /session: end the session and delete its data - GET /api/v1/demo: describes the standard demo dataset; POST /api/v1/demo/reset: restores it in the current session - GET /api/v1/flows: lists the server-side flows; POST /api/v1/flows/order-placement, /auth-and-data, /cross-protocol, /seed-store, /customer-journey, /process-order: run one flow - GET /api/v1/rate-limited/ping: the only rate-limited endpoint, for practising back-off Platform and discovery: - GET /health: liveness; GET /ready: readiness; GET /metrics: Prometheus metrics when enabled - GET /api: machine-readable overview of the sandbox - GET /openapi.json, GET /openapi.yaml: full OpenAPI 3 description; GET /openapi/rest.json: store-only OpenAPI description; GET /docs: Swagger UI - GET /graphql (without a query) and GET /schema.graphql: GraphQL SDL; GET /graphiql: GraphiQL explorer - GET /soap?wsdl and GET /soap/service.wsdl: WSDL; GET /soap: SOAP service overview - GET /learn: guided learning scenarios; GET /learn/{id}: one scenario - GET /status.json: operational status - GET /requirements/store.txt and GET /requirements/auth-testing.txt: these requirements documents as text; GET /requirements/store.json and GET /requirements/auth-testing.json: the same documents parsed into sections, requirements, rules, test data and scenarios ## 2.2 Sessions - Every response from a store, session, flow or authentication endpoint carries an x-session-id response header, including error responses. - The client sends that value as the x-session-id request header on every later request (REST, QUERY, GraphQL and SOAP) to keep working with the same data. Browsers may use the sandbox_session cookie instead. - A request without any session identifier joins the most recent active session created from the same client IP address, or starts a new one. - A request with x-session-id: new always starts a new, empty-of-user-data session, and the response header carries its id. Load generators and parallel test agents must use this so that virtual users never share data. - A request with an unknown, expired or malformed x-session-id starts a new session. It never joins another session. - Data in one session is not visible from another session. - A session and all its data expire after a period of inactivity (10 minutes by default). Each request resets the timer. GET /session reports the exact expires_at. - A new session is pre-loaded with a small standard demo dataset (3 users, 3 products, 3 orders with fixed ids) on its first store request through any interface. Tests must not assume exact record counts; they compare counts before and after an action. - A reserved, never-expiring session (id "demo" repeated 16 times) always holds the standard dataset. It is shared by everyone, so tests that change data must use their own session. ## 2.3 Response format - Success: { "success": true, "data": }. - List responses also include "pagination": { total, page, limit, pages, hasNext, hasPrev }. - Errors: { "success": false, "error": "", "message": "", "request_id": "" }. - Validation errors add "details": an array of { "field", "message" } with one entry per invalid field. The field is named exactly as in the request, for example "email" or "quantity". - 204 No Content responses have an empty body. An empty body on 204 is success, not failure. - Exceptions to the JSON envelope: GraphQL answers HTTP 200 with a standard { data, errors } body; SOAP answers XML (faults are soap:Fault); the OAuth 2.0 endpoints under /auth/oauth/ use the RFC 6749 token and error format; /graphiql, /docs, /ui and the OAuth consent pages are HTML. ## 2.4 Status and error codes - 200 OK: request succeeded - 201 Created: record created; the returned id can be used immediately - 204 No Content: delete succeeded - 400 VALIDATION_ERROR: a field, path parameter or query parameter breaks a rule; details names it - 400 BAD_REQUEST: the body is not valid JSON, is not a JSON object, or the request is otherwise malformed - 401 UNAUTHORIZED: credentials are missing, wrong, expired or tampered - 403 FORBIDDEN: credentials are valid but the role, scope or ownership does not allow the action - 404 NOT_FOUND: the id does not exist in the current session, or the route does not exist - 405 METHOD_NOT_ALLOWED: the path exists but not for this method; the Allow header lists the methods it supports - 409 SESSION_LIMIT: the session already holds the maximum number of records of that entity - 413 PAYLOAD_TOO_LARGE: the body exceeds 100 KB - 415 UNSUPPORTED_MEDIA_TYPE: the Content-Type is not accepted by that endpoint - 429 RATE_LIMITED: only from GET /api/v1/rate-limited/ping or a simulated error - 500 SERVER_ERROR: unexpected failure - 500 RANDOM_FAILURE: simulated by ?random_fail=true - 503 SERVICE_UNAVAILABLE: the sandbox is at its maximum number of active sessions - Any status with error SIMULATED_ERROR: requested with ?error= ## 2.5 Standard headers - x-request-id: present on every response. If the request carries an x-request-id of 1 to 128 letters, digits, dots, underscores, colons or dashes, the same value is returned; otherwise the server generates one. The same id is in the error body as request_id. - x-session-id: see section 2.2. - Security headers on every response: Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, Cross-Origin-Opener-Policy, and a Content-Security-Policy whose frame-ancestors directive allows only the sandbox and totalshiftleft.ai sites. - Cache-Control: no-store on every store, session, flow and authentication response, because their content is session-specific. - X-Sandbox-Simulated: present only when the response was shaped by a simulation parameter. Its value names the parameter, for example error=500, delay=2000 or random_fail. - CORS: browsers on totalshiftleft.ai, demo.totalshiftleft.ai and localhost may call the API with credentials; x-session-id and x-request-id are exposed to them. ## 2.6 Capacity, rate limits and performance testing - There is no per-client rate limit on any endpoint except GET /api/v1/rate-limited/ping. A 429 from any other endpoint only happens when the request asks for it with ?error=429. - Each session holds at most 10,000 records per entity. GET /session reports the effective limit as data.limits.records_per_entity. - The sandbox serves at most 100,000 active sessions at once. - Performance and security tools should start each virtual user with x-session-id: new, send the returned id on later requests, and treat responses that carry X-Sandbox-Simulated as intentional. ====================================================================== 3. FUNCTIONAL REQUIREMENTS ====================================================================== ### FR-001 Customer registration As a shopper, I want to create an account with my name and email, to be identified when I place orders. - POST /api/v1/users with name, email, optional role and age returns 201 with the full user: id (UUID), name, email, role, age (if given), created_at. - role defaults to "user" when omitted. - Invalid input on POST /api/v1/users → 400 VALIDATION_ERROR naming the field, and nothing is stored. ### FR-002 Browse the product catalogue As a shopper, I want to see all products with names, prices and stock levels, to decide what to buy. - GET /api/v1/products returns a paginated list (default page 1, limit 10, maximum limit 100). - Sorting: ?sort=&order=asc|desc (default sort created_at, order desc). ?sort=price&order=asc returns the cheapest first. - Filtering: any field as a query parameter is a case-insensitive "contains" match, for example ?category=electronics. - An empty result is 200 with an empty data array, not an error. - A limit above 100 on GET /api/v1/products → 400 VALIDATION_ERROR naming "limit". ### FR-003 View a single product As a shopper, I want to see full details of a product before buying. - GET /api/v1/products/{id} returns id, name, price, description, stock, category, created_at, and updated_at once changed. - Unknown id on GET /api/v1/products/{id} → 404 NOT_FOUND. - A malformed id (not a UUID) on GET /api/v1/products/{id} → 400 VALIDATION_ERROR naming "id". ### FR-004 Place an order As a shopper, I want to place an order for a product, linked to my customer account. - POST /api/v1/orders with user_id, product_id, quantity, status and notes, all optional, returns 201 with id, user_id, product_id, quantity (default 1), status (default "pending"), notes (default ""), created_at. - quantity must be a whole number of at least 1, and status must be one of the allowed values; otherwise POST /api/v1/orders → 400 VALIDATION_ERROR. ### FR-005 View order confirmation with full details As a shopper, I want my order with my customer details and the product in one response. - GET /api/v1/orders/{id}?expand=user,product returns the order with "user" and "product" objects embedded. - ?expand=user or ?expand=product embeds only that one. - If the linked record does not exist (deleted or unknown id), the embedded field is null and the order is still returned with 200. ### FR-006 Add a product to the catalogue As a store manager, I want to add products with name, price, description, stock and category. - POST /api/v1/products returns 201 with the full product and its id. - Defaults: stock 0, category "general", description "". ### FR-007 Update product details As a store manager, I want to change price or stock without resending everything. - PATCH /api/v1/products/{id} changes only the fields sent; all other fields keep their values; updated_at is set. - PUT /api/v1/products/{id} replaces the product: name and price are required, and omitted optional fields reset to their defaults (stock 0, category "general", description ""). id and created_at are kept. - Unknown id on PATCH /api/v1/products/{id} → 404 NOT_FOUND. An invalid value → 400 VALIDATION_ERROR and the product is not changed. ### FR-008 View and filter orders As a store manager, I want to list orders and filter them by status or customer. - GET /api/v1/orders?status=pending returns only pending orders. - ?user_id= returns only that customer's orders. - ?sort=created_at&order=asc returns the oldest first. ### FR-009 Process an order through its status lifecycle As a store manager, I want to move an order through pending, processing and completed, or cancel it. - PATCH /api/v1/orders/{id} { "status": "" } returns 200 with the new status and updated_at. - Allowed values: pending, processing, completed, cancelled. Any other value on PATCH /api/v1/orders/{id} → 400 VALIDATION_ERROR and the order is unchanged. - Other fields (quantity, notes) are unchanged by a status update. ### FR-010 Remove a product As a store manager, I want to remove a discontinued product permanently. - DELETE /api/v1/products/{id} returns 204 with an empty body. - A later GET or DELETE of the same id on /api/v1/products/{id} → 404 NOT_FOUND. - Orders referencing the product are kept (RULE O-05). ### FR-011 Log in and receive a JWT As a registered user, I want to log in and receive a token. - POST /auth/token { username, password } returns 200 with data.access_token, data.token_type "Bearer", data.expires_in 600 and data.user { id, name, role }. - Demo users: alice@demo.com / alice123 (admin, id usr_alice), bob@demo.com / bob123 (user, id usr_bob), charlie@demo.com / charlie123 (moderator, id usr_charlie). These login accounts are separate from the demo dataset's customer records. - Wrong password or unknown user on POST /auth/token → 401 UNAUTHORIZED. Missing username or password → 400 VALIDATION_ERROR naming the field. ### FR-012 Verify identity As an authenticated user, I want to confirm my credentials are read correctly. - GET /auth/me with Authorization: Bearer returns 200 { authenticated: true, method: "jwt", identity: { sub, role, iat, exp, ... } }. - GET /auth/me with x-api-key: returns 200 { authenticated: true, method: "apikey" }. - GET /auth/me without credentials returns 200 { authenticated: false }. - An invalid, tampered or expired token, or an unknown API key, on GET /auth/me → 401 UNAUTHORIZED. - POST /auth/verify { token } returns 200 { valid: true, claims } for a valid token; an invalid token on POST /auth/verify → 401 UNAUTHORIZED. ### FR-013 Machine-to-machine authentication with OAuth 2.0 client credentials As a backend service, I want to authenticate with a client id and secret. - POST /auth/oauth { grant_type: "client_credentials", client_id: "sandbox-client", client_secret: "sandbox-secret", scope: "read write" } returns 200 with access_token, token_type, expires_in and scope. - The token works on GET /auth/me: identity.sub is "sandbox-client", identity.client is true, identity.scopes is ["read","write"]. - A wrong client_secret on POST /auth/oauth → 401 UNAUTHORIZED. A grant_type other than "client_credentials" → 400 VALIDATION_ERROR. ### FR-014 Query store data with GraphQL As a developer, I want to fetch exactly the fields I need, including linked entities. - Queries: users, user(id), products, product(id), orders, order(id), sessionInfo. - users, products and orders accept page, limit, sort and order arguments and return { items, pageInfo { total, page, limit, pages, hasNext, hasPrev } }. - Mutations: createUser, updateUser, deleteUser, createProduct, updateProduct, deleteProduct, createOrder, updateOrder, deleteOrder. delete mutations return true when a record was deleted. - order { user { ... } product { ... } } resolves linked records, or null if not found. - user(id), product(id) and order(id) return null for an unknown id. - Invalid mutation input returns HTTP 200 with errors[0].extensions.code "VALIDATION_ERROR" and extensions.details naming the field. Nothing is stored. ### FR-015 Users, products and orders through SOAP As a legacy integrator, I want to use SOAP 1.1 XML against the same data. - Operations: CreateUser, GetUsers, GetUser, UpdateUser, DeleteUser, CreateProduct, GetProducts, GetProduct, UpdateProduct, DeleteProduct, CreateOrder, GetOrders, GetOrder, UpdateOrder, DeleteOrder. - POST /soap with SOAPAction set to the operation name returns 200 with a Response element, for example CreateUserResponse containing a user element. - An unknown id, invalid input, an unknown SOAPAction or malformed XML on POST /soap → 400 with a soap:Fault whose faultcode is soap:Client. - A message that contains a DOCTYPE declaration on POST /soap → 400 with a soap:Fault, because SOAP 1.1 forbids DTDs. - Records created through SOAP are immediately visible in REST, QUERY and GraphQL, and the other way round. ### FR-016 Simulate API failures As a tester, I want to cause errors, slow responses and random failures on store endpoints. - ?error= on any REST or QUERY store endpoint returns that HTTP status with error "SIMULATED_ERROR". Supported codes: 400, 401, 403, 404, 429, 500, 502, 503. Other codes are ignored. - ?delay= waits that long before responding (0 to 10000). A value above 10000 on GET /api/v1/users → 400 VALIDATION_ERROR naming "delay". - ?random_fail=true fails about 30% of calls with 500 error "RANDOM_FAILURE". - A simulated error is returned before the request is processed: a failed POST creates nothing. - Parameters can be combined, for example ?delay=1000&random_fail=true. - Every simulated response carries the X-Sandbox-Simulated header. ### FR-017 Search with the HTTP QUERY method As a developer, I want to send rich search criteria in a request body without changing data. - QUERY /api/v1/users, /api/v1/products and /api/v1/orders take a JSON body { filters, sort, order, page, limit, fields } and return the same paginated envelope as GET. - filters maps a field to a value (exact match), an array (any of), or an object of operators: eq, ne, contains, in, nin, gt, gte, lt, lte. Operators on one field are combined with AND. - fields is an optional projection: only the listed fields are returned on each record. - An empty body means no criteria and returns everything. - A body that is not valid JSON, or is not a JSON object, → 400 BAD_REQUEST. A limit above 100 → 400 VALIDATION_ERROR. - QUERY never changes data, and the simulation parameters of FR-016 apply to it. - QUERY /api/v1/echo returns the method, the parsed body and the safe, idempotent properties of QUERY. ### FR-018 Standard demo dataset and reset As a learner, I want ready-made data in every new session, and a way to restore it. - A new session holds these records after its first store request: users 11111111-1111-1111-1111-111111111111 (Alice Johnson), 22222222-2222-2222-2222-222222222222 (Bob Smith), 33333333-3333-3333-3333-333333333333 (Carol White); products aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa (Wireless Mouse, 24.99), bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb (Mechanical Keyboard, 79.99), cccccccc-cccc-cccc-cccc-cccccccccccc (Standing Desk, 349.00); orders dddddddd-dddd-dddd-dddd-dddddddddddd (completed), eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee (processing), ffffffff-ffff-ffff-ffff-ffffffffffff (pending). - The dataset is loaded once per session; later edits and deletions are never overwritten. - GET /api/v1/demo returns 200 with the record ids, whether the current session is already seeded, and the reserved session id. - POST /api/v1/demo/reset deletes every user, product and order in the current session and restores the standard dataset; it returns 200. ### FR-019 Session management As a tester, I want to control which session my requests use. - GET /session returns 200 with data.session_id, data.created_at, data.expires_at and data.limits { records_per_entity, max_sessions }. - POST /session/new returns 201 with a new session's data.session_id, and the x-session-id response header carries the same id. - A request carrying x-session-id: new starts a new session (section 2.2). - DELETE /session deletes the current session and all its data and returns 204. The next request with that id starts a new session. ### FR-020 Server-side end-to-end flows As a learner, I want to run a complete journey in one call and see every step. - GET /api/v1/flows returns the six flows with id, name, description, endpoint and steps. - POST /api/v1/flows/{flow} runs the flow in the current session and returns 200 with data.flow (the flow id) and data.steps, each step naming the API call made and its result. - Flows: order-placement (user, product, order), auth-and-data (token, create, list), cross-protocol (REST create, GraphQL read, REST delete), seed-store (4 catalogue products), customer-journey (register, browse, order, confirm), process-order (pending, processing, completed). - Records created by a flow stay in the session and can be used afterwards. ### FR-021 Rate-limit practice endpoint As a tester, I want one endpoint that enforces a rate limit, to practise back-off without affecting anything else. - GET /api/v1/rate-limited/ping allows 10 requests per 60 seconds per session and returns 200 with X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers. - The 11th request within the window on GET /api/v1/rate-limited/ping → 429 RATE_LIMITED with a Retry-After header. - A new session has its own window. ### FR-022 Protected store authentication As a security tester, I want a copy of the store that requires credentials, to test authentication handling. - Every /api/v1/secure/ endpoint requires Authorization: Bearer (from POST /auth/token or POST /auth/oauth) or x-api-key. - No credentials on GET /api/v1/secure/orders → 401 UNAUTHORIZED with a WWW-Authenticate: Bearer header. - An invalid, tampered or expired token, or an unknown API key, on GET /api/v1/secure/orders → 401 UNAUTHORIZED. - The protected store has the same fields, defaults and validation as the public store, plus an owner_id on every record that the server sets to the caller's identity. owner_id cannot be sent in a request body. - Protected records are separate from public records: neither store sees the other's data. Both are session-scoped. ### FR-023 Protected store roles and scopes As a security tester, I want role-based access rules, to test authorisation and privilege escalation. - Roles come from the credential: a JWT carries the demo user's role (admin, moderator, user); x-api-key demo-key-sandbox-2026 is role user; x-api-key admin-key-sandbox-2026 is role admin; an OAuth client-credentials token is role service. - Products: any authenticated caller may read. Creating or updating requires admin, moderator, or service with the write scope. Deleting requires admin. - Users and orders: any authenticated caller may create. Deleting requires admin. - A service token without the read scope cannot read, and without the write scope cannot write. - A caller whose role or scope does not allow the action → 403 FORBIDDEN, and nothing changes. ### FR-024 Protected store ownership As a security tester, I want object-level access control, to test broken object level authorisation (BOLA/IDOR). - For users and orders, a non-admin caller only sees records whose owner_id is their own identity. GET /api/v1/secure/orders lists only their records. - A non-admin reading, updating or deleting another owner's record on /api/v1/secure/orders/{id} → 403 FORBIDDEN. - An admin sees and manages every record in the session. - An id that does not exist on /api/v1/secure/orders/{id} → 404 NOT_FOUND, for every role. ### FR-025 Health, readiness and metrics As an operator, I want standard probes, to monitor the sandbox like any production service. - GET /health returns 200 { status: "ok", uptime, timestamp, version } whenever the process is running. - GET /ready returns 200 { status: "ready" } when the database is reachable. When it is not, it returns 503 { status: "degraded" } and the sandbox keeps serving from memory. - GET /metrics returns Prometheus text metrics when the operator enables them; otherwise it returns 404 NOT_FOUND. ### FR-026 Contract discovery As an API tester, I want to download every contract, to generate tests from them. - GET /openapi.json returns 200 with an OpenAPI 3 document describing the REST, authentication, session, flow and platform endpoints. GET /openapi/rest.json returns only the store endpoints. - GET /graphql without a query, and GET /schema.graphql, return the GraphQL SDL. - GET /soap?wsdl and GET /soap/service.wsdl return the WSDL, which declares all fifteen SOAP operations. - GET /api returns 200 with links to every interface. - GET /requirements/store.txt returns this document as plain text with LF line endings. GET /requirements/store.json returns it parsed, with data.version, data.frs, data.tds and data.e2es. - GET /learn lists the guided learning scenarios, and GET /learn/{id} returns one with its steps. - GET /status.json returns the operational status, including the rate-limit policy and the session idle timeout. ### FR-027 Standard error handling As an API tester, I want every failure to be predictable and traceable. - An unknown route → 404 NOT_FOUND in the standard error envelope. - A known path with an unsupported method, such as PUT /api/v1/users → 405 METHOD_NOT_ALLOWED with an Allow header. - A JSON body that cannot be parsed → 400 BAD_REQUEST. A body over 100 KB → 413 PAYLOAD_TOO_LARGE. An unsupported Content-Type → 415 UNSUPPORTED_MEDIA_TYPE. - Every JSON error body carries success false, error, message and request_id, and never contains a stack trace or file path. ====================================================================== 4. USE CASES ====================================================================== ### UC-001 Customer places a first order Actor: Shopper Covers: FR-001, FR-002, FR-004, FR-005 Goal: A new customer registers, finds a product and places an order. Preconditions: At least one product exists in the session. The standard dataset provides three. Main flow: 1. The customer sends POST /api/v1/users with name and email. The system returns the customer with a new id. 2. The customer sends GET /api/v1/products?sort=price&order=asc. The system returns the products, cheapest first. 3. The customer sends POST /api/v1/orders with the user_id from step 1 and a product_id from step 2. The system creates the order with status "pending". 4. The customer sends GET /api/v1/orders/{id}?expand=user,product. The system returns the order with the customer and product embedded. Postconditions: A pending order exists in the session, linked to the customer and the product. Alternative flows: A1. No products exist: GET /api/v1/products returns an empty list, and the customer cannot order. A2. Wrong email format: POST /api/v1/users → 400 VALIDATION_ERROR with details[0].field "email". The customer corrects it and retries. A3. product_id from another session: the order is created, and ?expand=product returns product null. Run live: POST /api/v1/flows/customer-journey ### UC-002 Manager processes and fulfils an order Actor: Store Manager Covers: FR-005, FR-008, FR-009 Goal: The manager accepts a pending order and marks it complete when dispatched. Preconditions: At least one pending order exists in the session. Main flow: 1. The manager sends GET /api/v1/orders?status=pending&sort=created_at&order=asc. The system returns the oldest unprocessed orders first. 2. The manager sends GET /api/v1/orders/{id}?expand=user,product to see the customer and product. 3. The manager sends PATCH /api/v1/orders/{id} with { "status": "processing" }. The system returns the order with updated_at set. 4. After dispatch, the manager sends PATCH /api/v1/orders/{id} with { "status": "completed" }. Postconditions: The order status is "completed". Alternative flows: A1. The manager rejects the order with PATCH { "status": "cancelled" }. A2. The order id is unknown: PATCH /api/v1/orders/{id} → 404 NOT_FOUND. A3. An invalid status value: PATCH /api/v1/orders/{id} → 400 VALIDATION_ERROR. Run live: POST /api/v1/flows/process-order ### UC-003 User authenticates and verifies identity Actor: Authenticated User Covers: FR-011, FR-012, FR-013 Goal: The user logs in, receives a JWT and confirms it works. Preconditions: None. Any demo account works. Main flow: 1. The user sends POST /auth/token with { "username": "alice@demo.com", "password": "alice123" }. The system returns an access_token. 2. The user sends GET /auth/me with Authorization: Bearer . The system returns the identity with sub and role. 3. The user sends POST /auth/verify with { "token": "" }. The system returns the claims, including exp. Postconditions: The user holds a token that is valid for 600 seconds. Alternative flows: A1. Wrong password: POST /auth/token → 401 UNAUTHORIZED. A2. API key: GET /auth/me with x-api-key returns method "apikey". A3. Service login: POST /auth/oauth with client credentials returns a token usable on GET /auth/me. Run live: POST /api/v1/flows/auth-and-data ### UC-004 Tester verifies cross-protocol data consistency Actor: API Tester Covers: FR-014, FR-015, FR-017 Goal: Prove that REST, QUERY, GraphQL and SOAP read and write the same session store. Preconditions: None. Main flow: 1. The tester sends POST /api/v1/users and notes the id. 2. The tester sends POST /graphql with { user(id: "") { id name email } }. The same user is returned. 3. The tester sends a SOAP CreateUser request to POST /soap. The new user is returned. 4. The tester sends QUERY /api/v1/users. Both users appear. 5. The tester sends DELETE /api/v1/users/{id}. The GraphQL query from step 2 now returns null. Postconditions: The four interfaces are views of one data store. Alternative flows: A1. A GraphQL query with an id from another session returns null. Run live: POST /api/v1/flows/cross-protocol ### UC-005 Tester validates error handling and resilience Actor: API Tester Covers: FR-016, FR-021, FR-027 Goal: Confirm that a client handles errors, timeouts, flaky responses and rate limits. Preconditions: None. Main flow: 1. GET /api/v1/users?error=500 → 500 SIMULATED_ERROR. The client shows an error state. 2. GET /api/v1/users?delay=5000 returns 200 after 5 seconds. The client does not time out early. 3. POST /api/v1/users without name → 400 VALIDATION_ERROR with details. The client shows the field error. 4. GET /api/v1/users?random_fail=true, ten times. About three fail with 500. The client retries. 5. GET /api/v1/rate-limited/ping eleven times. The eleventh → 429 RATE_LIMITED with Retry-After. The client waits and retries. Postconditions: The client handles each class of error without data loss. ### UC-006 Security tester probes the protected store Actor: API Tester Covers: FR-022, FR-023, FR-024 Goal: Confirm that authentication, roles and ownership are enforced. Preconditions: None. Main flow: 1. The tester calls GET /api/v1/secure/orders without credentials. The system refuses with 401. 2. The tester logs in as bob (role user) and creates an order on POST /api/v1/secure/orders. The order's owner_id is usr_bob. 3. The tester logs in as charlie and reads bob's order. The system refuses with 403. 4. As bob, the tester tries to delete a product. The system refuses with 403. 5. With the admin API key, the tester lists all secure orders and deletes bob's order. Postconditions: Every refused action left the data unchanged. ====================================================================== 5. FIELD CONSTRAINTS AND BUSINESS RULES ====================================================================== ## 5.1 Users RULE U-01 name — required string, 1 to 100 characters. Valid: "Alice Johnson", "Bob", "María García". Invalid → 400: missing, "", 101 characters. RULE U-02 email — required, valid email format, at most 255 characters. Valid: "alice@demo.com", "user+tag@example.co.uk". Invalid → 400: "notanemail", "@nodomain", "no@", "". RULE U-03 role — optional enum: user, admin, moderator. Default "user". Invalid → 400: "superadmin", "ADMIN" (wrong case), "", 123. RULE U-04 age — optional whole number 0 to 150. Valid: 0, 25, 150. Invalid → 400: -1, 151, 25.5, "twenty". RULE U-05 Unknown fields are rejected: a body with any field other than name, email, role and age → 400. ## 5.2 Products RULE P-01 name — required string, 1 to 100 characters. Invalid → 400: missing, "", 101 characters. RULE P-02 price — required number greater than 0. Valid: 0.01, 9.99, 10000. Invalid → 400: 0, -5, "free", missing. RULE P-03 stock — optional whole number of at least 0. Default 0. Valid: 0, 1, 500. Invalid → 400: -1, 10.5, "ten". RULE P-04 description — optional string, at most 500 characters. Default "". Invalid → 400: 501 characters. RULE P-05 category — optional string, at most 50 characters. Default "general". Invalid → 400: 51 characters. RULE P-06 PATCH keeps fields that are not sent; PUT requires name and price and resets omitted optional fields to their defaults. ## 5.3 Orders RULE O-01 quantity — optional whole number of at least 1. Default 1. Invalid → 400: 0, -1, 1.5, 2.5. RULE O-02 status — optional enum: pending, processing, completed, cancelled. Default "pending". Invalid → 400: "shipped", "PENDING", "". Transitions are not enforced: any allowed status may be set at any time. RULE O-03 notes — optional string, at most 500 characters. Default "". Invalid → 400: 501 characters. RULE O-04 user_id and product_id — optional; when given they must be UUIDs (else 400). The referenced record does not have to exist. RULE O-05 Deleting a user or product does not delete, cancel or modify the orders that reference it. Those orders stay retrievable, and their expanded user or product is null. RULE O-06 expand — GET /api/v1/orders/{id}?expand=user,product embeds linked records; an unknown order id gives 404 regardless of expand. RULE O-07 PATCH on an order accepts only quantity, status and notes. user_id and product_id are set at creation or replaced with PUT. ## 5.4 Authentication RULE A-01 POST /auth/token requires both username and password (missing → 400). Wrong credentials → 401. Token lifetime 600 seconds (claims.exp = claims.iat + 600). RULE A-02 API keys (x-api-key header): "demo-key-sandbox-2026" (role user) and "admin-key-sandbox-2026" (role admin). Any other key → 401 UNAUTHORIZED. No credentials at all on GET /auth/me → 200 { authenticated: false }. RULE A-03 OAuth 2.0: grant_type must be exactly "client_credentials"; client "sandbox-client" with secret "sandbox-secret". Wrong secret → 401; another or missing grant_type → 400. RULE A-04 When a request carries both Authorization: Bearer and x-api-key, the Bearer token decides, and an invalid Bearer token is refused even if the key is valid. ## 5.5 Protected store RULE SEC-01 Missing credentials → 401 with WWW-Authenticate: Bearer realm="sandbox". Invalid credentials → 401. Valid credentials without permission → 403. RULE SEC-02 owner_id is set by the server from the caller: the JWT sub (for example usr_bob), "apikey:demo", "apikey:admin" or "sandbox-client". A body containing owner_id → 400. RULE SEC-03 Permission table, by role: admin: every action on every record. moderator: read all products; create and update products; create, read and update own users and orders. user: read all products; create, read and update own users and orders. service: read with scope read; create and update products, users and orders with scope write; reads and updates of users and orders limited to own records. Only admin may delete. RULE SEC-04 A non-admin list request returns only the caller's own users and orders; it is never refused. RULE SEC-05 The simulation parameters of FR-016 do not apply to the protected store. ## 5.6 Sessions and platform RULE S-01 Each session holds at most 10,000 records per entity type. A create beyond that → 409 SESSION_LIMIT, with a message naming the entity and the limit. Deleting records allows creates again. GraphQL reports the same condition as extensions.code "SESSION_LIMIT"; SOAP as a soap:Client fault. RULE S-02 Records from one session are not visible in another: GET by id → 404, GraphQL by id → null, SOAP by id → soap:Fault. RULE S-03 There is no request rate limit, except on GET /api/v1/rate-limited/ping (10 requests per 60 seconds per session). RULE S-04 Any id path parameter that is not a UUID → 400 VALIDATION_ERROR naming "id". RULE S-05 x-session-id: new, or an unknown or expired x-session-id, starts a new session; the response's x-session-id header carries its id. RULE S-06 When the sandbox holds 100,000 active sessions, a request that would start another → 503 SERVICE_UNAVAILABLE. ## 5.7 HTTP QUERY RULE Q-01 Operators: eq, ne, contains (case-insensitive), in, nin, gt, gte, lt, lte. An array value means "in"; a scalar means "eq". RULE Q-02 page defaults to 1, limit to 10 (maximum 100), sort to created_at and order to desc, as with GET. RULE Q-03 fields projects each record to the listed fields; unknown field names are ignored. ====================================================================== 6. FUNCTIONAL TEST DATA ====================================================================== Layout: each row is four lines — ID, method and path, input, expected result. Rows run in order in one session that starts with x-session-id: new. {id of TD-X-nn} means the data.id returned by that earlier row. ## 6.1 Users TD-U-01 POST /api/v1/users { "name": "Alice Johnson", "email": "alice@demo.com" } 201, data.id is a UUID, data.role = "user" TD-U-02 POST /api/v1/users { "name": "Bob", "email": "bob@demo.com", "role": "admin", "age": 34 } 201, data.role = "admin", data.age = 34 TD-U-03 POST /api/v1/users { "email": "alice@demo.com" } 400 VALIDATION_ERROR, details[0].field: "name" TD-U-04 POST /api/v1/users { "name": "Eve", "email": "notanemail" } 400 VALIDATION_ERROR, details[0].field: "email" TD-U-05 POST /api/v1/users { "name": "Eve", "email": "e@x.com", "role": "superadmin" } 400 VALIDATION_ERROR, details[0].field: "role" TD-U-06 POST /api/v1/users { "name": "Eve", "email": "e@x.com", "age": -1 } 400 VALIDATION_ERROR, details[0].field: "age" TD-U-07 POST /api/v1/users { "name": "Eve", "email": "e@x.com", "age": 151 } 400 VALIDATION_ERROR, details[0].field: "age" TD-U-08 GET /api/v1/users/{id} where id = {id of TD-U-01} 200, data.name = "Alice Johnson" TD-U-09 GET /api/v1/users/{id} where id = 00000000-0000-0000-0000-000000000000 404 NOT_FOUND TD-U-10 GET /api/v1/users/{id} where id = not-a-uuid 400 VALIDATION_ERROR, details[0].field: "id" TD-U-11 PATCH /api/v1/users/{id} { "age": 40 }, where id = {id of TD-U-01} 200, data.age = 40, data.name = "Alice Johnson", data.email = "alice@demo.com" TD-U-12 DELETE /api/v1/users/{id} where id = {id of TD-U-02} 204, empty body TD-U-13 GET /api/v1/users/{id} where id = {id of TD-U-02} 404 NOT_FOUND TD-U-14 POST /api/v1/users { "name": "Eve", "email": "e@x.com", "nickname": "evie" } 400 VALIDATION_ERROR TD-U-15 PUT /api/v1/users/{id} { "name": "Alice Johnson", "email": "alice.r@demo.com" }, where id = {id of TD-U-01} 200, data.email = "alice.r@demo.com", data.role = "user", no data.age ## 6.2 Products TD-P-01 POST /api/v1/products { "name": "Wireless Headphones", "price": 89.99, "stock": 50, "category": "electronics" } 201, data.stock = 50 TD-P-02 POST /api/v1/products { "name": "USB-C Cable", "price": 9.99 } 201, data.stock = 0, data.category = "general", data.description = "" TD-P-03 POST /api/v1/products { "name": "Free Item", "price": 0 } 400 VALIDATION_ERROR, details[0].field: "price" TD-P-04 POST /api/v1/products { "name": "Bad Stock", "price": 5.00, "stock": -1 } 400 VALIDATION_ERROR, details[0].field: "stock" TD-P-05 POST /api/v1/products { "price": 5.00 } 400 VALIDATION_ERROR, details[0].field: "name" TD-P-06 GET /api/v1/products?category=electronics no body 200, every item has category containing "electronics" TD-P-07 GET /api/v1/products?sort=price&order=asc&limit=100 no body 200, data[].price in ascending order TD-P-08 GET /api/v1/products?limit=101 no body 400 VALIDATION_ERROR, details[0].field: "limit" TD-P-09 PATCH /api/v1/products/{id} { "price": 79.99 }, where id = {id of TD-P-01} 200, data.price = 79.99, data.stock = 50, data.category = "electronics" TD-P-10 PUT /api/v1/products/{id} { "name": "Renamed" }, where id = {id of TD-P-01} 400 VALIDATION_ERROR, details[0].field: "price" ## 6.3 Orders TD-O-01 POST /api/v1/orders { "user_id": "{id of TD-U-01}", "product_id": "{id of TD-P-01}", "quantity": 2 } 201, data.status = "pending", data.quantity = 2 TD-O-02 POST /api/v1/orders { } 201, data.quantity = 1, data.status = "pending", data.notes = "" TD-O-03 POST /api/v1/orders { "quantity": 0 } 400 VALIDATION_ERROR, details[0].field: "quantity" TD-O-04 POST /api/v1/orders { "status": "shipped" } 400 VALIDATION_ERROR, details[0].field: "status" TD-O-05 POST /api/v1/orders { "quantity": 1.5 } 400 VALIDATION_ERROR, details[0].field: "quantity" TD-O-06 POST /api/v1/orders { "user_id": "not-a-uuid" } 400 VALIDATION_ERROR, details[0].field: "user_id" TD-O-07 PATCH /api/v1/orders/{id} { "status": "processing" }, where id = {id of TD-O-01} 200, data.status = "processing", data.updated_at present TD-O-08 GET /api/v1/orders/{id}?expand=user,product where id = {id of TD-O-01} 200, data.user.name = "Alice Johnson", data.product.name = "Wireless Headphones" TD-O-09 GET /api/v1/orders?status=pending&limit=100 no body 200, every item has status = "pending" TD-O-10 GET /api/v1/orders/{id}?expand=user where id = 00000000-0000-0000-0000-000000000000 404 NOT_FOUND TD-O-11 PATCH /api/v1/orders/{id} { "user_id": "11111111-1111-1111-1111-111111111111" }, where id = {id of TD-O-01} 400 VALIDATION_ERROR TD-O-12 PUT /api/v1/orders/{id} { "quantity": 3, "status": "completed" }, where id = {id of TD-O-02} 200, data.quantity = 3, data.status = "completed", data.notes = "" TD-O-13 DELETE /api/v1/orders/{id} where id = {id of TD-O-02} 204, empty body ## 6.4 Authentication TD-A-01 POST /auth/token { "username": "alice@demo.com", "password": "alice123" } 200, data.access_token present, data.token_type = "Bearer", data.expires_in = 600 TD-A-02 POST /auth/token { "username": "alice@demo.com", "password": "wrongpassword" } 401 UNAUTHORIZED TD-A-03 POST /auth/token { "username": "alice@demo.com" } 400 VALIDATION_ERROR, details[0].field: "password" TD-A-04 GET /auth/me header x-api-key: demo-key-sandbox-2026 200, data.authenticated = true, data.method = "apikey" TD-A-05 GET /auth/me header x-api-key: wrong-key 401 UNAUTHORIZED TD-A-06 GET /auth/me no body 200, data.authenticated = false TD-A-07 POST /auth/oauth { "grant_type": "client_credentials", "client_id": "sandbox-client", "client_secret": "sandbox-secret" } 200, data.access_token present TD-A-08 POST /auth/oauth { "grant_type": "password", "client_id": "sandbox-client", "client_secret": "sandbox-secret" } 400 VALIDATION_ERROR, details[0].field: "grant_type" TD-A-09 POST /auth/verify { "token": "abc.def.ghi" } 401 UNAUTHORIZED TD-A-10 POST /auth/oauth { "grant_type": "client_credentials", "client_id": "sandbox-client", "client_secret": "wrong-secret" } 401 UNAUTHORIZED TD-A-11 GET /auth/apikey-info no body 200, data.keys has 2 items ## 6.5 Simulation and rate limiting TD-S-01 GET /api/v1/users?error=500 no body 500 SIMULATED_ERROR, success = false, header X-Sandbox-Simulated = "error=500" TD-S-02 GET /api/v1/users?error=503 no body 503 SIMULATED_ERROR TD-S-03 GET /api/v1/users?delay=3000 no body 200, response time ≥ 3000 ms, header X-Sandbox-Simulated = "delay=3000" TD-S-04 GET /api/v1/users?delay=20000 no body 400 VALIDATION_ERROR, details[0].field: "delay" TD-S-05 GET /api/v1/users?random_fail=true (repeat 20 times) each response is 200 or 500; every 500 has error = "RANDOM_FAILURE"; every 500 has header X-Sandbox-Simulated = "random_fail" TD-S-06 GET /api/v1/users?error=429 no body 429 SIMULATED_ERROR TD-S-07 GET /api/v1/users no body 200, header X-Sandbox-Simulated absent, header X-RateLimit-Limit absent TD-S-08 GET /api/v1/rate-limited/ping header x-session-id: new 200, header X-RateLimit-Limit = "10", header X-RateLimit-Remaining present ## 6.6 HTTP QUERY TD-Q-01 QUERY /api/v1/products { "filters": { "price": { "gte": 50 } }, "sort": "price", "order": "asc", "limit": 100 } 200, every item has price ≥ 50, data[].price in ascending order TD-Q-02 QUERY /api/v1/users { "filters": { "role": ["admin", "moderator"] }, "fields": ["id", "role"], "limit": 100 } 200, pagination.total ≥ 1 TD-Q-03 QUERY /api/v1/orders text not-json 400 BAD_REQUEST TD-Q-04 QUERY /api/v1/orders { "limit": 101 } 400 VALIDATION_ERROR TD-Q-05 QUERY /api/v1/echo { "probe": 1 } 200, data.method = "QUERY", data.received_body.probe = 1, data.properties.safe = true ## 6.7 Sessions and demo data TD-D-01 GET /session header x-session-id: new 200, data.session_id present, data.limits.records_per_entity = 10000 TD-D-02 POST /session/new no body 201, data.session_id present, header x-session-id present TD-D-03 GET /api/v1/users/{id} where id = 11111111-1111-1111-1111-111111111111, header x-session-id: new 200, data.name = "Alice Johnson" TD-D-04 GET /api/v1/demo no body 200, data.reserved_session.id = "demodemodemodemodemodemodemodemodemodemodemodemodemodemodemodemo" TD-D-05 POST /api/v1/demo/reset no body 200 TD-D-06 GET /session header x-session-id: 0000000000000000000000000000000000000000000000000000000000000000 200, data.session_id != "0000000000000000000000000000000000000000000000000000000000000000" ## 6.8 Flows TD-F-01 GET /api/v1/flows no body 200, data has 6 items TD-F-02 POST /api/v1/flows/order-placement no body 200, data.flow = "order-placement", data.steps has 3 items TD-F-03 POST /api/v1/flows/customer-journey no body 200, data.flow = "customer-journey" TD-F-04 POST /api/v1/flows/process-order no body 200, data.flow = "process-order" TD-F-05 POST /api/v1/flows/seed-store no body 200, data.flow = "seed-store", data.products has 4 items TD-F-06 POST /api/v1/flows/not-a-flow no body 404 NOT_FOUND TD-F-07 POST /api/v1/flows/auth-and-data no body 200, data.flow = "auth-and-data", data.steps has 3 items TD-F-08 POST /api/v1/flows/cross-protocol no body 200, data.flow = "cross-protocol", data.steps has 3 items ## 6.9 Protected store TD-SEC-01 GET /api/v1/secure/orders no body 401 UNAUTHORIZED, header WWW-Authenticate contains "Bearer" TD-SEC-02 GET /api/v1/secure/orders header Authorization: Bearer not.a.token 401 UNAUTHORIZED TD-SEC-03 GET /api/v1/secure/products header x-api-key: demo-key-sandbox-2026 200, pagination.total = 0 TD-SEC-04 POST /api/v1/secure/products { "name": "Locked Box", "price": 10 }, header x-api-key: demo-key-sandbox-2026 403 FORBIDDEN TD-SEC-05 POST /api/v1/secure/products { "name": "Locked Box", "price": 10 }, header x-api-key: admin-key-sandbox-2026 201, data.owner_id = "apikey:admin" TD-SEC-06 POST /api/v1/secure/orders { "quantity": 2 }, header x-api-key: demo-key-sandbox-2026 201, data.owner_id = "apikey:demo", data.status = "pending" TD-SEC-07 POST /api/v1/secure/orders { "quantity": 1, "owner_id": "usr_alice" }, header x-api-key: demo-key-sandbox-2026 400 VALIDATION_ERROR TD-SEC-08 DELETE /api/v1/secure/products/{id} where id = {id of TD-SEC-05}, header x-api-key: demo-key-sandbox-2026 403 FORBIDDEN TD-SEC-09 GET /api/v1/secure/orders/{id} where id = 00000000-0000-0000-0000-000000000000, header x-api-key: admin-key-sandbox-2026 404 NOT_FOUND TD-SEC-10 DELETE /api/v1/secure/orders/{id} where id = {id of TD-SEC-06}, header x-api-key: admin-key-sandbox-2026 204, empty body TD-SEC-11 GET /api/v1/secure/users?error=500 header x-api-key: demo-key-sandbox-2026 200 TD-SEC-12 POST /api/v1/secure/users { "name": "Dana Secure", "email": "dana@secure.test" }, header x-api-key: demo-key-sandbox-2026 201, data.owner_id = "apikey:demo" TD-SEC-13 GET /api/v1/secure/users/{id} where id = {id of TD-SEC-12}, header x-api-key: demo-key-sandbox-2026 200, data.name = "Dana Secure" TD-SEC-14 PATCH /api/v1/secure/users/{id} { "age": 33 }, where id = {id of TD-SEC-12}, header x-api-key: demo-key-sandbox-2026 200, data.age = 33, data.owner_id = "apikey:demo" TD-SEC-15 PUT /api/v1/secure/users/{id} { "name": "Dana Secure", "email": "dana@secure.test", "role": "moderator" }, where id = {id of TD-SEC-12}, header x-api-key: demo-key-sandbox-2026 200, data.role = "moderator", data.owner_id = "apikey:demo", no data.age TD-SEC-16 DELETE /api/v1/secure/users/{id} where id = {id of TD-SEC-12}, header x-api-key: admin-key-sandbox-2026 204, empty body TD-SEC-17 PUT /api/v1/secure/products/{id} { "name": "Locked Box v2", "price": 12 }, where id = {id of TD-SEC-05}, header x-api-key: admin-key-sandbox-2026 200, data.price = 12, data.stock = 0, data.owner_id = "apikey:admin" TD-SEC-18 POST /api/v1/secure/orders { "quantity": 1 }, header x-api-key: demo-key-sandbox-2026 201, data.owner_id = "apikey:demo" TD-SEC-19 PUT /api/v1/secure/orders/{id} { "quantity": 5, "status": "processing" }, where id = {id of TD-SEC-18}, header x-api-key: demo-key-sandbox-2026 200, data.quantity = 5, data.status = "processing", data.owner_id = "apikey:demo" ## 6.10 GraphQL and SOAP TD-G-01 POST /graphql { "query": "{ users(limit: 2) { items { id name } pageInfo { total limit } } }" } 200, data.users.pageInfo.limit = 2 TD-G-02 POST /graphql { "query": "mutation { createUser(input: { name: \"X\", email: \"bad\" }) { id } }" } 200, errors[0].extensions.code = "VALIDATION_ERROR", errors[0].extensions.details[0].field = "email" TD-G-03 POST /graphql { "query": "{ user(id: \"00000000-0000-0000-0000-000000000000\") { id } }" } 200, data.user = null TD-G-04 POST /graphql { "query": "{ orders { items { user { id name email role age created_at updated_at } product { id } } } }" } 200, data.orders.items present TD-G-05 POST /graphql { "query": "{ sessionInfo }" } 200, data.sessionInfo.sessionId present TD-X-01 POST /soap SOAPAction GetUsers 5 200, GetUsersResponse.total present TD-X-02 POST /soap SOAPAction GetUser 00000000-0000-0000-0000-000000000000 400, soap:Fault faultcode = "soap:Client" TD-X-03 POST /soap SOAPAction CreateUser Badbad 400, soap:Fault faultcode = "soap:Client" TD-X-04 POST /soap SOAPAction LaunchRocket 400, soap:Fault faultcode = "soap:Client" TD-X-05 POST /soap xml ]>, header SOAPAction: GetUsers 400, soap:Fault faultcode = "soap:Client" ## 6.11 Platform, errors and headers TD-H-01 GET /health no body 200, status = "ok", header x-request-id present TD-H-02 GET /ready no body 200 or 503, status one of ["ready", "degraded"] TD-H-03 GET /api/v1/users header x-request-id: trace-abc-123 200, header x-request-id = "trace-abc-123" TD-H-04 GET /api/v1/nothing-here no body 404 NOT_FOUND, request_id present TD-H-05 PUT /api/v1/users { "name": "X", "email": "x@x.com" } 405 METHOD_NOT_ALLOWED, header Allow contains "GET" TD-H-06 POST /api/v1/users text {bad json, header Content-Type: application/json 400 BAD_REQUEST TD-H-07 POST /api/v1/users form name=x 415 UNSUPPORTED_MEDIA_TYPE TD-H-08 GET /api/v1/users no body 200, header X-Content-Type-Options = "nosniff", header Strict-Transport-Security present, header Cache-Control contains "no-store", header Content-Security-Policy contains "frame-ancestors" TD-H-09 GET /openapi.json no body 200, openapi present, paths present TD-H-10 GET /soap?wsdl no body 200, header Content-Type contains "xml" TD-H-11 GET /schema.graphql no body 200, header Content-Type contains "graphql" TD-H-12 GET /metrics no body 404 TD-H-13 GET /requirements/store.txt no body 200, header Content-Type contains "text/plain" TD-H-14 GET /requirements/auth-testing.txt no body 200, header Content-Type contains "text/plain" TD-H-15 GET /requirements/store.json no body 200, data.version = "3.0", data.frs present TD-H-16 GET /requirements/auth-testing.json no body 200, data.name = "auth-testing" TD-H-17 GET /graphql no body 200, header Content-Type contains "graphql" TD-H-18 GET /soap/service.wsdl no body 200, header Content-Type contains "xml" TD-H-19 GET /openapi.yaml no body 200, header Content-Type contains "yaml" TD-H-20 GET /learn no body 200, data contains "rest-basics" TD-H-21 GET /learn/{id} where id = rest-basics 200 TD-H-22 GET /status.json no body 200, status = "operational" ====================================================================== 7. END-TO-END SCENARIOS ====================================================================== Rules for every scenario: - Steps run in order. A scenario passes only if every step matches its expected result. - Each scenario starts with x-session-id: new and sends the returned x-session-id on every later request, including GraphQL and SOAP. Steps marked (session B) use a second session started the same way. - {name} placeholders mean "the value saved in an earlier step". Paths use the specification's own parameter name, {id}, bound with "where id = ...". - SOAP steps are POST /soap with Content-Type text/xml, the SOAPAction header set to the operation, and the request element wrapped in a SOAP 1.1 envelope. - Each scenario creates its own data and does not depend on the demo dataset, unless it says so. ### E2E-01 Shopper-to-fulfilment order lifecycle Covers: FR-001, FR-002, FR-004, FR-005, FR-006, FR-008, FR-009, UC-001, UC-002 Goal: A new shopper orders a newly listed product, and the manager takes the order from pending to completed. Step 1 Request: POST /api/v1/users { "name": "Dana Shopper", "email": "dana@shop.test" } Expected: 201; role = "user". Save data.id as {user_id}. Step 2 Request: POST /api/v1/products { "name": "Noise-Cancel Headphones", "price": 129.50, "stock": 10, "category": "audio", "description": "Over-ear" } Expected: 201; stock = 10. Save data.id as {product_id}. Step 3 Request: GET /api/v1/products?category=audio&sort=price&order=asc Expected: 200; data contains {product_id}. Step 4 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}", "quantity": 2, "notes": "Leave at door" } Expected: 201; status = "pending"; quantity = 2. Save data.id as {order_id}. Step 5 Request: GET /api/v1/orders/{id}?expand=user,product where id = {order_id} Expected: 200; user.name = "Dana Shopper"; product.name = "Noise-Cancel Headphones"; product.price = 129.5. Step 6 Request: GET /api/v1/orders?status=pending&sort=created_at&order=asc&limit=100 Expected: 200; data contains {order_id}. Step 7 Request: PATCH /api/v1/orders/{id} { "status": "processing" } where id = {order_id} Expected: 200; status = "processing"; updated_at present. Step 8 Request: PATCH /api/v1/orders/{id} { "status": "completed" } where id = {order_id} Expected: 200; status = "completed"; quantity = 2; notes = "Leave at door". Step 9 Request: GET /api/v1/orders?status=completed&limit=100 Expected: 200; data contains {order_id}. Step 10 Request: GET /api/v1/orders?status=pending&limit=100 Expected: 200; data does not contain {order_id}. Step 11 Request: GET /api/v1/products/{id} where id = {product_id} Expected: 200; stock = 10 (orders do not reserve stock). ### E2E-02 Order cancellation and rejected status change Covers: FR-009, RULE O-02 Goal: An order in processing is cancelled; an invalid status is rejected without changing the order. Step 1 Request: POST /api/v1/users { "name": "Cara Cancel", "email": "cara@shop.test" } Expected: 201. Save data.id as {user_id}. Step 2 Request: POST /api/v1/products { "name": "Phone Case", "price": 15.00, "stock": 5 } Expected: 201. Save data.id as {product_id}. Step 3 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}" } Expected: 201; quantity = 1; status = "pending". Save data.id as {order_id}. Step 4 Request: PATCH /api/v1/orders/{id} { "status": "processing" } where id = {order_id} Expected: 200; status = "processing". Step 5 Request: PATCH /api/v1/orders/{id} { "status": "cancelled" } where id = {order_id} Expected: 200; status = "cancelled". Step 6 Request: PATCH /api/v1/orders/{id} { "status": "shipped" } where id = {order_id} Expected: 400 VALIDATION_ERROR; details[0].field: "status". Step 7 Request: GET /api/v1/orders/{id} where id = {order_id} Expected: 200; status = "cancelled". Step 8 Request: GET /api/v1/orders?status=cancelled&limit=100 Expected: 200; data contains {order_id}. Step 9 Request: PATCH /api/v1/orders/{id} { "status": "processing" } where id = 00000000-0000-0000-0000-000000000000 Expected: 404 NOT_FOUND. ### E2E-03 Catalogue maintenance with PATCH, PUT and DELETE Covers: FR-006, FR-007, FR-010, RULE P-06 Goal: PATCH keeps omitted fields, PUT resets them, DELETE removes the product permanently. Step 1 Request: POST /api/v1/products { "name": "Desk Lamp", "price": 39.99, "stock": 12, "category": "home", "description": "LED, dimmable" } Expected: 201. Save data.id as {product_id} and data.created_at as {created_at}. Step 2 Request: PATCH /api/v1/products/{id} { "price": 34.99 } where id = {product_id} Expected: 200; price = 34.99; stock = 12; category = "home"; description = "LED, dimmable". Step 3 Request: PATCH /api/v1/products/{id} { "stock": -5 } where id = {product_id} Expected: 400 VALIDATION_ERROR; details[0].field: "stock". Step 4 Request: GET /api/v1/products/{id} where id = {product_id} Expected: 200; stock = 12 (the rejected update was not applied). Step 5 Request: PUT /api/v1/products/{id} { "name": "Desk Lamp v2", "price": 44.99 } where id = {product_id} Expected: 200; id = {product_id}; created_at = {created_at}; name = "Desk Lamp v2"; stock = 0; category = "general"; description = "". Step 6 Request: PUT /api/v1/products/{id} { "name": "No Price" } where id = {product_id} Expected: 400 VALIDATION_ERROR; details[0].field: "price". Step 7 Request: DELETE /api/v1/products/{id} where id = {product_id} Expected: 204; empty body. Step 8 Request: GET /api/v1/products/{id} where id = {product_id} Expected: 404 NOT_FOUND. Step 9 Request: DELETE /api/v1/products/{id} where id = {product_id} Expected: 404 NOT_FOUND. ### E2E-04 Orders survive deletion of linked records Covers: FR-005, FR-010, FR-014, RULE O-04, RULE O-05 Goal: After the customer and product are deleted, the order is unchanged and its links resolve to null in REST and GraphQL. Step 1 Request: POST /api/v1/users { "name": "Temp User", "email": "temp@shop.test" } Expected: 201. Save data.id as {user_id}. Step 2 Request: POST /api/v1/products { "name": "Temp Product", "price": 5.00 } Expected: 201. Save data.id as {product_id}. Step 3 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}" } Expected: 201. Save data.id as {order_id}. Step 4 Request: DELETE /api/v1/users/{id} where id = {user_id} Expected: 204. Step 5 Request: GET /api/v1/orders/{id}?expand=user,product where id = {order_id} Expected: 200; status = "pending"; user_id = {user_id}; user = null; product.name = "Temp Product". Step 6 Request: DELETE /api/v1/products/{id} where id = {product_id} Expected: 204. Step 7 Request: GET /api/v1/orders/{id}?expand=user,product where id = {order_id} Expected: 200; status = "pending"; user = null; product = null. Step 8 Request: POST /graphql { "query": "{ order(id: \"{order_id}\") { id status user { id } product { id } } }" } Expected: 200; data.order.status = "pending"; data.order.user = null; data.order.product = null. Step 9 Request: POST /api/v1/orders { "user_id": "00000000-0000-0000-0000-000000000000", "product_id": "00000000-0000-0000-0000-000000000000" } Expected: 201. Save data.id as {orphan_order_id}. Step 10 Request: GET /api/v1/orders/{id}?expand=user,product where id = {orphan_order_id} Expected: 200; user = null; product = null. ### E2E-05 Cross-protocol order lifecycle with SOAP, GraphQL and REST Covers: FR-004, FR-005, FR-009, FR-014, FR-015, UC-004 Goal: An order built from records created in different protocols is processed across all three, and each change is visible everywhere immediately. Step 1 Request: POST /soap SOAPAction CreateUser Sam Soapsam@shop.test Expected: HTTP 200; CreateUserResponse.user.name = "Sam Soap". Save CreateUserResponse.user.id as {user_id}. Step 2 Request: POST /graphql { "query": "mutation { createProduct(input: { name: \"GQL Gadget\", price: 15.5, stock: 3 }) { id name } }" } Expected: 200; data.createProduct.name = "GQL Gadget". Save data.createProduct.id as {product_id}. Step 3 Request: GET /api/v1/users?limit=100 Expected: 200; data contains id {user_id} with name = "Sam Soap". Step 4 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}", "quantity": 3 } Expected: 201. Save data.id as {order_id}. Step 5 Request: POST /graphql { "query": "{ order(id: \"{order_id}\") { status quantity user { name } product { name price } } }" } Expected: 200; data.order.status = "pending"; data.order.quantity = 3; data.order.user.name = "Sam Soap"; data.order.product.price = 15.5. Step 6 Request: POST /graphql { "query": "mutation { updateOrder(id: \"{order_id}\", input: { status: \"processing\" }) { status } }" } Expected: 200; data.updateOrder.status = "processing". Step 7 Request: POST /soap SOAPAction GetOrder {order_id} Expected: HTTP 200; GetOrderResponse.order.status = "processing". Step 8 Request: POST /soap SOAPAction UpdateOrder {order_id}completed Expected: HTTP 200; UpdateOrderResponse.order.status = "completed". Step 9 Request: GET /api/v1/orders/{id}?expand=user,product where id = {order_id} Expected: 200; status = "completed"; user.name = "Sam Soap"; product.name = "GQL Gadget". Step 10 Request: DELETE /api/v1/users/{id} where id = {user_id} Expected: 204. Step 11 Request: POST /graphql { "query": "{ user(id: \"{user_id}\") { id } }" } Expected: 200; data.user = null. Step 12 Request: POST /soap SOAPAction GetUser {user_id} Expected: HTTP 400; soap:Fault faultcode = "soap:Client". ### E2E-06 Authentication journey with JWT, API key and OAuth 2.0 Covers: FR-011, FR-012, FR-013, RULE A-01, A-02, A-03, UC-003 Goal: Each credential type is issued, verified and rejected when wrong or tampered. Step 1 Request: POST /auth/token { "username": "alice@demo.com", "password": "wrongpassword" } Expected: 401 UNAUTHORIZED; no data.access_token. Step 2 Request: POST /auth/token { "username": "alice@demo.com", "password": "alice123" } Expected: 200; token_type = "Bearer"; expires_in = 600. Save data.access_token as {jwt}. Step 3 Request: GET /auth/me, header Authorization: Bearer {jwt} Expected: 200; authenticated = true; method = "jwt"; identity.sub = "usr_alice"; identity.role = "admin". Step 4 Request: POST /auth/verify { "token": "{jwt}" } Expected: 200; valid = true; claims.exp - claims.iat = 600. Step 5 Request: GET /auth/me, header Authorization: Bearer {jwt}x (one extra character appended) Expected: 401 UNAUTHORIZED. Step 6 Request: GET /auth/me, header x-api-key: demo-key-sandbox-2026 Expected: 200; authenticated = true; method = "apikey". Step 7 Request: GET /auth/me, header x-api-key: not-a-real-key Expected: 401 UNAUTHORIZED. Step 8 Request: GET /auth/me Expected: 200; authenticated = false. Step 9 Request: POST /auth/oauth { "grant_type": "client_credentials", "client_id": "sandbox-client", "client_secret": "sandbox-secret", "scope": "read write" } Expected: 200. Save data.access_token as {m2m_token}. Step 10 Request: GET /auth/me, header Authorization: Bearer {m2m_token} Expected: 200; authenticated = true; identity.sub = "sandbox-client"; identity.client = true; identity.scopes = ["read","write"]. Step 11 Request: POST /auth/oauth { "grant_type": "client_credentials", "client_id": "sandbox-client", "client_secret": "wrong-secret" } Expected: 401 UNAUTHORIZED. Step 12 Request: POST /auth/oauth { "grant_type": "password", "client_id": "sandbox-client", "client_secret": "sandbox-secret" } Expected: 400 VALIDATION_ERROR. ### E2E-07 Validation error recovery without partial writes Covers: FR-001, FR-004, RULE U-02, RULE O-01 Goal: Rejected requests never store data; corrected retries succeed. Step 1 Request: GET /api/v1/users Expected: 200. Save pagination.total as {user_count}. Step 2 Request: POST /api/v1/users { "name": "Eve", "email": "not-an-email" } Expected: 400 VALIDATION_ERROR; details[0].field: "email". Step 3 Request: GET /api/v1/users Expected: 200; pagination.total = {user_count}. Step 4 Request: POST /api/v1/users { "name": "Eve", "email": "eve@shop.test", "age": 30 } Expected: 201. Save data.id as {user_id}. Step 5 Request: GET /api/v1/users Expected: 200; pagination.total = {user_count} + 1. Step 6 Request: POST /api/v1/products { "name": "Notebook", "price": 4.50 } Expected: 201. Save data.id as {product_id}. Step 7 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}", "quantity": 0 } Expected: 400 VALIDATION_ERROR; details[0].field: "quantity". Step 8 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}", "quantity": 1.5 } Expected: 400 VALIDATION_ERROR; details[0].field: "quantity". Step 9 Request: GET /api/v1/orders?user_id={user_id} Expected: 200; pagination.total = 0. Step 10 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}", "quantity": 1 } Expected: 201; status = "pending". Step 11 Request: GET /api/v1/orders?user_id={user_id} Expected: 200; pagination.total = 1. ### E2E-08 Resilience under simulated failures Covers: FR-016, UC-005 Goal: Simulated failures have no side effects and are clearly marked, and the client recovers by retrying. Precondition: a client timeout of at least 12 seconds. Step 1 Request: GET /api/v1/users Expected: 200. Save pagination.total as {user_count}. Step 2 Request: POST /api/v1/users?error=500 { "name": "Ghost", "email": "ghost@shop.test" } Expected: 500 SIMULATED_ERROR; success = false; header X-Sandbox-Simulated = "error=500". Step 3 Request: GET /api/v1/users Expected: 200; pagination.total = {user_count}. Step 4 Request: POST /api/v1/users { "name": "Ghost", "email": "ghost@shop.test" } Expected: 201. Step 5 Request: GET /api/v1/users?error=404 Expected: 404 SIMULATED_ERROR. Step 6 Request: GET /api/v1/products?delay=2000 Expected: 200; response time ≥ 2000 ms. Step 7 Request: GET /api/v1/products?delay=20000 Expected: 400 VALIDATION_ERROR; details[0].field: "delay". Step 8 Request: GET /api/v1/users?random_fail=true (repeat 20 times) Expected: each response is 200 or 500; every 500 has error = "RANDOM_FAILURE"; at least one response is 200. Step 9 Request: GET /api/v1/users?error=429 Expected: 429 SIMULATED_ERROR; header X-Sandbox-Simulated = "error=429". Step 10 Request: GET /api/v1/users Expected: 200; header X-Sandbox-Simulated absent. ### E2E-09 Session data isolation Covers: RULE S-02, RULE S-05, FR-019 Goal: Data created in one session is not visible from, and cannot be changed by, another session. Step 1 (session A) Request: POST /api/v1/users { "name": "Only In A", "email": "a@iso.test" } Expected: 201. Save data.id as {a_user_id}. Step 2 (session B) Request: GET /api/v1/users/{id} where id = {a_user_id} Expected: 404 NOT_FOUND. Step 3 (session B) Request: POST /graphql { "query": "{ user(id: \"{a_user_id}\") { id } }" } Expected: 200; data.user = null. Step 4 (session B) Request: DELETE /api/v1/users/{id} where id = {a_user_id} Expected: 404 NOT_FOUND. Step 5 (session B) Request: POST /soap SOAPAction GetUser {a_user_id} Expected: HTTP 400; soap:Fault. Step 6 (session A) Request: GET /api/v1/users/{id} where id = {a_user_id} Expected: 200; name = "Only In A" (session B could not affect it). ### E2E-10 Omnichannel order across all four interfaces Covers: FR-004, FR-005, FR-010, FR-014, FR-015, FR-017, RULE O-05 Goal: A customer registered through SOAP orders a product listed through GraphQL; the order is placed through REST, found through QUERY, processed through SOAP, and outlives its product. Step 1 Request: POST /soap SOAPAction CreateUser Omni Customeromni@shop.testuser Expected: HTTP 200. Save CreateUserResponse.user.id as {user_id}. Step 2 Request: POST /graphql { "query": "mutation { createProduct(input: { name: \"Omni Speaker\", price: 59.0, stock: 8, category: \"omni\" }) { id } }" } Expected: 200. Save data.createProduct.id as {product_id}. Step 3 Request: POST /api/v1/orders { "user_id": "{user_id}", "product_id": "{product_id}", "quantity": 4, "notes": "omnichannel" } Expected: 201; status = "pending". Save data.id as {order_id}. Step 4 Request: QUERY /api/v1/orders { "filters": { "notes": { "contains": "omni" }, "quantity": { "gte": 4 } }, "limit": 100 } Expected: 200; data contains {order_id}. Step 5 Request: QUERY /api/v1/products { "filters": { "category": "omni" }, "fields": ["id", "price"] } Expected: 200; data contains id {product_id} with price = 59. Step 6 Request: POST /soap SOAPAction UpdateOrder {order_id}processing Expected: HTTP 200; UpdateOrderResponse.order.status = "processing". Step 7 Request: POST /graphql { "query": "{ order(id: \"{order_id}\") { status quantity user { name } product { name } } }" } Expected: 200; data.order.status = "processing"; data.order.user.name = "Omni Customer"; data.order.product.name = "Omni Speaker". Step 8 Request: DELETE /api/v1/products/{id} where id = {product_id} Expected: 204. Step 9 Request: POST /soap SOAPAction GetOrder {order_id} Expected: HTTP 200; GetOrderResponse.order.status = "processing". Step 10 Request: POST /graphql { "query": "{ order(id: \"{order_id}\") { status product { id } } }" } Expected: 200; data.order.product = null. Step 11 Request: PATCH /api/v1/orders/{id} { "status": "completed" } where id = {order_id} Expected: 200; status = "completed"; quantity = 4. ### E2E-11 Catalogue bulk load, paging and search Covers: FR-002, FR-006, FR-007, FR-010, FR-017, FR-018, RULE Q-01 Goal: Pagination, filtering, QUERY search, replacement and deletion stay consistent over a larger catalogue, and the demo reset restores the starting state. Step 1 Request: POST /api/v1/products { "name": "Bulk Item", "price": 5, "category": "bulkload" } (repeat 12 times) Expected: each response is 201. Step 2 Request: POST /api/v1/products { "name": "Bulk Premium", "price": 50, "category": "bulkload" } Expected: 201. Save data.id as {premium_id}. Step 3 Request: GET /api/v1/products?category=bulkload&limit=5&page=1 Expected: 200; pagination.total = 13; pagination.pages = 3; pagination.hasNext = true; pagination.hasPrev = false. Step 4 Request: GET /api/v1/products?category=bulkload&limit=5&page=3 Expected: 200; data has 3 items; pagination.hasNext = false; pagination.hasPrev = true. Step 5 Request: QUERY /api/v1/products { "filters": { "category": "bulkload", "price": { "gt": 10 } } } Expected: 200; pagination.total = 1; data contains {premium_id}. Step 6 Request: QUERY /api/v1/products { "filters": { "category": "bulkload" }, "sort": "price", "order": "desc", "limit": 1, "fields": ["id", "price"] } Expected: 200; data[0].id = {premium_id}; data[0].price = 50; no data[0].name. Step 7 Request: PUT /api/v1/products/{id} { "name": "Bulk Premium Plus", "price": 55, "category": "bulkload" } where id = {premium_id} Expected: 200; price = 55; stock = 0. Step 8 Request: DELETE /api/v1/products/{id} where id = {premium_id} Expected: 204. Step 9 Request: GET /api/v1/products?category=bulkload&limit=100 Expected: 200; pagination.total = 12; data does not contain {premium_id}. Step 10 Request: POST /api/v1/demo/reset Expected: 200. Step 11 Request: GET /api/v1/products?limit=100 Expected: 200; pagination.total = 3; data contains "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa". ### E2E-12 Protected store authorisation and ownership Covers: FR-022, FR-023, FR-024, RULE SEC-01, SEC-02, SEC-03, SEC-04, UC-006 Goal: The protected store refuses missing credentials, enforces roles, and keeps each customer's orders private. Step 1 Request: GET /api/v1/secure/orders Expected: 401 UNAUTHORIZED; header WWW-Authenticate contains "Bearer". Step 2 Request: POST /auth/token { "username": "bob@demo.com", "password": "bob123" } Expected: 200. Save data.access_token as {bob}. Step 3 Request: POST /auth/token { "username": "charlie@demo.com", "password": "charlie123" } Expected: 200. Save data.access_token as {charlie}. Step 4 Request: POST /api/v1/secure/orders { "quantity": 2, "notes": "bob's order" }, header Authorization: Bearer {bob} Expected: 201; owner_id = "usr_bob". Save data.id as {bob_order}. Step 5 Request: GET /api/v1/secure/orders/{id} where id = {bob_order}, header Authorization: Bearer {bob} Expected: 200; notes = "bob's order". Step 6 Request: GET /api/v1/secure/orders/{id} where id = {bob_order}, header Authorization: Bearer {charlie} Expected: 403 FORBIDDEN. Step 7 Request: PATCH /api/v1/secure/orders/{id} { "status": "cancelled" } where id = {bob_order}, header Authorization: Bearer {charlie} Expected: 403 FORBIDDEN. Step 8 Request: GET /api/v1/secure/orders, header Authorization: Bearer {charlie} Expected: 200; data does not contain {bob_order}. Step 9 Request: DELETE /api/v1/secure/orders/{id} where id = {bob_order}, header Authorization: Bearer {bob} Expected: 403 FORBIDDEN. Step 10 Request: GET /api/v1/secure/orders, header x-api-key: admin-key-sandbox-2026 Expected: 200; data contains {bob_order}. Step 11 Request: DELETE /api/v1/secure/orders/{id} where id = {bob_order}, header x-api-key: admin-key-sandbox-2026 Expected: 204. Step 12 Request: GET /api/v1/orders/{id} where id = {bob_order} Expected: 404 NOT_FOUND (protected records never appear in the public store). ### E2E-13 Service integration through the protected store Covers: FR-013, FR-022, FR-023, RULE SEC-03 Goal: A backend service with client credentials maintains the catalogue that a logged-in customer then orders from, and scopes are enforced. Step 1 Request: POST /auth/oauth { "grant_type": "client_credentials", "client_id": "sandbox-client", "client_secret": "sandbox-secret", "scope": "read" } Expected: 200. Save data.access_token as {reader}. Step 2 Request: POST /api/v1/secure/products { "name": "Service Widget", "price": 12.5, "stock": 40 }, header Authorization: Bearer {reader} Expected: 403 FORBIDDEN. Step 3 Request: POST /auth/oauth { "grant_type": "client_credentials", "client_id": "sandbox-client", "client_secret": "sandbox-secret", "scope": "read write" } Expected: 200. Save data.access_token as {writer}. Step 4 Request: POST /api/v1/secure/products { "name": "Service Widget", "price": 12.5, "stock": 40 }, header Authorization: Bearer {writer} Expected: 201; owner_id = "sandbox-client". Save data.id as {product_id}. Step 5 Request: POST /auth/token { "username": "bob@demo.com", "password": "bob123" } Expected: 200. Save data.access_token as {bob}. Step 6 Request: GET /api/v1/secure/products/{id} where id = {product_id}, header Authorization: Bearer {bob} Expected: 200; name = "Service Widget". Step 7 Request: POST /api/v1/secure/orders { "product_id": "{product_id}", "quantity": 3 }, header Authorization: Bearer {bob} Expected: 201; owner_id = "usr_bob". Save data.id as {order_id}. Step 8 Request: PATCH /api/v1/secure/products/{id} { "stock": 37 } where id = {product_id}, header Authorization: Bearer {writer} Expected: 200; stock = 37. Step 9 Request: PATCH /api/v1/secure/products/{id} { "price": 1 } where id = {product_id}, header Authorization: Bearer {bob} Expected: 403 FORBIDDEN. Step 10 Request: GET /api/v1/secure/products/{id} where id = {product_id}, header Authorization: Bearer {reader} Expected: 200; price = 12.5; stock = 37. Step 11 Request: DELETE /api/v1/secure/products/{id} where id = {product_id}, header Authorization: Bearer {writer} Expected: 403 FORBIDDEN. ### E2E-14 Rate-limit back-off and recovery Covers: FR-021, RULE S-03, UC-005 Goal: The practice endpoint enforces its limit and reports it, while the rest of the API stays unlimited for the same client. Step 1 Request: GET /api/v1/rate-limited/ping (repeat 10 times) Expected: each response is 200. Step 2 Request: GET /api/v1/rate-limited/ping Expected: 429 RATE_LIMITED; header Retry-After present; header X-RateLimit-Remaining = "0". Step 3 Request: GET /api/v1/users (repeat 30 times) Expected: each response is 200. Step 4 (session B) Request: GET /api/v1/rate-limited/ping Expected: 200; header X-RateLimit-Remaining = "9". ### E2E-15 Session lifecycle Covers: FR-018, FR-019, RULE S-05 Goal: A tester creates, inspects, isolates, resets and ends a session. Step 1 Request: POST /session/new Expected: 201. Save data.session_id as {sid}. Step 2 Request: GET /session Expected: 200; session_id = {sid}; limits.records_per_entity = 10000. Step 3 Request: GET /api/v1/users/{id} where id = 11111111-1111-1111-1111-111111111111 Expected: 200; name = "Alice Johnson". Step 4 Request: DELETE /api/v1/users/{id} where id = 11111111-1111-1111-1111-111111111111 Expected: 204. Step 5 Request: GET /api/v1/users/{id} where id = 11111111-1111-1111-1111-111111111111 Expected: 404 NOT_FOUND. Step 6 Request: POST /api/v1/demo/reset Expected: 200. Step 7 Request: GET /api/v1/users/{id} where id = 11111111-1111-1111-1111-111111111111 Expected: 200; name = "Alice Johnson". Step 8 Request: DELETE /session Expected: 204. Step 9 Request: GET /session, header x-session-id: {sid} Expected: 200; session_id != {sid}. ### E2E-16 Contract discovery and platform readiness Covers: FR-025, FR-026, FR-027 Goal: Every contract can be downloaded, the probes answer, and errors are standard and traceable. Step 1 Request: GET /health Expected: 200; status = "ok". Step 2 Request: GET /openapi.json Expected: 200; openapi present; paths present. Step 3 Request: GET /openapi/rest.json Expected: 200; paths present. Step 4 Request: GET /soap?wsdl Expected: 200; header Content-Type contains "xml". Step 5 Request: GET /schema.graphql Expected: 200; header Content-Type contains "graphql". Step 6 Request: GET /api Expected: 200; protocols.graphql present. Step 7 Request: GET /api/v1/flows Expected: 200; data has 6 items. Step 8 Request: GET /api/v1/missing-route, header x-request-id: e2e-16-step-8 Expected: 404 NOT_FOUND; request_id = "e2e-16-step-8"; header x-request-id = "e2e-16-step-8". Step 9 Request: DELETE /api/v1/users Expected: 405 METHOD_NOT_ALLOWED; header Allow contains "POST". Step 10 Request: GET /requirements/store.txt Expected: 200; header Content-Type contains "text/plain". ### E2E-17 SOAP back-office integration Covers: FR-005, FR-015, RULE O-05 Goal: A legacy back-office system maintains the catalogue, customers and orders entirely through SOAP, and the REST view stays consistent with every change. Step 1 Request: POST /soap SOAPAction CreateProduct SOAP Kettle304soap-shop Expected: HTTP 200; CreateProductResponse.product.name = "SOAP Kettle". Save CreateProductResponse.product.id as {product_id}. Step 2 Request: POST /soap SOAPAction UpdateProduct {product_id}27.5 Expected: HTTP 200; UpdateProductResponse.product.price = 27.5; UpdateProductResponse.product.stock = 4. Step 3 Request: POST /soap SOAPAction GetProduct {product_id} Expected: HTTP 200; GetProductResponse.product.price = 27.5. Step 4 Request: POST /soap SOAPAction GetProducts 100 Expected: HTTP 200; GetProductsResponse.products.product contains {product_id}. Step 5 Request: POST /soap SOAPAction CreateUser Back Officebo@shop.test Expected: HTTP 200. Save CreateUserResponse.user.id as {user_id}. Step 6 Request: POST /soap SOAPAction UpdateUser {user_id}Back Office Ltdmoderator Expected: HTTP 200; UpdateUserResponse.user.name = "Back Office Ltd"; UpdateUserResponse.user.role = "moderator". Step 7 Request: POST /soap SOAPAction CreateOrder {user_id}{product_id}2 Expected: HTTP 200; CreateOrderResponse.order.status = "pending". Save CreateOrderResponse.order.id as {order_id}. Step 8 Request: POST /soap SOAPAction GetOrders 100pending Expected: HTTP 200; GetOrdersResponse.orders.order contains {order_id}. Step 9 Request: GET /api/v1/orders/{id}?expand=user,product where id = {order_id} Expected: 200; quantity = 2; user.name = "Back Office Ltd"; product.price = 27.5. Step 10 Request: POST /soap SOAPAction DeleteProduct {product_id} Expected: HTTP 200; DeleteProductResponse.success = "true". Step 11 Request: POST /soap SOAPAction DeleteOrder {order_id} Expected: HTTP 200; DeleteOrderResponse.success = "true". Step 12 Request: POST /soap SOAPAction DeleteUser {user_id} Expected: HTTP 200; DeleteUserResponse.success = "true". ### E2E-18 GraphQL-only storefront Covers: FR-014, RULE O-05 Goal: A single-page app manages customers, catalogue and orders through GraphQL alone, and every mutation is reflected in REST. Step 1 Request: POST /graphql { "query": "mutation { createUser(input: { name: \"Gina Graph\", email: \"gina@shop.test\" }) { id role } }" } Expected: 200; data.createUser.role = "user". Save data.createUser.id as {user_id}. Step 2 Request: POST /graphql { "query": "mutation { updateUser(id: \"{user_id}\", input: { age: 29 }) { id name age } }" } Expected: 200; data.updateUser.age = 29; data.updateUser.name = "Gina Graph". Step 3 Request: POST /graphql { "query": "mutation { createProduct(input: { name: \"Graph Lamp\", price: 42, stock: 6, category: \"graph-shop\" }) { id } }" } Expected: 200. Save data.createProduct.id as {product_id}. Step 4 Request: POST /graphql { "query": "mutation { updateProduct(id: \"{product_id}\", input: { price: 39.5 }) { price stock } }" } Expected: 200; data.updateProduct.price = 39.5; data.updateProduct.stock = 6. Step 5 Request: POST /graphql { "query": "{ products(limit: 100, sort: \"price\", order: \"asc\") { items { id price } pageInfo { total } } }" } Expected: 200; data.products.items contains {product_id}; data.products.items[].price in ascending order. Step 6 Request: POST /graphql { "query": "mutation { createOrder(input: { user_id: \"{user_id}\", product_id: \"{product_id}\", quantity: 3 }) { id status user { name } product { price } } }" } Expected: 200; data.createOrder.status = "pending"; data.createOrder.user.name = "Gina Graph"; data.createOrder.product.price = 39.5. Save data.createOrder.id as {order_id}. Step 7 Request: GET /api/v1/orders/{id} where id = {order_id} Expected: 200; quantity = 3; user_id = {user_id}. Step 8 Request: POST /graphql { "query": "mutation { deleteProduct(id: \"{product_id}\") }" } Expected: 200; data.deleteProduct = true. Step 9 Request: POST /graphql { "query": "{ order(id: \"{order_id}\") { status product { id } user { name } } }" } Expected: 200; data.order.product = null; data.order.user.name = "Gina Graph". Step 10 Request: POST /graphql { "query": "mutation { deleteOrder(id: \"{order_id}\") }" } Expected: 200; data.deleteOrder = true. Step 11 Request: POST /graphql { "query": "mutation { deleteUser(id: \"{user_id}\") }" } Expected: 200; data.deleteUser = true. Step 12 Request: GET /api/v1/users/{id} where id = {user_id} Expected: 404 NOT_FOUND. ====================================================================== 8. KNOWN BEHAVIOURS (NOT DEFECTS) ====================================================================== - Placing an order does not reduce product stock. - Order status transitions are not enforced; any allowed status can be set at any time, including moving a cancelled order back to pending. - Orders may reference user_id and product_id values that do not exist; they are created and resolve to null when expanded. - Email addresses are not required to be unique. - The public store endpoints (/api/v1/users, /api/v1/products, /api/v1/orders, /graphql, /soap) do not require authentication. Only /api/v1/secure/ and the authentication endpoints check credentials. - Requests sent without any session identifier from the same IP address share that IP's most recent session. Use x-session-id: new for isolation. - The reserved demo session is shared by every visitor, so its data can change at any time. - Responses that carry X-Sandbox-Simulated were requested by the client and are not server faults. - GraphQL reports errors with HTTP 200 and an errors array, as the GraphQL over HTTP convention allows.