You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Class name: take the file name, strip `_test` suffix, convert `snake_case` → `PascalCase`, append `Test`.
70
71
72
+
**Integration specs that drive traffic through the programmable proxy** (they reference `create_proxy_session()`, proxy rules, or `uts/test/realtime/integration/helpers/proxy.md`) follow a different translation flow — see the **Proxy integration tests** section at the end of this skill instead of the unit-test rules below.
Package: derived from the output path under `kotlin/`.
@@ -417,3 +420,173 @@ For any place where the generated test diverges from the spec pseudocode (adapte
417
420
-[ ] The deviation is recorded in `uts/src/test/kotlin/io/ably/lib/deviations.md`
418
421
419
422
If you find gaps during this review, fix them and re-run Steps 5–6 before finishing.
423
+
424
+
---
425
+
426
+
## Proxy integration tests
427
+
428
+
Some specs are **integration tests** that exercise fault-handling behaviour against the **real Ably sandbox** instead of a mocked transport. They route the SDK through the [`ably/uts-proxy`](https://github.com/ably/uts-proxy) — a programmable HTTP/WebSocket proxy that forwards traffic transparently by default but can inject faults (dropped connections, modified/injected/delayed frames, error responses) via rules.
429
+
430
+
Recognise them by: a reference to `create_proxy_session()`, proxy `rules`, `trigger_action`, `get_log`, or a pointer to `uts/test/realtime/integration/helpers/proxy.md`.
431
+
432
+
### When proxy tests are the right tool
433
+
434
+
| Test type | When the spec uses it |
435
+
|---|---|
436
+
|**Unit test** (mock HTTP/WS — the rest of this skill) | Client-side logic, state machines, request formation, error parsing. Fast, deterministic. |
|**Proxy integration test**| Fault behaviour against the real backend: connection failures, resume, heartbeat starvation, token renewal under network errors, channel error injection. |
439
+
440
+
### Infrastructure
441
+
442
+
Three helpers live in `uts/src/test/kotlin/io/ably/lib/test/helper/`. **Read them before translating a proxy spec** — they hold the exact method signatures.
443
+
444
+
-**`ProxyManager`** — downloads/starts the shared `uts-proxy` process and exposes the sandbox host. Call `ProxyManager.ensureProxy()` once per suite in setup. `ProxyManager.sandboxRealtimeHost` / `sandboxRestHost` are the upstream sandbox hosts (the default target of every session).
445
+
-**`ProxySession`** — one programmable session wrapping the proxy control API.
446
+
-**`SandboxApp`** — provisions/deletes a sandbox test app from the shared `test-app-setup.json` in ably-common. `SandboxApp.create()` returns a `SandboxApp` with `appId`, `defaultKey`, and `keys` (`defaultKey` is a full-capability `appId.keyId:keySecret`); `app.delete()` tears it down. Provision in suite setup, delete in teardown.
447
+
448
+
`ensureProxy()`, the `ProxySession` methods, and the `SandboxApp` methods are all **`suspend`** functions. Per-test bodies use `runTest { }`; JUnit5 `@BeforeAll`/`@AfterAll` (with `@TestInstance(Lifecycle.PER_CLASS)`) wrap their suspend calls in `runBlocking { }`.
449
+
450
+
### Test class docstring
451
+
452
+
Give every proxy integration test class this KDoc:
453
+
454
+
```kotlin
455
+
/**
456
+
* Proxy integration test against Ably Sandbox endpoint.
457
+
*
458
+
* Uses the programmable proxy (`uts/test/proxy/`) to inject transport-level faults while the
459
+
* SDK communicates with the real Ably backend. See
460
+
* `uts/test/realtime/integration/helpers/proxy.md` for proxy infrastructure details.
461
+
*/
462
+
```
463
+
464
+
### Session lifecycle
465
+
466
+
`create_proxy_session(endpoint: "nonprod:sandbox", rules: [...])` → `ProxySession.create(...)`. The sandbox is already the default target, so an empty rule set is just:
467
+
468
+
```kotlin
469
+
val session =ProxySession.create(rules = emptyList())
470
+
```
471
+
472
+
Always close the session (and the client) in a `finally` block:
473
+
474
+
```kotlin
475
+
ProxyManager.ensureProxy()
476
+
val session =ProxySession.create(rules =listOf(
477
+
wsConnectRule(action =mapOf("type" to "refuse_connection"), count =2),
478
+
))
479
+
try {
480
+
val client =TestRealtimeClient {
481
+
key = sandboxKey
482
+
connectThroughProxy(session) // routes realtime + REST through the proxy
Call `connectThroughProxy(session)` inside the client builder block. It is a `ClientOptionsBuilder` extension (in `ProxySession.kt`) that wires the SDK through the proxy:
506
+
507
+
```kotlin
508
+
val client =TestRealtimeClient {
509
+
key = sandboxKey
510
+
connectThroughProxy(session)
511
+
autoConnect =false
512
+
}
513
+
```
514
+
515
+
ably-java has **no `endpoint` ClientOptions field**; `connectThroughProxy` sets the discrete host fields for you:
516
+
517
+
| Proxy-def option | What `connectThroughProxy` sets |
|`useBinaryProtocol: false`| already the `ClientOptionsBuilder` default — left untouched |
523
+
524
+
It does **not** touch `autoConnect`, so set that yourself. Setting explicit hosts disables fallback hosts automatically, so don't add `fallbackHosts`.
525
+
526
+
### Auth in proxy tests
527
+
528
+
A spec that needs to observe (re-)authentication uses an `authCallback`. Where the pseudocode "generates a JWT from the key parts", the idiomatic ably-java equivalent is a **locally-signed `TokenRequest`** from the same sandbox key — no JWT library required. The realtime client exchanges it for a token through the proxy:
529
+
530
+
```kotlin
531
+
val tokenSigner =AblyRest(app.defaultKey) // local signing only; no network
532
+
val authCallbackCount =AtomicInteger(0)
533
+
val authCallback =Auth.TokenCallback { params ->
534
+
authCallbackCount.incrementAndGet()
535
+
tokenSigner.auth.createTokenRequest(params, null)
536
+
}
537
+
val client =TestRealtimeClient {
538
+
this.authCallback = authCallback
539
+
connectThroughProxy(session)
540
+
autoConnect =false
541
+
}
542
+
```
543
+
544
+
`TokenParams`/`TokenRequest` are nested in `io.ably.lib.rest.Auth`; `AuthDetails` is nested in `io.ably.lib.types.ProtocolMessage`.
545
+
546
+
### Rule factory helpers
547
+
548
+
Build rules with the factory helpers in `ProxySession.kt` rather than raw map literals. Rules are evaluated in order, first match wins, unmatched traffic passes through, and `times` auto-removes a rule after N firings.
`messageAction` is the protocol action **number** (e.g. `4` CONNECTED, `6` DISCONNECTED, `9` ERROR, `10` ATTACH, `11` ATTACHED) — see the action-number table in `proxy.md`.
559
+
560
+
Actions are passed as `mapOf(...)`, e.g.:
561
+
562
+
```kotlin
563
+
mapOf("type" to "refuse_connection")
564
+
mapOf("type" to "disconnect")
565
+
mapOf("type" to "suppress")
566
+
mapOf("type" to "delay", "delayMs" to 2000)
567
+
mapOf("type" to "inject_to_client", "message" to mapOf("action" to 6))
568
+
mapOf("type" to "http_respond", "status" to 401, "body" to mapOf(...))
569
+
```
570
+
571
+
### Verifying the event log
572
+
573
+
`getLog()` returns a typed `List<Event>`. Access fields via dot notation (`it.type`, `it.direction`, `it.queryParams`, `it.status`); numeric fields (`status`, `closeCode`) are already `Int?`. The raw protocol message is exposed as `Event.message` (a Gson `JsonObject?`) — introspect it with `it.message?.get("action")?.asInt`.
574
+
575
+
```kotlin
576
+
val log = session.getLog()
577
+
val wsConnects = log.filter { it.type =="ws_connect" }
578
+
assertTrue(wsConnects.size >=2)
579
+
val queryParams = wsConnects.first().queryParams
580
+
assertNotNull(queryParams["resume"])
581
+
```
582
+
583
+
### Conventions
584
+
585
+
1. Each test references the spec point and (where it exists) the corresponding unit test.
586
+
2.`ProxyManager.ensureProxy()` and sandbox-app provisioning go in `@BeforeAll` / suite setup; clean up in `@AfterAll`.
587
+
3. Each test creates its own `ProxySession` and closes it (and the client) in `finally`.
588
+
4. Use `awaitState` / `awaitChannelState` for state assertions; verify via SDK state **and** the proxy log where useful.
589
+
5. Use generous timeouts (10–30s) — real network is involved: `awaitState(client, ConnectionState.connected, 15.seconds)`.
590
+
6. Don't set `fallbackHosts`; explicit hosts already disable fallbacks.
591
+
592
+
Steps 5 (compile) and 6 (run) still apply. Note that proxy tests hit the live sandbox and download the proxy binary on first run, so they are slower and require network access.
0 commit comments