From 434133f2aba153b92fcccc29e9552a2976a3cdf2 Mon Sep 17 00:00:00 2001 From: Umair Date: Mon, 24 Feb 2025 17:11:35 +0000 Subject: [PATCH] Extends public API to allow users to update and delete their messages --- Example/AblyChatExample/Mocks/Misc.swift | 5 +- .../AblyChatExample/Mocks/MockClients.swift | 20 ++- .../AblyChatExample/Mocks/MockRealtime.swift | 2 +- Sources/AblyChat/ChatAPI.swift | 115 +++++++++++++++++- Sources/AblyChat/ChatClient.swift | 5 +- Sources/AblyChat/DefaultMessages.swift | 33 ++++- Sources/AblyChat/Events.swift | 10 +- Sources/AblyChat/Message.swift | 70 ++++++++++- Sources/AblyChat/Messages.swift | 83 ++++++++++++- Tests/AblyChatTests/ChatAPITests.swift | 12 +- Tests/AblyChatTests/IntegrationTests.swift | 57 +++++++++ .../MessageSubscriptionTests.swift | 2 +- Tests/AblyChatTests/MessageTests.swift | 16 ++- .../Mocks/MockHTTPPaginatedResponse.swift | 3 + 14 files changed, 407 insertions(+), 26 deletions(-) diff --git a/Example/AblyChatExample/Mocks/Misc.swift b/Example/AblyChatExample/Mocks/Misc.swift index 790b4e1..18bf18d 100644 --- a/Example/AblyChatExample/Mocks/Misc.swift +++ b/Example/AblyChatExample/Mocks/Misc.swift @@ -18,7 +18,10 @@ final class MockMessagesPaginatedResult: PaginatedResult { text: MockStrings.randomPhrase(), createdAt: Date(), metadata: [:], - headers: [:] + headers: [:], + version: "", + timestamp: Date(), + operation: nil ) } } diff --git a/Example/AblyChatExample/Mocks/MockClients.swift b/Example/AblyChatExample/Mocks/MockClients.swift index 2cca15b..cbe24a6 100644 --- a/Example/AblyChatExample/Mocks/MockClients.swift +++ b/Example/AblyChatExample/Mocks/MockClients.swift @@ -15,7 +15,7 @@ actor MockChatClient: ChatClient { } nonisolated var clientID: String { - fatalError("Not yet implemented") + realtime.clientId ?? "AblyTest" } } @@ -108,7 +108,10 @@ actor MockMessages: Messages { text: MockStrings.randomPhrase(), createdAt: Date(), metadata: [:], - headers: [:] + headers: [:], + version: "", + timestamp: Date(), + operation: nil ) }, interval: 3) } @@ -132,12 +135,23 @@ actor MockMessages: Messages { text: params.text, createdAt: Date(), metadata: params.metadata ?? [:], - headers: params.headers ?? [:] + headers: params.headers ?? [:], + version: "", + timestamp: Date(), + operation: nil ) mockSubscriptions.emit(message) return message } + func update(newMessage _: Message, description _: String?, metadata _: OperationMetadata?) async throws -> Message { + fatalError("Not yet implemented") + } + + func delete(message _: Message, params _: DeleteMessageParams) async throws -> Message { + fatalError("Not yet implemented") + } + func onDiscontinuity(bufferingPolicy _: BufferingPolicy) -> Subscription { fatalError("Not yet implemented") } diff --git a/Example/AblyChatExample/Mocks/MockRealtime.swift b/Example/AblyChatExample/Mocks/MockRealtime.swift index 7ed2dba..ff8f389 100644 --- a/Example/AblyChatExample/Mocks/MockRealtime.swift +++ b/Example/AblyChatExample/Mocks/MockRealtime.swift @@ -10,7 +10,7 @@ final class MockRealtime: NSObject, RealtimeClientProtocol, Sendable { } var clientId: String? { - fatalError("Not implemented") + "AblyTest" } let channels = Channels() diff --git a/Sources/AblyChat/ChatAPI.swift b/Sources/AblyChat/ChatAPI.swift index 6404029..1ddbe19 100644 --- a/Sources/AblyChat/ChatAPI.swift +++ b/Sources/AblyChat/ChatAPI.swift @@ -25,6 +25,19 @@ internal final class ChatAPI: Sendable { } } + internal struct MessageOperationResponse: JSONObjectDecodable { + internal let version: String + internal let timestamp: Int64 + + internal init(jsonObject: [String: JSONValue]) throws { + version = try jsonObject.stringValueForKey("version") + timestamp = try Int64(jsonObject.numberValueForKey("timestamp")) + } + } + + internal typealias UpdateMessageResponse = MessageOperationResponse + internal typealias DeleteMessageResponse = MessageOperationResponse + // (CHA-M3) Messages are sent to Ably via the Chat REST API, using the send method. // (CHA-M3a) When a message is sent successfully, the caller shall receive a struct representing the Message in response (as if it were received via Realtime event). internal func sendMessage(roomId: String, params: SendMessageParams) async throws -> Message { @@ -48,16 +61,110 @@ internal final class ChatAPI: Sendable { // response.createdAt is in milliseconds, convert it to seconds let createdAtInSeconds = TimeInterval(Double(response.createdAt) / 1000) - + let createdAtDate = Date(timeIntervalSince1970: createdAtInSeconds) let message = Message( serial: response.serial, action: .create, clientID: clientId, roomID: roomId, text: params.text, - createdAt: Date(timeIntervalSince1970: createdAtInSeconds), + createdAt: createdAtDate, metadata: params.metadata ?? [:], - headers: params.headers ?? [:] + headers: params.headers ?? [:], + version: response.serial, + timestamp: createdAtDate + ) + return message + } + + // (CHA-M8) A client must be able to update a message in a room. + // (CHA-M8a) A client may update a message via the Chat REST API by calling the update method. + internal func updateMessage(with modifiedMessage: Message, description: String?, metadata: OperationMetadata?) async throws -> Message { + guard let clientID = realtime.clientId else { + throw ARTErrorInfo.create(withCode: 40000, message: "Ensure your Realtime instance is initialized with a clientId.") + } + + let endpoint = "\(apiVersionV2)/rooms/\(modifiedMessage.roomID)/messages/\(modifiedMessage.serial)" + var body: [String: JSONValue] = [:] + let messageObject: [String: JSONValue] = [ + "text": .string(modifiedMessage.text), + "metadata": .object(modifiedMessage.metadata), + "headers": .object(modifiedMessage.headers.mapValues(\.toJSONValue)), + ] + + body["message"] = .object(messageObject) + + if let description { + body["description"] = .string(description) + } + + if let metadata { + body["metadata"] = .object(metadata) + } + + // (CHA-M8c) An update operation has PUT semantics. If a field is not specified in the update, it is assumed to be removed. + let response: UpdateMessageResponse = try await makeRequest(endpoint, method: "PUT", body: body) + + // response.timestamp is in milliseconds, convert it to seconds + let timestampInSeconds = TimeInterval(Double(response.timestamp) / 1000) + + // (CHA-M8b) When a message is updated successfully via the REST API, the caller shall receive a struct representing the Message in response, as if it were received via Realtime event. + let message = Message( + serial: modifiedMessage.serial, + action: .update, + clientID: modifiedMessage.clientID, + roomID: modifiedMessage.roomID, + text: modifiedMessage.text, + createdAt: modifiedMessage.createdAt, + metadata: modifiedMessage.metadata, + headers: modifiedMessage.headers, + version: response.version, + timestamp: Date(timeIntervalSince1970: timestampInSeconds), + operation: .init( + clientID: clientID, + description: description, + metadata: metadata + ) + ) + return message + } + + // (CHA-M9) A client must be able to delete a message in a room. + // (CHA-M9a) A client may delete a message via the Chat REST API by calling the delete method. + internal func deleteMessage(message: Message, params: DeleteMessageParams) async throws -> Message { + let endpoint = "\(apiVersionV2)/rooms/\(message.roomID)/messages/\(message.serial)/delete" + var body: [String: JSONValue] = [:] + + if let description = params.description { + body["description"] = .string(description) + } + + if let metadata = params.metadata { + body["metadata"] = .object(metadata) + } + + let response: DeleteMessageResponse = try await makeRequest(endpoint, method: "POST", body: body) + + // response.timestamp is in milliseconds, convert it to seconds + let timestampInSeconds = TimeInterval(Double(response.timestamp) / 1000) + + // (CHA-M9b) When a message is deleted successfully via the REST API, the caller shall receive a struct representing the Message in response, as if it were received via Realtime event. + let message = Message( + serial: message.serial, + action: .delete, + clientID: message.clientID, + roomID: message.roomID, + text: message.text, + createdAt: message.createdAt, + metadata: message.metadata, + headers: message.headers, + version: response.version, + timestamp: Date(timeIntervalSince1970: timestampInSeconds), + operation: .init( + clientID: message.clientID, + description: params.description, + metadata: params.metadata + ) ) return message } @@ -78,7 +185,7 @@ internal final class ChatAPI: Sendable { do { try realtime.request(method, path: url, params: [:], body: ablyCocoaBody, headers: [:]) { paginatedResponse, error in if let error { - // (CHA-M3e) If an error is returned from the REST API, its ErrorInfo representation shall be thrown as the result of the send call. + // (CHA-M3e & CHA-M8d & CHA-M9c) If an error is returned from the REST API, its ErrorInfo representation shall be thrown as the result of the send call. continuation.resume(throwing: ARTErrorInfo.create(from: error)) return } diff --git a/Sources/AblyChat/ChatClient.swift b/Sources/AblyChat/ChatClient.swift index c931ffa..9f92570 100644 --- a/Sources/AblyChat/ChatClient.swift +++ b/Sources/AblyChat/ChatClient.swift @@ -73,7 +73,10 @@ public actor DefaultChatClient: ChatClient { } public nonisolated var clientID: String { - fatalError("Not yet implemented") + guard let clientID = realtime.clientId else { + fatalError("Ensure your Realtime instance is initialized with a clientId.") + } + return clientID } } diff --git a/Sources/AblyChat/DefaultMessages.swift b/Sources/AblyChat/DefaultMessages.swift index 62901ca..8aa562b 100644 --- a/Sources/AblyChat/DefaultMessages.swift +++ b/Sources/AblyChat/DefaultMessages.swift @@ -76,6 +76,10 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities { throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without clientId") } + guard let version = message.version else { + throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message without version") + } + let metadata = try data.optionalObjectValueForKey("metadata") let headers: Headers? = if let headersJSONObject = try extras.optionalObjectValueForKey("headers") { @@ -88,7 +92,8 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities { return } - let message = Message( + // `message.operation?.toChatOperation()` is throwing but the linter prefers putting the `try` on Message initialization instead of having it nested. + let message = try Message( serial: serial, action: action, clientID: clientID, @@ -96,7 +101,10 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities { text: text, createdAt: message.timestamp, metadata: metadata ?? .init(), - headers: headers ?? .init() + headers: headers ?? .init(), + version: version, + timestamp: message.timestamp, + operation: message.operation?.toChatOperation() ) messageSubscription.emit(message) @@ -127,6 +135,14 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities { try await chatAPI.sendMessage(roomId: roomID, params: params) } + internal func update(newMessage: Message, description: String?, metadata: OperationMetadata?) async throws -> Message { + try await chatAPI.updateMessage(with: newMessage, description: description, metadata: metadata) + } + + internal func delete(message: Message, params: DeleteMessageParams) async throws -> Message { + try await chatAPI.deleteMessage(message: message, params: params) + } + // (CHA-M7) Users may subscribe to discontinuity events to know when there’s been a break in messages that they need to resolve. Their listener will be called when a discontinuity event is triggered from the room lifecycle. internal func onDiscontinuity(bufferingPolicy: BufferingPolicy) async -> Subscription { await featureChannel.onDiscontinuity(bufferingPolicy: bufferingPolicy) @@ -273,3 +289,16 @@ internal final class DefaultMessages: Messages, EmitsDiscontinuities { case noReferenceToSelf } } + +private extension ARTMessageOperation { + func toChatOperation() throws -> MessageOperation { + guard let clientId else { + throw ARTErrorInfo.create(withCode: 50000, status: 500, message: "Received incoming message where Operation clientId is nil") + } + return MessageOperation( + clientID: clientId, + description: descriptionText, + metadata: metadata != nil ? JSONValue(ablyCocoaData: metadata!).objectValue : nil + ) + } +} diff --git a/Sources/AblyChat/Events.swift b/Sources/AblyChat/Events.swift index 2b71d85..319da30 100644 --- a/Sources/AblyChat/Events.swift +++ b/Sources/AblyChat/Events.swift @@ -8,15 +8,19 @@ public enum MessageAction: String, Sendable { * Action applied to a new message. */ case create = "message.create" + case update = "message.update" + case delete = "message.delete" internal static func fromRealtimeAction(_ action: ARTMessageAction) -> Self? { switch action { case .create: .create + case .update: + .update + case .delete: + .delete // ignore any other actions except `message.create` for now - case .update, - .delete, - .metaOccupancy, + case .metaOccupancy, .messageSummary: nil @unknown default: diff --git a/Sources/AblyChat/Message.swift b/Sources/AblyChat/Message.swift index a198796..a7f6a3d 100644 --- a/Sources/AblyChat/Message.swift +++ b/Sources/AblyChat/Message.swift @@ -10,6 +10,11 @@ public typealias MessageHeaders = Headers */ public typealias MessageMetadata = Metadata +/** + * ``Metadata`` type used for the metadata within an operation e.g. updating or deleting a message + */ +public typealias OperationMetadata = Metadata + /** * Represents a single message in a chat room. */ @@ -77,7 +82,24 @@ public struct Message: Sendable, Identifiable, Equatable { */ public var headers: MessageHeaders - public init(serial: String, action: MessageAction, clientID: String, roomID: String, text: String, createdAt: Date?, metadata: MessageMetadata, headers: MessageHeaders) { + // (CHA-M10a) + /** + * A unique identifier for the latest version of this message. + */ + public var version: String + + /** + * The timestamp at which this version was updated, deleted, or created. + */ + public var timestamp: Date? + + /** + * The details of the operation that modified the message. This is only set for update and delete actions. It contains + * information about the operation: the clientId of the user who performed the operation, a description, and metadata. + */ + public var operation: MessageOperation? + + public init(serial: String, action: MessageAction, clientID: String, roomID: String, text: String, createdAt: Date?, metadata: MessageMetadata, headers: MessageHeaders, version: String, timestamp: Date?, operation: MessageOperation? = nil) { self.serial = serial self.action = action self.clientID = clientID @@ -86,11 +108,46 @@ public struct Message: Sendable, Identifiable, Equatable { self.createdAt = createdAt self.metadata = metadata self.headers = headers + self.version = version + self.timestamp = timestamp + self.operation = operation + } + + /** + * Helper function to copy a message with updated values. This is useful when updating a message e.g. `room().messages.update(newMessage: messageCopy...)`. + * If metadata/headers are not provided, it keeps the metadata/headers from the original message. + * If metadata/headers are explicitly passed in, the new `Message` will have these values. You can set them to `[:]` if you wish to remove them. + */ + public func copy( + text: String? = nil, + metadata: MessageMetadata? = nil, + headers: MessageHeaders? = nil + ) -> Message { + Message( + serial: serial, + action: action, + clientID: clientID, + roomID: roomID, + text: text ?? self.text, + createdAt: createdAt, + metadata: metadata ?? self.metadata, + headers: headers ?? self.headers, + version: version, + timestamp: timestamp, + operation: operation + ) } } +public struct MessageOperation: Sendable, Equatable { + public var clientID: String + public var description: String? + public var metadata: MessageMetadata? +} + extension Message: JSONObjectDecodable { internal init(jsonObject: [String: JSONValue]) throws { + let operation = try? jsonObject.objectValueForKey("operation") try self.init( serial: jsonObject.stringValueForKey("serial"), action: jsonObject.rawRepresentableValueForKey("action"), @@ -99,7 +156,16 @@ extension Message: JSONObjectDecodable { text: jsonObject.stringValueForKey("text"), createdAt: jsonObject.optionalAblyProtocolDateValueForKey("createdAt"), metadata: jsonObject.objectValueForKey("metadata"), - headers: jsonObject.objectValueForKey("headers").mapValues { try .init(jsonValue: $0) } + headers: jsonObject.objectValueForKey("headers").mapValues { try .init(jsonValue: $0) }, + version: jsonObject.stringValueForKey("version"), + timestamp: jsonObject.optionalAblyProtocolDateValueForKey("timestamp"), + operation: operation.map { op in + try .init( + clientID: op.stringValueForKey("clientId"), + description: try? op.stringValueForKey("description"), + metadata: try? op.objectValueForKey("metadata") + ) + } ) } } diff --git a/Sources/AblyChat/Messages.swift b/Sources/AblyChat/Messages.swift index 366b732..f73c3ae 100644 --- a/Sources/AblyChat/Messages.swift +++ b/Sources/AblyChat/Messages.swift @@ -40,12 +40,43 @@ public protocol Messages: AnyObject, Sendable, EmitsDiscontinuities { * - Parameters: * - params: An object containing `text`, `headers` and `metadata` for the message. * - * - Returns: The published message. + * - Returns: The published message, with the action of the message set as `.create`. * * - Note: It is possible to receive your own message via the messages subscription before this method returns. */ func send(params: SendMessageParams) async throws -> Message + /** + * Updates a message in the chat room. + * + * This method uses the Ably Chat API endpoint for updating messages. + * + * - Parameters: + * - newMessage: A copy of the `Message` object with the intended edits applied. Use the provided `copy` method on the existing message. + * - description: Optional description of the update action. + * - metadata: Optional metadata of the update action. (The metadata of the message itself still resides within the newMessage object above). + * + * - Returns: The updated message, with the `action` of the message set as `.update`. + * + * - Note: It is possible to receive your own message via the messages subscription before this method returns. + */ + func update(newMessage: Message, description: String?, metadata: OperationMetadata?) async throws -> Message + + /** + * Deletes a message in the chat room. + * + * This method uses the Ably Chat API endpoint for deleting messages. + * + * - Parameters: + * - message: The message you wish to delete. + * - params: Contains an optional description and metadata of the delete action. + * + * - Returns: The deleted message, with the action of the message set as `.delete`. + * + * - Note: It is possible to receive your own message via the messages subscription before this method returns. + */ + func delete(message: Message, params: DeleteMessageParams) async throws -> Message + /** * Get the underlying Ably realtime channel used for the messages in this chat room. * @@ -107,6 +138,56 @@ public struct SendMessageParams: Sendable { } } +/** + * Params for updating a text message. All fields are updated and, if omitted, they are set to empty. + */ +public struct UpdateMessageParams: Sendable { + /** + * The params to update including the text of the message. + */ + public var message: SendMessageParams + + /** + * Optional description of the update action. + */ + public var description: String? + + /** + * Optional metadata of the update action. + * + * The metadata is a map of extra information that can be attached to the update action. + * It is not used by Ably and is sent as part of the realtime + * message payload. Example use cases are setting custom styling like + * background or text colors or fonts, adding links to external images, + * emojis, etc. + * + * Do not use metadata for authoritative information. There is no server-side + * validation. When reading the metadata treat it like user input. + * + */ + public var metadata: OperationMetadata? + + public init(message: SendMessageParams, description: String? = nil, metadata: OperationMetadata? = nil) { + self.message = message + self.description = description + self.metadata = metadata + } +} + +/** + * Params for deleting a message. + */ +public struct DeleteMessageParams: Sendable { + public var description: String? + + public var metadata: OperationMetadata? + + public init(description: String? = nil, metadata: OperationMetadata? = nil) { + self.description = description + self.metadata = metadata + } +} + /** * Options for querying messages in a chat room. */ diff --git a/Tests/AblyChatTests/ChatAPITests.swift b/Tests/AblyChatTests/ChatAPITests.swift index 1c2267b..1b80835 100644 --- a/Tests/AblyChatTests/ChatAPITests.swift +++ b/Tests/AblyChatTests/ChatAPITests.swift @@ -47,7 +47,9 @@ struct ChatAPITests { text: "hello", createdAt: Date(timeIntervalSince1970: 1_631_840_000), metadata: [:], - headers: [:] + headers: [:], + version: "3446456", + timestamp: Date(timeIntervalSince1970: 1_631_840_000) ) #expect(message == expectedMessage) } @@ -143,7 +145,9 @@ struct ChatAPITests { text: "hello", createdAt: .init(timeIntervalSince1970: 1_730_943_049.269), metadata: [:], - headers: [:] + headers: [:], + version: "3446456", + timestamp: Date(timeIntervalSince1970: 1_730_943_049.269) ), Message( serial: "3446457", @@ -153,7 +157,9 @@ struct ChatAPITests { text: "hello response", createdAt: nil, metadata: [:], - headers: [:] + headers: [:], + version: "3446457", + timestamp: nil ), ] ) diff --git a/Tests/AblyChatTests/IntegrationTests.swift b/Tests/AblyChatTests/IntegrationTests.swift index 2f091f9..473298b 100644 --- a/Tests/AblyChatTests/IntegrationTests.swift +++ b/Tests/AblyChatTests/IntegrationTests.swift @@ -159,6 +159,63 @@ struct IntegrationTests { try #require(rxMessagesBeforeSubscribing.items.count == 1) #expect(rxMessagesBeforeSubscribing.items[0] == txMessageBeforeRxSubscribe) + // MARK: - Editing and Deleting Messages + + // Reuse message subscription and message from (5) and (6) above + let rxMessageEditDeleteSubscription = rxMessageSubscription + let messageToEditDelete = txMessageAfterRxSubscribe + + // (1) Edit the message on the other client + let txEditedMessage = try await txRoom.messages.update( + newMessage: messageToEditDelete.copy( + text: "edited message", + metadata: ["someEditedKey": 123, "someOtherEditedKey": "foo"], + headers: nil + ), + description: "random", + metadata: nil + ) + + // (2) Check that we received the edited message on the subscription + let rxEditedMessageFromSubscription = try #require(await rxMessageEditDeleteSubscription.first { _ in true }) + + // The createdAt varies by milliseconds so we can't compare the entire objects directly + #expect(rxEditedMessageFromSubscription.roomID == txEditedMessage.roomID) + #expect(rxEditedMessageFromSubscription.serial == txEditedMessage.serial) + #expect(rxEditedMessageFromSubscription.clientID == txEditedMessage.clientID) + #expect(rxEditedMessageFromSubscription.version == txEditedMessage.version) + #expect(rxEditedMessageFromSubscription.id == txEditedMessage.id) + #expect(rxEditedMessageFromSubscription.operation == txEditedMessage.operation) + // Ensures text has been edited from original message + #expect(rxEditedMessageFromSubscription.text == txEditedMessage.text) + // Ensure headers are now null when compared to original message + #expect(rxEditedMessageFromSubscription.headers == txEditedMessage.headers) + // Ensures metadata has been updated from original message + #expect(rxEditedMessageFromSubscription.metadata == txEditedMessage.metadata) + + // (3) Delete the message on the other client + let txDeleteMessage = try await txRoom.messages.delete( + message: rxEditedMessageFromSubscription, + params: .init( + description: "deleted in testing", + metadata: nil // TODO: Setting as nil for now as a metadata with any non-string value causes a decoding error atm... https://github.com/ably/ably-chat-swift/issues/226 + ) + ) + + // (4) Check that we received the deleted message on the subscription + let rxDeletedMessageFromSubscription = try #require(await rxMessageEditDeleteSubscription.first { _ in true }) + + // The createdAt varies by milliseconds so we can't compare the entire objects directly + #expect(rxDeletedMessageFromSubscription.roomID == txDeleteMessage.roomID) + #expect(rxDeletedMessageFromSubscription.serial == txDeleteMessage.serial) + #expect(rxDeletedMessageFromSubscription.clientID == txDeleteMessage.clientID) + #expect(rxDeletedMessageFromSubscription.version == txDeleteMessage.version) + #expect(rxDeletedMessageFromSubscription.id == txDeleteMessage.id) + #expect(rxDeletedMessageFromSubscription.operation == txDeleteMessage.operation) + #expect(rxDeletedMessageFromSubscription.text == txDeleteMessage.text) + #expect(rxDeletedMessageFromSubscription.headers == txDeleteMessage.headers) + #expect(rxDeletedMessageFromSubscription.metadata == txDeleteMessage.metadata) + // MARK: - Reactions // (1) Subscribe to reactions diff --git a/Tests/AblyChatTests/MessageSubscriptionTests.swift b/Tests/AblyChatTests/MessageSubscriptionTests.swift index c6d27ad..9eabba4 100644 --- a/Tests/AblyChatTests/MessageSubscriptionTests.swift +++ b/Tests/AblyChatTests/MessageSubscriptionTests.swift @@ -26,7 +26,7 @@ private final class MockPaginatedResult: PaginatedResult { struct MessageSubscriptionTests { let messages = ["First", "Second"].map { text in - Message(serial: "", action: .create, clientID: "", roomID: "", text: text, createdAt: .init(), metadata: [:], headers: [:]) + Message(serial: "", action: .create, clientID: "", roomID: "", text: text, createdAt: .init(), metadata: [:], headers: [:], version: "", timestamp: nil) } @Test diff --git a/Tests/AblyChatTests/MessageTests.swift b/Tests/AblyChatTests/MessageTests.swift index e085d72..1e8bc08 100644 --- a/Tests/AblyChatTests/MessageTests.swift +++ b/Tests/AblyChatTests/MessageTests.swift @@ -10,7 +10,9 @@ struct MessageTests { text: "hello", createdAt: nil, metadata: [:], - headers: [:] + headers: [:], + version: "ABC123@1631840000000-5:2", + timestamp: nil ) let laterMessage = Message( @@ -21,7 +23,9 @@ struct MessageTests { text: "hello", createdAt: nil, metadata: [:], - headers: [:] + headers: [:], + version: "ABC123@1631840000000-5:2", + timestamp: nil ) let invalidMessage = Message( @@ -32,7 +36,9 @@ struct MessageTests { text: "hello", createdAt: nil, metadata: [:], - headers: [:] + headers: [:], + version: "invalid", + timestamp: nil ) // MARK: isBefore Tests @@ -76,7 +82,9 @@ struct MessageTests { text: "", createdAt: nil, metadata: [:], - headers: [:] + headers: [:], + version: "ABC123@1631840000000-5:2", + timestamp: nil ) #expect(earlierMessage.serial == duplicateOfEarlierMessage.serial) } diff --git a/Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift b/Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift index 43587ae..ed89469 100644 --- a/Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift +++ b/Tests/AblyChatTests/Mocks/MockHTTPPaginatedResponse.swift @@ -105,6 +105,8 @@ extension MockHTTPPaginatedResponse { "text": "hello", "metadata": [:], "headers": [:], + "version": "3446456", + "timestamp": 1_730_943_049_269, ], [ "clientId": "random", @@ -114,6 +116,7 @@ extension MockHTTPPaginatedResponse { "text": "hello response", "metadata": [:], "headers": [:], + "version": "3446457", ], ], statusCode: 200,