From a4a303925ea6f4e6babfd313cb4d40ae7217a880 Mon Sep 17 00:00:00 2001 From: owenpearson Date: Mon, 21 Sep 2026 17:48:07 +0100 Subject: [PATCH] UTS: fix publish-body assertions and pin protocol per test Corrects three publish request-body assertions that contradicted RSL4c3/RSL1k, and makes rest/unit tests pin the protocol (JSON vs msgpack) they assume so that parse_json(request.body) assertions are only made where they hold. Closes #527 Co-Authored-By: Claude Opus 4.8 --- uts/docs/writing-test-specs.md | 1 + uts/rest/unit/batch_publish.md | 4 +- uts/rest/unit/channel/annotations.md | 17 +++++-- uts/rest/unit/channel/idempotency.md | 13 +++-- uts/rest/unit/channel/publish.md | 49 ++++++++++++++----- .../unit/channel/update_delete_message.md | 35 ++++++++++--- uts/rest/unit/push/push_admin_publish.md | 15 ++++-- .../unit/push/push_channel_subscriptions.md | 5 +- uts/rest/unit/push/push_channels.md | 10 +++- .../unit/push/push_device_registrations.md | 5 +- 10 files changed, 119 insertions(+), 35 deletions(-) diff --git a/uts/docs/writing-test-specs.md b/uts/docs/writing-test-specs.md index aa403e6b2..c9034462f 100644 --- a/uts/docs/writing-test-specs.md +++ b/uts/docs/writing-test-specs.md @@ -10,6 +10,7 @@ This guide provides comprehensive guidance for writing portable test specificati - Test client-side validation and error handling - Token strings are opaque - any arbitrary string works for unit tests - No network calls - fast and deterministic +- **Protocol (JSON vs msgpack):** `useBinaryProtocol` defaults to `true` (TO3f), so a plain-key client sends msgpack request bodies. A spec that reads the request body — e.g. `body = parse_json(request.body)` — or otherwise makes assertions that depend on the encoding (a stringified `data`, an `encoding` value, base64 for binary payloads) must pin the protocol on the client under test. Set `useBinaryProtocol: false` in `ClientOptions` to derive against JSON; see `rest/unit/encoding/message_encoding.md` for the pattern in both directions. A spec that does not pin `useBinaryProtocol` is to be derived against JSON. ### Integration Tests (Ably Sandbox) - Run against `https://sandbox.realtime.ably-nonprod.net` diff --git a/uts/rest/unit/batch_publish.md b/uts/rest/unit/batch_publish.md index a0b86701b..287730390 100644 --- a/uts/rest/unit/batch_publish.md +++ b/uts/rest/unit/batch_publish.md @@ -127,13 +127,13 @@ Then the BatchResult contains results for all three channels ```pseudo channel_name = "test-RSC22c6-${random_id()}" -Given a REST client with mock HTTP +Given a REST client with mock HTTP configured with useBinaryProtocol: false And the mock is configured to capture requests When batchPublish is called with messages containing: - String data - Binary data (Uint8List/[]byte) - JSON object data -Then the captured request shows each message is encoded per RSL4: +Then the captured request shows each message is encoded per RSL4 (JSON protocol branch): - String: data as-is, no encoding - Binary: base64 encoded, encoding: "base64" - JSON: JSON stringified, encoding: "json" diff --git a/uts/rest/unit/channel/annotations.md b/uts/rest/unit/channel/annotations.md index dade657d7..44425f383 100644 --- a/uts/rest/unit/channel/annotations.md +++ b/uts/rest/unit/channel/annotations.md @@ -64,7 +64,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -152,7 +155,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -201,6 +207,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: true )) channel = client.channels.get(channel_name) @@ -255,6 +262,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: false )) channel = client.channels.get(channel_name) @@ -297,7 +305,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` diff --git a/uts/rest/unit/channel/idempotency.md b/uts/rest/unit/channel/idempotency.md index b21a9cfdb..6b44751ff 100644 --- a/uts/rest/unit/channel/idempotency.md +++ b/uts/rest/unit/channel/idempotency.md @@ -67,6 +67,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: true )) channel = client.channels.get(channel_name) @@ -123,6 +124,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: true )) channel = client.channels.get(channel_name) @@ -185,6 +187,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: true )) channel = client.channels.get(channel_name) @@ -234,6 +237,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: false )) channel = client.channels.get(channel_name) @@ -279,6 +283,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: true # Even with this enabled )) channel = client.channels.get(channel_name) @@ -334,6 +339,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: true )) channel = client.channels.get(channel_name) @@ -361,7 +367,7 @@ ASSERT body1["id"] == body2["id"] **Test ID**: `rest/unit/RSL1k/mixed-ids-in-batch-1` -**Spec requirement:** In a batch publish, messages with client-supplied IDs must be preserved, while messages without IDs receive library-generated IDs using the standard format. +**Spec requirement:** RSL1k3 - if more than one message is passed to `publish()` and one or more of those messages has a non-empty `id`, then all message ids (present or absent) are preserved on sending the batch. No ids are generated for the id-less messages. Tests batch publishing with some messages having client IDs and some not. @@ -381,6 +387,7 @@ install_mock(mock_http) client = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies idempotentRestPublishing: true )) channel = client.channels.get(channel_name) @@ -405,6 +412,6 @@ body = parse_json(request.body) ASSERT body[0]["id"] == "client-id-1" ASSERT body[2]["id"] == "client-id-2" -# Library-generated ID for middle message -ASSERT body[1]["id"] matches pattern "[A-Za-z0-9_-]+:[0-9]+" +# RSL1k3: an absent id is preserved as absent +ASSERT "id" NOT IN body[1] ``` diff --git a/uts/rest/unit/channel/publish.md b/uts/rest/unit/channel/publish.md index ca187b302..717f6e5d7 100644 --- a/uts/rest/unit/channel/publish.md +++ b/uts/rest/unit/channel/publish.md @@ -44,7 +44,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -100,7 +103,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -126,7 +132,9 @@ ASSERT body.length == 3 ASSERT body[0]["name"] == "event1" ASSERT body[0]["data"] == "data1" ASSERT body[1]["name"] == "event2" -ASSERT body[1]["data"] == { "key": "value" } +ASSERT body[1]["data"] IS String +ASSERT parse_json(body[1]["data"]) == { "key": "value" } +ASSERT body[1]["encoding"] == "json" # Note: binary data encoding tested separately in encoding tests ``` @@ -154,17 +162,20 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` ### Test Cases -| ID | name | data | Expected body | -|----|------|------|---------------| -| 1 | `null` | `"hello"` | `[{"data": "hello"}]` | -| 2 | `"event"` | `null` | `[{"name": "event"}]` | -| 3 | `null` | `null` | `[{}]` | +| ID | name | data | +|----|------|------| +| 1 | `null` | `"hello"` | +| 2 | `"event"` | `null` | +| 3 | `null` | `null` | ### Test Steps ```pseudo @@ -174,7 +185,9 @@ FOR EACH test_case IN test_cases: AWAIT channel.publish(name: test_case.name, data: test_case.data) body = parse_json(captured_requests[0].body) - ASSERT body == [test_case.expected_body] + ASSERT body.length == 1 + ASSERT body[0]["name"] == test_case.name IF test_case.name IS NOT null + ASSERT body[0]["data"] == test_case.data IF test_case.data IS NOT null ASSERT "name" NOT IN body[0] IF test_case.name IS null ASSERT "data" NOT IN body[0] IF test_case.data IS null ``` @@ -205,7 +218,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -308,7 +324,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -439,6 +458,7 @@ captured_requests = [] client_with_id = Rest(options: ClientOptions( key: "appId.keyId:keySecret", + useBinaryProtocol: false, # pins JSON so parse_json(request.body) applies clientId: "lib-client" )) AWAIT client_with_id.channels.get(channel_name_m1).publish(name: "e", data: "d") @@ -461,7 +481,10 @@ ASSERT body["clientId"] == "lib-client" # Explicit clientId preserved # RSL1m3 - Unidentified client with message clientId captured_requests = [] -client_no_id = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client_no_id = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) AWAIT client_no_id.channels.get(channel_name_m3).publish( message: Message(name: "e", data: "d", clientId: "msg-client") ) diff --git a/uts/rest/unit/channel/update_delete_message.md b/uts/rest/unit/channel/update_delete_message.md index b7cceeb88..50daa3563 100644 --- a/uts/rest/unit/channel/update_delete_message.md +++ b/uts/rest/unit/channel/update_delete_message.md @@ -36,7 +36,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -88,7 +91,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -136,7 +142,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -182,7 +191,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -231,7 +243,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -272,7 +287,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` @@ -487,7 +505,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) channel = client.channels.get(channel_name) ``` diff --git a/uts/rest/unit/push/push_admin_publish.md b/uts/rest/unit/push/push_admin_publish.md index c99e06ca6..2a41f378d 100644 --- a/uts/rest/unit/push/push_admin_publish.md +++ b/uts/rest/unit/push/push_admin_publish.md @@ -61,7 +61,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) ``` ### Test Steps @@ -118,7 +121,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) ``` ### Test Steps @@ -167,7 +173,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) ``` ### Test Steps diff --git a/uts/rest/unit/push/push_channel_subscriptions.md b/uts/rest/unit/push/push_channel_subscriptions.md index 0a5a4e6d0..93681bf07 100644 --- a/uts/rest/unit/push/push_channel_subscriptions.md +++ b/uts/rest/unit/push/push_channel_subscriptions.md @@ -262,7 +262,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) ``` ### Test Steps diff --git a/uts/rest/unit/push/push_channels.md b/uts/rest/unit/push/push_channels.md index 1a06eba17..91415d0e2 100644 --- a/uts/rest/unit/push/push_channels.md +++ b/uts/rest/unit/push/push_channels.md @@ -47,7 +47,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) # Configure the local device as a registered push target client.device = LocalDevice( @@ -148,7 +151,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) # Configure the local device with a clientId client.device = LocalDevice( diff --git a/uts/rest/unit/push/push_device_registrations.md b/uts/rest/unit/push/push_device_registrations.md index 754eba6f6..123efe895 100644 --- a/uts/rest/unit/push/push_device_registrations.md +++ b/uts/rest/unit/push/push_device_registrations.md @@ -332,7 +332,10 @@ mock_http = MockHttpClient( ) install_mock(mock_http) -client = Rest(options: ClientOptions(key: "appId.keyId:keySecret")) +client = Rest(options: ClientOptions( + key: "appId.keyId:keySecret", + useBinaryProtocol: false # pins JSON so parse_json(request.body) applies +)) ``` ### Test Steps