# Testing

> **TL;DR** — There is no sandbox: tests call `https://www.wandura.ai`. Keep test traffic in your **test organisation** (sample films only, plays never counted) and trigger any error on demand with the `Wandura-Simulate` header. Run your CI against a mock built from the OpenAPI file. Never load-test production. Then run the test plan below on real phones.

## Where to test
| Layer | Use | Counts as views? |
|---|---|---|
| Unit and CI tests | A mock server generated from the OpenAPI file, for example `prism mock https://docs.wandura.ai/developers/openapi/delivery-v1.json`, or the fakes in the [examples](https://docs.wandura.ai/developers/guides/catalog-sync#code) | No (nothing reaches Wandura) |
| Integration tests | Your **test organisation**'s keys, against production | No |
| Acceptance on real phones | Your test organisation, then a final check in production | Production: yes |

## Test organisation
Every partner gets one **test organisation** next to its production one. Wandura creates it on request in your team's Discord channel and adds your Admins and Developers to it. On partner.wandura.ai, switch to "*Your organisation* (test)" and create keys under **Developers** as usual. The examples on this page call such a key `WANDURA_TEST_KEY`.

Test keys call the same production URL, `https://www.wandura.ai`, and behave exactly like production keys, with three differences:

1. **Not counted.** Its plays are never counted as views, usage or revenue.
2. **Sample films only.** It can only stream Wandura's sample films `ninety-days` and `the-fire-clause` (short episodes, with English subtitles). Open video is not available in a test organisation.
3. **Simulated errors.** It may send the `Wandura-Simulate` header (below). Production keys ignore that header completely.

`GET /config` tells you which kind of key you hold:

```json
{
  "key": { "scopes": ["metadata:read", "playback"], "test_organisation": true }
}
```

**Real states, on request.** For what simulation cannot show (for example a key that really stops working, or a licence that really ends), ask in your channel: Wandura can suspend your test organisation, suspend or end the licence of a sample film, switch streaming off, or lower your playback limit to test `429` for real. Changes take effect within 30 seconds. The test organisation's playback limit is 600 tickets per minute.

## Simulated errors
With a test key, send `Wandura-Simulate: <scenario>`. You get the same status, body and headers as the real error, plus `Wandura-Simulated: <scenario>` on the response. A simulated answer issues no ticket and does not count against your limits; simulated answers are themselves limited to 300 per minute per organisation. The key, scope and version checks still run first, so a bad key still answers `401`. If the response has no `Wandura-Simulated` header, nothing was simulated (unknown name, or a scenario that does not apply to that endpoint) and the request ran normally.

| Scenario | Endpoints | You get |
|---|---|---|
| `rate_limited` | playback, catalog, config | `429` `rate_limited` with `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Window` |
| `prefetch_shed` | playback | `429` `rate_limited`, `"reason": "prefetch_shed"` |
| `service_unavailable` | playback, catalog, config | `503` `service_unavailable`, `Retry-After: 1` |
| `service_unavailable:provider` | playback | `503` `service_unavailable` (video provider), `Retry-After: 2` |
| `service_unavailable:catalog` | catalog | `503` `service_unavailable` (`Could not read the catalog in time — retry`) |
| `server_error` | playback, catalog, config | `500` with a JSON body carrying `request_id`, as for a real unexpected error |
| `unsupported_version` | playback, catalog, config | `400` `unsupported_version` with `supported_versions`, `latest_version`, `upgrade_to` |
| `not_entitled` | playback | `403` `not_entitled`, `reason` `entitlement_not_found` |
| `not_entitled:<reason>` | playback | `403` `not_entitled` with `reason` `entitlement_suspended`, `streaming_disabled`, `outside_window` or `org_suspended` |
| `org_suspended` | playback, catalog, config | Playback: as `not_entitled:org_suspended`; catalog and config: `403` with `error` `org_suspended` |
| `not_ready` | playback | `404` `not_ready` |
| `not_ready:no_rendition` | playback | `503` `not_ready` |
| `sdk_required` | playback | `409` `sdk_required` with `protection` |
| `partial_batch` | playback, batch of 2–3 | Item 0 is a **real** ticket (not counted, as in any test organisation); later items `503` with `retry_after: 2`, or `429` `prefetch_shed` |
| `empty_catalog` | catalog | `200` with empty `films` and `episodes` |
| `version_deprecated` | catalog, config | A real `200` plus `Deprecation`, `Sunset` and `Link` headers |

In a batch, a per-episode scenario answers `200` `{ "items": [...] }` with the error on each item, exactly like a real batch.

What cannot be simulated: an expired Player SDK licence (it lives on the device).

### Example: `rate_limited`
```bash
curl -s -i https://www.wandura.ai/api/delivery/v1/catalog \
  -H "Authorization: Bearer $WANDURA_TEST_KEY" \
  -H "Wandura-Simulate: rate_limited"
```

```text
HTTP/2 429
retry-after: 1
x-ratelimit-limit: 600
x-ratelimit-window: 60
wandura-simulated: rate_limited
```

```json
{
  "error": "Too many catalog requests for this account. This is an abuse ceiling, not a quota — if a normal integration reaches it, contact us and it will be raised.",
  "error_code": "rate_limited"
}
```

### Example: `not_entitled:outside_window`
No device values are needed: nothing is issued.

```bash
curl -s https://www.wandura.ai/api/delivery/v1/playback \
  -H "Authorization: Bearer $WANDURA_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Wandura-Simulate: not_entitled:outside_window" \
  -d '{ "film_ref": "ninety-days", "episode_no": 1, "intent": "play" }'
```

```json
{
  "error": "outside_window",
  "error_code": "not_entitled",
  "reason": "outside_window"
}
```

### Example: `partial_batch`
Item 0 is a real ticket, so send the device values of a real device (see [Quickstart · Part B](https://docs.wandura.ai/developers/quickstart#part-b)).

```bash
curl -s https://www.wandura.ai/api/delivery/v1/playback \
  -H "Authorization: Bearer $WANDURA_TEST_KEY" \
  -H "Content-Type: application/json" \
  -H "Wandura-Simulate: partial_batch" \
  -d '{ "film_ref": "ninety-days", "episode_no": 1, "next": 2, "intent": "play",
        "union_info": "'"$UNION_INFO"'", "drm_kek": "'"$DRM_KEK"'", "viewer_ref": "qa-viewer-1:device-1" }'
```

```json
{
  "items": [
    { "request": { "film_ref": "ninety-days", "episode_no": 1 }, "status": 200, "vid": "v0d2f8c1a9e4b7", "play_auth_token": "placeholder-play-token", "expires_at": "2026-10-02T12:30:00.000Z", "protection": "private-encrypted", "subtitles": [] },
    { "request": { "film_ref": "ninety-days", "episode_no": 2 }, "status": 503, "error": "The video provider did not answer in time — retry", "error_code": "service_unavailable", "retry_after": 2 },
    { "request": { "film_ref": "ninety-days", "episode_no": 3 }, "status": 429, "error": "Prefetch requests are paused while your playback traffic is close to its ceiling — tickets for viewers who pressed play are still issued. Retry the prefetch after Retry-After, or request it when the viewer plays.", "error_code": "rate_limited", "reason": "prefetch_shed", "retry_after": 1 }
  ]
}
```

## What you can reproduce with any key
| Scenario | How |
|---|---|
| `401` | No key, or a revoked one (refused within about a second of revoking). |
| `403` scope | A key with only `metadata:read`, calling `POST /playback`. |
| `400` `invalid_device_info` | A device value of `"(null)"` or under 8 characters. |
| `400` `invalid_request` | Only one device value, or a malformed batch. |
| `409` `sdk_required` | Neither device value. |
| `404` `not_ready` | An unknown `episode_id`, or `next: 2` on the last episode. |
| `400` `unsupported_version` | `Wandura-Version: 1999-01-01`. |
| Creatives `404` `4040`, `422` | An unknown ID; a bad `limit` or `cursor`. |
| Expired ticket | Pause playback for more than 30 minutes. |

## Load tests
Do not load-test production. To test your own system under load, mock Wandura. To test `429` handling against Wandura, use `Wandura-Simulate: rate_limited`, or ask for a lower limit on your test organisation.

## Test plan
Run it on a release build, on a real iPhone and a real Android phone.

**Keys and security**

- [ ] No `wnd_live_` in the built apps.
- [ ] No full key, `play_auth_token` or raw device value in your logs ([Observability](https://docs.wandura.ai/developers/production/observability#never-log)).
- [ ] Catalog without a key: `401`; with a key lacking `metadata:read`: `403`. Your job alerts and keeps its copy.

**Catalog**

- [ ] The films shown match the licences Wandura confirmed, in the API's order.
- [ ] Free episodes open without payment; the next one shows your paywall.
- [ ] Placeholders show "coming soon" and cannot be tapped; `stream_ready: false` episodes are greyed out.
- [ ] With the sync job's network blocked for one run, the catalog in the app does not change.

**Playback**

- [ ] A free episode plays, with subtitles; switching language works; an episode without subtitles plays.
- [ ] A locked episode shows your paywall and makes no Wandura request.
- [ ] Starting an episode sends one request (`play` + unlocked prefetches); moving on starts the next at once and sends a new `play`.
- [ ] Pausing more than 30 minutes, then resuming, leads to one fresh ticket and playback from the same position.
- [ ] Airplane mode mid-episode and back: recovery without a crash.
- [ ] `429`, `503`, `500`, `not_entitled`, `not_ready` and `partial_batch`, simulated with `Wandura-Simulate`, each show the right screen.

**Rights ending**

- [ ] Ask Wandura to end the licence of one sample film in your test organisation. After the next two syncs it is hidden, and a ticket request answers `403` `not_entitled`.

## Related
- [Go-live checklist](https://docs.wandura.ai/developers/production/go-live)
- [Errors](https://docs.wandura.ai/developers/concepts/errors)
