Skip to content

Commit 1ffc3d4

Browse files
committed
feat: add createApnsBroadcast (RSH1d) and liveActivity methods (RSH1e1–RSH1e3) in PushAdmin
- Include tests for `start`, `update`, and `end` methods with various scenarios and error propagation. - Update related specifications and documentation to reflect test coverage and implementation.
1 parent 6dc3ac9 commit 1ffc3d4

5 files changed

Lines changed: 616 additions & 1 deletion

File tree

specifications/api-docstrings.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,6 +1084,23 @@ Enables the management of device registrations and push notification subscriptio
10841084
|| `data` ||| A JSON object containing the push notification payload. |
10851085
| deviceRegistrations: PushDeviceRegistrations ||| RSH1b | A [`PushDeviceRegistrations`]{@link PushDeviceRegistrations} object. |
10861086
| channelSubscriptions: PushChannelSubscriptions ||| RSH1c | A [`PushChannelSubscriptions`]{@link PushChannelSubscriptions} object. |
1087+
| createApnsBroadcast(options: PushApnsBroadcastOptions) => io PushApnsBroadcast ||| RSH1d | Creates an APNs broadcast channel for use with an iOS Live Activity. Call once before starting the Live Activity and persist the returned identifiers for the session. |
1088+
|| `options` ||| An object containing the `message-storage-policy` for the broadcast. |
1089+
|| `returns` ||| The created broadcast, containing its `id` and `apns-channel-id`. |
1090+
| liveActivity: PushLiveActivity ||| RSH1e | A [`PushLiveActivity`]{@link PushLiveActivity} object. |
1091+
1092+
## class PushLiveActivity
1093+
1094+
Controls the lifecycle of an iOS Live Activity over an APNs broadcast channel created with [`PushAdmin.createApnsBroadcast`]{@link PushAdmin#createApnsBroadcast}.
1095+
1096+
| Method / Property | Parameter | Returns | Spec | Description |
1097+
|---|---|---|---|---|
1098+
| start(params: PushLiveActivityStartParams) => io ||| RSH1e1 | Sends a push-to-start notification to all devices subscribed to the given Ably channels, causing each device to start a new Live Activity. |
1099+
|| `params` ||| An object containing the recipient `channels` (and optionally a `device-id`), the broadcast `apns-broadcast` identifier, and the `apns` Live Activity start payload. |
1100+
| update(params: PushLiveActivityUpdateParams) => io ||| RSH1e2 | Sends a content-state update to all devices with an active Live Activity on the broadcast channel. |
1101+
|| `params` ||| An object containing the broadcast `apns-broadcast` identifier, the `apns` Live Activity update payload, and optional APNs delivery `headers`. |
1102+
| end(params: PushLiveActivityEndParams) => io ||| RSH1e3 | Ends the Live Activity on all subscribed devices and cleans up the APNs channel. |
1103+
|| `params` ||| An object containing the broadcast `apns-broadcast` identifier, the `apns` Live Activity end payload, and optional APNs delivery `headers`. |
10871104

10881105
## class JsonObject
10891106

specifications/features.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,11 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info
10481048
- `(RSH1c3)` `#save(pushChannelSubscription)` issues a `POST` request to [/push/channelSubscriptions](/api/rest-api#post-channel-subscription) using the `PushChannelSubscription` object (and optionally a JSON-encodable object) argument. If the client has been [activated as a push target device](#activation-state-machine), and the specified `PushChannelSubscription` contains a `deviceId` matching that of the present client, then this request must include [push device authentication](#push-device-authentication). A test should exist for a successful save, a successful subsequent save with an update, and a failed save operation
10491049
- `(RSH1c4)` `#remove(push_channel_subscription)` issues a `DELETE` request to [/push/channelSubscriptions](/api/rest-api#delete-channel-subscription) and deletes the channel subscription using the attributes as params to the `DELETE` request. If the client has been [activated as a push target device](#activation-state-machine), and the specified `PushChannelSubscription` contains a `deviceId` matching that of the present client, then this request must include [push device authentication](#push-device-authentication). A test should exist that deletes a `clientId` and `deviceId` channel subscription separately and both succeed, and then also deletes a subscription that does not exist but still succeeds
10501050
- `(RSH1c5)` `#removeWhere(params)` issues a `DELETE` request to [/push/channelSubscriptions](/api/rest-api#delete-channel-subscription) and deletes the matching channel subscriptions provided in `params`. A test should exist that deletes channel subscriptions by `clientId` and by `deviceId` separately, then additionally issues a delete for subscriptions with no matching params and checks the operation still succeeds.
1051+
- `(RSH1d)` `#createApnsBroadcast(options)` issues a `POST` request to `/push/apnsBroadcastChannels` with a body containing the `messageStoragePolicy` (either `0` or `1`), and returns the created broadcast as `{ id, apnsChannelId }`. This creates an APNs broadcast channel for use with an iOS Live Activity. A test should exist verifying the request method, path and body, and that the returned `id` and `apnsChannelId` are parsed from the response.
1052+
- `(RSH1e)` `#liveActivity` provides access to the admin `PushLiveActivity` object, which controls the lifecycle of an iOS Live Activity over an APNs broadcast channel created with `#createApnsBroadcast`. It provides the following methods:
1053+
- `(RSH1e1)` `#start(params)` issues a `POST` request to `/push/apnsBroadcastChannels/:apnsBroadcast/start` with a body carrying the `apns` payload together with the recipient `channels` and/or `deviceId`. A test should exist for the channels-only case, and separately for the case where a `deviceId` is supplied, verifying that the `apnsBroadcast` id is URL-encoded in the request path.
1054+
- `(RSH1e2)` `#update(params)` issues a `POST` request to `/push/apnsBroadcastChannels/:apnsBroadcast/broadcast` with a body carrying the `apns` payload. Optional APNs delivery `headers` (such as `apns-priority` and `apns-expiration`), when supplied, are included in the request body under a `headers` key alongside `apns`. A test should exist both with and without the optional `headers`.
1055+
- `(RSH1e3)` `#end(params)` issues a `POST` request to `/push/apnsBroadcastChannels/:apnsBroadcast/end` with a body carrying the `apns` payload. Optional APNs delivery `headers` (such as `apns-priority` and `apns-expiration`), when supplied, are included in the request body under a `headers` key alongside `apns`.
10511056
- `(RSH2)` The following should only apply to platforms that support receiving push notifications:
10521057
- `(RSH2a)` `Push#activate` sends a `CalledActivate` event to [the state machine](#RSH3).
10531058
- `(RSH2b)` `Push#deactivate` sends a `CalledDeactivate` event to [the state machine](#RSH3).
@@ -2781,6 +2786,8 @@ Each type, method, and attribute is labelled with the name of one or more clause
27812786
publish(recipient: JsonObject, data: JsonObject) => io // RSH1a
27822787
deviceRegistrations: PushDeviceRegistrations // RSH1b
27832788
channelSubscriptions: PushChannelSubscriptions // RSH1c
2789+
createApnsBroadcast(options: PushApnsBroadcastOptions) => io PushApnsBroadcast // RSH1d
2790+
liveActivity: PushLiveActivity // RSH1e
27842791

27852792
class PushDeviceRegistrations: // RSH1b
27862793
get(deviceId: String) => io DeviceDetails // RSH1b1
@@ -2796,6 +2803,11 @@ Each type, method, and attribute is labelled with the name of one or more clause
27962803
remove(PushChannelSubscription) => io // RSH1c4
27972804
removeWhere(params: Dict<String, String>) => io // RSH1c5
27982805

2806+
class PushLiveActivity: // RSH1e
2807+
start(params: PushLiveActivityStartParams) => io // RSH1e1
2808+
update(params: PushLiveActivityUpdateParams) => io // RSH1e2
2809+
end(params: PushLiveActivityEndParams) => io // RSH1e3
2810+
27992811
enum DevicePlatform: // PCD6
28002812
"android"
28012813
"ios"
@@ -2818,6 +2830,32 @@ Each type, method, and attribute is labelled with the name of one or more clause
28182830
clientId: String? // PCS3
28192831
channel: String // PCS4
28202832

2833+
class PushApnsBroadcastOptions: // RSH1d
2834+
messageStoragePolicy: Int // 0 or 1
2835+
2836+
class PushApnsBroadcast: // RSH1d
2837+
id: String
2838+
apnsChannelId: String
2839+
2840+
class PushLiveActivityStartParams: // RSH1e1
2841+
recipient: PushLiveActivityRecipient
2842+
apnsBroadcast: String
2843+
apns: JsonObject
2844+
2845+
class PushLiveActivityRecipient: // RSH1e1
2846+
channels: List<String>
2847+
deviceId: String?
2848+
2849+
class PushLiveActivityUpdateParams: // RSH1e2
2850+
apnsBroadcast: String
2851+
apns: JsonObject
2852+
headers: JsonObject?
2853+
2854+
class PushLiveActivityEndParams: // RSH1e3
2855+
apnsBroadcast: String
2856+
apns: JsonObject
2857+
headers: JsonObject?
2858+
28212859
class ErrorInfo: // TI*
28222860
code: Int // TI1
28232861
href: String? // TI1, TI4

uts/docs/completion-status.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ This matrix lists all spec items from the [Ably features spec](../../specificati
297297

298298
| Spec item | Description | UTS test spec |
299299
|-----------|-------------|---------------|
300-
| RSH1 | Push#admin object (RSH1a–RSH1c5) | Yes — `rest/unit/push/push_admin_publish.md` (RSH1, RSH1a), `rest/unit/push/push_device_registrations.md` (RSH1b1–RSH1b5), `rest/unit/push/push_channel_subscriptions.md` (RSH1c1–RSH1c5), `rest/integration/push_admin.md` (RSH1a–RSH1c5) |
300+
| RSH1 | Push#admin object (RSH1a–RSH1e3) | Yes — `rest/unit/push/push_admin_publish.md` (RSH1, RSH1a), `rest/unit/push/push_device_registrations.md` (RSH1b1–RSH1b5), `rest/unit/push/push_channel_subscriptions.md` (RSH1c1–RSH1c5), `rest/unit/push/push_admin_apns_broadcast.md` (RSH1d), `rest/unit/push/push_admin_live_activity.md` (RSH1e1–RSH1e3), `rest/integration/push_admin.md` (RSH1a–RSH1c5) |
301301
| RSH2 | Platform-specific push operations (RSH2a–RSH2e) | |
302302
| RSH3 | Activation state machine (RSH3a–RSH3g3) | |
303303
| RSH4–RSH5 | Event queueing and sequential handling | |
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# PushAdmin createApnsBroadcast Tests
2+
3+
Spec points: `RSH1`, `RSH1d`
4+
5+
## Test Type
6+
Unit test with mocked HTTP client
7+
8+
## Mock HTTP Infrastructure
9+
10+
See `uts/test/rest/unit/helpers/mock_http.md` for the full Mock HTTP Infrastructure specification.
11+
12+
---
13+
14+
## RSH1d — createApnsBroadcast sends POST to /push/apnsBroadcastChannels
15+
16+
**Test ID**: `rest/unit/RSH1d/create-apns-broadcast-post-0`
17+
18+
**Spec requirement:** RSH1d — `createApnsBroadcast(options)` issues a `POST` request to `/push/apnsBroadcastChannels`.
19+
20+
Tests that `push.admin.createApnsBroadcast()` sends a POST to the correct path.
21+
22+
### Setup
23+
```pseudo
24+
captured_requests = []
25+
26+
mock_http = MockHttpClient(
27+
onConnectionAttempt: (conn) => conn.respond_with_success(),
28+
onRequest: (req) => {
29+
captured_requests.append(req)
30+
req.respond_with(201, { "id": "broadcast-1", "apnsChannelId": "apns-1" })
31+
}
32+
)
33+
install_mock(mock_http)
34+
35+
client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
36+
```
37+
38+
### Test Steps
39+
```pseudo
40+
AWAIT client.push.admin.createApnsBroadcast(
41+
options: { "messageStoragePolicy": 1 }
42+
)
43+
```
44+
45+
### Assertions
46+
```pseudo
47+
ASSERT captured_requests.length == 1
48+
49+
request = captured_requests[0]
50+
ASSERT request.method == "POST"
51+
ASSERT request.url.path == "/push/apnsBroadcastChannels"
52+
```
53+
54+
---
55+
56+
## RSH1d — createApnsBroadcast body contains messageStoragePolicy
57+
58+
**Test ID**: `rest/unit/RSH1d/message-storage-policy-1`
59+
60+
**Spec requirement:** RSH1d — the request body contains the `messageStoragePolicy`.
61+
62+
Tests that the supplied `messageStoragePolicy` is serialized into the request body.
63+
64+
### Setup
65+
```pseudo
66+
captured_requests = []
67+
68+
mock_http = MockHttpClient(
69+
onConnectionAttempt: (conn) => conn.respond_with_success(),
70+
onRequest: (req) => {
71+
captured_requests.append(req)
72+
req.respond_with(201, { "id": "broadcast-1", "apnsChannelId": "apns-1" })
73+
}
74+
)
75+
install_mock(mock_http)
76+
77+
client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
78+
```
79+
80+
### Test Steps
81+
```pseudo
82+
AWAIT client.push.admin.createApnsBroadcast(
83+
options: { "messageStoragePolicy": 0 }
84+
)
85+
```
86+
87+
### Assertions
88+
```pseudo
89+
ASSERT captured_requests.length == 1
90+
91+
body = parse_json(captured_requests[0].body)
92+
ASSERT body["messageStoragePolicy"] == 0
93+
```
94+
95+
---
96+
97+
## RSH1d — createApnsBroadcast returns id and apnsChannelId
98+
99+
**Test ID**: `rest/unit/RSH1d/returns-ids-2`
100+
101+
**Spec requirement:** RSH1d — returns the created broadcast as `{ id, apnsChannelId }`.
102+
103+
Tests that the `id` and `apnsChannelId` are parsed from the response body and returned.
104+
105+
### Setup
106+
```pseudo
107+
mock_http = MockHttpClient(
108+
onConnectionAttempt: (conn) => conn.respond_with_success(),
109+
onRequest: (req) => req.respond_with(201, {
110+
"id": "broadcast-xyz",
111+
"apnsChannelId": "apple-channel-abc"
112+
})
113+
)
114+
install_mock(mock_http)
115+
116+
client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
117+
```
118+
119+
### Test Steps
120+
```pseudo
121+
result = AWAIT client.push.admin.createApnsBroadcast(
122+
options: { "messageStoragePolicy": 1 }
123+
)
124+
```
125+
126+
### Assertions
127+
```pseudo
128+
ASSERT result.id == "broadcast-xyz"
129+
ASSERT result.apnsChannelId == "apple-channel-abc"
130+
```
131+
132+
---
133+
134+
## RSH1d — createApnsBroadcast request includes an auth header
135+
136+
**Test ID**: `rest/unit/RSH1d/auth-header-3`
137+
138+
**Spec requirement:** RSH1d — the request is authenticated.
139+
140+
Tests that the request carries a Basic authorization header derived from the API key.
141+
142+
### Setup
143+
```pseudo
144+
captured_requests = []
145+
146+
mock_http = MockHttpClient(
147+
onConnectionAttempt: (conn) => conn.respond_with_success(),
148+
onRequest: (req) => {
149+
captured_requests.append(req)
150+
req.respond_with(201, { "id": "broadcast-1", "apnsChannelId": "apns-1" })
151+
}
152+
)
153+
install_mock(mock_http)
154+
155+
client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
156+
```
157+
158+
### Test Steps
159+
```pseudo
160+
AWAIT client.push.admin.createApnsBroadcast(
161+
options: { "messageStoragePolicy": 1 }
162+
)
163+
```
164+
165+
### Assertions
166+
```pseudo
167+
ASSERT captured_requests.length == 1
168+
ASSERT captured_requests[0].headers["Authorization"] STARTS_WITH "Basic "
169+
```
170+
171+
---
172+
173+
## RSH1d — createApnsBroadcast propagates server error
174+
175+
**Test ID**: `rest/unit/RSH1d/server-error-4`
176+
177+
**Spec requirement:** RSH1d — a server error response is propagated to the caller.
178+
179+
Tests that an error response from the server is surfaced to the caller.
180+
181+
### Setup
182+
```pseudo
183+
mock_http = MockHttpClient(
184+
onConnectionAttempt: (conn) => conn.respond_with_success(),
185+
onRequest: (req) => req.respond_with(400, {
186+
"error": {
187+
"code": 40000,
188+
"statusCode": 400,
189+
"message": "Invalid request"
190+
}
191+
})
192+
)
193+
install_mock(mock_http)
194+
195+
client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
196+
```
197+
198+
### Test Steps and Assertions
199+
```pseudo
200+
AWAIT client.push.admin.createApnsBroadcast(
201+
options: { "messageStoragePolicy": 1 }
202+
) FAILS WITH error
203+
ASSERT error.code == 40000
204+
```

0 commit comments

Comments
 (0)