Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions specifications/api-docstrings.md
Original file line number Diff line number Diff line change
Expand Up @@ -1084,6 +1084,23 @@ Enables the management of device registrations and push notification subscriptio
|| `data` ||| A JSON object containing the push notification payload. |
| deviceRegistrations: PushDeviceRegistrations ||| RSH1b | A [`PushDeviceRegistrations`]{@link PushDeviceRegistrations} object. |
| channelSubscriptions: PushChannelSubscriptions ||| RSH1c | A [`PushChannelSubscriptions`]{@link PushChannelSubscriptions} object. |
| 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. |
|| `options` ||| An object containing the `message-storage-policy` for the broadcast. |
|| `returns` ||| The created broadcast, containing its `id` and `apns-channel-id`. |
| liveActivity: PushLiveActivity ||| RSH1e | A [`PushLiveActivity`]{@link PushLiveActivity} object. |

## class PushLiveActivity

Controls the lifecycle of an iOS Live Activity over an APNs broadcast channel created with [`PushAdmin.createApnsBroadcast`]{@link PushAdmin#createApnsBroadcast}.

| Method / Property | Parameter | Returns | Spec | Description |
|---|---|---|---|---|
| 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. |
|| `params` ||| An object containing the recipient (either a list of `channels` or a single `device-id`), the broadcast `apns-broadcast` identifier, the `apns` Live Activity start payload, and optional APNs delivery `headers`. |
| update(params: PushLiveActivityUpdateParams) => io ||| RSH1e2 | Sends a content-state update to all devices with an active Live Activity on the broadcast channel. |
|| `params` ||| An object containing the broadcast `apns-broadcast` identifier, the `apns` Live Activity update payload, and optional APNs delivery `headers`. |
| end(params: PushLiveActivityEndParams) => io ||| RSH1e3 | Ends the Live Activity on all subscribed devices and cleans up the APNs channel. |
|| `params` ||| An object containing the broadcast `apns-broadcast` identifier, the `apns` Live Activity end payload, and optional APNs delivery `headers`. |

## class JsonObject

Expand Down
40 changes: 40 additions & 0 deletions specifications/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -1048,6 +1048,12 @@ The core SDK provides an API for wrapper SDKs to supply Ably with analytics info
- `(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
- `(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
- `(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.
- `(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.
- `(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:
- `(RSH1e1)` `#start(params)` issues a `POST` request to `/push/apnsBroadcastChannels/:apnsBroadcast/start` with a body carrying the `apns` payload together with the recipient, which is either a list of `channels` or a single `deviceId`. 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`.
- `(RSH1e1a)` The `recipient` must specify exactly one of `channels` (a non-empty list) or `deviceId`. If neither is provided, or both are provided, the request is rejected before any HTTP request is made with an error with code `40000` and status code `400`.
- `(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`.
- `(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`.
- `(RSH2)` The following should only apply to platforms that support receiving push notifications:
- `(RSH2a)` `Push#activate` sends a `CalledActivate` event to [the state machine](#RSH3).
- `(RSH2b)` `Push#deactivate` sends a `CalledDeactivate` event to [the state machine](#RSH3).
Expand Down Expand Up @@ -2781,6 +2787,8 @@ Each type, method, and attribute is labelled with the name of one or more clause
publish(recipient: JsonObject, data: JsonObject) => io // RSH1a
deviceRegistrations: PushDeviceRegistrations // RSH1b
channelSubscriptions: PushChannelSubscriptions // RSH1c
createApnsBroadcast(options: PushApnsBroadcastOptions) => io PushApnsBroadcast // RSH1d
liveActivity: PushLiveActivity // RSH1e

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

class PushLiveActivity: // RSH1e
start(params: PushLiveActivityStartParams) => io // RSH1e1
update(params: PushLiveActivityUpdateParams) => io // RSH1e2
end(params: PushLiveActivityEndParams) => io // RSH1e3

enum DevicePlatform: // PCD6
"android"
"ios"
Expand All @@ -2818,6 +2831,33 @@ Each type, method, and attribute is labelled with the name of one or more clause
clientId: String? // PCS3
channel: String // PCS4

class PushApnsBroadcastOptions: // RSH1d
messageStoragePolicy: Int // 0 or 1

class PushApnsBroadcast: // RSH1d
id: String
apnsChannelId: String

class PushLiveActivityStartParams: // RSH1e1
recipient: PushLiveActivityRecipient
apnsBroadcast: String
apns: JsonObject
headers: Dict<String, String>?

class PushLiveActivityRecipient: // RSH1e1
channels: List<String>? // either channels or deviceId
deviceId: String? // either channels or deviceId

class PushLiveActivityUpdateParams: // RSH1e2
apnsBroadcast: String
apns: JsonObject
headers: Dict<String, String>?

class PushLiveActivityEndParams: // RSH1e3
apnsBroadcast: String
apns: JsonObject
headers: Dict<String, String>?

class ErrorInfo: // TI*
code: Int // TI1
href: String? // TI1, TI4
Expand Down
2 changes: 1 addition & 1 deletion uts/docs/completion-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ This matrix lists all spec items from the [Ably features spec](../../specificati

| Spec item | Description | UTS test spec |
|-----------|-------------|---------------|
| 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) |
| 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) |
| RSH2 | Platform-specific push operations (RSH2a–RSH2e) | |
| RSH3 | Activation state machine (RSH3a–RSH3g3) | |
| RSH4–RSH5 | Event queueing and sequential handling | |
Expand Down
204 changes: 204 additions & 0 deletions uts/rest/unit/push/push_admin_apns_broadcast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# PushAdmin createApnsBroadcast Tests

Spec points: `RSH1`, `RSH1d`

## Test Type
Unit test with mocked HTTP client

## Mock HTTP Infrastructure

See `uts/test/rest/unit/helpers/mock_http.md` for the full Mock HTTP Infrastructure specification.

---

## RSH1d — createApnsBroadcast sends POST to /push/apnsBroadcastChannels

**Test ID**: `rest/unit/RSH1d/create-apns-broadcast-post-0`

**Spec requirement:** RSH1d — `createApnsBroadcast(options)` issues a `POST` request to `/push/apnsBroadcastChannels`.

Tests that `push.admin.createApnsBroadcast()` sends a POST to the correct path.

### Setup
```pseudo
captured_requests = []

mock_http = MockHttpClient(
onConnectionAttempt: (conn) => conn.respond_with_success(),
onRequest: (req) => {
captured_requests.append(req)
req.respond_with(201, { "id": "broadcast-1", "apnsChannelId": "apns-1" })
}
)
install_mock(mock_http)

client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
```

### Test Steps
```pseudo
AWAIT client.push.admin.createApnsBroadcast(
options: { "messageStoragePolicy": 1 }
)
```

### Assertions
```pseudo
ASSERT captured_requests.length == 1

request = captured_requests[0]
ASSERT request.method == "POST"
ASSERT request.url.path == "/push/apnsBroadcastChannels"
```

---

## RSH1d — createApnsBroadcast body contains messageStoragePolicy

**Test ID**: `rest/unit/RSH1d/message-storage-policy-1`

**Spec requirement:** RSH1d — the request body contains the `messageStoragePolicy`.

Tests that the supplied `messageStoragePolicy` is serialized into the request body.

### Setup
```pseudo
captured_requests = []

mock_http = MockHttpClient(
onConnectionAttempt: (conn) => conn.respond_with_success(),
onRequest: (req) => {
captured_requests.append(req)
req.respond_with(201, { "id": "broadcast-1", "apnsChannelId": "apns-1" })
}
)
install_mock(mock_http)

client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
```

### Test Steps
```pseudo
AWAIT client.push.admin.createApnsBroadcast(
options: { "messageStoragePolicy": 0 }
)
```

### Assertions
```pseudo
ASSERT captured_requests.length == 1

body = parse_json(captured_requests[0].body)
ASSERT body["messageStoragePolicy"] == 0
```

---

## RSH1d — createApnsBroadcast returns id and apnsChannelId

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we combine several of these tests? They're doing the same thing, just with different assertions


**Test ID**: `rest/unit/RSH1d/returns-ids-2`

**Spec requirement:** RSH1d — returns the created broadcast as `{ id, apnsChannelId }`.

Tests that the `id` and `apnsChannelId` are parsed from the response body and returned.

### Setup
```pseudo
mock_http = MockHttpClient(
onConnectionAttempt: (conn) => conn.respond_with_success(),
onRequest: (req) => req.respond_with(201, {
"id": "broadcast-xyz",
"apnsChannelId": "apple-channel-abc"
})
)
install_mock(mock_http)

client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
```

### Test Steps
```pseudo
result = AWAIT client.push.admin.createApnsBroadcast(
options: { "messageStoragePolicy": 1 }
)
```

### Assertions
```pseudo
ASSERT result.id == "broadcast-xyz"
ASSERT result.apnsChannelId == "apple-channel-abc"
```

---

## RSH1d — createApnsBroadcast request includes an auth header

**Test ID**: `rest/unit/RSH1d/auth-header-3`

**Spec requirement:** RSH1d — the request is authenticated.

Tests that the request carries a Basic authorization header derived from the API key.

### Setup
```pseudo
captured_requests = []

mock_http = MockHttpClient(
onConnectionAttempt: (conn) => conn.respond_with_success(),
onRequest: (req) => {
captured_requests.append(req)
req.respond_with(201, { "id": "broadcast-1", "apnsChannelId": "apns-1" })
}
)
install_mock(mock_http)

client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
```

### Test Steps
```pseudo
AWAIT client.push.admin.createApnsBroadcast(
options: { "messageStoragePolicy": 1 }
)
```

### Assertions
```pseudo
ASSERT captured_requests.length == 1
ASSERT captured_requests[0].headers["Authorization"] STARTS_WITH "Basic "
```

---

## RSH1d — createApnsBroadcast propagates server error

**Test ID**: `rest/unit/RSH1d/server-error-4`

**Spec requirement:** RSH1d — a server error response is propagated to the caller.

Tests that an error response from the server is surfaced to the caller.

### Setup
```pseudo
mock_http = MockHttpClient(
onConnectionAttempt: (conn) => conn.respond_with_success(),
onRequest: (req) => req.respond_with(400, {
"error": {
"code": 40000,
"statusCode": 400,
"message": "Invalid request"
}
})
)
install_mock(mock_http)

client = Rest(options: ClientOptions(key: "appId.keyId:keySecret"))
```

### Test Steps and Assertions
```pseudo
AWAIT client.push.admin.createApnsBroadcast(
options: { "messageStoragePolicy": 1 }
) FAILS WITH error
ASSERT error.code == 40000
```
Loading
Loading