From f410c1ea73acf69ea456404ffcd54f1e8f78abee Mon Sep 17 00:00:00 2001 From: Kyle Browning Date: Thu, 15 Jan 2026 16:09:53 -0800 Subject: [PATCH 1/2] =?UTF-8?q?Add=E2=80=99s=20push=20type=20for=20Broadca?= =?UTF-8?q?st?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift | 9 ++++++++- Sources/APNSTestServer/APNSTestServer.swift | 11 +++++++---- .../Broadcast/APNSBroadcastChannelTests.swift | 10 ++++++---- .../Broadcast/APNSBroadcastClientTests.swift | 3 +++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift b/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift index 6aa3203..092dd86 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift @@ -17,6 +17,7 @@ public struct APNSBroadcastChannel: Codable, Sendable { enum CodingKeys: String, CodingKey { case channelID = "channel-id" case messageStoragePolicy = "message-storage-policy" + case pushType = "push-type" } /// The unique identifier for the broadcast channel (only present in responses). @@ -25,17 +26,23 @@ public struct APNSBroadcastChannel: Codable, Sendable { /// The message storage policy for this channel. public let messageStoragePolicy: APNSBroadcastMessageStoragePolicy + /// The push type for this broadcast channel. + /// Currently only "LiveActivity" is supported for broadcast channels. + public let pushType: String + /// Creates a new broadcast channel configuration. /// /// - Parameter messageStoragePolicy: The storage policy for messages in this channel. public init(messageStoragePolicy: APNSBroadcastMessageStoragePolicy) { self.channelID = nil self.messageStoragePolicy = messageStoragePolicy + self.pushType = "LiveActivity" } /// Internal initializer used for decoding responses that include channel ID. - public init(channelID: String?, messageStoragePolicy: APNSBroadcastMessageStoragePolicy) { + public init(channelID: String?, messageStoragePolicy: APNSBroadcastMessageStoragePolicy, pushType: String = "LiveActivity") { self.channelID = channelID self.messageStoragePolicy = messageStoragePolicy + self.pushType = pushType } } diff --git a/Sources/APNSTestServer/APNSTestServer.swift b/Sources/APNSTestServer/APNSTestServer.swift index e4b8ad5..c114c57 100644 --- a/Sources/APNSTestServer/APNSTestServer.swift +++ b/Sources/APNSTestServer/APNSTestServer.swift @@ -77,10 +77,12 @@ public final class APNSTestServer: @unchecked Sendable { struct MockBroadcastChannel: Codable { let channelID: String let messageStoragePolicy: Int + let pushType: String enum CodingKeys: String, CodingKey { case channelID = "channel-id" case messageStoragePolicy = "message-storage-policy" + case pushType = "push-type" } } @@ -193,21 +195,22 @@ public final class APNSTestServer: @unchecked Sendable { let data = Data(bytes) guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], - let policy = json["message-storage-policy"] as? Int else { + let policy = json["message-storage-policy"] as? Int, + let pushType = json["push-type"] as? String else { var headers = HTTPHeaders() headers.add(name: "content-type", value: "application/json") return (.badRequest, headers, "{\"reason\":\"BadRequest\"}") } let channelID = UUID().uuidString - let channel = MockBroadcastChannel(channelID: channelID, messageStoragePolicy: policy) + let channel = MockBroadcastChannel(channelID: channelID, messageStoragePolicy: policy, pushType: pushType) broadcastChannels[channelID] = channel var headers = HTTPHeaders() headers.add(name: "content-type", value: "application/json") let responseJSON = """ - {"channel-id":"\(channelID)","message-storage-policy":\(policy)} + {"channel-id":"\(channelID)","message-storage-policy":\(policy),"push-type":"\(pushType)"} """ return (.created, headers, responseJSON) } @@ -231,7 +234,7 @@ public final class APNSTestServer: @unchecked Sendable { } let responseJSON = """ - {"channel-id":"\(channel.channelID)","message-storage-policy":\(channel.messageStoragePolicy)} + {"channel-id":"\(channel.channelID)","message-storage-policy":\(channel.messageStoragePolicy),"push-type":"\(channel.pushType)"} """ return (.ok, headers, responseJSON) } diff --git a/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift b/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift index b7084d6..649f90a 100644 --- a/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift +++ b/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift @@ -22,7 +22,7 @@ final class APNSBroadcastChannelTests: XCTestCase { let data = try encoder.encode(channel) let expectedJSONString = """ - {"message-storage-policy":1} + {"message-storage-policy":1,"push-type":"LiveActivity"} """ let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary @@ -35,7 +35,7 @@ final class APNSBroadcastChannelTests: XCTestCase { let data = try encoder.encode(channel) let expectedJSONString = """ - {"message-storage-policy":0} + {"message-storage-policy":0,"push-type":"LiveActivity"} """ let jsonObject1 = try JSONSerialization.jsonObject(with: data) as! NSDictionary let jsonObject2 = try JSONSerialization.jsonObject(with: expectedJSONString.data(using: .utf8)!) as! NSDictionary @@ -44,7 +44,7 @@ final class APNSBroadcastChannelTests: XCTestCase { func testDecode() throws { let jsonString = """ - {"channel-id":"test-channel-123","message-storage-policy":1} + {"channel-id":"test-channel-123","message-storage-policy":1,"push-type":"LiveActivity"} """ let data = jsonString.data(using: .utf8)! let decoder = JSONDecoder() @@ -52,11 +52,12 @@ final class APNSBroadcastChannelTests: XCTestCase { XCTAssertEqual(channel.channelID, "test-channel-123") XCTAssertEqual(channel.messageStoragePolicy, .mostRecentMessageStored) + XCTAssertEqual(channel.pushType, "LiveActivity") } func testDecode_withoutChannelID() throws { let jsonString = """ - {"message-storage-policy":0} + {"message-storage-policy":0,"push-type":"LiveActivity"} """ let data = jsonString.data(using: .utf8)! let decoder = JSONDecoder() @@ -64,5 +65,6 @@ final class APNSBroadcastChannelTests: XCTestCase { XCTAssertNil(channel.channelID) XCTAssertEqual(channel.messageStoragePolicy, .noMessageStored) + XCTAssertEqual(channel.pushType, "LiveActivity") } } diff --git a/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift b/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift index 3bb5033..4db7f5d 100644 --- a/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift +++ b/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift @@ -58,6 +58,7 @@ final class APNSBroadcastClientTests: XCTestCase { XCTAssertNotNil(response.apnsRequestID) XCTAssertNotNil(response.body.channelID) XCTAssertEqual(response.body.messageStoragePolicy, .mostRecentMessageStored) + XCTAssertEqual(response.body.pushType, "LiveActivity") } func testCreateChannel_noMessageStored() async throws { @@ -67,6 +68,7 @@ final class APNSBroadcastClientTests: XCTestCase { XCTAssertNotNil(response.apnsRequestID) XCTAssertNotNil(response.body.channelID) XCTAssertEqual(response.body.messageStoragePolicy, .noMessageStored) + XCTAssertEqual(response.body.pushType, "LiveActivity") } func testReadChannel() async throws { @@ -81,6 +83,7 @@ final class APNSBroadcastClientTests: XCTestCase { XCTAssertNotNil(readResponse.apnsRequestID) XCTAssertEqual(readResponse.body.channelID, channelID) XCTAssertEqual(readResponse.body.messageStoragePolicy, .mostRecentMessageStored) + XCTAssertEqual(readResponse.body.pushType, "LiveActivity") } func testReadChannel_notFound() async throws { From 294203a9f17425af1f4592a475e92459bc055760 Mon Sep 17 00:00:00 2001 From: Kyle Browning Date: Thu, 26 Mar 2026 09:01:53 -0700 Subject: [PATCH 2/2] Fix broadcast channel management to match Apple's APNs API Channel ID is now sent via apns-channel-id header instead of URL path for read/delete operations. Response body is optional since create returns only a channel ID header and delete returns 204 No Content. Channel ID moved from APNSBroadcastChannel body to APNSBroadcastResponse. Test server and tests updated to match. Based on work by @mattglover in #235. --- Sources/APNS/APNSBroadcastClient.swift | 16 +++++++-- .../Broadcast/APNSBroadcastChannel.swift | 8 +---- .../APNSBroadcastClientProtocol.swift | 2 +- .../Broadcast/APNSBroadcastRequest.swift | 14 ++++++-- .../Broadcast/APNSBroadcastResponse.swift | 8 +++-- Sources/APNSTestServer/APNSTestServer.swift | 33 +++++++++-------- .../Broadcast/APNSBroadcastChannelTests.swift | 16 +-------- .../Broadcast/APNSBroadcastClientTests.swift | 36 +++++++++---------- 8 files changed, 66 insertions(+), 67 deletions(-) diff --git a/Sources/APNS/APNSBroadcastClient.swift b/Sources/APNS/APNSBroadcastClient.swift index 8cab985..3bd0229 100644 --- a/Sources/APNS/APNSBroadcastClient.swift +++ b/Sources/APNS/APNSBroadcastClient.swift @@ -145,6 +145,13 @@ extension APNSBroadcastClient { headers.add(name: "authorization", value: token) } + // Append operation specific HTTPS headers + if let operationHeaders = request.operation.headers { + for (name, value) in operationHeaders { + headers.add(name: name, value: value) + } + } + // Build the request URL let requestURL = "\(self.environment.url):\(self.environment.port)/1/apps/\(self.bundleID)\(request.operation.path)" @@ -166,11 +173,14 @@ extension APNSBroadcastClient { // Extract request ID from response let apnsRequestID = response.headers.first(name: "apns-request-id").flatMap { UUID(uuidString: $0) } + // Extract channel ID from response, or from request headers (as 'read' operation doesn't return in payload + let channelID = response.headers.first(name: "apns-channel-id") ?? request.operation.headers?["apns-channel-id"] + // Handle successful responses - if response.status == .ok || response.status == .created { + if response.status == .ok || response.status == .created || response.status == .noContent { let body = try await response.body.collect(upTo: 1024 * 1024) // 1MB max - let responseBody = try responseDecoder.decode(ResponseBody.self, from: body) - return APNSBroadcastResponse(apnsRequestID: apnsRequestID, body: responseBody) + let responseBody = try? responseDecoder.decode(ResponseBody.self, from: body) + return APNSBroadcastResponse(apnsRequestID: apnsRequestID, channelID: channelID, body: responseBody) } // Handle error responses diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift b/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift index 092dd86..67686a0 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastChannel.swift @@ -15,14 +15,10 @@ /// Represents a broadcast channel configuration. public struct APNSBroadcastChannel: Codable, Sendable { enum CodingKeys: String, CodingKey { - case channelID = "channel-id" case messageStoragePolicy = "message-storage-policy" case pushType = "push-type" } - /// The unique identifier for the broadcast channel (only present in responses). - public let channelID: String? - /// The message storage policy for this channel. public let messageStoragePolicy: APNSBroadcastMessageStoragePolicy @@ -34,14 +30,12 @@ public struct APNSBroadcastChannel: Codable, Sendable { /// /// - Parameter messageStoragePolicy: The storage policy for messages in this channel. public init(messageStoragePolicy: APNSBroadcastMessageStoragePolicy) { - self.channelID = nil self.messageStoragePolicy = messageStoragePolicy self.pushType = "LiveActivity" } /// Internal initializer used for decoding responses that include channel ID. - public init(channelID: String?, messageStoragePolicy: APNSBroadcastMessageStoragePolicy, pushType: String = "LiveActivity") { - self.channelID = channelID + public init(messageStoragePolicy: APNSBroadcastMessageStoragePolicy, pushType: String = "LiveActivity") { self.messageStoragePolicy = messageStoragePolicy self.pushType = pushType } diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift b/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift index 28c5458..ae91701 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastClientProtocol.swift @@ -35,7 +35,7 @@ extension APNSBroadcastClientProtocol { public func create( channel: APNSBroadcastChannel, apnsRequestID: UUID? = nil - ) async throws -> APNSBroadcastResponse { + ) async throws -> APNSBroadcastResponse { let request = APNSBroadcastRequest( operation: .create, message: channel, diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift index 4f48bfa..43b24c0 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastRequest.swift @@ -42,10 +42,18 @@ public struct APNSBroadcastRequest: Sendable where Message: /// The path for this operation. public var path: String { switch self { - case .create, .listAll: + case .create, .delete, .read, .listAll: return "/channels" - case .read(let channelID), .delete(let channelID): - return "/channels/\(channelID)" + } + } + + /// HTTP Headers for this operation. + public var headers: [String: String]? { + switch self { + case .delete(let channelID), .read(channelID: let channelID): + return ["apns-channel-id": channelID] + default: + return nil } } } diff --git a/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift b/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift index 9e2d3fb..2ef2ba4 100644 --- a/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift +++ b/Sources/APNSCore/Broadcast/APNSBroadcastResponse.swift @@ -19,11 +19,15 @@ public struct APNSBroadcastResponse: Sendable where Body: Senda /// The request ID returned by APNs. public let apnsRequestID: UUID? + /// The channel ID returned by APNs. + public let channelID: String? + /// The response body. - public let body: Body + public let body: Body? - public init(apnsRequestID: UUID?, body: Body) { + public init(apnsRequestID: UUID?, channelID: String?, body: Body?) { self.apnsRequestID = apnsRequestID + self.channelID = channelID self.body = body } } diff --git a/Sources/APNSTestServer/APNSTestServer.swift b/Sources/APNSTestServer/APNSTestServer.swift index c114c57..4e53b03 100644 --- a/Sources/APNSTestServer/APNSTestServer.swift +++ b/Sources/APNSTestServer/APNSTestServer.swift @@ -131,21 +131,24 @@ public final class APNSTestServer: @unchecked Sendable { // Parse the URI let components = uri.split(separator: "/") - // Broadcast channel endpoints: /1/apps/{bundleID}/channels[/{channelID}] - // Expected format: ["1", "apps", "{bundleID}", "channels"] or ["1", "apps", "{bundleID}", "channels", "{channelID}"] + // Broadcast channel endpoints: /1/apps/{bundleID}/channels + // Channel ID is passed via apns-channel-id header for read/delete operations switch (method, components.count) { case (.POST, 4) where components[0] == "1" && components[1] == "apps" && components[3] == "channels": return handleCreateChannel(body: body) case (.GET, 4) where components[0] == "1" && components[1] == "apps" && components[3] == "channels": + if let channelID = headers.first(name: "apns-channel-id") { + return handleReadChannel(channelID: channelID) + } return handleListChannels() - case (.GET, 5) where components[0] == "1" && components[1] == "apps" && components[3] == "channels": - let channelID = String(components[4]) - return handleReadChannel(channelID: channelID) - - case (.DELETE, 5) where components[0] == "1" && components[1] == "apps" && components[3] == "channels": - let channelID = String(components[4]) + case (.DELETE, 4) where components[0] == "1" && components[1] == "apps" && components[3] == "channels": + guard let channelID = headers.first(name: "apns-channel-id") else { + var responseHeaders = HTTPHeaders() + responseHeaders.add(name: "content-type", value: "application/json") + return (.badRequest, responseHeaders, "{\"reason\":\"MissingChannelID\"}") + } return handleDeleteChannel(channelID: channelID) // Regular push notification endpoint: POST /3/device/{token} @@ -207,12 +210,8 @@ public final class APNSTestServer: @unchecked Sendable { broadcastChannels[channelID] = channel var headers = HTTPHeaders() - headers.add(name: "content-type", value: "application/json") - - let responseJSON = """ - {"channel-id":"\(channelID)","message-storage-policy":\(policy),"push-type":"\(pushType)"} - """ - return (.created, headers, responseJSON) + headers.add(name: "apns-channel-id", value: channelID) + return (.ok, headers, "") } private func handleListChannels() -> (status: HTTPResponseStatus, headers: HTTPHeaders, body: String) { @@ -234,20 +233,20 @@ public final class APNSTestServer: @unchecked Sendable { } let responseJSON = """ - {"channel-id":"\(channel.channelID)","message-storage-policy":\(channel.messageStoragePolicy),"push-type":"\(channel.pushType)"} + {"message-storage-policy":\(channel.messageStoragePolicy),"push-type":"\(channel.pushType)"} """ return (.ok, headers, responseJSON) } private func handleDeleteChannel(channelID: String) -> (status: HTTPResponseStatus, headers: HTTPHeaders, body: String) { var headers = HTTPHeaders() - headers.add(name: "content-type", value: "application/json") guard broadcastChannels.removeValue(forKey: channelID) != nil else { + headers.add(name: "content-type", value: "application/json") return (.notFound, headers, "{\"reason\":\"NotFound\"}") } - return (.ok, headers, "{}") + return (.noContent, headers, "") } // MARK: - Push Notification Handler diff --git a/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift b/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift index 649f90a..6a56af7 100644 --- a/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift +++ b/Tests/APNSTests/Broadcast/APNSBroadcastChannelTests.swift @@ -44,27 +44,13 @@ final class APNSBroadcastChannelTests: XCTestCase { func testDecode() throws { let jsonString = """ - {"channel-id":"test-channel-123","message-storage-policy":1,"push-type":"LiveActivity"} + {"message-storage-policy":1,"push-type":"LiveActivity"} """ let data = jsonString.data(using: .utf8)! let decoder = JSONDecoder() let channel = try decoder.decode(APNSBroadcastChannel.self, from: data) - XCTAssertEqual(channel.channelID, "test-channel-123") XCTAssertEqual(channel.messageStoragePolicy, .mostRecentMessageStored) XCTAssertEqual(channel.pushType, "LiveActivity") } - - func testDecode_withoutChannelID() throws { - let jsonString = """ - {"message-storage-policy":0,"push-type":"LiveActivity"} - """ - let data = jsonString.data(using: .utf8)! - let decoder = JSONDecoder() - let channel = try decoder.decode(APNSBroadcastChannel.self, from: data) - - XCTAssertNil(channel.channelID) - XCTAssertEqual(channel.messageStoragePolicy, .noMessageStored) - XCTAssertEqual(channel.pushType, "LiveActivity") - } } diff --git a/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift b/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift index 4db7f5d..aeb2e3b 100644 --- a/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift +++ b/Tests/APNSTests/Broadcast/APNSBroadcastClientTests.swift @@ -56,9 +56,7 @@ final class APNSBroadcastClientTests: XCTestCase { let response = try await client.create(channel: channel, apnsRequestID: nil) XCTAssertNotNil(response.apnsRequestID) - XCTAssertNotNil(response.body.channelID) - XCTAssertEqual(response.body.messageStoragePolicy, .mostRecentMessageStored) - XCTAssertEqual(response.body.pushType, "LiveActivity") + XCTAssertNotNil(response.channelID) } func testCreateChannel_noMessageStored() async throws { @@ -66,24 +64,22 @@ final class APNSBroadcastClientTests: XCTestCase { let response = try await client.create(channel: channel, apnsRequestID: nil) XCTAssertNotNil(response.apnsRequestID) - XCTAssertNotNil(response.body.channelID) - XCTAssertEqual(response.body.messageStoragePolicy, .noMessageStored) - XCTAssertEqual(response.body.pushType, "LiveActivity") + XCTAssertNotNil(response.channelID) } func testReadChannel() async throws { // First, create a channel let channel = APNSBroadcastChannel(messageStoragePolicy: .mostRecentMessageStored) let createResponse = try await client.create(channel: channel, apnsRequestID: nil) - let channelID = createResponse.body.channelID! + let channelID = createResponse.channelID! // Now read it back let readResponse = try await client.read(channelID: channelID, apnsRequestID: nil) XCTAssertNotNil(readResponse.apnsRequestID) - XCTAssertEqual(readResponse.body.channelID, channelID) - XCTAssertEqual(readResponse.body.messageStoragePolicy, .mostRecentMessageStored) - XCTAssertEqual(readResponse.body.pushType, "LiveActivity") + XCTAssertEqual(readResponse.channelID, channelID) + XCTAssertEqual(readResponse.body?.messageStoragePolicy, .mostRecentMessageStored) + XCTAssertEqual(readResponse.body?.pushType, "LiveActivity") } func testReadChannel_notFound() async throws { @@ -99,7 +95,7 @@ final class APNSBroadcastClientTests: XCTestCase { // First, create a channel let channel = APNSBroadcastChannel(messageStoragePolicy: .noMessageStored) let createResponse = try await client.create(channel: channel, apnsRequestID: nil) - let channelID = createResponse.body.channelID! + let channelID = createResponse.channelID! // Delete it let deleteResponse = try await client.delete(channelID: channelID, apnsRequestID: nil) @@ -133,25 +129,27 @@ final class APNSBroadcastClientTests: XCTestCase { let response2 = try await client.create(channel: channel2, apnsRequestID: nil) let response3 = try await client.create(channel: channel3, apnsRequestID: nil) - let channelID1 = response1.body.channelID! - let channelID2 = response2.body.channelID! - let channelID3 = response3.body.channelID! + let channelID1 = response1.channelID! + let channelID2 = response2.channelID! + let channelID3 = response3.channelID! // List all channels let listResponse = try await client.readAllChannelIDs(apnsRequestID: nil) XCTAssertNotNil(listResponse.apnsRequestID) - XCTAssertEqual(listResponse.body.channels.count, 3) - XCTAssertTrue(listResponse.body.channels.contains(channelID1)) - XCTAssertTrue(listResponse.body.channels.contains(channelID2)) - XCTAssertTrue(listResponse.body.channels.contains(channelID3)) + let channels = try XCTUnwrap(listResponse.body?.channels) + XCTAssertEqual(channels.count, 3) + XCTAssertTrue(channels.contains(channelID1)) + XCTAssertTrue(channels.contains(channelID2)) + XCTAssertTrue(channels.contains(channelID3)) } func testListAllChannels_empty() async throws { let listResponse = try await client.readAllChannelIDs(apnsRequestID: nil) XCTAssertNotNil(listResponse.apnsRequestID) - XCTAssertEqual(listResponse.body.channels.count, 0) + let channels = try XCTUnwrap(listResponse.body?.channels) + XCTAssertEqual(channels.count, 0) } func testRequestID() async throws {