diff --git a/README.md b/README.md index e7f756fc..102a252c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# XMTP-iOS +# xmtp-ios -![Lint](https://github.com/xmtp/xmtp-ios/actions/workflows/lint.yml/badge.svg) ![Status](https://img.shields.io/badge/Project_Status-Production-31CA54) +![Test](https://github.com/xmtp/xmtp-ios/actions/workflows/test.yml/badge.svg) ![Lint](https://github.com/xmtp/xmtp-ios/actions/workflows/lint.yml/badge.svg) ![Status](https://img.shields.io/badge/Feature_status-Alpha-orange) `xmtp-ios` provides a Swift implementation of an XMTP message API client for use with iOS apps. @@ -12,261 +12,203 @@ To learn more about XMTP and get answers to frequently asked questions, see the ![x-red-sm](https://user-images.githubusercontent.com/510695/163488403-1fb37e86-c673-4b48-954e-8460ae4d4b05.png) -## Quickstart and example apps built with `xmtp-ios` +## Example app built with `xmtp-ios` -- Use the [XMTP iOS quickstart app](https://github.com/xmtp/xmtp-ios/tree/main/XMTPiOSExample/XMTPiOSExample) as a tool to start building an app with XMTP. This basic messaging app has an intentionally unopinionated UI to help make it easier for you to build with. +Use the [XMTP iOS quickstart app](https://github.com/xmtp/xmtp-ios/tree/main/example) as a tool to start building an app with XMTP. This basic messaging app has an intentionally unopinionated UI to help make it easier for you to build with. -- Use the [XMTP Inbox iOS example app](https://github.com/xmtp-labs/xmtp-inbox-ios) as a reference implementation to understand how to implement features following developer and user experience best practices. +To learn about example app push notifications, see [Enable the quickstart app to send push notifications](library/src/main/java/org/xmtp/ios/library/push/README.md). ## Reference docs -> **View the reference** -> Access the [Swift client SDK reference documentation](https://xmtp.github.io/xmtp-ios/documentation/xmtp). +> **View the reference** +> Access the [Swift client SDK reference documentation](https://xmtp.github.io/xmtp-ios/). -## Install with Swift Package Manager +## Install from Swift Package Manager -Use Xcode to add to the project (**File** > **Add Packages…**) or add this to your `Package.swift` file: - -```swift -.package(url: "https://github.com/xmtp/xmtp-ios", branch: "main") -``` +You can add XMTP-iOS via Swift Package Manager by adding it to your `Package.swift` file or using Xcode’s “Add Package Dependency” feature. ## Usage overview -The XMTP message API revolves around a message API client (client) that allows retrieving and sending messages to other XMTP network participants. A client must connect to a wallet app on startup. If this is the very first time the client is created, the client will generate a key bundle that is used to encrypt and authenticate messages. The key bundle persists encrypted in the network using an account signature. The public side of the key bundle is also regularly advertised on the network to allow parties to establish shared encryption keys. All of this happens transparently, without requiring any additional code. +The XMTP message API revolves around a message API client (client) that allows retrieving and sending messages to other XMTP network participants. A client must connect to a wallet app on startup. If this is the very first time the client is created, the client will generate an identity with an encrypted local database to store and retrieve messages. Each additional log in will create a new installation if a local database is not present. ```swift -import XMTPiOS - // You'll want to replace this with a wallet from your application. -let account = try PrivateKey.generate() +let account = PrivateKey() + +// A key to encrypt the local database +let encryptionKey = SymmetricKey(size: .bits256) + +// Application context for creating the local database +let context = UIApplication.shared.delegate + +// The required client options +let clientOptions = ClientOptions( + api: .init(env: .dev, isSecure: true), + dbEncryptionKey: encryptionKey, + appContext: context +) // Create the client with your wallet. This will connect to the XMTP `dev` network by default. // The account is anything that conforms to the `XMTP.SigningKey` protocol. -let client = try await Client.create(account: account) +let client = try Client().create(account: account, options: clientOptions) -// Start a conversation with XMTP -let conversation = try await client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") +// Start a dm conversation +let conversation = try client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") +// Or a group conversation +let groupConversation = try client.conversations.newGroup(with: ["0x3F11b27F323b62B159D2642964fa27C46C841897"]) -// Load all messages in the conversation +// Load all messages in the conversations let messages = try await conversation.messages() + // Send a message -try await conversation.send(content: "gm") +try await conversation.send("gm") + // Listen for new messages in the conversation -for try await message in conversation.streamMessages() { - print("\(message.senderAddress): \(message.body)") +Task { + for await message in try await conversation.streamMessages() { + print("\(message.senderAddress): \(message.body)") + } } ``` -## Create a client -A client is created with `Client.create(account: SigningKey) async throws -> Client` that requires passing in an object capable of creating signatures on your behalf. The client will request a signature in two cases: +## Create a client -1. To sign the newly generated key bundle. This happens only the very first time when a key bundle is not found in storage. -2. To sign a random salt used to encrypt the key bundle in storage. This happens every time the client is started, including the very first time. +A client is created with `Client().create(account: SigningKey, options: ClientOptions): Client` that requires passing in an object capable of creating signatures on your behalf. The client will request a signature for any new installation. -> **Important** +> **Note** > The client connects to the XMTP `dev` environment by default. [Use `ClientOptions`](#configure-the-client) to change this and other parameters of the network connection. ```swift -import XMTPiOS // Create the client with a `SigningKey` from your app -let client = try await Client.create(account: account, options: .init(api: .init(env: .production))) +let options = ClientOptions(api: ClientOptions.Api(env: .production, isSecure: true), dbEncryptionKey: encryptionKey, appContext: context) +let client = try Client().create(account: account, options: options) ``` +### Create a client from saved encryptionKey -### Create a client from saved keys - -You can save your keys from the client via the `privateKeyBundle` property: +You can save your encryptionKey for the local database and build the client via address: ```swift // Create the client with a `SigningKey` from your app -let client = try await Client.create(account: account, options: .init(api: .init(env: .production))) - -// Get the key bundle -let keys = client.privateKeyBundle - -// Serialize the key bundle and store it somewhere safe -let keysData = try keys.serializedData() -``` - -Once you have those keys, you can create a new client with `Client.from`: - -```swift -let keys = try PrivateKeyBundle(serializedData: keysData) -let client = try Client.from(bundle: keys, options: .init(api: .init(env: .production))) -``` - +let options = ClientOptions(api: ClientOptions.Api(env: .production, isSecure: true), dbEncryptionKey: encryptionKey, appContext: context) +let client = try Client().build(address: account.address, options: options) +`` ### Configure the client -You can configure the client's network connection and key storage method with these optional parameters of `Client.create`: - -| Parameter | Default | Description | -| --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| env | `dev` | Connect to the specified XMTP network environment. Valid values include `.dev`, `.production`, or `.local`. For important details about working with these environments, see [XMTP `production` and `dev` network environments](#xmtp-production-and-dev-network-environments). | - -#### Configure `env` - -```swift -// Configure the client to use the `production` network -let clientOptions = ClientOptions(api: .init(env: .production)) -let client = try await Client.create(account: account, options: clientOptions) -``` - -## Configure content types +You can configure the client with these parameters of `Client.create`: -You can use custom content types by calling `Client.register`. The SDK comes with two commonly used content type codecs, `AttachmentCodec` and `RemoteAttachmentCodec`: +| Parameter | Default | Description | +| ---------- |-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| env | `DEV` | Connect to the specified XMTP network environment. Valid values include `DEV`, `.PRODUCTION`, or `LOCAL`. For important details about working with these environments, see [XMTP `production` and `dev` network environments](#xmtp-production-and-dev-network-environments). | +| appContext | `REQUIRED` | The application context used to create and access the local database. | +| dbEncryptionKey | `REQUIRED` | A 32 ByteArray used to encrypt the local database. | +| historySyncUrl | `https://message-history.dev.ephemera.network/` | The history sync url used to specify where history can be synced from other devices on the network. | +| appVersion | `undefined` | Add a client app version identifier that's included with API requests.
For example, you can use the following format: `appVersion: APP_NAME + '/' + APP_VERSION`.
Setting this value provides telemetry that shows which apps are using the XMTP client SDK. This information can help XMTP developers provide app support, especially around communicating important SDK updates, including deprecations and required upgrades. | -```swift -Client.register(AttachmentCodec()) -Client.register(RemoteAttachmentCodec()) -``` - -To learn more about using `AttachmentCodec` and `RemoteAttachmentCodec`, see [Handle different content types](#handle-different-content-types). +**Configure `env`** ## Handle conversations Most of the time, when interacting with the network, you'll want to do it through `conversations`. Conversations are between two accounts. -```swift -import XMTPiOS -// Create the client with a wallet from your app -let client = try await Client.create(account: account) -let conversations = try await client.conversations.list() -``` - -### List existing conversations - -You can get a list of all conversations that have one or more messages. +### List all dm & group conversations +If your app would like to handle groups and dms differently you can check whether a conversation is a dm or group for the type ```swift -let allConversations = try await client.conversations.list() +let conversations = try await client.conversations.list() -for conversation in allConversations { - print("Saying GM to \(conversation.peerAddress)") - try await conversation.send(content: "gm") +for conversation in conversations { + switch conversation.type { + case .group: + // Handle group + case .dm: + // Handle DM + } } ``` - +### List all groups +```swift +let groups = try await client.conversations.listGroups() +``` +### List all DMs +```swift +let dms = try await client.conversations.listDms() +``` These conversations include all conversations for a user **regardless of which app created the conversation.** This functionality provides the concept of an [interoperable inbox](https://xmtp.org/docs/concepts/interoperable-inbox), which enables a user to access all of their conversations in any app built with XMTP. ### Listen for new conversations You can also listen for new conversations being started in real-time. This will allow apps to display incoming messages from new contacts. -> **Warning** -> This stream will continue infinitely. To end the stream, break from the loop. - ```swift -for try await conversation in client.conversations.stream() { - print("New conversation started with \(conversation.peerAddress)") - - // Say hello to your new friend - try await conversation.send(content: "Hi there!") - - // Break from the loop to stop listening - break +Task { + for await conversation in try await client.conversations.stream() { + print("New conversation started with \(conversation.peerAddress)") + // Say hello to your new friend + try await conversation.send("Hi there!") + } } ``` - ### Start a new conversation You can create a new conversation with any Ethereum address on the XMTP network. ```swift -let newConversation = try await client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") +let newDm = try client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") +``` +```swift +let newGroup = try client.conversations.newGroup(with: ["0x3F11b27F323b62B159D2642964fa27C46C841897"]) ``` - ### Send messages -To be able to send a message, the recipient must have already created a client at least once and consequently advertised their key bundle on the network. Messages are addressed using account addresses. By default, the message payload supports plain strings. - -To learn about support for other content types, see [Handle different content types](#handle-different-content-types). +To be able to send a message, the recipient must have already created a client at least once. Messages are addressed using account addresses. In this example, the message payload is a plain text string. ```swift -let conversation = try await client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") -try await conversation.send(content: "Hello world") + +let conversation = try client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") +try await conversation.send("Hello world") ``` +To learn how to send other types of content, see [Handle different content types](#handle-different-types-of-content). ### List messages in a conversation You can receive the complete message history in a conversation by calling `conversation.messages()` ```swift -for conversation in client.conversations.list() { - let messagesInConversation = try await conversation.messages() -} -``` +let messages = try await conversation.messages() +`` ### List messages in a conversation with pagination It may be helpful to retrieve and process the messages in a conversation page by page. You can do this by calling `conversation.messages(limit: Int, before: Date)` which will return the specified number of messages sent before that time. + ```swift -let conversation = try await client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") let messages = try await conversation.messages(limit: 25) -let nextPage = try await conversation.messages(limit: 25, before: messages[0].sent) +let nextPage = try await conversation.messages(limit: 25, before: messages.first?.sentDate) ``` - ### Listen for new messages in a conversation You can listen for any new messages (incoming or outgoing) in a conversation by calling `conversation.streamMessages()`. A successfully received message (that makes it through the decoding and decryption without throwing) can be trusted to be authentic. Authentic means that it was sent by the owner of the `message.senderAddress` account and that it wasn't modified in transit. The `message.sent` timestamp can be trusted to have been set by the sender. -The stream returned by the `stream` methods is an asynchronous iterator and as such is usable by a for-await-of loop. Note however that it is by its nature infinite, so any looping construct used with it will not terminate, unless the termination is explicitly initiated (by breaking the loop). - -```swift -let conversation = try await client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") +The flow returned by the `stream` methods is an asynchronous data stream that sequentially emits values and completes normally or with an exception. -for try await message in conversation.streamMessages() { - if message.senderAddress == client.address { - // This message was sent from me - continue - } - - print("New message from \(message.senderAddress): \(message.body)") -} -``` - -### Decode a single message - -You can decode a single `Envelope` from XMTP using the `decode` method: ```swift -let conversation = try await client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") - -// Assume this function returns an Envelope that contains a message for the above conversation -let envelope = getEnvelopeFromXMTP() - -let decodedMessage = try conversation.decode(envelope) -``` - -### Serialize/Deserialize conversations - -You can save a conversation object locally using its `encodedContainer` property. This returns a `ConversationContainer` object which conforms to `Codable`. - -```swift -// Get a conversation -let conversation = try await client.conversations.newConversation(with: "0x3F11b27F323b62B159D2642964fa27C46C841897") - -// Get a container -let container = conversation.encodedContainer - -// Dump it to JSON -let encoder = JSONEncoder() -let data = try encoder.encode(container) -// Get it back from JSON -let decoder = JSONDecoder() -let containerAgain = try decoder.decode(ConversationContainer.self, from: data) - -// Get an actual Conversation object like we had above -let decodedConversation = containerAgain.decode(with: client) -try await decodedConversation.send(text: "hi") +Task { + for await message in try await conversation.streamMessages() { + if message.senderAddress == client.address { + // This message was sent from me + } + print("New message from \(message.senderAddress): \(message.body)") + } +} ``` - ## Request and respect user consent ![Feature status](https://img.shields.io/badge/Feature_status-Alpha-orange) @@ -279,165 +221,37 @@ The user consent feature enables your app to request and respect user consent pr To learn more, see [Request and respect user consent](https://xmtp.org/docs/build/user-consent). -## Handle different content types +## Handle different types of content -All of the send functions support `SendOptions` as an optional parameter. The `contentType` option allows specifying different types of content other than the default simple string standard content type, which is identified with content type identifier `ContentTypeText`. +All the send functions support `SendOptions` as an optional parameter. The `contentType` option allows specifying different types of content than the default simple string, which is identified with content type identifier `ContentTypeText`. To learn more about content types, see [Content types with XMTP](https://xmtp.org/docs/concepts/content-types). -Support for other content types can be added by registering additional `ContentCodec`s with the client. Every codec is associated with a content type identifier, `ContentTypeID`, which is used to signal to the client which codec should be used to process the content that is being sent or received. - -For example, see the [Codecs](https://github.com/xmtp/xmtp-ios/tree/main/Sources/XMTP/Codecs) available in `xmtp-ios`. - -### Send a remote attachment - -Use the [RemoteAttachmentCodec](https://github.com/xmtp/xmtp-ios/blob/main/Sources/XMTP/Codecs/RemoteAttachmentCodec.swift) package to enable your app to send and receive message attachments. - -Message attachments are files. More specifically, attachments are objects that have: - -- `filename` Most files have names, at least the most common file types. -- `mimeType` What kind of file is it? You can often assume this from the file extension, but it's nice to have a specific field for it. [Here's a list of common mime types.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types) -- `data` What is this file's data? Most files have data. If the file doesn't have data then it's probably not the most interesting thing to send. - -Because XMTP messages can only be up to 1MB in size, we need to store the attachment somewhere other than the XMTP network. In other words, we need to store it in a remote location. - -End-to-end encryption must apply not only to XMTP messages, but to message attachments as well. For this reason, we need to encrypt the attachment before we store it. - -#### Create an attachment object - -```swift -let attachment = Attachment( - filename: "screenshot.png", - mimeType: "image/png", - data: Data(somePNGData) -) -``` - -#### Encrypt the attachment - -Use the `RemoteAttachmentCodec.encodeEncrypted` to encrypt the attachment: - -```swift -// Encode the attachment and encrypt that encoded content -const encryptedAttachment = try RemoteAttachment.encodeEncrypted( - content: attachment, - codec: AttachmentCodec() -) -``` - -#### Upload the encrypted attachment - -Upload the encrypted attachment anywhere where it will be accessible via an HTTPS GET request. For example, you can use web3.storage: +Support for other types of content can be added by registering additional `ContentCodec`s with the Client. Every codec is associated with a content type identifier, `ContentTypeId`, which is used to signal to the Client which codec should be used to process the content that is being sent or received. ```swift -func upload(data: Data, token: String): String { - let url = URL(string: "https://api.web3.storage/upload")! - var request = URLRequest(url: url) - request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - request.addValue("XMTP", forHTTPHeaderField: "X-NAME") - request.httpMethod = "POST" - - let responseData = try await URLSession.shared.upload(for: request, from: data).0 - let response = try JSONDecoder().decode(Web3Storage.Response.self, from: responseData) - - return "https://\(response.cid).ipfs.w3s.link" -} - -let url = upload(data: encryptedAttachment.payload, token: YOUR_WEB3_STORAGE_TOKEN) -``` - -#### Create a remote attachment -Now that you have a `url`, you can create a `RemoteAttachment`. +// Assuming we've loaded a fictional NumberCodec that can be used to encode numbers, +// and is identified with ContentTypeNumber, we can use it as follows. +Client.register(codec: NumberCodec()) -```swift -let remoteAttachment = try RemoteAttachment( - url: url, - encryptedEncodedContent: encryptedEncodedContent -) +let options = SendOptions(contentType: .number, contentFallback: "sending you a pie") +try await aliceConversation.send(3.14, options: options) ``` +As shown in the example above, you must provide a `contentFallback` value. Use it to provide an alt text-like description of the original content. Providing a `contentFallback` value enables clients that don't support the content type to still display something meaningful. -#### Send a remote attachment - -Now that you have a remote attachment, you can send it: - -```swift -try await conversation.send( - content: remoteAttachment, - options: .init( - contentType: ContentTypeRemoteAttachment, - contentFallback: "a description of the image" - ) -) -``` +> **Caution** +> If you don't provide a `contentFallback` value, clients that don't support the content type will display an empty message. This results in a poor user experience and breaks interoperability. -Note that we’re using `contentFallback` to enable clients that don't support these content types to still display something. For cases where clients *do* support these types, they can use the content fallback as alt text for accessibility purposes. - -#### Receive a remote attachment - -Now that you can send a remote attachment, you need a way to receive a remote attachment. For example: - -```swift -let messages = try await conversation.messages() -let message = messages[0] - -guard message.encodedContent.contentType == ContentTypeRemoteAttachment else { - return -} - -const remoteAttachment: RemoteAttachment = try message.content() -``` - -#### Download, decrypt, and decode the attachment - -Now that you can receive a remote attachment, you need to download, decrypt, and decode it so your app can display it. For example: - -```swift -let attachment: Attachment = try await remoteAttachment.content() -``` - -You now have the original attachment: - -```swift -attachment.filename // => "screenshot.png" -attachment.mimeType // => "image/png", -attachment.data // => [the PNG data] -``` - -#### Display the attachment - -Display the attachment in your app as you please. For example, you can display it as an image: - -```swift -import UIKIt -import SwiftUI - -struct ContentView: View { - var body: some View { - Image(uiImage: UIImage(data: attachment.data)) - } -} -``` - -#### Handle custom content types +### Handle custom content types Beyond this, custom codecs and content types may be proposed as interoperable standards through XRCs. To learn more about the custom content type proposal process, see [XIP-5](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-5-message-content-types.md). -## Compression - -Message content can be optionally compressed using the compression option. The value of the option is the name of the compression algorithm to use. Currently supported are gzip and deflate. Compression is applied to the bytes produced by the content codec. - -Content will be decompressed transparently on the receiving end. Note that Client enforces maximum content size. The default limit can be overridden through the ClientOptions. Consequently a message that would expand beyond that limit on the receiving end will fail to decode. - -```swift -try await conversation.send(text: '#'.repeat(1000), options: .init(compression: .gzip)) -``` - ## 🏗 Breaking revisions -Because `xmtp-ios` is in active development, you should expect breaking revisions that might require you to adopt the latest SDK release to enable your app to continue working as expected. +Because `xmtp-android` is in active development, you should expect breaking revisions that might require you to adopt the latest SDK release to enable your app to continue working as expected. -XMTP communicates about breaking revisions in the [XMTP Discord community](https://discord.gg/xmtp), providing as much advance notice as possible. Additionally, breaking revisions in an `xmtp-ios` release are described on the [Releases page](https://github.com/xmtp/xmtp-ios/releases). +XMTP communicates about breaking revisions in the [XMTP Discord community](https://discord.gg/xmtp), providing as much advance notice as possible. Additionally, breaking revisions in an `xmtp-android` release are described on the [Releases page](https://github.com/xmtp/xmtp-android/releases). ## Deprecation @@ -448,11 +262,11 @@ Older versions of the SDK will eventually be deprecated, which means: The following table provides the deprecation schedule. -| Announced | Effective | Minimum Version | Rationale | -| ---------- | ---------- | --------------- | ---------------------------------------------------------------------------------------------------------------- | -| There are no deprecations scheduled for `xmtp-ios` at this time. | | | | +| Announced | Effective | Minimum Version | Rationale | +|------------------------|---------------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| No more support for V2 | March 1, 2025 | 3.0.0 | In a move towards better security with MLS and the ability to decentralize we will be shutting down V2 and moving entirely to V3 MLS. You can see the legacy branch here: https://github.com/xmtp/xmtp-android/tree/xmtp-legacy | -Bug reports, feature requests, and PRs are welcome in accordance with these [contribution guidelines](https://github.com/xmtp/xmtp-ios/blob/main/CONTRIBUTING.md). +Bug reports, feature requests, and PRs are welcome in accordance with these [contribution guidelines](https://github.com/xmtp/xmtp-android/blob/main/CONTRIBUTING.md). ## XMTP `production` and `dev` network environments @@ -461,7 +275,7 @@ XMTP provides both `production` and `dev` network environments to support the de The `production` and `dev` networks are completely separate and not interchangeable. For example, for a given blockchain account, its XMTP identity on `dev` network is completely distinct from its XMTP identity on the `production` network, as are the messages associated with these identities. In addition, XMTP identities and messages created on the `dev` network can't be accessed from or moved to the `production` network, and vice versa. -> **Important** +> **Note** > When you [create a client](#create-a-client), it connects to the XMTP `dev` environment by default. To learn how to use the `env` parameter to set your client's network environment, see [Configure the client](#configure-the-client). The `env` parameter accepts one of three valid values: `dev`, `production`, or `local`. Here are some best practices for when to use each environment: @@ -473,8 +287,3 @@ The `env` parameter accepts one of three valid values: `dev`, `production`, or ` - `local`: Use to have a client communicate with an XMTP node you are running locally. For example, an XMTP node developer can set `env` to `local` to generate client traffic to test a node running locally. The `production` network is configured to store messages indefinitely. XMTP may occasionally delete messages and keys from the `dev` network, and will provide advance notice in the [XMTP Discord community](https://discord.gg/xmtp). - -## Generate Protobufs -``` -buf generate buf.build/xmtp/proto -``` diff --git a/Sources/XMTPTestHelpers/TestHelpers.swift b/Sources/XMTPTestHelpers/TestHelpers.swift index d8cc65fc..d32a1462 100644 --- a/Sources/XMTPTestHelpers/TestHelpers.swift +++ b/Sources/XMTPTestHelpers/TestHelpers.swift @@ -1,145 +1,100 @@ -// -// TestHelpers.swift -// -// -// Created by Pat Nakajima on 12/6/22. -// - #if canImport(XCTest) -import Combine -import CryptoKit -import XCTest -@testable import XMTPiOS -import LibXMTP - -public struct TestConfig { - static let TEST_SERVER_ENABLED = _env("TEST_SERVER_ENABLED") == "true" - // TODO: change Client constructor to accept these explicitly (so we can config CI): - // static let TEST_SERVER_HOST = _env("TEST_SERVER_HOST") ?? "127.0.0.1" - // static let TEST_SERVER_PORT = Int(_env("TEST_SERVER_PORT")) ?? 5556 - // static let TEST_SERVER_IS_SECURE = _env("TEST_SERVER_IS_SECURE") == "true" - - static private func _env(_ key: String) -> String? { - ProcessInfo.processInfo.environment[key] - } - - static public func skipIfNotRunningLocalNodeTests() throws { - try XCTSkipIf(!TEST_SERVER_ENABLED, "requires local node") - } - - static public func skip(because: String) throws { - try XCTSkipIf(true, because) - } -} - -// Helper for tests gathering transcripts in a background task. -public actor TestTranscript { - public var messages: [String] = [] - public init() {} - public func add(_ message: String) { - messages.append(message) - } -} - -public struct FakeWallet: SigningKey { - public static func generate() throws -> FakeWallet { - let key = try PrivateKey.generate() - return FakeWallet(key) + import Combine + import CryptoKit + import XCTest + @testable import XMTPiOS + import LibXMTP + + public struct TestConfig { + static let TEST_SERVER_ENABLED = _env("TEST_SERVER_ENABLED") == "true" + // TODO: change Client constructor to accept these explicitly (so we can config CI): + // static let TEST_SERVER_HOST = _env("TEST_SERVER_HOST") ?? "127.0.0.1" + // static let TEST_SERVER_PORT = Int(_env("TEST_SERVER_PORT")) ?? 5556 + // static let TEST_SERVER_IS_SECURE = _env("TEST_SERVER_IS_SECURE") == "true" + + static private func _env(_ key: String) -> String? { + ProcessInfo.processInfo.environment[key] + } + + static public func skipIfNotRunningLocalNodeTests() throws { + try XCTSkipIf(!TEST_SERVER_ENABLED, "requires local node") + } + + static public func skip(because: String) throws { + try XCTSkipIf(true, because) + } } - public var address: String { - key.walletAddress + // Helper for tests gathering transcripts in a background task. + public actor TestTranscript { + public var messages: [String] = [] + public init() {} + public func add(_ message: String) { + messages.append(message) + } } - public func sign(_ data: Data) async throws -> XMTPiOS.Signature { - let signature = try await key.sign(data) - return signature - } + public struct FakeWallet: SigningKey { + public static func generate() throws -> FakeWallet { + let key = try PrivateKey.generate() + return FakeWallet(key) + } - public func sign(message: String) async throws -> XMTPiOS.Signature { - let signature = try await key.sign(message: message) - return signature - } + public var address: String { + key.walletAddress + } - public var key: PrivateKey + public func sign(_ data: Data) async throws -> XMTPiOS.Signature { + let signature = try await key.sign(data) + return signature + } - public init(_ key: PrivateKey) { - self.key = key - } -} - -public struct FakeSCWWallet: SigningKey { - public var walletAddress: String - private var internalSignature: String - - public init() throws { - // Simulate a wallet address (could be derived from a hash of some internal data) - self.walletAddress = UUID().uuidString // Using UUID for uniqueness in this fake example - self.internalSignature = Data(repeating: 0x01, count: 64).toHex // Fake internal signature - } - - public var address: String { - walletAddress - } + public func sign(message: String) async throws -> XMTPiOS.Signature { + let signature = try await key.sign(message: message) + return signature + } - public var type: WalletType { - WalletType.SCW - } - - public var chainId: Int64? { - 1 - } - - public static func generate() throws -> FakeSCWWallet { - return try FakeSCWWallet() - } - - public func signSCW(message: String) async throws -> Data { - // swiftlint:disable force_unwrapping - let digest = SHA256.hash(data: message.data(using: .utf8)!) - // swiftlint:enable force_unwrapping - return Data(digest) - } -} - -@available(iOS 15, *) -public struct Fixtures { - public var alice: PrivateKey! - public var aliceClient: Client! - - public var bob: PrivateKey! - public var bobClient: Client! - public let clientOptions: ClientOptions? = ClientOptions( - api: ClientOptions.Api(env: XMTPEnvironment.local, isSecure: false) - ) - - init() async throws { - alice = try PrivateKey.generate() - bob = try PrivateKey.generate() + public var key: PrivateKey - aliceClient = try await Client.create(account: alice, options: clientOptions) - bobClient = try await Client.create(account: bob, options: clientOptions) + public init(_ key: PrivateKey) { + self.key = key + } } - public func publishLegacyContact(client: Client) async throws { - var contactBundle = ContactBundle() - contactBundle.v1.keyBundle = try client.v1keys.toPublicKeyBundle() - - var envelope = Envelope() - envelope.contentTopic = Topic.contact(client.address).description - envelope.timestampNs = UInt64(Date().millisecondsSinceEpoch * 1_000_000) - envelope.message = try contactBundle.serializedData() - - try await client.publish(envelopes: [envelope]) + @available(iOS 15, *) + public struct Fixtures { + public var alix: PrivateKey! + public var alixClient: Client! + public var bo: PrivateKey! + public var boClient: Client! + public var caro: PrivateKey! + public var caroClient: Client! + + init() async throws { + alix = try PrivateKey.generate() + bo = try PrivateKey.generate() + caro = try PrivateKey.generate() + + let key = try Crypto.secureRandomBytes(count: 32) + let clientOptions: ClientOptions = ClientOptions( + api: ClientOptions.Api( + env: XMTPEnvironment.local, isSecure: false), + dbEncryptionKey: key + ) + + alixClient = try await Client.create( + account: alix, options: clientOptions) + boClient = try await Client.create( + account: bo, options: clientOptions) + caroClient = try await Client.create( + account: caro, options: clientOptions) + } } -} -public extension XCTestCase { - @available(iOS 15, *) - func fixtures() async -> Fixtures { - // swiftlint:disable force_try - return try! await Fixtures() - // swiftlint:enable force_try + extension XCTestCase { + @available(iOS 15, *) + public func fixtures() async throws -> Fixtures { + return try await Fixtures() + } } -} #endif diff --git a/Sources/XMTPiOS/Client.swift b/Sources/XMTPiOS/Client.swift index 39513260..93a2e245 100644 --- a/Sources/XMTPiOS/Client.swift +++ b/Sources/XMTPiOS/Client.swift @@ -59,14 +59,14 @@ public struct ClientOptions { api: Api = Api(), codecs: [any ContentCodec] = [], preAuthenticateToInboxCallback: PreEventCallback? = nil, - encryptionKey: Data, + dbEncryptionKey: Data, dbDirectory: String? = nil, historySyncUrl: String? = nil ) { self.api = api self.codecs = codecs self.preAuthenticateToInboxCallback = preAuthenticateToInboxCallback - self.dbEncryptionKey = encryptionKey + self.dbEncryptionKey = dbEncryptionKey self.dbDirectory = dbDirectory if historySyncUrl == nil { switch api.env { diff --git a/Sources/XMTPiOS/Conversation.swift b/Sources/XMTPiOS/Conversation.swift index 0f0adf99..56c4ee8a 100644 --- a/Sources/XMTPiOS/Conversation.swift +++ b/Sources/XMTPiOS/Conversation.swift @@ -1,22 +1,28 @@ import Foundation import LibXMTP -public enum Conversation { +public enum Conversation: Identifiable, Equatable, Hashable { case group(Group) case dm(Dm) + + public static func == (lhs: Conversation, rhs: Conversation) -> Bool { + lhs.topic == rhs.topic + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(topic) + } public enum ConversationType { case group, dm } public var id: String { - get throws { - switch self { - case let .group(group): - return group.id - case let .dm(dm): - return dm.id - } + switch self { + case let .group(group): + return group.id + case let .dm(dm): + return dm.id } } @@ -189,13 +195,3 @@ public enum Conversation { } } } - -extension Conversation: Hashable, Equatable { - public static func == (lhs: Conversation, rhs: Conversation) -> Bool { - lhs.topic == rhs.topic - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(topic) - } -} diff --git a/Sources/XMTPiOS/PrivatePreferences.swift b/Sources/XMTPiOS/PrivatePreferences.swift index a6cf292b..c8980f3d 100644 --- a/Sources/XMTPiOS/PrivatePreferences.swift +++ b/Sources/XMTPiOS/PrivatePreferences.swift @@ -76,10 +76,10 @@ public class ConsentList { ).fromFFI } - func conversationState(groupId: String) async throws -> ConsentState { + func conversationState(conversationId: String) async throws -> ConsentState { return try await ffiClient.getConsentState( entityType: .conversationId, - entity: groupId + entity: conversationId ).fromFFI } diff --git a/Tests/XMTPTests/AttachmentTests.swift b/Tests/XMTPTests/AttachmentTests.swift index f551afe9..c720ec30 100644 --- a/Tests/XMTPTests/AttachmentTests.swift +++ b/Tests/XMTPTests/AttachmentTests.swift @@ -1,28 +1,29 @@ -// -// AttachmentsTests.swift -// -// -// Created by Pat on 2/14/23. -// import Foundation - import XCTest + @testable import XMTPiOS @available(iOS 15, *) class AttachmentsTests: XCTestCase { func testCanUseAttachmentCodec() async throws { // swiftlint:disable force_try - let iconData = Data(base64Encoded: Data("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAACymlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj40NjA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjQ2MDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgr0TTmKAAAC5ElEQVQ4EW2TWWxMYRTHf3e2zjClVa0trVoqFRk1VKmIWhJ0JmkNETvvEtIHLwixxoM1xIOIiAjzxhBCQ9ESlRJNJEj7gJraJ63SdDrbvc53Z6xx7r253/lyzvnO/3/+n7a69KTBnyae1anJZ0nviq9pkIzppKLK+TMYbH+74Bhsobslzmv6yJQgJUHFuMiryCL+Tf8r5XcBqWxzWWhv+c6cDSPYsm4ehWPy5XSNd28j3Aw+49apMOO92aT6pRN5lf0qoJI7nvay4/JcFi+ZTiKepLPjC4ahM3VGCZVVk6iqaWWv/w5F3gEkFRyzgPxV221y8s5L6eSbocdUB25QhFUeBE6C0MWF1K6aReqqzs6aBkorBhHv0bEpwr4K5tlrhrM4MJ36K084HXhEfcjH/WvtJBM685dO5MymRyacmpWVNKx7Sdv5LrLL7FhU64ow//rJxGMJTix5QP4CF/P9Xjbv81F3wM8CWQ/1uDixqpn+aJzqtR5eSY6alMUQCIrXwuJ8PrzrokfaDTf0cnhbiPxhOQwbkcvBrZd5e/07SYl83xmhaGyBgm/az0ll3DQxulCc5fzFr7nuIs5Dotjtsm8emo61KZEobXS+iTCzaiJuGUxJTQ51u2t5H46QTKao21NL9+cgG6cNl04LCJ6+xxDsGCkDqyfPt2vgJyvdWg+LlgvWMhvNFzpwF2sEjzdzO/iCyurx+FaU45k2hicP2zgSaGLUFBlln4FNiSKnwkHT+Y/UL31sTkLXDdHCdSbIKVHp90PBWRbuH0dPJMrdo2EKSp3osQwE1b+SZ4nXzYFAI1pIw7esgv5+b0ZIBucONXJ2+3NG4mTk1AFyJ4QlxbzkWj1D/bsUg7oIfkihg0vH2nkVfoM7105untsk7UVrmL7WGLnlWSR6M3dBESem/XsbHYMsdLXERBtRU4UqaFz2QJyjbRgJaTuTqPaV/Z5V2jflObjMQbnLKW2mcSaErP8lq5QfTHkZ9teKBsUAAAAASUVORK5CYII=".utf8))! - let fixtures = await fixtures() - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + let iconData = Data( + base64Encoded: Data( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAACymlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj40NjA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjQ2MDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgr0TTmKAAAC5ElEQVQ4EW2TWWxMYRTHf3e2zjClVa0trVoqFRk1VKmIWhJ0JmkNETvvEtIHLwixxoM1xIOIiAjzxhBCQ9ESlRJNJEj7gJraJ63SdDrbvc53Z6xx7r253/lyzvnO/3/+n7a69KTBnyae1anJZ0nviq9pkIzppKLK+TMYbH+74Bhsobslzmv6yJQgJUHFuMiryCL+Tf8r5XcBqWxzWWhv+c6cDSPYsm4ehWPy5XSNd28j3Aw+49apMOO92aT6pRN5lf0qoJI7nvay4/JcFi+ZTiKepLPjC4ahM3VGCZVVk6iqaWWv/w5F3gEkFRyzgPxV221y8s5L6eSbocdUB25QhFUeBE6C0MWF1K6aReqqzs6aBkorBhHv0bEpwr4K5tlrhrM4MJ36K084HXhEfcjH/WvtJBM685dO5MymRyacmpWVNKx7Sdv5LrLL7FhU64ow//rJxGMJTix5QP4CF/P9Xjbv81F3wM8CWQ/1uDixqpn+aJzqtR5eSY6alMUQCIrXwuJ8PrzrokfaDTf0cnhbiPxhOQwbkcvBrZd5e/07SYl83xmhaGyBgm/az0ll3DQxulCc5fzFr7nuIs5Dotjtsm8emo61KZEobXS+iTCzaiJuGUxJTQ51u2t5H46QTKao21NL9+cgG6cNl04LCJ6+xxDsGCkDqyfPt2vgJyvdWg+LlgvWMhvNFzpwF2sEjzdzO/iCyurx+FaU45k2hicP2zgSaGLUFBlln4FNiSKnwkHT+Y/UL31sTkLXDdHCdSbIKVHp90PBWRbuH0dPJMrdo2EKSp3osQwE1b+SZ4nXzYFAI1pIw7esgv5+b0ZIBucONXJ2+3NG4mTk1AFyJ4QlxbzkWj1D/bsUg7oIfkihg0vH2nkVfoM7105untsk7UVrmL7WGLnlWSR6M3dBESem/XsbHYMsdLXERBtRU4UqaFz2QJyjbRgJaTuTqPaV/Z5V2jflObjMQbnLKW2mcSaErP8lq5QfTHkZ9teKBsUAAAAASUVORK5CYII=" + .utf8))! + let fixtures = try await fixtures() + let conversation = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) - fixtures.aliceClient.register(codec: AttachmentCodec()) + fixtures.alixClient.register(codec: AttachmentCodec()) - try await conversation.send(content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), options: .init(contentType: ContentTypeAttachment)) + try await conversation.send( + content: Attachment( + filename: "icon.png", mimeType: "image/png", data: iconData), + options: .init(contentType: ContentTypeAttachment)) let messages = try await conversation.messages() - XCTAssertEqual(1, messages.count) + XCTAssertEqual(2, messages.count) let message = messages[0] let attachment: Attachment = try message.content() diff --git a/Tests/XMTPTests/AuthenticationTests.swift b/Tests/XMTPTests/AuthenticationTests.swift deleted file mode 100644 index ad1568ba..00000000 --- a/Tests/XMTPTests/AuthenticationTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// AuthenticationTests.swift -// -// -// Created by Pat Nakajima on 11/17/22. -// - -import Foundation - -import XCTest -@testable import XMTPiOS - -final class AuthenticationTests: XCTestCase { - func testCreateToken() async throws { - let key = try PrivateKey.generate() - let identity = try PrivateKey.generate() - - // Prompt them to sign "XMTP : Create Identity ..." - let authorized = try await key.createIdentity(identity) - - // Create the `Authorization: Bearer $authToken` for API calls. - let authToken = try await authorized.createAuthToken() - - guard let tokenData = authToken.data(using: .utf8), - let base64TokenData = Data(base64Encoded: tokenData) - else { - XCTFail("could not get token data") - return - } - - let token = try Token(serializedData: base64TokenData) - let authData = try AuthData(serializedData: token.authDataBytes) - - XCTAssertEqual(authData.walletAddr, authorized.address) - } - - func testEnablingSavingAndLoadingOfStoredKeys() async throws { - let alice = try PrivateKey.generate() - let identity = try PrivateKey.generate() - - let authorized = try await alice.createIdentity(identity) - - let bundle = try authorized.toBundle - let encryptedBundle = try await bundle.encrypted(with: alice) - - let decrypted = try await encryptedBundle.decrypted(with: alice) - XCTAssertEqual(decrypted.v1.identityKey.secp256K1.bytes, identity.secp256K1.bytes) - XCTAssertEqual(decrypted.v1.identityKey.publicKey, authorized.authorized) - } -} diff --git a/Tests/XMTPTests/ClientTests.swift b/Tests/XMTPTests/ClientTests.swift index 27044352..040ecdef 100644 --- a/Tests/XMTPTests/ClientTests.swift +++ b/Tests/XMTPTests/ClientTests.swift @@ -1,70 +1,24 @@ -// -// ClientTests.swift -// -// -// Created by Pat Nakajima on 11/22/22. -// - import Foundation - -import XCTest -@testable import XMTPiOS import LibXMTP +import XCTest import XMTPTestHelpers +@testable import XMTPiOS + @available(iOS 15, *) class ClientTests: XCTestCase { func testTakesAWallet() async throws { - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) - let fakeWallet = try PrivateKey.generate() - _ = try await Client.create(account: fakeWallet, options: opts) - } - - func testPassingSavedKeysWithNoSignerWithMLSErrors() async throws { let key = try Crypto.secureRandomBytes(count: 32) - let bo = try PrivateKey.generate() - - do { - let client = try await Client.create( - account: bo, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - ) - } catch { - XCTAssert(error.localizedDescription.contains("no keys")) - } - } - - func testPassingSavedKeysWithMLS() async throws { - let key = try Crypto.secureRandomBytes(count: 32) - let bo = try PrivateKey.generate() - let client = try await Client.create( - account: bo, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - ) - - let keys = try client.privateKeyBundle - let otherClient = try await Client.from( - bundle: keys, - options: .init( - api: .init(env: .local, isSecure: false), - // Should not need to pass the signer again - enableV3: true, - encryptionKey: key - ) + let clientOptions: ClientOptions = ClientOptions( + api: ClientOptions.Api( + env: XMTPEnvironment.local, isSecure: false), + dbEncryptionKey: key ) - - XCTAssertEqual(client.address, otherClient.address) + let fakeWallet = try PrivateKey.generate() + _ = try await Client.create(account: fakeWallet, options: clientOptions) } - func testPassingencryptionKey() async throws { + func testPassingEncryptionKey() async throws { let bo = try PrivateKey.generate() let key = try Crypto.secureRandomBytes(count: 32) @@ -72,27 +26,11 @@ class ClientTests: XCTestCase { account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) - - do { - _ = try await Client.create( - account: bo, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: nil // No key should error - ) - ) - - XCTFail("did not throw") - } catch { - XCTAssert(true) - } } - + func testCanDeleteDatabase() async throws { let key = try Crypto.secureRandomBytes(count: 32) let bo = try PrivateKey.generate() @@ -101,21 +39,20 @@ class ClientTests: XCTestCase { account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) - + let alixClient = try await Client.create( account: alix, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) - _ = try await boClient.conversations.newGroup(with: [alixClient.address]) + _ = try await boClient.conversations.newGroup(with: [alixClient.address] + ) try await boClient.conversations.sync() var groupCount = try await boClient.conversations.listGroups().count @@ -128,8 +65,7 @@ class ClientTests: XCTestCase { account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) @@ -137,30 +73,29 @@ class ClientTests: XCTestCase { groupCount = try await boClient.conversations.listGroups().count XCTAssertEqual(groupCount, 0) } - + func testCanDropReconnectDatabase() async throws { let key = try Crypto.secureRandomBytes(count: 32) let bo = try PrivateKey.generate() let alix = try PrivateKey.generate() - var boClient = try await Client.create( + let boClient = try await Client.create( account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) - + let alixClient = try await Client.create( account: alix, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) - _ = try await boClient.conversations.newGroup(with: [alixClient.address]) + _ = try await boClient.conversations.newGroup(with: [alixClient.address] + ) try await boClient.conversations.sync() var groupCount = try await boClient.conversations.listGroups().count @@ -168,7 +103,8 @@ class ClientTests: XCTestCase { try boClient.dropLocalDatabaseConnection() - await assertThrowsAsyncError(try await boClient.conversations.listGroups()) + await assertThrowsAsyncError( + try await boClient.conversations.listGroups()) try await boClient.reconnectLocalDatabase() @@ -177,98 +113,33 @@ class ClientTests: XCTestCase { } func testCanMessage() async throws { - let fixtures = await fixtures() + let fixtures = try await fixtures() let notOnNetwork = try PrivateKey.generate() - let canMessage = try await fixtures.aliceClient.canMessage(fixtures.bobClient.address) - let cannotMessage = try await fixtures.aliceClient.canMessage(notOnNetwork.address) + let canMessage = try await fixtures.alixClient.canMessage( + address: fixtures.boClient.address) + let cannotMessage = try await fixtures.alixClient.canMessage( + address: notOnNetwork.address) XCTAssertTrue(canMessage) XCTAssertFalse(cannotMessage) } - func testStaticCanMessage() async throws { - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) - - let aliceWallet = try PrivateKey.generate() - let notOnNetwork = try PrivateKey.generate() - let alice = try await Client.create(account: aliceWallet, options: opts) - - let canMessage = try await Client.canMessage(alice.address, options: opts) - let cannotMessage = try await Client.canMessage(notOnNetwork.address, options: opts) - XCTAssertTrue(canMessage) - XCTAssertFalse(cannotMessage) - } - - func testHasPrivateKeyBundleV1() async throws { - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) + func testPreAuthenticateToInboxCallback() async throws { let fakeWallet = try PrivateKey.generate() - let client = try await Client.create(account: fakeWallet, options: opts) - - XCTAssertEqual(1, try client.v1keys.preKeys.count) - - let preKey = try client.v1keys.preKeys[0] - - XCTAssert(preKey.publicKey.hasSignature, "prekey not signed") - } - - func testCanBeCreatedWithBundle() async throws { - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) - let fakeWallet = try PrivateKey.generate() - let client = try await Client.create(account: fakeWallet, options: opts) - - let bundle = try client.privateKeyBundle - let clientFromV1Bundle = try await Client.from(bundle: bundle, options: opts) - - XCTAssertEqual(client.address, clientFromV1Bundle.address) - XCTAssertEqual(try client.v1keys.identityKey, try clientFromV1Bundle.v1keys.identityKey) - XCTAssertEqual(try client.v1keys.preKeys, try clientFromV1Bundle.v1keys.preKeys) - } - - func testCanBeCreatedWithV1Bundle() async throws { - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) - let fakeWallet = try PrivateKey.generate() - let client = try await Client.create(account: fakeWallet, options: opts) - - let bundleV1 = try client.v1keys - let clientFromV1Bundle = try await Client.from(v1Bundle: bundleV1, options: opts) - - XCTAssertEqual(client.address, clientFromV1Bundle.address) - XCTAssertEqual(try client.v1keys.identityKey, try clientFromV1Bundle.v1keys.identityKey) - XCTAssertEqual(try client.v1keys.preKeys, try clientFromV1Bundle.v1keys.preKeys) - } - - func testCanAccessPublicKeyBundle() async throws { - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) - let fakeWallet = try PrivateKey.generate() - let client = try await Client.create(account: fakeWallet, options: opts) - - let publicKeyBundle = try client.keys.getPublicKeyBundle() - XCTAssertEqual(publicKeyBundle, try client.publicKeyBundle) - } - - func testCanSignWithPrivateIdentityKey() async throws { - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false)) - let fakeWallet = try PrivateKey.generate() - let client = try await Client.create(account: fakeWallet, options: opts) - - let digest = Util.keccak256(Data("hello world".utf8)) - let signature = try await client.keys.identityKey.sign(digest) - - let recovered = try KeyUtilx.recoverPublicKeyKeccak256(from: signature.rawData, message: Data("hello world".utf8)) - let bytes = try client.keys.identityKey.publicKey.secp256K1Uncompressed.bytes - XCTAssertEqual(recovered, bytes) - } - - func testPreEnableIdentityCallback() async throws { - let fakeWallet = try PrivateKey.generate() - let expectation = XCTestExpectation(description: "preEnableIdentityCallback is called") + let expectation = XCTestExpectation( + description: "preAuthenticateToInboxCallback is called") + let key = try Crypto.secureRandomBytes(count: 32) - let preEnableIdentityCallback: () async throws -> Void = { - print("preEnableIdentityCallback called") - expectation.fulfill() + let preAuthenticateToInboxCallback: () async throws -> Void = { + print("preAuthenticateToInboxCallback called") + expectation.fulfill() } - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false), preEnableIdentityCallback: preEnableIdentityCallback ) + let opts = ClientOptions( + api: ClientOptions.Api(env: .local, isSecure: false), + preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, + dbEncryptionKey: key + ) do { _ = try await Client.create(account: fakeWallet, options: opts) await XCTWaiter().fulfillment(of: [expectation], timeout: 30) @@ -277,49 +148,7 @@ class ClientTests: XCTestCase { } } - func testPreCreateIdentityCallback() async throws { - let fakeWallet = try PrivateKey.generate() - let expectation = XCTestExpectation(description: "preCreateIdentityCallback is called") - - let preCreateIdentityCallback: () async throws -> Void = { - print("preCreateIdentityCallback called") - expectation.fulfill() - } - - let opts = ClientOptions(api: ClientOptions.Api(env: .local, isSecure: false), preCreateIdentityCallback: preCreateIdentityCallback ) - do { - _ = try await Client.create(account: fakeWallet, options: opts) - await XCTWaiter().fulfillment(of: [expectation], timeout: 30) - } catch { - XCTFail("Error: \(error)") - } - } - - func testPreAuthenticateToInboxCallback() async throws { - let fakeWallet = try PrivateKey.generate() - let expectation = XCTestExpectation(description: "preAuthenticateToInboxCallback is called") - let key = try Crypto.secureRandomBytes(count: 32) - - let preAuthenticateToInboxCallback: () async throws -> Void = { - print("preAuthenticateToInboxCallback called") - expectation.fulfill() - } - - let opts = ClientOptions( - api: ClientOptions.Api(env: .local, isSecure: false), - preAuthenticateToInboxCallback: preAuthenticateToInboxCallback, - enableV3: true, - encryptionKey: key - ) - do { - _ = try await Client.create(account: fakeWallet, options: opts) - await XCTWaiter().fulfillment(of: [expectation], timeout: 30) - } catch { - XCTFail("Error: \(error)") - } - } - - func testPassingencryptionKeyAndDatabaseDirectory() async throws { + func testPassingEncryptionKeyAndDatabaseDirectory() async throws { let bo = try PrivateKey.generate() let key = try Crypto.secureRandomBytes(count: 32) @@ -327,19 +156,16 @@ class ClientTests: XCTestCase { account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key, + dbEncryptionKey: key, dbDirectory: "xmtp_db" ) ) - let keys = try client.privateKeyBundle - let bundleClient = try await Client.from( - bundle: keys, + let bundleClient = try await Client.build( + address: bo.address, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key, + dbEncryptionKey: key, dbDirectory: "xmtp_db" ) ) @@ -349,30 +175,17 @@ class ClientTests: XCTestCase { XCTAssert(!client.installationID.isEmpty) await assertThrowsAsyncError( - _ = try await Client.from( - bundle: keys, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: nil, - dbDirectory: "xmtp_db" - ) - ) - ) - - await assertThrowsAsyncError( - _ = try await Client.from( - bundle: keys, + _ = try await Client.build( + address: bo.address, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key, + dbEncryptionKey: key, dbDirectory: nil ) ) ) } - + func testEncryptionKeyCanDecryptCorrectly() async throws { let bo = try PrivateKey.generate() let alix = try PrivateKey.generate() @@ -382,38 +195,37 @@ class ClientTests: XCTestCase { account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key, + dbEncryptionKey: key, dbDirectory: "xmtp_db" ) ) - + let alixClient = try await Client.create( account: alix, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key, + dbEncryptionKey: key, dbDirectory: "xmtp_db" ) ) - let group = try await boClient.conversations.newGroup(with: [alixClient.address]) - + _ = try await boClient.conversations.newGroup(with: [ + alixClient.address + ]) + let key2 = try Crypto.secureRandomBytes(count: 32) await assertThrowsAsyncError( try await Client.create( account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key2, + dbEncryptionKey: key2, dbDirectory: "xmtp_db" ) ) ) } - + func testCanGetAnInboxIdFromAddress() async throws { let key = try Crypto.secureRandomBytes(count: 32) let bo = try PrivateKey.generate() @@ -422,34 +234,32 @@ class ClientTests: XCTestCase { account: bo, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) - + let alixClient = try await Client.create( account: alix, options: .init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key + dbEncryptionKey: key ) ) - let boInboxId = try await alixClient.inboxIdFromAddress(address: boClient.address) + let boInboxId = try await alixClient.inboxIdFromAddress( + address: boClient.address) XCTAssertEqual(boClient.inboxID, boInboxId) } - + func testCreatesAV3Client() async throws { let key = try Crypto.secureRandomBytes(count: 32) let alix = try PrivateKey.generate() let options = ClientOptions.init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - + dbEncryptionKey: key + ) - let inboxId = try await Client.getOrCreateInboxId(options: options, address: alix.address) + let inboxId = try await Client.getOrCreateInboxId( + options: options, address: alix.address) let alixClient = try await Client.create( account: alix, options: options @@ -457,41 +267,39 @@ class ClientTests: XCTestCase { XCTAssertEqual(inboxId, alixClient.inboxID) } - - func testCreatesAPureV3Client() async throws { + + func testCreatesAClient() async throws { let key = try Crypto.secureRandomBytes(count: 32) let alix = try PrivateKey.generate() let options = ClientOptions.init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - + dbEncryptionKey: key + ) - let inboxId = try await Client.getOrCreateInboxId(options: options, address: alix.address) - let alixClient = try await Client.createV3( + let inboxId = try await Client.getOrCreateInboxId( + options: options, address: alix.address) + let alixClient = try await Client.create( account: alix, options: options ) XCTAssertEqual(inboxId, alixClient.inboxID) - - let alixClient2 = try await Client.buildV3( + + let alixClient2 = try await Client.build( address: alix.address, options: options ) - + XCTAssertEqual(alixClient2.inboxID, alixClient.inboxID) } - + func testRevokesAllOtherInstallations() async throws { let key = try Crypto.secureRandomBytes(count: 32) let alix = try PrivateKey.generate() let options = ClientOptions.init( api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) + dbEncryptionKey: key + ) let alixClient = try await Client.create( account: alix, @@ -499,7 +307,7 @@ class ClientTests: XCTestCase { ) try alixClient.dropLocalDatabaseConnection() try alixClient.deleteLocalDatabase() - + let alixClient2 = try await Client.create( account: alix, options: options @@ -511,38 +319,15 @@ class ClientTests: XCTestCase { account: alix, options: options ) - + let state = try await alixClient3.inboxState(refreshFromNetwork: true) XCTAssertEqual(state.installations.count, 3) XCTAssert(state.installations.first?.createdAt != nil) - - try await alixClient3.revokeAllOtherInstallations(signingKey: alix) - - let newState = try await alixClient3.inboxState(refreshFromNetwork: true) - XCTAssertEqual(newState.installations.count, 1) - } - - func testCreatesASCWClient() async throws { - throw XCTSkip("TODO: Need to write a SCW local deploy with anvil") - let key = try Crypto.secureRandomBytes(count: 32) - let alix = try FakeSCWWallet.generate() - let options = ClientOptions.init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) + try await alixClient3.revokeAllOtherInstallations(signingKey: alix) - let inboxId = try await Client.getOrCreateInboxId(options: options, address: alix.address) - - let alixClient = try await Client.createV3( - account: alix, - options: options - ) - - let alixClient2 = try await Client.buildV3(address: alix.address, options: options) - XCTAssertEqual(inboxId, alixClient.inboxID) - XCTAssertEqual(alixClient2.inboxID, alixClient.inboxID) - + let newState = try await alixClient3.inboxState( + refreshFromNetwork: true) + XCTAssertEqual(newState.installations.count, 1) } } diff --git a/Tests/XMTPTests/CodecTests.swift b/Tests/XMTPTests/CodecTests.swift index 9bd46890..bb406ccf 100644 --- a/Tests/XMTPTests/CodecTests.swift +++ b/Tests/XMTPTests/CodecTests.swift @@ -1,38 +1,40 @@ -// -// CodecTests.swift -// -// -// Created by Pat Nakajima on 12/21/22. -// - import XCTest + @testable import XMTPiOS struct NumberCodec: ContentCodec { func shouldPush(content: Double) throws -> Bool { return false } - + func fallback(content: Double) throws -> String? { return "pi" } - + typealias T = Double var contentType: XMTPiOS.ContentTypeID { - ContentTypeID(authorityID: "example.com", typeID: "number", versionMajor: 1, versionMinor: 1) + ContentTypeID( + authorityID: "example.com", typeID: "number", versionMajor: 1, + versionMinor: 1) } - func encode(content: Double, client _: Client) throws -> XMTPiOS.EncodedContent { + func encode(content: Double, client _: Client) throws + -> XMTPiOS.EncodedContent + { var encodedContent = EncodedContent() - encodedContent.type = ContentTypeID(authorityID: "example.com", typeID: "number", versionMajor: 1, versionMinor: 1) + encodedContent.type = ContentTypeID( + authorityID: "example.com", typeID: "number", versionMajor: 1, + versionMinor: 1) encodedContent.content = try JSONEncoder().encode(content) return encodedContent } - func decode(content: XMTPiOS.EncodedContent, client _: Client) throws -> Double { + func decode(content: XMTPiOS.EncodedContent, client _: Client) throws + -> Double + { return try JSONDecoder().decode(Double.self, from: content.content) } } @@ -40,17 +42,20 @@ struct NumberCodec: ContentCodec { @available(iOS 15, *) class CodecTests: XCTestCase { func testCanRoundTripWithCustomContentType() async throws { - let fixtures = await fixtures() + let fixtures = try await fixtures() - let aliceClient = fixtures.aliceClient! - let aliceConversation = try await aliceClient.conversations.newConversation(with: fixtures.bob.address) + let alixClient = fixtures.alixClient! + let alixConversation = try await alixClient.conversations + .newConversation(with: fixtures.bo.address) - aliceClient.register(codec: NumberCodec()) + alixClient.register(codec: NumberCodec()) - try await aliceConversation.send(content: 3.14, options: .init(contentType: NumberCodec().contentType)) + try await alixConversation.send( + content: 3.14, + options: .init(contentType: NumberCodec().contentType)) - let messages = try await aliceConversation.messages() - XCTAssertEqual(messages.count, 1) + let messages = try await alixConversation.messages() + XCTAssertEqual(messages.count, 2) if messages.count == 1 { let content: Double = try messages[0].content() @@ -59,20 +64,23 @@ class CodecTests: XCTestCase { } func testFallsBackToFallbackContentWhenCannotDecode() async throws { - let fixtures = await fixtures() + let fixtures = try await fixtures() - let aliceClient = fixtures.aliceClient! - let aliceConversation = try await aliceClient.conversations.newConversation(with: fixtures.bob.address) + let alixClient = fixtures.alixClient! + let alixConversation = try await alixClient.conversations + .newConversation(with: fixtures.bo.address) - aliceClient.register(codec: NumberCodec()) + alixClient.register(codec: NumberCodec()) - try await aliceConversation.send(content: 3.14, options: .init(contentType: NumberCodec().contentType)) + try await alixConversation.send( + content: 3.14, + options: .init(contentType: NumberCodec().contentType)) // Remove number codec from registry - aliceClient.codecRegistry.codecs.removeValue(forKey: NumberCodec().id) + alixClient.codecRegistry.codecs.removeValue(forKey: NumberCodec().id) - let messages = try await aliceConversation.messages() - XCTAssertEqual(messages.count, 1) + let messages = try await alixConversation.messages() + XCTAssertEqual(messages.count, 2) let content: Double? = try? messages[0].content() XCTAssertEqual(nil, content) diff --git a/Tests/XMTPTests/ContactTests.swift b/Tests/XMTPTests/ContactTests.swift deleted file mode 100644 index 838942e0..00000000 --- a/Tests/XMTPTests/ContactTests.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// ContactTests.swift -// -// -// Created by Pat Nakajima on 11/23/22. -// - -import XCTest -@testable import XMTPiOS - -class ContactTests: XCTestCase { - func testParsingV2Bundle() throws { - let data = Data( - [ - 18, 181, 2, 10, 178, 2, 10, 150, 1, 10, 76, 8, 140, 241, 170, 138, 182, - 48, 26, 67, 10, 65, 4, 33, 132, 132, 43, 80, 179, 54, 132, 47, 151, 245, - 23, 108, 148, 94, 190, 2, 33, 232, 232, 185, 73, 64, 44, 47, 65, 168, 25, - 56, 252, 1, 58, 243, 20, 103, 8, 253, 118, 10, 1, 108, 158, 125, 149, 128, - 37, 28, 250, 204, 1, 66, 194, 61, 119, 197, 121, 158, 210, 234, 92, 79, - 181, 1, 150, 18, 70, 18, 68, 10, 64, 43, 154, 228, 249, 69, 206, 218, 165, - 35, 55, 141, 145, 183, 129, 104, 75, 106, 62, 28, 73, 69, 7, 170, 65, 66, - 93, 11, 184, 229, 204, 140, 101, 71, 74, 0, 227, 140, 89, 53, 35, 203, 180, - 87, 102, 89, 176, 57, 128, 165, 42, 214, 173, 199, 17, 159, 200, 254, 25, - 80, 227, 20, 16, 189, 92, 16, 1, 18, 150, 1, 10, 76, 8, 244, 246, 171, 138, - 182, 48, 26, 67, 10, 65, 4, 104, 191, 167, 212, 49, 159, 46, 123, 133, 52, - 69, 73, 137, 157, 76, 63, 233, 223, 129, 64, 138, 86, 91, 26, 191, 241, 109, - 249, 216, 96, 226, 255, 103, 29, 192, 3, 181, 228, 63, 52, 101, 88, 96, 141, - 236, 194, 111, 16, 105, 88, 127, 215, 255, 63, 92, 135, 251, 14, 176, 85, 65, - 211, 88, 80, 18, 70, 10, 68, 10, 64, 252, 165, 96, 161, 187, 19, 203, 60, 89, - 195, 73, 176, 189, 203, 109, 113, 106, 39, 71, 116, 44, 101, 180, 16, 243, - 70, 128, 58, 46, 10, 55, 243, 43, 115, 21, 23, 153, 241, 208, 212, 162, 205, - 197, 139, 2, 117, 1, 40, 200, 252, 136, 148, 18, 125, 39, 175, 130, 113, - 103, 83, 120, 60, 232, 109, 16, 1, - ] - ) - - var envelope = Envelope() - envelope.message = data - let contactBundle = try ContactBundle.from(envelope: envelope) - - XCTAssert(!contactBundle.v1.hasKeyBundle) - XCTAssert(contactBundle.v2.hasKeyBundle) - - XCTAssertEqual(contactBundle.walletAddress, "0x66942eC8b0A6d0cff51AEA9C7fd00494556E705F") - } - - func testParsingV1Bundle() throws { - let message = Data( - [ - // This is a serialized PublicKeyBundle (instead of a ContactBundle) - 10, 146, 1, 8, 236, 130, 192, 166, 148, 48, 18, 68, - 10, 66, 10, 64, 70, 34, 101, 46, 39, 87, 114, 210, - 103, 135, 87, 49, 162, 200, 82, 177, 11, 4, 137, - 31, 235, 91, 185, 46, 177, 208, 228, 102, 44, 61, - 40, 131, 109, 210, 93, 42, 44, 235, 177, 73, 72, - 234, 18, 32, 230, 61, 146, 58, 65, 78, 178, 163, - 164, 241, 118, 167, 77, 240, 13, 100, 151, 70, 190, - 15, 26, 67, 10, 65, 4, 8, 71, 173, 223, 174, 185, - 150, 4, 179, 111, 144, 35, 5, 210, 6, 60, 21, 131, - 135, 52, 37, 221, 72, 126, 21, 103, 208, 31, 182, - 76, 187, 72, 66, 92, 193, 74, 161, 45, 135, 204, - 55, 10, 20, 119, 145, 136, 45, 194, 140, 164, 124, - 47, 238, 17, 198, 243, 102, 171, 67, 128, 164, 117, - 7, 83, - ] - ) - - var envelope = Envelope() - envelope.message = message - - let contactBundle = try ContactBundle.from(envelope: envelope) - XCTAssertEqual(contactBundle.walletAddress, "0x66942eC8b0A6d0cff51AEA9C7fd00494556E705F") - XCTAssertEqual(contactBundle.identityAddress, "0xD320f1454e33ab9393c0cc596E6321d80e4b481e") - XCTAssert(contactBundle.v1.keyBundle.hasPreKey == false, "should not have pre key") - } -} diff --git a/Tests/XMTPTests/ContactsTests.swift b/Tests/XMTPTests/ContactsTests.swift deleted file mode 100644 index 6efcd574..00000000 --- a/Tests/XMTPTests/ContactsTests.swift +++ /dev/null @@ -1,87 +0,0 @@ -// -// ContactsTests.swift -// -// -// Created by Pat Nakajima on 12/8/22. -// - -import XCTest -@testable import XMTPiOS -import XMTPTestHelpers - -@available(iOS 15, *) -class ContactsTests: XCTestCase { - func testNormalizesAddresses() async throws { - let fixtures = await fixtures() - try await fixtures.bobClient.ensureUserContactPublished() - - let bobAddressLowerCased = fixtures.bobClient.address.lowercased() - let bobContact = try await fixtures.aliceClient.getUserContact(peerAddress: bobAddressLowerCased) - - XCTAssertNotNil(bobContact) - } - - func testCanFindContact() async throws { - let fixtures = await fixtures() - - try await fixtures.bobClient.ensureUserContactPublished() - guard let contactBundle = try await fixtures.aliceClient.contacts.find(fixtures.bob.walletAddress) else { - XCTFail("did not find contact bundle") - return - } - - XCTAssertEqual(contactBundle.walletAddress, fixtures.bob.walletAddress) - } - - func testAllowAddress() async throws { - let fixtures = await fixtures() - - let contacts = fixtures.bobClient.contacts - var result = try await contacts.isAllowed(fixtures.alice.address) - - XCTAssertFalse(result) - - try await contacts.allow(addresses: [fixtures.alice.address]) - - result = try await contacts.isAllowed(fixtures.alice.address) - XCTAssertTrue(result) - } - - func testDenyAddress() async throws { - let fixtures = await fixtures() - - let contacts = fixtures.bobClient.contacts - var result = try await contacts.isAllowed(fixtures.alice.address) - - XCTAssertFalse(result) - - try await contacts.deny(addresses: [fixtures.alice.address]) - - result = try await contacts.isDenied(fixtures.alice.address) - XCTAssertTrue(result) - } - - func testHandleMultipleAddresses() async throws { - let fixtures = await fixtures() - let caro = try PrivateKey.generate() - let caroClient = try await Client.create(account: caro, options: fixtures.clientOptions) - - let contacts = fixtures.bobClient.contacts - var result = try await contacts.isAllowed(fixtures.alice.address) - XCTAssertFalse(result) - result = try await contacts.isAllowed(caroClient.address) - XCTAssertFalse(result) - - try await contacts.deny(addresses: [fixtures.alice.address, caroClient.address]) - - var aliceResult = try await contacts.isDenied(fixtures.alice.address) - XCTAssertTrue(aliceResult) - var caroResult = try await contacts.isDenied(fixtures.alice.address) - XCTAssertTrue(caroResult) - try await contacts.allow(addresses: [fixtures.alice.address, caroClient.address]) - aliceResult = try await contacts.isAllowed(fixtures.alice.address) - XCTAssertTrue(aliceResult) - caroResult = try await contacts.isAllowed(fixtures.alice.address) - XCTAssertTrue(caroResult) - } -} diff --git a/Tests/XMTPTests/CryptoTests.swift b/Tests/XMTPTests/CryptoTests.swift index 7f4e03cd..c961d051 100644 --- a/Tests/XMTPTests/CryptoTests.swift +++ b/Tests/XMTPTests/CryptoTests.swift @@ -1,12 +1,6 @@ -// -// CryptoTests.swift -// -// -// Created by Pat Nakajima on 11/17/22. -// - -import secp256k1 import XCTest +import secp256k1 + @testable import XMTPiOS final class CryptoTests: XCTestCase { @@ -21,70 +15,50 @@ final class CryptoTests: XCTestCase { func testDecryptingKnownCypherText() throws { let message = Data([5, 5, 5]) let secret = Data([1, 2, 3, 4]) - let encrypted = try CipherText(serializedData: Data([ - // This was generated using xmtp-js code for encrypt(). - 10, 69, 10, 32, 23, 10, 217, 190, 235, 216, 145, - 38, 49, 224, 165, 169, 22, 55, 152, 150, 176, 65, - 207, 91, 45, 45, 16, 171, 146, 125, 143, 60, 152, - 128, 0, 120, 18, 12, 219, 247, 207, 184, 141, 179, - 171, 100, 251, 171, 120, 137, 26, 19, 216, 215, 152, - 167, 118, 59, 93, 177, 53, 242, 147, 10, 87, 143, - 27, 245, 154, 169, 109, - ])) + let encrypted = try CipherText( + serializedData: Data([ + // This was generated using xmtp-js code for encrypt(). + 10, 69, 10, 32, 23, 10, 217, 190, 235, 216, 145, + 38, 49, 224, 165, 169, 22, 55, 152, 150, 176, 65, + 207, 91, 45, 45, 16, 171, 146, 125, 143, 60, 152, + 128, 0, 120, 18, 12, 219, 247, 207, 184, 141, 179, + 171, 100, 251, 171, 120, 137, 26, 19, 216, 215, 152, + 167, 118, 59, 93, 177, 53, 242, 147, 10, 87, 143, + 27, 245, 154, 169, 109, + ])) let decrypted = try Crypto.decrypt(secret, encrypted) XCTAssertEqual(message, decrypted) } - func testMessages() async throws { - let aliceWallet = try PrivateKey.generate() - let bobWallet = try PrivateKey.generate() - - let alice = try await PrivateKeyBundleV1.generate(wallet: aliceWallet) - let bob = try await PrivateKeyBundleV1.generate(wallet: bobWallet) - - let msg = "Hello world" - let decrypted = Data(msg.utf8) - - let alicePublic = alice.toPublicKeyBundle() - let bobPublic = bob.toPublicKeyBundle() - - let aliceSecret = try alice.sharedSecret(peer: bobPublic, myPreKey: alicePublic.preKey, isRecipient: false) - - let encrypted = try Crypto.encrypt(aliceSecret, decrypted) - - let bobSecret = try bob.sharedSecret(peer: alicePublic, myPreKey: bobPublic.preKey, isRecipient: true) - let bobDecrypted = try Crypto.decrypt(bobSecret, encrypted) - - let decryptedText = String(data: bobDecrypted, encoding: .utf8) - - XCTAssertEqual(decryptedText, msg) - } - func testGenerateAndValidateHmac() async throws { let secret = try Crypto.secureRandomBytes(count: 32) let info = try Crypto.secureRandomBytes(count: 32) let message = try Crypto.secureRandomBytes(count: 32) - let hmac = try Crypto.generateHmacSignature(secret: secret, info: info, message: message) + let hmac = try Crypto.generateHmacSignature( + secret: secret, info: info, message: message) let key = try Crypto.hkdfHmacKey(secret: secret, info: info) - let valid = Crypto.verifyHmacSignature(key: key, signature: hmac, message: message) - + let valid = Crypto.verifyHmacSignature( + key: key, signature: hmac, message: message) + XCTAssertTrue(valid) } - + func testGenerateAndValidateHmacWithExportedKey() async throws { let secret = try Crypto.secureRandomBytes(count: 32) let info = try Crypto.secureRandomBytes(count: 32) let message = try Crypto.secureRandomBytes(count: 32) - let hmac = try Crypto.generateHmacSignature(secret: secret, info: info, message: message) + let hmac = try Crypto.generateHmacSignature( + secret: secret, info: info, message: message) let key = try Crypto.hkdfHmacKey(secret: secret, info: info) let exportedKey = Crypto.exportHmacKey(key: key) let importedKey = Crypto.importHmacKey(keyData: exportedKey) - let valid = Crypto.verifyHmacSignature(key: importedKey, signature: hmac, message: message) - + let valid = Crypto.verifyHmacSignature( + key: importedKey, signature: hmac, message: message) + XCTAssertTrue(valid) } - + func testGenerateDifferentHmacKeysWithDifferentInfos() async throws { let secret = try Crypto.secureRandomBytes(count: 32) let info1 = try Crypto.secureRandomBytes(count: 32) @@ -93,30 +67,32 @@ final class CryptoTests: XCTestCase { let key2 = try Crypto.hkdfHmacKey(secret: secret, info: info2) let exportedKey1 = Crypto.exportHmacKey(key: key1) let exportedKey2 = Crypto.exportHmacKey(key: key2) - + XCTAssertNotEqual(exportedKey1, exportedKey2) } - + func testValidateHmacWithWrongMessage() async throws { let secret = try Crypto.secureRandomBytes(count: 32) let info = try Crypto.secureRandomBytes(count: 32) let message = try Crypto.secureRandomBytes(count: 32) - let hmac = try Crypto.generateHmacSignature(secret: secret, info: info, message: message) + let hmac = try Crypto.generateHmacSignature( + secret: secret, info: info, message: message) let key = try Crypto.hkdfHmacKey(secret: secret, info: info) let valid = Crypto.verifyHmacSignature( key: key, signature: hmac, message: try Crypto.secureRandomBytes(count: 32) ) - + XCTAssertFalse(valid) } - + func testValidateHmacWithWrongKey() async throws { let secret = try Crypto.secureRandomBytes(count: 32) let info = try Crypto.secureRandomBytes(count: 32) let message = try Crypto.secureRandomBytes(count: 32) - let hmac = try Crypto.generateHmacSignature(secret: secret, info: info, message: message) + let hmac = try Crypto.generateHmacSignature( + secret: secret, info: info, message: message) let valid = Crypto.verifyHmacSignature( key: try Crypto.hkdfHmacKey( secret: try Crypto.secureRandomBytes(count: 32), @@ -124,7 +100,7 @@ final class CryptoTests: XCTestCase { signature: hmac, message: message ) - + XCTAssertFalse(valid) } } diff --git a/Tests/XMTPTests/DmTests.swift b/Tests/XMTPTests/DmTests.swift index 2788228b..81adc6e7 100644 --- a/Tests/XMTPTests/DmTests.swift +++ b/Tests/XMTPTests/DmTests.swift @@ -1,126 +1,75 @@ -// -// DmTests.swift -// XMTPiOS -// -// Created by Naomi Plasterer on 10/23/24. -// - import CryptoKit -import XCTest -@testable import XMTPiOS import LibXMTP +import XCTest import XMTPTestHelpers +@testable import XMTPiOS + @available(iOS 16, *) class DmTests: XCTestCase { - struct LocalFixtures { - var alix: PrivateKey! - var bo: PrivateKey! - var caro: PrivateKey! - var alixClient: Client! - var boClient: Client! - var caroClient: Client! - } - - func localFixtures() async throws -> LocalFixtures { - let key = try Crypto.secureRandomBytes(count: 32) - let alix = try PrivateKey.generate() - let alixClient = try await Client.createV3( - account: alix, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) - ) - let bo = try PrivateKey.generate() - let boClient = try await Client.createV3( - account: bo, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) - ) - let caro = try PrivateKey.generate() - let caroClient = try await Client.createV3( - account: caro, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) - ) - - return .init( - alix: alix, - bo: bo, - caro: caro, - alixClient: alixClient, - boClient: boClient, - caroClient: caroClient - ) - } - + func testCanCreateADm() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let convo1 = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + let convo1 = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) try await fixtures.alixClient.conversations.sync() - let sameConvo1 = try await fixtures.alixClient.conversations.findOrCreateDm(with: fixtures.bo.walletAddress) + let sameConvo1 = try await fixtures.alixClient.conversations + .findOrCreateDm(with: fixtures.bo.walletAddress) XCTAssertEqual(convo1.id, sameConvo1.id) } func testCanListDmMembers() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) - var members = try await dm.members + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) + let members = try await dm.members XCTAssertEqual(members.count, 2) - let peer = try await dm.peerInboxId + let peer = try dm.peerInboxId XCTAssertEqual(peer, fixtures.alixClient.inboxID) } func testCannotStartGroupWithSelf() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() await assertThrowsAsyncError( - try await fixtures.alixClient.conversations.findOrCreateDm(with: fixtures.alix.address) + try await fixtures.alixClient.conversations.findOrCreateDm( + with: fixtures.alix.address) ) } func testCannotStartGroupWithNonRegisteredIdentity() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() let nonRegistered = try PrivateKey.generate() await assertThrowsAsyncError( - try await fixtures.alixClient.conversations.findOrCreateDm(with: nonRegistered.address) + try await fixtures.alixClient.conversations.findOrCreateDm( + with: nonRegistered.address) ) } func testDmStartsWithAllowedState() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) _ = try await dm.send(content: "howdy") _ = try await dm.send(content: "gm") try await dm.sync() - let isAllowed = try await fixtures.boClient.contacts.isGroupAllowed(groupId: dm.id) - let dmState = try await fixtures.boClient.contacts.consentList.conversationState(groupId: dm.id) - XCTAssertTrue(isAllowed) + let dmState = try await fixtures.boClient.preferences.consentList + .conversationState(conversationId: dm.id) XCTAssertEqual(dmState, .allowed) XCTAssertEqual(try dm.consentState(), .allowed) } func testCanSendMessageToDm() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) _ = try await dm.send(content: "howdy") let messageId = try await dm.send(content: "gm") try await dm.sync() @@ -142,14 +91,15 @@ class DmTests: XCTestCase { } func testCanStreamDmMessages() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) try await fixtures.alixClient.conversations.sync() - + let expectation1 = XCTestExpectation(description: "got a message") expectation1.expectedFulfillmentCount = 1 - + Task(priority: .userInitiated) { for try await _ in dm.streamMessages() { expectation1.fulfill() @@ -157,49 +107,62 @@ class DmTests: XCTestCase { } _ = try await dm.send(content: "hi") - + await fulfillment(of: [expectation1], timeout: 3) } func testCanStreamAllDecryptedDmMessages() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) try await fixtures.alixClient.conversations.sync() - + let expectation1 = XCTestExpectation(description: "got a message") expectation1.expectedFulfillmentCount = 2 - + Task(priority: .userInitiated) { - for try await _ in await fixtures.alixClient.conversations.streamAllMessages() { + for try await _ in await fixtures.alixClient.conversations + .streamAllMessages() + { expectation1.fulfill() } } _ = try await dm.send(content: "hi") - let caroDm = try await fixtures.caroClient.conversations.findOrCreateDm(with: fixtures.alixClient.address) + let caroDm = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.alixClient.address) _ = try await caroDm.send(content: "hi") - + await fulfillment(of: [expectation1], timeout: 3) } func testDmConsent() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let dm = try await fixtures.boClient.conversations.findOrCreateDm(with: fixtures.alix.walletAddress) + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) - let isGroup = try await fixtures.boClient.contacts.isGroupAllowed(groupId: dm.id) - XCTAssertTrue(isGroup) + let isDm = try await fixtures.boClient.preferences.consentList + .conversationState(conversationId: dm.id) + XCTAssertEqual(isDm, .allowed) XCTAssertEqual(try dm.consentState(), .allowed) - try await fixtures.boClient.contacts.denyGroups(groupIds: [dm.id]) - let isDenied = try await fixtures.boClient.contacts.isGroupDenied(groupId: dm.id) - XCTAssertTrue(isDenied) + try await fixtures.boClient.preferences.consentList.setConsentState( + entries: [ + ConsentListEntry( + value: dm.id, entryType: .conversation_id, + consentType: .denied) + ]) + let isDenied = try await fixtures.boClient.preferences.consentList + .conversationState(conversationId: dm.id) + XCTAssertEqual(isDenied, .denied) XCTAssertEqual(try dm.consentState(), .denied) try await dm.updateConsentState(state: .allowed) - let isAllowed = try await fixtures.boClient.contacts.isGroupAllowed(groupId: dm.id) - XCTAssertTrue(isAllowed) + let isAllowed = try await fixtures.boClient.preferences.consentList + .conversationState(conversationId: dm.id) + XCTAssertEqual(isAllowed, .allowed) XCTAssertEqual(try dm.consentState(), .allowed) } } diff --git a/Tests/XMTPTests/FramesTests.swift b/Tests/XMTPTests/FramesTests.swift deleted file mode 100644 index 94828061..00000000 --- a/Tests/XMTPTests/FramesTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// FramesTests.swift -// -// -// Created by Alex Risch on 4/1/24. -// - -import Foundation -import secp256k1 -import XCTest -@testable import XMTPiOS - -final class FramesTests: XCTestCase { - func testInstantiateFramesClient() async throws { - let frameUrl = "https://fc-polls-five.vercel.app/polls/01032f47-e976-42ee-9e3d-3aac1324f4b8" - - let key = try Crypto.secureRandomBytes(count: 32) - let bo = try PrivateKey.generate() - let client = try await Client.create( - account: bo, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - ) - - let framesClient = FramesClient(xmtpClient: client) - let metadata = try await framesClient.proxy.readMetadata(url: frameUrl) - let conversationTopic = "foo" - let participantAccountAddresses = ["amal", "bola"] - let dmInputs = DmActionInputs( - conversationTopic: conversationTopic, participantAccountAddresses: participantAccountAddresses) - let conversationInputs = ConversationActionInputs.dm(dmInputs) - let frameInputs = FrameActionInputs(frameUrl: frameUrl, buttonIndex: 1, inputText: nil, state: nil, conversationInputs: conversationInputs) - let signedPayload = try await framesClient.signFrameAction(inputs: frameInputs) - - guard let postUrl = metadata.extractedTags["fc:frame:post_url"] else { - throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "postUrl should exist"]) - } - let response = try await framesClient.proxy.post(url: postUrl, payload: signedPayload) - - guard response.extractedTags["fc:frame"] == "vNext" else { - throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "response should have expected extractedTags"]) - } - - guard let imageUrl = response.extractedTags["fc:frame:image"] else { - throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "imageUrl should exist"]) - } - - let mediaUrl = try await framesClient.proxy.mediaUrl(url: imageUrl) - - let (_, mediaResponse) = try await URLSession.shared.data(from: URL(string: mediaUrl)!) - - guard (mediaResponse as? HTTPURLResponse)?.statusCode == 200 else { - throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "downloadedMedia should be ok"]) - } - - guard (mediaResponse as? HTTPURLResponse)?.mimeType == "image/png" else { - throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "downloadedMedia should be image/png"]) - } - } -} diff --git a/Tests/XMTPTests/GroupPermissionsTests.swift b/Tests/XMTPTests/GroupPermissionsTests.swift index 59292905..f650a197 100644 --- a/Tests/XMTPTests/GroupPermissionsTests.swift +++ b/Tests/XMTPTests/GroupPermissionsTests.swift @@ -1,400 +1,415 @@ -// -// GroupPermissionTests.swift -// -// -// Created by Cameron Voell on 5/29/24. -// - import CryptoKit -import XCTest -import XMTPiOS import LibXMTP +import XCTest import XMTPTestHelpers +import XMTPiOS @available(iOS 16, *) class GroupPermissionTests: XCTestCase { - // Use these fixtures to talk to the local node - struct LocalFixtures { - public var alice: PrivateKey! - public var bob: PrivateKey! - public var caro: PrivateKey! - public var aliceClient: Client! - public var bobClient: Client! - public var caroClient: Client! - } - - enum CryptoError: Error { - case randomBytes, combinedPayload, hmacSignatureError - } - - public func secureRandomBytes(count: Int) throws -> Data { - var bytes = [UInt8](repeating: 0, count: count) - - // Fill bytes with secure random data - let status = SecRandomCopyBytes( - kSecRandomDefault, - count, - &bytes - ) - - // A status of errSecSuccess indicates success - if status == errSecSuccess { - return Data(bytes) - } else { - throw CryptoError.randomBytes - } - } - - func localFixtures() async throws -> LocalFixtures { - let key = try secureRandomBytes(count: 32) - let alice = try PrivateKey.generate() - let aliceClient = try await Client.create( - account: alice, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) - ) - let bob = try PrivateKey.generate() - let bobClient = try await Client.create( - account: bob, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) - ) - let caro = try PrivateKey.generate() - let caroClient = try await Client.create( - account: caro, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) - ) - - return .init( - alice: alice, - bob: bob, - caro: caro, - aliceClient: aliceClient, - bobClient: bobClient, - caroClient: caroClient - ) - } - - func testGroupCreatedWithCorrectAdminList() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - XCTAssertFalse(try bobGroup.isAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssertTrue(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssertFalse(try aliceGroup.isCreator()) - XCTAssertFalse(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID)) - XCTAssertFalse(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) - - let adminList = try bobGroup.listAdmins() - let superAdminList = try bobGroup.listSuperAdmins() - - XCTAssertEqual(adminList.count, 0) - XCTAssertFalse(adminList.contains(fixtures.bobClient.inboxID)) - XCTAssertEqual(superAdminList.count, 1) - XCTAssertTrue(superAdminList.contains(fixtures.bobClient.inboxID)) - } - - func testGroupCanUpdateAdminList() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address, fixtures.caro.address], permissions: .adminOnly) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - XCTAssertFalse(try bobGroup.isAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssertTrue(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssertFalse(try aliceGroup.isCreator()) - XCTAssertFalse(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID)) - XCTAssertFalse(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) - - var adminList = try bobGroup.listAdmins() - var superAdminList = try bobGroup.listSuperAdmins() - XCTAssertEqual(adminList.count, 0) - XCTAssertFalse(adminList.contains(fixtures.bobClient.inboxID)) - XCTAssertEqual(superAdminList.count, 1) - XCTAssertTrue(superAdminList.contains(fixtures.bobClient.inboxID)) - - // Verify that alice can NOT update group name - XCTAssertEqual(try bobGroup.groupName(), "") - await assertThrowsAsyncError( - try await aliceGroup.updateGroupName(groupName: "Alice group name") - ) - - try await aliceGroup.sync() - try await bobGroup.sync() - XCTAssertEqual(try bobGroup.groupName(), "") - XCTAssertEqual(try aliceGroup.groupName(), "") - - try await bobGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID) - try await bobGroup.sync() - try await aliceGroup.sync() - - adminList = try bobGroup.listAdmins() - superAdminList = try bobGroup.listSuperAdmins() - - XCTAssertTrue(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID)) - XCTAssertEqual(adminList.count, 1) - XCTAssertTrue(adminList.contains(fixtures.aliceClient.inboxID)) - XCTAssertEqual(superAdminList.count, 1) - - // Verify that alice can now update group name - try await aliceGroup.updateGroupName(groupName: "Alice group name") - try await aliceGroup.sync() - try await bobGroup.sync() - XCTAssertEqual(try bobGroup.groupName(), "Alice group name") - XCTAssertEqual(try aliceGroup.groupName(), "Alice group name") - - try await bobGroup.removeAdmin(inboxId: fixtures.aliceClient.inboxID) - try await bobGroup.sync() - try await aliceGroup.sync() - - adminList = try bobGroup.listAdmins() - superAdminList = try bobGroup.listSuperAdmins() - - XCTAssertFalse(try aliceGroup.isAdmin(inboxId: fixtures.aliceClient.inboxID)) - XCTAssertEqual(adminList.count, 0) - XCTAssertFalse(adminList.contains(fixtures.aliceClient.inboxID)) - XCTAssertEqual(superAdminList.count, 1) - - // Verify that alice can NOT update group name - await assertThrowsAsyncError( - try await aliceGroup.updateGroupName(groupName: "Alice group name 2") - ) - } - - func testGroupCanUpdateSuperAdminList() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address, fixtures.caro.address], permissions: .adminOnly) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - XCTAssertTrue(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssertFalse(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) - - // Attempt to remove bob as a super admin by alice should fail since she is not a super admin - await assertThrowsAsyncError( - try await aliceGroup.removeSuperAdmin(inboxId: fixtures.bobClient.inboxID) - ) - - // Make alice a super admin - try await bobGroup.addSuperAdmin(inboxId: fixtures.aliceClient.inboxID) - try await bobGroup.sync() - try await aliceGroup.sync() - XCTAssertTrue(try aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) - - // Now alice should be able to remove bob as a super admin - try await aliceGroup.removeSuperAdmin(inboxId: fixtures.bobClient.inboxID) - try await aliceGroup.sync() - try await bobGroup.sync() - - let superAdminList = try bobGroup.listSuperAdmins() - XCTAssertFalse(superAdminList.contains(fixtures.bobClient.inboxID)) - XCTAssertTrue(superAdminList.contains(fixtures.aliceClient.inboxID)) - } - - func testGroupMembersAndPermissionLevel() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address, fixtures.caro.address], permissions: .adminOnly) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - // Initial checks for group members and their permissions - var members = try await bobGroup.members - var admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } - var superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } - var regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } - - XCTAssertEqual(admins.count, 0) - XCTAssertEqual(superAdmins.count, 1) - XCTAssertEqual(regularMembers.count, 2) - - // Add alice as an admin - try await bobGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID) - try await bobGroup.sync() - try await aliceGroup.sync() - - members = try await bobGroup.members - admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } - superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } - regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } - - XCTAssertEqual(admins.count, 1) - XCTAssertEqual(superAdmins.count, 1) - XCTAssertEqual(regularMembers.count, 1) - - // Add caro as a super admin - try await bobGroup.addSuperAdmin(inboxId: fixtures.caroClient.inboxID) - try await bobGroup.sync() - try await aliceGroup.sync() - - members = try await bobGroup.members - admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } - superAdmins = members.filter { $0.permissionLevel == PermissionLevel.SuperAdmin } - regularMembers = members.filter { $0.permissionLevel == PermissionLevel.Member } - - XCTAssertEqual(admins.count, 1) - XCTAssertEqual(superAdmins.count, 2) - XCTAssertTrue(regularMembers.isEmpty) - } - - func testCanCommitAfterInvalidPermissionsCommit() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address, fixtures.caro.address], permissions: .allMembers) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - // Verify that alice can NOT add an admin - XCTAssertEqual(try bobGroup.groupName(), "") - await assertThrowsAsyncError( - try await aliceGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID) - ) - - try await aliceGroup.sync() - try await bobGroup.sync() - - // Verify that alice can update group name - try await bobGroup.sync() - try await aliceGroup.sync() - try await aliceGroup.updateGroupName(groupName: "Alice group name") - try await aliceGroup.sync() - try await bobGroup.sync() - - XCTAssertEqual(try bobGroup.groupName(), "Alice group name") - XCTAssertEqual(try aliceGroup.groupName(), "Alice group name") - } - - func testCanUpdatePermissions() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup( - with: [fixtures.alice.address, fixtures.caro.address], - permissions: .adminOnly - ) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - // Verify that Alice cannot update group description - XCTAssertEqual(try bobGroup.groupDescription(), "") - await assertThrowsAsyncError( - try await aliceGroup.updateGroupDescription(groupDescription: "new group description") - ) - - try await aliceGroup.sync() - try await bobGroup.sync() - XCTAssertEqual(try bobGroup.permissionPolicySet().updateGroupDescriptionPolicy, .admin) - - // Update group description permissions so Alice can update - try await bobGroup.updateGroupDescriptionPermission(newPermissionOption: .allow) - try await bobGroup.sync() - try await aliceGroup.sync() - XCTAssertEqual(try bobGroup.permissionPolicySet().updateGroupDescriptionPolicy, .allow) - - // Verify that Alice can now update group description - try await aliceGroup.updateGroupDescription(groupDescription: "Alice group description") - try await aliceGroup.sync() - try await bobGroup.sync() - XCTAssertEqual(try bobGroup.groupDescription(), "Alice group description") - XCTAssertEqual(try aliceGroup.groupDescription(), "Alice group description") - } - - func testCanUpdatePinnedFrameUrl() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup( - with: [fixtures.alice.address, fixtures.caro.address], - permissions: .adminOnly, - pinnedFrameUrl: "initial url" - ) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - // Verify that Alice cannot update group pinned frame url - XCTAssertEqual(try bobGroup.groupPinnedFrameUrl(), "initial url") - await assertThrowsAsyncError( - try await aliceGroup.updateGroupPinnedFrameUrl(groupPinnedFrameUrl: "https://foo/bar.com") - ) - - try await aliceGroup.sync() - try await bobGroup.sync() - XCTAssertEqual(try bobGroup.permissionPolicySet().updateGroupPinnedFrameUrlPolicy, .admin) - - // Update group pinned frame url permissions so Alice can update - try await bobGroup.updateGroupPinnedFrameUrlPermission(newPermissionOption: .allow) - try await bobGroup.sync() - try await aliceGroup.sync() - XCTAssertEqual(try bobGroup.permissionPolicySet().updateGroupPinnedFrameUrlPolicy, .allow) - - // Verify that Alice can now update group pinned frame url - try await aliceGroup.updateGroupPinnedFrameUrl(groupPinnedFrameUrl: "https://foo/barz.com") - try await aliceGroup.sync() - try await bobGroup.sync() - XCTAssertEqual(try bobGroup.groupPinnedFrameUrl(), "https://foo/barz.com") - XCTAssertEqual(try aliceGroup.groupPinnedFrameUrl(), "https://foo/barz.com") - } - - func testCanCreateGroupWithCustomPermissions() async throws { - let fixtures = try await localFixtures() - let permissionPolicySet = PermissionPolicySet( - addMemberPolicy: PermissionOption.admin, - removeMemberPolicy: PermissionOption.deny, - addAdminPolicy: PermissionOption.admin, - removeAdminPolicy: PermissionOption.superAdmin, - updateGroupNamePolicy: PermissionOption.admin, - updateGroupDescriptionPolicy: PermissionOption.allow, - updateGroupImagePolicy: PermissionOption.admin, - updateGroupPinnedFrameUrlPolicy: PermissionOption.deny - ) - let _bobGroup = try await fixtures.bobClient.conversations.newGroupCustomPermissions( - with: [fixtures.alice.address, fixtures.caro.address], - permissionPolicySet: permissionPolicySet, - pinnedFrameUrl: "initial url" - ) - - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - - let alicePermissionSet = try aliceGroup.permissionPolicySet() - XCTAssert(alicePermissionSet.addMemberPolicy == PermissionOption.admin) - XCTAssert(alicePermissionSet.removeMemberPolicy == PermissionOption.deny) - XCTAssert(alicePermissionSet.addAdminPolicy == PermissionOption.admin) - XCTAssert(alicePermissionSet.removeAdminPolicy == PermissionOption.superAdmin) - XCTAssert(alicePermissionSet.updateGroupNamePolicy == PermissionOption.admin) - XCTAssert(alicePermissionSet.updateGroupDescriptionPolicy == PermissionOption.allow) - XCTAssert(alicePermissionSet.updateGroupImagePolicy == PermissionOption.admin) - XCTAssert(alicePermissionSet.updateGroupPinnedFrameUrlPolicy == PermissionOption.deny) - } - - func testCreateGroupWithInvalidPermissionsFails() async throws { - let fixtures = try await localFixtures() - // Add / remove admin can not be set to "allow" - let permissionPolicySetInvalid = PermissionPolicySet( - addMemberPolicy: PermissionOption.admin, - removeMemberPolicy: PermissionOption.deny, - addAdminPolicy: PermissionOption.allow, - removeAdminPolicy: PermissionOption.superAdmin, - updateGroupNamePolicy: PermissionOption.admin, - updateGroupDescriptionPolicy: PermissionOption.allow, - updateGroupImagePolicy: PermissionOption.admin, - updateGroupPinnedFrameUrlPolicy: PermissionOption.deny - ) - await assertThrowsAsyncError( - try await fixtures.bobClient.conversations.newGroupCustomPermissions( - with: [fixtures.alice.address, fixtures.caro.address], - permissionPolicySet: permissionPolicySetInvalid, - pinnedFrameUrl: "initial url" - ) - ) - } + enum CryptoError: Error { + case randomBytes, combinedPayload, hmacSignatureError + } + + public func secureRandomBytes(count: Int) throws -> Data { + var bytes = [UInt8](repeating: 0, count: count) + + // Fill bytes with secure random data + let status = SecRandomCopyBytes( + kSecRandomDefault, + count, + &bytes + ) + + // A status of errSecSuccess indicates success + if status == errSecSuccess { + return Data(bytes) + } else { + throw CryptoError.randomBytes + } + } + + func testGroupCreatedWithCorrectAdminList() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address]) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + XCTAssertFalse( + try boGroup.isAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssertTrue( + try boGroup.isSuperAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssertFalse(try alixGroup.isCreator()) + XCTAssertFalse( + try alixGroup.isAdmin(inboxId: fixtures.alixClient.inboxID)) + XCTAssertFalse( + try alixGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) + + let adminList = try boGroup.listAdmins() + let superAdminList = try boGroup.listSuperAdmins() + + XCTAssertEqual(adminList.count, 0) + XCTAssertFalse(adminList.contains(fixtures.boClient.inboxID)) + XCTAssertEqual(superAdminList.count, 1) + XCTAssertTrue(superAdminList.contains(fixtures.boClient.inboxID)) + } + + func testGroupCanUpdateAdminList() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address, fixtures.caro.address], + permissions: .adminOnly) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + XCTAssertFalse( + try boGroup.isAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssertTrue( + try boGroup.isSuperAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssertFalse(try alixGroup.isCreator()) + XCTAssertFalse( + try alixGroup.isAdmin(inboxId: fixtures.alixClient.inboxID)) + XCTAssertFalse( + try alixGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) + + var adminList = try boGroup.listAdmins() + var superAdminList = try boGroup.listSuperAdmins() + XCTAssertEqual(adminList.count, 0) + XCTAssertFalse(adminList.contains(fixtures.boClient.inboxID)) + XCTAssertEqual(superAdminList.count, 1) + XCTAssertTrue(superAdminList.contains(fixtures.boClient.inboxID)) + + // Verify that alix can NOT update group name + XCTAssertEqual(try boGroup.groupName(), "") + await assertThrowsAsyncError( + try await alixGroup.updateGroupName(groupName: "alix group name") + ) + + try await alixGroup.sync() + try await boGroup.sync() + XCTAssertEqual(try boGroup.groupName(), "") + XCTAssertEqual(try alixGroup.groupName(), "") + + try await boGroup.addAdmin(inboxId: fixtures.alixClient.inboxID) + try await boGroup.sync() + try await alixGroup.sync() + + adminList = try boGroup.listAdmins() + superAdminList = try boGroup.listSuperAdmins() + + XCTAssertTrue( + try alixGroup.isAdmin(inboxId: fixtures.alixClient.inboxID)) + XCTAssertEqual(adminList.count, 1) + XCTAssertTrue(adminList.contains(fixtures.alixClient.inboxID)) + XCTAssertEqual(superAdminList.count, 1) + + // Verify that alix can now update group name + try await alixGroup.updateGroupName(groupName: "alix group name") + try await alixGroup.sync() + try await boGroup.sync() + XCTAssertEqual(try boGroup.groupName(), "alix group name") + XCTAssertEqual(try alixGroup.groupName(), "alix group name") + + try await boGroup.removeAdmin(inboxId: fixtures.alixClient.inboxID) + try await boGroup.sync() + try await alixGroup.sync() + + adminList = try boGroup.listAdmins() + superAdminList = try boGroup.listSuperAdmins() + + XCTAssertFalse( + try alixGroup.isAdmin(inboxId: fixtures.alixClient.inboxID)) + XCTAssertEqual(adminList.count, 0) + XCTAssertFalse(adminList.contains(fixtures.alixClient.inboxID)) + XCTAssertEqual(superAdminList.count, 1) + + // Verify that alix can NOT update group name + await assertThrowsAsyncError( + try await alixGroup.updateGroupName( + groupName: "alix group name 2") + ) + } + + func testGroupCanUpdateSuperAdminList() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address, fixtures.caro.address], + permissions: .adminOnly) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + XCTAssertTrue( + try boGroup.isSuperAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssertFalse( + try alixGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) + + // Attempt to remove bo as a super admin by alix should fail since she is not a super admin + await assertThrowsAsyncError( + try await alixGroup.removeSuperAdmin( + inboxId: fixtures.boClient.inboxID) + ) + + // Make alix a super admin + try await boGroup.addSuperAdmin(inboxId: fixtures.alixClient.inboxID) + try await boGroup.sync() + try await alixGroup.sync() + XCTAssertTrue( + try alixGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) + + // Now alix should be able to remove bo as a super admin + try await alixGroup.removeSuperAdmin( + inboxId: fixtures.boClient.inboxID) + try await alixGroup.sync() + try await boGroup.sync() + + let superAdminList = try boGroup.listSuperAdmins() + XCTAssertFalse(superAdminList.contains(fixtures.boClient.inboxID)) + XCTAssertTrue(superAdminList.contains(fixtures.alixClient.inboxID)) + } + + func testGroupMembersAndPermissionLevel() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address, fixtures.caro.address], + permissions: .adminOnly) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + // Initial checks for group members and their permissions + var members = try await boGroup.members + var admins = members.filter { + $0.permissionLevel == PermissionLevel.Admin + } + var superAdmins = members.filter { + $0.permissionLevel == PermissionLevel.SuperAdmin + } + var regularMembers = members.filter { + $0.permissionLevel == PermissionLevel.Member + } + + XCTAssertEqual(admins.count, 0) + XCTAssertEqual(superAdmins.count, 1) + XCTAssertEqual(regularMembers.count, 2) + + // Add alix as an admin + try await boGroup.addAdmin(inboxId: fixtures.alixClient.inboxID) + try await boGroup.sync() + try await alixGroup.sync() + + members = try await boGroup.members + admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } + superAdmins = members.filter { + $0.permissionLevel == PermissionLevel.SuperAdmin + } + regularMembers = members.filter { + $0.permissionLevel == PermissionLevel.Member + } + + XCTAssertEqual(admins.count, 1) + XCTAssertEqual(superAdmins.count, 1) + XCTAssertEqual(regularMembers.count, 1) + + // Add caro as a super admin + try await boGroup.addSuperAdmin(inboxId: fixtures.caroClient.inboxID) + try await boGroup.sync() + try await alixGroup.sync() + + members = try await boGroup.members + admins = members.filter { $0.permissionLevel == PermissionLevel.Admin } + superAdmins = members.filter { + $0.permissionLevel == PermissionLevel.SuperAdmin + } + regularMembers = members.filter { + $0.permissionLevel == PermissionLevel.Member + } + + XCTAssertEqual(admins.count, 1) + XCTAssertEqual(superAdmins.count, 2) + XCTAssertTrue(regularMembers.isEmpty) + } + + func testCanCommitAfterInvalidPermissionsCommit() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address, fixtures.caro.address], + permissions: .allMembers) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + // Verify that alix can NOT add an admin + XCTAssertEqual(try boGroup.groupName(), "") + await assertThrowsAsyncError( + try await alixGroup.addAdmin(inboxId: fixtures.alixClient.inboxID) + ) + + try await alixGroup.sync() + try await boGroup.sync() + + // Verify that alix can update group name + try await boGroup.sync() + try await alixGroup.sync() + try await alixGroup.updateGroupName(groupName: "alix group name") + try await alixGroup.sync() + try await boGroup.sync() + + XCTAssertEqual(try boGroup.groupName(), "alix group name") + XCTAssertEqual(try alixGroup.groupName(), "alix group name") + } + + func testCanUpdatePermissions() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address, fixtures.caro.address], + permissions: .adminOnly + ) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + // Verify that alix cannot update group description + XCTAssertEqual(try boGroup.groupDescription(), "") + await assertThrowsAsyncError( + try await alixGroup.updateGroupDescription( + groupDescription: "new group description") + ) + + try await alixGroup.sync() + try await boGroup.sync() + XCTAssertEqual( + try boGroup.permissionPolicySet().updateGroupDescriptionPolicy, + .admin) + + // Update group description permissions so alix can update + try await boGroup.updateGroupDescriptionPermission( + newPermissionOption: .allow) + try await boGroup.sync() + try await alixGroup.sync() + XCTAssertEqual( + try boGroup.permissionPolicySet().updateGroupDescriptionPolicy, + .allow) + + // Verify that alix can now update group description + try await alixGroup.updateGroupDescription( + groupDescription: "alix group description") + try await alixGroup.sync() + try await boGroup.sync() + XCTAssertEqual( + try boGroup.groupDescription(), "alix group description") + XCTAssertEqual( + try alixGroup.groupDescription(), "alix group description") + } + + func testCanUpdatePinnedFrameUrl() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address, fixtures.caro.address], + permissions: .adminOnly, + pinnedFrameUrl: "initial url" + ) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + // Verify that alix cannot update group pinned frame url + XCTAssertEqual(try boGroup.groupPinnedFrameUrl(), "initial url") + await assertThrowsAsyncError( + try await alixGroup.updateGroupPinnedFrameUrl( + groupPinnedFrameUrl: "https://foo/bar.com") + ) + + try await alixGroup.sync() + try await boGroup.sync() + XCTAssertEqual( + try boGroup.permissionPolicySet().updateGroupPinnedFrameUrlPolicy, + .admin) + + // Update group pinned frame url permissions so alix can update + try await boGroup.updateGroupPinnedFrameUrlPermission( + newPermissionOption: .allow) + try await boGroup.sync() + try await alixGroup.sync() + XCTAssertEqual( + try boGroup.permissionPolicySet().updateGroupPinnedFrameUrlPolicy, + .allow) + + // Verify that alix can now update group pinned frame url + try await alixGroup.updateGroupPinnedFrameUrl( + groupPinnedFrameUrl: "https://foo/barz.com") + try await alixGroup.sync() + try await boGroup.sync() + XCTAssertEqual( + try boGroup.groupPinnedFrameUrl(), "https://foo/barz.com") + XCTAssertEqual( + try alixGroup.groupPinnedFrameUrl(), "https://foo/barz.com") + } + + func testCanCreateGroupWithCustomPermissions() async throws { + let fixtures = try await fixtures() + let permissionPolicySet = PermissionPolicySet( + addMemberPolicy: PermissionOption.admin, + removeMemberPolicy: PermissionOption.deny, + addAdminPolicy: PermissionOption.admin, + removeAdminPolicy: PermissionOption.superAdmin, + updateGroupNamePolicy: PermissionOption.admin, + updateGroupDescriptionPolicy: PermissionOption.allow, + updateGroupImagePolicy: PermissionOption.admin, + updateGroupPinnedFrameUrlPolicy: PermissionOption.deny + ) + _ = try await fixtures.boClient.conversations + .newGroupCustomPermissions( + with: [fixtures.alix.address, fixtures.caro.address], + permissionPolicySet: permissionPolicySet, + pinnedFrameUrl: "initial url" + ) + + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + + let alixPermissionSet = try alixGroup.permissionPolicySet() + XCTAssert(alixPermissionSet.addMemberPolicy == PermissionOption.admin) + XCTAssert( + alixPermissionSet.removeMemberPolicy == PermissionOption.deny) + XCTAssert(alixPermissionSet.addAdminPolicy == PermissionOption.admin) + XCTAssert( + alixPermissionSet.removeAdminPolicy == PermissionOption.superAdmin) + XCTAssert( + alixPermissionSet.updateGroupNamePolicy == PermissionOption.admin) + XCTAssert( + alixPermissionSet.updateGroupDescriptionPolicy + == PermissionOption.allow) + XCTAssert( + alixPermissionSet.updateGroupImagePolicy == PermissionOption.admin) + XCTAssert( + alixPermissionSet.updateGroupPinnedFrameUrlPolicy + == PermissionOption.deny) + } + + func testCreateGroupWithInvalidPermissionsFails() async throws { + let fixtures = try await fixtures() + // Add / remove admin can not be set to "allow" + let permissionPolicySetInvalid = PermissionPolicySet( + addMemberPolicy: PermissionOption.admin, + removeMemberPolicy: PermissionOption.deny, + addAdminPolicy: PermissionOption.allow, + removeAdminPolicy: PermissionOption.superAdmin, + updateGroupNamePolicy: PermissionOption.admin, + updateGroupDescriptionPolicy: PermissionOption.allow, + updateGroupImagePolicy: PermissionOption.admin, + updateGroupPinnedFrameUrlPolicy: PermissionOption.deny + ) + await assertThrowsAsyncError( + try await fixtures.boClient.conversations + .newGroupCustomPermissions( + with: [fixtures.alix.address, fixtures.caro.address], + permissionPolicySet: permissionPolicySetInvalid, + pinnedFrameUrl: "initial url" + ) + ) + } } diff --git a/Tests/XMTPTests/GroupTests.swift b/Tests/XMTPTests/GroupTests.swift index e3d5b5c6..c4050a9a 100644 --- a/Tests/XMTPTests/GroupTests.swift +++ b/Tests/XMTPTests/GroupTests.swift @@ -1,488 +1,558 @@ -// -// GroupTests.swift -// -// -// Created by Pat Nakajima on 2/1/24. -// - import CryptoKit -import XCTest -@testable import XMTPiOS import LibXMTP +import XCTest import XMTPTestHelpers +@testable import XMTPiOS + func assertThrowsAsyncError( - _ expression: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - _ errorHandler: (_ error: Error) -> Void = { _ in } + _ expression: @autoclosure () async throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line, + _ errorHandler: (_ error: Error) -> Void = { _ in } ) async { - do { - _ = try await expression() - // expected error to be thrown, but it was not - let customMessage = message() - if customMessage.isEmpty { - XCTFail("Asynchronous call did not throw an error.", file: file, line: line) - } else { - XCTFail(customMessage, file: file, line: line) - } - } catch { - errorHandler(error) + do { + _ = try await expression() + // expected error to be thrown, but it was not + let customMessage = message() + if customMessage.isEmpty { + XCTFail( + "Asynchronous call did not throw an error.", file: file, + line: line) + } else { + XCTFail(customMessage, file: file, line: line) } + } catch { + errorHandler(error) + } } @available(iOS 16, *) class GroupTests: XCTestCase { - // Use these fixtures to talk to the local node - struct LocalFixtures { - var alice: PrivateKey! - var bob: PrivateKey! - var fred: PrivateKey! - var davonV3: PrivateKey! - var aliceClient: Client! - var bobClient: Client! - var fredClient: Client! - var davonV3Client: Client! - } - - func localFixtures() async throws -> LocalFixtures { - let key = try Crypto.secureRandomBytes(count: 32) - let options = ClientOptions.init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true, - encryptionKey: key - ) - let alice = try PrivateKey.generate() - let aliceClient = try await Client.create( - account: alice, - options: options - ) - let bob = try PrivateKey.generate() - let bobClient = try await Client.create( - account: bob, - options: options - ) - let fred = try PrivateKey.generate() - let fredClient = try await Client.create( - account: fred, - options: options - ) - - let davonV3 = try PrivateKey.generate() - let davonV3Client = try await Client.createV3( - account: davonV3, - options: options - ) - - return .init( - alice: alice, - bob: bob, - fred: fred, - davonV3: davonV3, - aliceClient: aliceClient, - bobClient: bobClient, - fredClient: fredClient, - davonV3Client: davonV3Client - ) - } - - func testCanDualSendConversations() async throws { - let fixtures = try await localFixtures() - let v2Convo = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bob.walletAddress) + func testCanCreateAGroupWithDefaultPermissions() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address]) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + XCTAssert(!boGroup.id.isEmpty) + XCTAssert(!alixGroup.id.isEmpty) + + try await alixGroup.addMembers(addresses: [fixtures.caro.address]) + try await boGroup.sync() - try await fixtures.aliceClient.conversations.sync() - try await fixtures.bobClient.conversations.sync() + var alixMembersCount = try await alixGroup.members.count + var boMembersCount = try await boGroup.members.count + XCTAssertEqual(alixMembersCount, 3) + XCTAssertEqual(boMembersCount, 3) - let alixDm = try await fixtures.aliceClient.findDm(address: fixtures.bob.walletAddress) - let boDm = try await fixtures.bobClient.findDm(address: fixtures.alice.walletAddress) + try await boGroup.addAdmin(inboxId: fixtures.alixClient.inboxID) - XCTAssertEqual(alixDm?.id, boDm?.id) + try await alixGroup.removeMembers(addresses: [fixtures.caro.address]) + try await boGroup.sync() - let alixConversationsListCount = try await fixtures.aliceClient.conversations.list().count - XCTAssertEqual(alixConversationsListCount, 1) + alixMembersCount = try await alixGroup.members.count + boMembersCount = try await boGroup.members.count + XCTAssertEqual(alixMembersCount, 2) + XCTAssertEqual(boMembersCount, 2) - let alixDmsListCount = try await fixtures.aliceClient.conversations.listDms().count - XCTAssertEqual(alixDmsListCount, 1) + try await boGroup.addMembers(addresses: [fixtures.caro.address]) + try await alixGroup.sync() - let boDmsListCount = try await fixtures.bobClient.conversations.listDms().count - XCTAssertEqual(boDmsListCount, 1) + try await boGroup.removeAdmin(inboxId: fixtures.alixClient.inboxID) + try await alixGroup.sync() - let boConversationsListCount = try await fixtures.bobClient.conversations.list().count - XCTAssertEqual(boConversationsListCount, 1) + alixMembersCount = try await alixGroup.members.count + boMembersCount = try await boGroup.members.count + XCTAssertEqual(alixMembersCount, 3) + XCTAssertEqual(boMembersCount, 3) + + XCTAssertEqual( + try boGroup.permissionPolicySet().addMemberPolicy, .allow) + XCTAssertEqual( + try alixGroup.permissionPolicySet().addMemberPolicy, .allow) + + XCTAssert( + try boGroup.isSuperAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssert( + try !boGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) + XCTAssert( + try alixGroup.isSuperAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssert( + try !alixGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) - let boFirstTopic = try await fixtures.bobClient.conversations.list().first?.topic - XCTAssertEqual(v2Convo.topic, boFirstTopic) } - func testCanDualSendMessages() async throws { - let fixtures = try await localFixtures() - let alixV2Convo = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bob.walletAddress) - let boV2Convo = try await fixtures.bobClient.conversations.list().first! - - try await fixtures.bobClient.conversations.sync() + func testCanCreateAGroupWithAdminPermissions() async throws { + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address], + permissions: GroupPermissionPreconfiguration.adminOnly) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try await fixtures.alixClient.conversations + .listGroups().first! + XCTAssert(!boGroup.id.isEmpty) + XCTAssert(!alixGroup.id.isEmpty) + + let boConsentResult = try boGroup.consentState() + XCTAssertEqual(boConsentResult, ConsentState.allowed) + + let alixConsentResult = try await fixtures.alixClient.preferences + .consentList.conversationState(conversationId: alixGroup.id) + XCTAssertEqual(alixConsentResult, ConsentState.unknown) + + try await boGroup.addMembers(addresses: [fixtures.caro.address]) + try await alixGroup.sync() - let alixDm = try await fixtures.aliceClient.findDm(address: fixtures.bob.walletAddress) - let boDm = try await fixtures.bobClient.findDm(address: fixtures.alice.walletAddress) + var alixMembersCount = try await alixGroup.members.count + var boMembersCount = try await boGroup.members.count + XCTAssertEqual(alixMembersCount, 3) + XCTAssertEqual(boMembersCount, 3) - try await alixV2Convo.send(content: "first") - try await boV2Convo.send(content: "second") + await assertThrowsAsyncError( + try await alixGroup.removeMembers(addresses: [ + fixtures.caro.address + ]) + ) + try await boGroup.sync() - try await alixDm?.sync() - try await boDm?.sync() + alixMembersCount = try await alixGroup.members.count + boMembersCount = try await boGroup.members.count + XCTAssertEqual(alixMembersCount, 3) + XCTAssertEqual(boMembersCount, 3) - let alixV2ConvoMessageCount = try await alixV2Convo.messages().count - XCTAssertEqual(alixV2ConvoMessageCount, 2) + try await boGroup.removeMembers(addresses: [fixtures.caro.address]) + try await alixGroup.sync() - let boV2ConvoMessageCount = try await boV2Convo.messages().count - XCTAssertEqual(alixV2ConvoMessageCount, boV2ConvoMessageCount) + alixMembersCount = try await alixGroup.members.count + boMembersCount = try await boGroup.members.count + XCTAssertEqual(alixMembersCount, 2) + XCTAssertEqual(boMembersCount, 2) - let boDmMessageCount = try await boDm?.messages().count - XCTAssertEqual(boDmMessageCount, 2) + await assertThrowsAsyncError( + try await alixGroup.addMembers(addresses: [fixtures.caro.address]) + ) + try await boGroup.sync() - let alixDmMessageCount = try await alixDm?.messages().count - XCTAssertEqual(alixDmMessageCount, 3) // Including the group membership update in the DM + alixMembersCount = try await alixGroup.members.count + boMembersCount = try await boGroup.members.count + XCTAssertEqual(alixMembersCount, 2) + XCTAssertEqual(boMembersCount, 2) + + XCTAssertEqual( + try boGroup.permissionPolicySet().addMemberPolicy, .admin) + XCTAssertEqual( + try alixGroup.permissionPolicySet().addMemberPolicy, .admin) + XCTAssert( + try boGroup.isSuperAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssert( + try !boGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) + XCTAssert( + try alixGroup.isSuperAdmin(inboxId: fixtures.boClient.inboxID)) + XCTAssert( + try !alixGroup.isSuperAdmin(inboxId: fixtures.alixClient.inboxID)) } - func testCanCreateAGroupWithDefaultPermissions() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - XCTAssert(!bobGroup.id.isEmpty) - XCTAssert(!aliceGroup.id.isEmpty) - - try await aliceGroup.addMembers(addresses: [fixtures.fred.address]) - try await bobGroup.sync() - - var aliceMembersCount = try await aliceGroup.members.count - var bobMembersCount = try await bobGroup.members.count - XCTAssertEqual(aliceMembersCount, 3) - XCTAssertEqual(bobMembersCount, 3) - - try await bobGroup.addAdmin(inboxId: fixtures.aliceClient.inboxID) - - try await aliceGroup.removeMembers(addresses: [fixtures.fred.address]) - try await bobGroup.sync() - - aliceMembersCount = try await aliceGroup.members.count - bobMembersCount = try await bobGroup.members.count - XCTAssertEqual(aliceMembersCount, 2) - XCTAssertEqual(bobMembersCount, 2) - - try await bobGroup.addMembers(addresses: [fixtures.fred.address]) - try await aliceGroup.sync() - - try await bobGroup.removeAdmin(inboxId: fixtures.aliceClient.inboxID) - try await aliceGroup.sync() - - aliceMembersCount = try await aliceGroup.members.count - bobMembersCount = try await bobGroup.members.count - XCTAssertEqual(aliceMembersCount, 3) - XCTAssertEqual(bobMembersCount, 3) - - XCTAssertEqual(try bobGroup.permissionPolicySet().addMemberPolicy, .allow) - XCTAssertEqual(try aliceGroup.permissionPolicySet().addMemberPolicy, .allow) - - XCTAssert(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssert(try !bobGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) - XCTAssert(try aliceGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssert(try !aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) - + func testCanListGroups() async throws { + let fixtures = try await fixtures() + _ = try await fixtures.alixClient.conversations.newGroup(with: [ + fixtures.bo.address + ]) + _ = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.bo.address) + _ = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.alix.address) + + try await fixtures.alixClient.conversations.sync() + let alixGroupCount = try await fixtures.alixClient.conversations + .listGroups().count + + try await fixtures.boClient.conversations.sync() + let boGroupCount = try await fixtures.boClient.conversations + .listGroups().count + + XCTAssertEqual(1, alixGroupCount) + XCTAssertEqual(1, boGroupCount) + } + + func testCanFindConversationByTopic() async throws { + let fixtures = try await fixtures() + + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.caro.walletAddress + ]) + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.caro.walletAddress) + + let sameDm = try fixtures.boClient.findConversationByTopic( + topic: dm.topic) + let sameGroup = try fixtures.boClient.findConversationByTopic( + topic: group.topic) + + XCTAssertEqual(group.id, try sameGroup?.id) + XCTAssertEqual(dm.id, try sameDm?.id) } - func testCanCreateAGroupWithAdminPermissions() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address], permissions: GroupPermissionPreconfiguration.adminOnly) - try await fixtures.aliceClient.conversations.sync() - let aliceGroup = try await fixtures.aliceClient.conversations.listGroups().first! - XCTAssert(!bobGroup.id.isEmpty) - XCTAssert(!aliceGroup.id.isEmpty) - - let bobConsentResult = try await fixtures.bobClient.contacts.consentList.conversationState(groupId: bobGroup.id) - XCTAssertEqual(bobConsentResult, ConsentState.allowed) - - let aliceConsentResult = try await fixtures.aliceClient.contacts.consentList.conversationState(groupId: aliceGroup.id) - XCTAssertEqual(aliceConsentResult, ConsentState.unknown) + func testCanListConversations() async throws { + let fixtures = try await fixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.caro.walletAddress) + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.caro.walletAddress + ]) + + let convoCount = try await fixtures.boClient.conversations + .list().count + let dmCount = try await fixtures.boClient.conversations.listDms().count + let groupCount = try await fixtures.boClient.conversations.listGroups() + .count + XCTAssertEqual(convoCount, 2) + XCTAssertEqual(dmCount, 1) + XCTAssertEqual(groupCount, 1) + + try await fixtures.caroClient.conversations.sync() + let convoCount2 = try await fixtures.caroClient.conversations.list() + .count + let groupCount2 = try await fixtures.caroClient.conversations + .listGroups().count + XCTAssertEqual(convoCount2, 2) + XCTAssertEqual(groupCount2, 1) + } - try await bobGroup.addMembers(addresses: [fixtures.fred.address]) - try await aliceGroup.sync() + func testCanListConversationsFiltered() async throws { + let fixtures = try await fixtures() - var aliceMembersCount = try await aliceGroup.members.count - var bobMembersCount = try await bobGroup.members.count - XCTAssertEqual(aliceMembersCount, 3) - XCTAssertEqual(bobMembersCount, 3) + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.caro.walletAddress) + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.caro.walletAddress + ]) - await assertThrowsAsyncError( - try await aliceGroup.removeMembers(addresses: [fixtures.fred.address]) - ) - try await bobGroup.sync() + let convoCount = try await fixtures.boClient.conversations + .list().count + let convoCountConsent = try await fixtures.boClient.conversations + .list(consentState: .allowed).count - aliceMembersCount = try await aliceGroup.members.count - bobMembersCount = try await bobGroup.members.count - XCTAssertEqual(aliceMembersCount, 3) - XCTAssertEqual(bobMembersCount, 3) - - try await bobGroup.removeMembers(addresses: [fixtures.fred.address]) - try await aliceGroup.sync() + XCTAssertEqual(convoCount, 2) + XCTAssertEqual(convoCountConsent, 2) - aliceMembersCount = try await aliceGroup.members.count - bobMembersCount = try await bobGroup.members.count - XCTAssertEqual(aliceMembersCount, 2) - XCTAssertEqual(bobMembersCount, 2) + try await group.updateConsentState(state: .denied) - await assertThrowsAsyncError( - try await aliceGroup.addMembers(addresses: [fixtures.fred.address]) - ) - try await bobGroup.sync() + let convoCountAllowed = try await fixtures.boClient.conversations + .list(consentState: .allowed).count + let convoCountDenied = try await fixtures.boClient.conversations + .list(consentState: .denied).count - aliceMembersCount = try await aliceGroup.members.count - bobMembersCount = try await bobGroup.members.count - XCTAssertEqual(aliceMembersCount, 2) - XCTAssertEqual(bobMembersCount, 2) - - XCTAssertEqual(try bobGroup.permissionPolicySet().addMemberPolicy, .admin) - XCTAssertEqual(try aliceGroup.permissionPolicySet().addMemberPolicy, .admin) - XCTAssert(try bobGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssert(try !bobGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) - XCTAssert(try aliceGroup.isSuperAdmin(inboxId: fixtures.bobClient.inboxID)) - XCTAssert(try !aliceGroup.isSuperAdmin(inboxId: fixtures.aliceClient.inboxID)) + XCTAssertEqual(convoCountAllowed, 1) + XCTAssertEqual(convoCountDenied, 1) } - func testCanListGroups() async throws { - let fixtures = try await localFixtures() - _ = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.bob.address) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) - - try await fixtures.aliceClient.conversations.sync() - let aliceGroupCount = try await fixtures.aliceClient.conversations.listGroups().count - - try await fixtures.bobClient.conversations.sync() - let bobGroupCount = try await fixtures.bobClient.conversations.listGroups().count - - XCTAssertEqual(1, aliceGroupCount) - XCTAssertEqual(1, bobGroupCount) + func testCanListConversationsOrder() async throws { + let fixtures = try await fixtures() + + let dm = try await fixtures.boClient.conversations.findOrCreateDm( + with: fixtures.caro.walletAddress) + let group1 = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.caro.walletAddress]) + let group2 = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.caro.walletAddress]) + + _ = try await dm.send(content: "Howdy") + _ = try await group2.send(content: "Howdy") + _ = try await fixtures.boClient.conversations.syncAllConversations() + + let conversations = try await fixtures.boClient.conversations + .list() + let conversationsOrdered = try await fixtures.boClient.conversations + .list(order: .lastMessage) + + XCTAssertEqual(conversations.count, 3) + XCTAssertEqual(conversationsOrdered.count, 3) + + XCTAssertEqual( + try conversations.map { try $0.id }, [dm.id, group1.id, group2.id]) + XCTAssertEqual( + try conversationsOrdered.map { try $0.id }, + [group2.id, dm.id, group1.id]) } - - func testCanListGroupsAndConversations() async throws { - let fixtures = try await localFixtures() - _ = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) - _ = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bob.address) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.bob.walletAddress) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.walletAddress) - - let aliceGroupCount = try await fixtures.aliceClient.conversations.list().count - try await fixtures.bobClient.conversations.sync() - let bobGroupCount = try await fixtures.bobClient.conversations.list().count - - XCTAssertEqual(2, aliceGroupCount) - XCTAssertEqual(2, bobGroupCount) + func testCanListGroupsAndConversations() async throws { + let fixtures = try await fixtures() + _ = try await fixtures.alixClient.conversations.newGroup(with: [ + fixtures.bo.address + ]) + _ = try await fixtures.alixClient.conversations.newConversation( + with: fixtures.bo.address) + _ = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.bo.walletAddress) + _ = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.alix.walletAddress) + + let alixGroupCount = try await fixtures.alixClient.conversations + .list().count + + try await fixtures.boClient.conversations.sync() + let boGroupCount = try await fixtures.boClient.conversations.list() + .count + + XCTAssertEqual(2, alixGroupCount) + XCTAssertEqual(3, boGroupCount) } func testCanListGroupMembers() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) + let fixtures = try await fixtures() + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) try await group.sync() let members = try await group.members.map(\.inboxId).sorted() let peerMembers = try await group.peerInboxIds.sorted() - XCTAssertEqual([fixtures.bobClient.inboxID, fixtures.aliceClient.inboxID].sorted(), members) - XCTAssertEqual([fixtures.bobClient.inboxID].sorted(), peerMembers) + XCTAssertEqual( + [fixtures.boClient.inboxID, fixtures.alixClient.inboxID].sorted(), + members) + XCTAssertEqual([fixtures.boClient.inboxID].sorted(), peerMembers) } func testCanAddGroupMembers() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) + let fixtures = try await fixtures() + fixtures.alixClient.register(codec: GroupUpdatedCodec()) + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) - try await group.addMembers(addresses: [fixtures.fred.address]) + try await group.addMembers(addresses: [fixtures.caro.address]) try await group.sync() let members = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - fixtures.fredClient.inboxID - ].sorted(), members) - - let groupChangedMessage: GroupUpdated = try await group.messages().first!.content() - XCTAssertEqual(groupChangedMessage.addedInboxes.map(\.inboxID), [fixtures.fredClient.inboxID]) + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + fixtures.caroClient.inboxID, + ].sorted(), members) + + let groupChangedMessage: GroupUpdated = try await group.messages() + .first!.content() + XCTAssertEqual( + groupChangedMessage.addedInboxes.map(\.inboxID), + [fixtures.caroClient.inboxID]) } - + func testCanAddGroupMembersByInboxId() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) + let fixtures = try await fixtures() + fixtures.alixClient.register(codec: GroupUpdatedCodec()) + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) - try await group.addMembersByInboxId(inboxIds: [fixtures.fredClient.inboxID]) + try await group.addMembersByInboxId(inboxIds: [ + fixtures.caroClient.inboxID + ]) try await group.sync() let members = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - fixtures.fredClient.inboxID - ].sorted(), members) - - let groupChangedMessage: GroupUpdated = try await group.messages().first!.content() - XCTAssertEqual(groupChangedMessage.addedInboxes.map(\.inboxID), [fixtures.fredClient.inboxID]) + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + fixtures.caroClient.inboxID, + ].sorted(), members) + + let groupChangedMessage: GroupUpdated = try await group.messages() + .first!.content() + XCTAssertEqual( + groupChangedMessage.addedInboxes.map(\.inboxID), + [fixtures.caroClient.inboxID]) } func testCanRemoveMembers() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) + let fixtures = try await fixtures() + fixtures.alixClient.register(codec: GroupUpdatedCodec()) + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address, fixtures.caro.address]) try await group.sync() let members = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - fixtures.fredClient.inboxID - ].sorted(), members) + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + fixtures.caroClient.inboxID, + ].sorted(), members) - try await group.removeMembers(addresses: [fixtures.fred.address]) + try await group.removeMembers(addresses: [fixtures.caro.address]) try await group.sync() let newMembers = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - ].sorted(), newMembers) - - let groupChangedMessage: GroupUpdated = try await group.messages().first!.content() - XCTAssertEqual(groupChangedMessage.removedInboxes.map(\.inboxID), [fixtures.fredClient.inboxID]) + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + ].sorted(), newMembers) + + let groupChangedMessage: GroupUpdated = try await group.messages() + .first!.content() + XCTAssertEqual( + groupChangedMessage.removedInboxes.map(\.inboxID), + [fixtures.caroClient.inboxID]) } - + func testCanRemoveMembersByInboxId() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) + let fixtures = try await fixtures() + fixtures.alixClient.register(codec: GroupUpdatedCodec()) + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address, fixtures.caro.address]) try await group.sync() let members = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - fixtures.fredClient.inboxID - ].sorted(), members) + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + fixtures.caroClient.inboxID, + ].sorted(), members) - try await group.removeMembersByInboxId(inboxIds: [fixtures.fredClient.inboxID]) + try await group.removeMembersByInboxId(inboxIds: [ + fixtures.caroClient.inboxID + ]) try await group.sync() let newMembers = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - ].sorted(), newMembers) - - let groupChangedMessage: GroupUpdated = try await group.messages().first!.content() - XCTAssertEqual(groupChangedMessage.removedInboxes.map(\.inboxID), [fixtures.fredClient.inboxID]) + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + ].sorted(), newMembers) + + let groupChangedMessage: GroupUpdated = try await group.messages() + .first!.content() + XCTAssertEqual( + groupChangedMessage.removedInboxes.map(\.inboxID), + [fixtures.caroClient.inboxID]) } - + func testCanMessage() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() let notOnNetwork = try PrivateKey.generate() - let canMessage = try await fixtures.aliceClient.canMessageV3(address: fixtures.bobClient.address) - let cannotMessage = try await fixtures.aliceClient.canMessageV3(addresses: [notOnNetwork.address, fixtures.bobClient.address]) + let canMessage = try await fixtures.alixClient.canMessage( + address: fixtures.boClient.address) + let cannotMessage = try await fixtures.alixClient.canMessage( + addresses: [notOnNetwork.address, fixtures.boClient.address]) XCTAssert(canMessage) XCTAssert(!(cannotMessage[notOnNetwork.address.lowercased()] ?? true)) } - + func testIsActive() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address, fixtures.fred.address]) + let fixtures = try await fixtures() + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address, fixtures.caro.address]) try await group.sync() let members = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - fixtures.fredClient.inboxID - ].sorted(), members) - - try await fixtures.fredClient.conversations.sync() - let fredGroup = try await fixtures.fredClient.conversations.listGroups().first - try await fredGroup?.sync() + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + fixtures.caroClient.inboxID, + ].sorted(), members) + + try await fixtures.caroClient.conversations.sync() + let caroGroup = try await fixtures.caroClient.conversations.listGroups() + .first + try await caroGroup?.sync() + + var isalixActive = try group.isActive() + var iscaroActive = try caroGroup!.isActive() - var isAliceActive = try group.isActive() - var isFredActive = try fredGroup!.isActive() - - XCTAssert(isAliceActive) - XCTAssert(isFredActive) + XCTAssert(isalixActive) + XCTAssert(iscaroActive) - try await group.removeMembers(addresses: [fixtures.fred.address]) + try await group.removeMembers(addresses: [fixtures.caro.address]) try await group.sync() let newMembers = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual([ - fixtures.bobClient.inboxID, - fixtures.aliceClient.inboxID, - ].sorted(), newMembers) - - try await fredGroup?.sync() - - isAliceActive = try group.isActive() - isFredActive = try fredGroup!.isActive() - - XCTAssert(isAliceActive) - XCTAssert(!isFredActive) + XCTAssertEqual( + [ + fixtures.boClient.inboxID, + fixtures.alixClient.inboxID, + ].sorted(), newMembers) + + try await caroGroup?.sync() + + isalixActive = try group.isActive() + iscaroActive = try caroGroup!.isActive() + + XCTAssert(isalixActive) + XCTAssert(!iscaroActive) } func testAddedByAddress() async throws { // Create clients - let fixtures = try await localFixtures() - - // Alice creates a group and adds Bob to the group - _ = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) - - // Bob syncs groups - this will decrypt the Welcome and then - // identify who added Bob to the group - try await fixtures.bobClient.conversations.sync() - - // Check Bob's group for the added_by_address of the inviter - let bobGroup = try await fixtures.bobClient.conversations.listGroups().first - let aliceAddress = fixtures.aliceClient.inboxID - let whoAddedBob = try bobGroup?.addedByInboxId() - + let fixtures = try await fixtures() + + // alix creates a group and adds bo to the group + _ = try await fixtures.alixClient.conversations.newGroup(with: [ + fixtures.bo.address + ]) + + // bo syncs groups - this will decrypt the Welcome and then + // identify who added bo to the group + try await fixtures.boClient.conversations.sync() + + // Check bo's group for the added_by_address of the inviter + let boGroup = try await fixtures.boClient.conversations.listGroups() + .first + let alixAddress = fixtures.alixClient.inboxID + let whoAddedbo = try boGroup?.addedByInboxId() + // Verify the welcome host_credential is equal to Amal's - XCTAssertEqual(aliceAddress, whoAddedBob) + XCTAssertEqual(alixAddress, whoAddedbo) } func testCannotStartGroupWithSelf() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() await assertThrowsAsyncError( - try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.alice.address]) + try await fixtures.alixClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) ) } func testCanStartEmptyGroup() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: []) + let fixtures = try await fixtures() + let group = try await fixtures.alixClient.conversations.newGroup( + with: []) XCTAssert(!group.id.isEmpty) } func testCannotStartGroupWithNonRegisteredIdentity() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() let nonRegistered = try PrivateKey.generate() do { - _ = try await fixtures.aliceClient.conversations.newGroup(with: [nonRegistered.address]) + _ = try await fixtures.alixClient.conversations.newGroup(with: [ + nonRegistered.address + ]) XCTFail("did not throw error") } catch { - if case let ConversationError.memberNotRegistered(addresses) = error { - XCTAssertEqual([nonRegistered.address.lowercased()], addresses.map { $0.lowercased() }) + if case let ConversationError.memberNotRegistered(addresses) = error + { + XCTAssertEqual( + [nonRegistered.address.lowercased()], + addresses.map { $0.lowercased() }) } else { XCTFail("did not throw correct error") } @@ -490,84 +560,103 @@ class GroupTests: XCTestCase { } func testGroupStartsWithAllowedState() async throws { - let fixtures = try await localFixtures() - let bobGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.walletAddress]) + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.walletAddress]) - _ = try await bobGroup.send(content: "howdy") - _ = try await bobGroup.send(content: "gm") - try await bobGroup.sync() - - let isGroupAllowedResult = try await fixtures.bobClient.contacts.isGroupAllowed(groupId: bobGroup.id) - XCTAssertTrue(isGroupAllowedResult) + _ = try await boGroup.send(content: "howdy") + _ = try await boGroup.send(content: "gm") + try await boGroup.sync() - let groupStateResult = try await fixtures.bobClient.contacts.consentList.conversationState(groupId: bobGroup.id) + let groupStateResult = try boGroup.consentState() XCTAssertEqual(groupStateResult, ConsentState.allowed) } - + func testCanSendMessagesToGroup() async throws { - let fixtures = try await localFixtures() - let aliceGroup = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) + let fixtures = try await fixtures() + fixtures.boClient.register(codec: GroupUpdatedCodec()) + fixtures.alixClient.register(codec: GroupUpdatedCodec()) + let alixGroup = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) let membershipChange = GroupUpdated() - try await fixtures.bobClient.conversations.sync() - let bobGroup = try await fixtures.bobClient.conversations.listGroups()[0] - - _ = try await aliceGroup.send(content: "sup gang original") - let messageId = try await aliceGroup.send(content: "sup gang") - _ = try await aliceGroup.send(content: membershipChange, options: SendOptions(contentType: ContentTypeGroupUpdated)) + try await fixtures.boClient.conversations.sync() + let boGroup = try await fixtures.boClient.conversations.listGroups()[ + 0] - try await aliceGroup.sync() - let aliceGroupsCount = try await aliceGroup.messages().count - XCTAssertEqual(3, aliceGroupsCount) - let aliceMessage = try await aliceGroup.messages().first! + _ = try await alixGroup.send(content: "sup gang original") + let messageId = try await alixGroup.send(content: "sup gang") + _ = try await alixGroup.send( + content: membershipChange, + options: SendOptions(contentType: ContentTypeGroupUpdated)) - try await bobGroup.sync() - let bobGroupsCount = try await bobGroup.messages().count - XCTAssertEqual(2, bobGroupsCount) - let bobMessage = try await bobGroup.messages().first! + try await alixGroup.sync() + let alixGroupsCount = try await alixGroup.messages().count + XCTAssertEqual(3, alixGroupsCount) + let alixMessage = try await alixGroup.messages().first! - XCTAssertEqual("sup gang", try aliceMessage.content()) - XCTAssertEqual(messageId, aliceMessage.id) - XCTAssertEqual(.published, aliceMessage.deliveryStatus) - XCTAssertEqual("sup gang", try bobMessage.content()) + try await boGroup.sync() + let boGroupsCount = try await boGroup.messages().count + XCTAssertEqual(2, boGroupsCount) + let boMessage = try await boGroup.messages().first! + + XCTAssertEqual("sup gang", try alixMessage.content()) + XCTAssertEqual(messageId, alixMessage.id) + XCTAssertEqual(.published, alixMessage.deliveryStatus) + XCTAssertEqual("sup gang", try boMessage.content()) } - + func testCanListGroupMessages() async throws { - let fixtures = try await localFixtures() - let aliceGroup = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) - _ = try await aliceGroup.send(content: "howdy") - _ = try await aliceGroup.send(content: "gm") - - var aliceMessagesCount = try await aliceGroup.messages().count - var aliceMessagesPublishedCount = try await aliceGroup.messages(deliveryStatus: .published).count - XCTAssertEqual(3, aliceMessagesCount) - XCTAssertEqual(3, aliceMessagesPublishedCount) - - try await aliceGroup.sync() - - aliceMessagesCount = try await aliceGroup.messages().count - let aliceMessagesUnpublishedCount = try await aliceGroup.messages(deliveryStatus: .unpublished).count - aliceMessagesPublishedCount = try await aliceGroup.messages(deliveryStatus: .published).count - XCTAssertEqual(3, aliceMessagesCount) - XCTAssertEqual(0, aliceMessagesUnpublishedCount) - XCTAssertEqual(3, aliceMessagesPublishedCount) - - try await fixtures.bobClient.conversations.sync() - let bobGroup = try await fixtures.bobClient.conversations.listGroups()[0] - try await bobGroup.sync() - - let bobMessagesCount = try await bobGroup.messages().count - let bobMessagesUnpublishedCount = try await bobGroup.messages(deliveryStatus: .unpublished).count - let bobMessagesPublishedCount = try await bobGroup.messages(deliveryStatus: .published).count - XCTAssertEqual(2, bobMessagesCount) - XCTAssertEqual(0, bobMessagesUnpublishedCount) - XCTAssertEqual(2, bobMessagesPublishedCount) + let fixtures = try await fixtures() + let alixGroup = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) + _ = try await alixGroup.send(content: "howdy") + _ = try await alixGroup.send(content: "gm") + + var alixMessagesCount = try await alixGroup.messages().count + var alixMessagesPublishedCount = try await alixGroup.messages( + deliveryStatus: .published + ).count + XCTAssertEqual(3, alixMessagesCount) + XCTAssertEqual(3, alixMessagesPublishedCount) + + try await alixGroup.sync() + + alixMessagesCount = try await alixGroup.messages().count + let alixMessagesUnpublishedCount = try await alixGroup.messages( + deliveryStatus: .unpublished + ).count + alixMessagesPublishedCount = try await alixGroup.messages( + deliveryStatus: .published + ).count + XCTAssertEqual(3, alixMessagesCount) + XCTAssertEqual(0, alixMessagesUnpublishedCount) + XCTAssertEqual(3, alixMessagesPublishedCount) + + try await fixtures.boClient.conversations.sync() + let boGroup = try await fixtures.boClient.conversations.listGroups()[ + 0] + try await boGroup.sync() + + let boMessagesCount = try await boGroup.messages().count + let boMessagesUnpublishedCount = try await boGroup.messages( + deliveryStatus: .unpublished + ).count + let boMessagesPublishedCount = try await boGroup.messages( + deliveryStatus: .published + ).count + XCTAssertEqual(2, boMessagesCount) + XCTAssertEqual(0, boMessagesUnpublishedCount) + XCTAssertEqual(2, boMessagesPublishedCount) } - + func testCanStreamGroupMessages() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + let fixtures = try await fixtures() + fixtures.boClient.register(codec: GroupUpdatedCodec()) + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) let membershipChange = GroupUpdated() let expectation1 = XCTestExpectation(description: "got a message") expectation1.expectedFulfillmentCount = 1 @@ -579,132 +668,171 @@ class GroupTests: XCTestCase { } _ = try await group.send(content: "hi") - _ = try await group.send(content: membershipChange, options: SendOptions(contentType: ContentTypeGroupUpdated)) + _ = try await group.send( + content: membershipChange, + options: SendOptions(contentType: ContentTypeGroupUpdated)) await fulfillment(of: [expectation1], timeout: 3) } - + func testCanStreamGroups() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() let expectation1 = XCTestExpectation(description: "got a group") Task(priority: .userInitiated) { - for try await _ in try await fixtures.aliceClient.conversations.stream() { + for try await _ in await fixtures.alixClient.conversations + .stream() + { expectation1.fulfill() } } - _ = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) + _ = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) + _ = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.alix.address) await fulfillment(of: [expectation1], timeout: 3) } - + func testCanStreamGroupsAndConversationsWorksGroups() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() let expectation1 = XCTestExpectation(description: "got a conversation") expectation1.expectedFulfillmentCount = 2 Task(priority: .userInitiated) { - for try await _ in await fixtures.aliceClient.conversations.stream() { + for try await _ in await fixtures.alixClient.conversations.stream() + { expectation1.fulfill() } } - _ = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - _ = try await fixtures.bobClient.conversations.newConversation(with: fixtures.alice.address) - _ = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) + _ = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) + _ = try await fixtures.boClient.conversations.newConversation( + with: fixtures.alix.address) + _ = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.alix.address) await fulfillment(of: [expectation1], timeout: 3) } - + func testStreamGroupsAndAllMessages() async throws { - let fixtures = try await localFixtures() - + let fixtures = try await fixtures() + let expectation1 = XCTestExpectation(description: "got a group") let expectation2 = XCTestExpectation(description: "got a message") - Task(priority: .userInitiated) { - for try await _ in try await fixtures.aliceClient.conversations.stream() { + for try await _ in await fixtures.alixClient.conversations + .stream() + { expectation1.fulfill() } } - + Task(priority: .userInitiated) { - for try await _ in await fixtures.aliceClient.conversations.streamAllMessages() { + for try await _ in await fixtures.alixClient.conversations + .streamAllMessages() + { expectation2.fulfill() } } - let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) _ = try await group.send(content: "hello") await fulfillment(of: [expectation1, expectation2], timeout: 3) } - + func testCanStreamAndUpdateNameWithoutForkingGroup() async throws { - let fixtures = try await localFixtures() - + let fixtures = try await fixtures() + let expectation = XCTestExpectation(description: "got a message") expectation.expectedFulfillmentCount = 5 Task(priority: .userInitiated) { - for try await _ in await fixtures.bobClient.conversations.streamAllMessages(){ + for try await _ in await fixtures.boClient.conversations + .streamAllMessages() + { expectation.fulfill() } } - let alixGroup = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) + let alixGroup = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) try await alixGroup.updateGroupName(groupName: "hello") _ = try await alixGroup.send(content: "hello1") - - try await fixtures.bobClient.conversations.sync() - let boGroups = try await fixtures.bobClient.conversations.listGroups() + try await fixtures.boClient.conversations.sync() + + let boGroups = try await fixtures.boClient.conversations.listGroups() XCTAssertEqual(boGroups.count, 1, "bo should have 1 group") let boGroup = boGroups[0] try await boGroup.sync() - + let boMessages1 = try await boGroup.messages() - XCTAssertEqual(boMessages1.count, 2, "should have 2 messages on first load received \(boMessages1.count)") - + XCTAssertEqual( + boMessages1.count, 2, + "should have 2 messages on first load received \(boMessages1.count)" + ) + _ = try await boGroup.send(content: "hello2") _ = try await boGroup.send(content: "hello3") try await alixGroup.sync() let alixMessages = try await alixGroup.messages() for message in alixMessages { - print("message", message.encodedContent.type, message.encodedContent.type.typeID) + print( + "message", message.encodedContent.type, + message.encodedContent.type.typeID) } - XCTAssertEqual(alixMessages.count, 5, "should have 5 messages on first load received \(alixMessages.count)") + XCTAssertEqual( + alixMessages.count, 5, + "should have 5 messages on first load received \(alixMessages.count)" + ) _ = try await alixGroup.send(content: "hello4") try await boGroup.sync() let boMessages2 = try await boGroup.messages() for message in boMessages2 { - print("message", message.encodedContent.type, message.encodedContent.type.typeID) + print( + "message", message.encodedContent.type, + message.encodedContent.type.typeID) } - XCTAssertEqual(boMessages2.count, 5, "should have 5 messages on second load received \(boMessages2.count)") + XCTAssertEqual( + boMessages2.count, 5, + "should have 5 messages on second load received \(boMessages2.count)" + ) await fulfillment(of: [expectation], timeout: 3) } - + func testCanStreamAllMessages() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() let expectation1 = XCTestExpectation(description: "got a conversation") expectation1.expectedFulfillmentCount = 2 - let convo = try await fixtures.bobClient.conversations.newConversation(with: fixtures.alice.address) - let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - let dm = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) - - try await fixtures.aliceClient.conversations.sync() + let convo = try await fixtures.boClient.conversations.newConversation( + with: fixtures.alix.address) + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) + let dm = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.alix.address) + + try await fixtures.alixClient.conversations.sync() Task(priority: .userInitiated) { - for try await _ in try await fixtures.aliceClient.conversations.streamAllMessages() { + for try await _ in await fixtures.alixClient.conversations + .streamAllMessages() + { expectation1.fulfill() } } @@ -715,17 +843,22 @@ class GroupTests: XCTestCase { await fulfillment(of: [expectation1], timeout: 3) } - + func testCanStreamAllGroupMessages() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() let expectation1 = XCTestExpectation(description: "got a conversation") - let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - let dm = try await fixtures.davonV3Client.conversations.findOrCreateDm(with: fixtures.alice.address) - try await fixtures.aliceClient.conversations.sync() + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) + let dm = try await fixtures.caroClient.conversations.findOrCreateDm( + with: fixtures.alix.address) + try await fixtures.alixClient.conversations.sync() Task(priority: .userInitiated) { - for try await _ in await fixtures.aliceClient.conversations.streamAllMessages() { + for try await _ in await fixtures.alixClient.conversations + .streamAllMessages() + { expectation1.fulfill() } } @@ -735,151 +868,175 @@ class GroupTests: XCTestCase { await fulfillment(of: [expectation1], timeout: 3) } - - func testCanUpdateGroupMetadata() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address], name: "Start Name", imageUrlSquare: "starturl.com") - - var groupName = try group.groupName() + + func testCanUpdateGroupMetadata() async throws { + let fixtures = try await fixtures() + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address], name: "Start Name", + imageUrlSquare: "starturl.com") + + var groupName = try group.groupName() var groupImageUrlSquare = try group.groupImageUrlSquare() - - XCTAssertEqual(groupName, "Start Name") - XCTAssertEqual(groupImageUrlSquare, "starturl.com") + XCTAssertEqual(groupName, "Start Name") + XCTAssertEqual(groupImageUrlSquare, "starturl.com") - try await group.updateGroupName(groupName: "Test Group Name 1") + try await group.updateGroupName(groupName: "Test Group Name 1") try await group.updateGroupImageUrlSquare(imageUrlSquare: "newurl.com") - - groupName = try group.groupName() + + groupName = try group.groupName() groupImageUrlSquare = try group.groupImageUrlSquare() - XCTAssertEqual(groupName, "Test Group Name 1") + XCTAssertEqual(groupName, "Test Group Name 1") XCTAssertEqual(groupImageUrlSquare, "newurl.com") - - let bobConv = try await fixtures.bobClient.conversations.list()[0] - let bobGroup: Group; - switch bobConv { - case .group(let group): - bobGroup = group - case .dm(_): - XCTFail("failed converting conversation to group") - return - } - groupName = try bobGroup.groupName() - XCTAssertEqual(groupName, "Start Name") - - try await bobGroup.sync() - groupName = try bobGroup.groupName() - groupImageUrlSquare = try bobGroup.groupImageUrlSquare() - + + try await fixtures.boClient.conversations.sync() + let boGroup = try fixtures.boClient.findGroup(groupId: group.id)! + groupName = try boGroup.groupName() + XCTAssertEqual(groupName, "Start Name") + + try await boGroup.sync() + groupName = try boGroup.groupName() + groupImageUrlSquare = try boGroup.groupImageUrlSquare() + XCTAssertEqual(groupImageUrlSquare, "newurl.com") - XCTAssertEqual(groupName, "Test Group Name 1") - } - + XCTAssertEqual(groupName, "Test Group Name 1") + } + func testGroupConsent() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - let isAllowed = try await fixtures.bobClient.contacts.isGroupAllowed(groupId: group.id) - XCTAssert(isAllowed) + let fixtures = try await fixtures() + let group = try await fixtures.boClient.conversations.newGroup(with: [ + fixtures.alix.address + ]) XCTAssertEqual(try group.consentState(), .allowed) - - try await fixtures.bobClient.contacts.denyGroups(groupIds: [group.id]) - let isDenied = try await fixtures.bobClient.contacts.isGroupDenied(groupId: group.id) - XCTAssert(isDenied) + + try await group.updateConsentState(state: .denied) + let isDenied = try await fixtures.boClient.preferences.consentList + .conversationState(conversationId: group.id) + XCTAssertEqual(isDenied, .denied) XCTAssertEqual(try group.consentState(), .denied) - + try await group.updateConsentState(state: .allowed) - let isAllowed2 = try await fixtures.bobClient.contacts.isGroupAllowed(groupId: group.id) - XCTAssert(isAllowed2) XCTAssertEqual(try group.consentState(), .allowed) } - + func testCanAllowAndDenyInboxId() async throws { - let fixtures = try await localFixtures() - let boGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - var isInboxAllowed = try await fixtures.bobClient.contacts.isInboxAllowed(inboxId: fixtures.aliceClient.address) - var isInboxDenied = try await fixtures.bobClient.contacts.isInboxDenied(inboxId: fixtures.aliceClient.address) - XCTAssert(!isInboxAllowed) - XCTAssert(!isInboxDenied) - - - try await fixtures.bobClient.contacts.allowInboxes(inboxIds: [fixtures.aliceClient.inboxID]) - var alixMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.aliceClient.inboxID }) + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address]) + let inboxState = try await fixtures.boClient.preferences.consentList + .inboxIdState( + inboxId: fixtures.alixClient.inboxID) + XCTAssertEqual(inboxState, .unknown) + + try await fixtures.boClient.preferences.consentList.setConsentState( + entries: [ + ConsentListEntry( + value: fixtures.alixClient.inboxID, entryType: .inbox_id, + consentType: .allowed) + ]) + var alixMember = try await boGroup.members.first(where: { member in + member.inboxId == fixtures.alixClient.inboxID + }) XCTAssertEqual(alixMember?.consentState, .allowed) - isInboxAllowed = try await fixtures.bobClient.contacts.isInboxAllowed(inboxId: fixtures.aliceClient.inboxID) - XCTAssert(isInboxAllowed) - isInboxDenied = try await fixtures.bobClient.contacts.isInboxDenied(inboxId: fixtures.aliceClient.inboxID) - XCTAssert(!isInboxDenied) - - - try await fixtures.bobClient.contacts.denyInboxes(inboxIds: [fixtures.aliceClient.inboxID]) - alixMember = try await boGroup.members.first(where: { member in member.inboxId == fixtures.aliceClient.inboxID }) + let inboxState2 = try await fixtures.boClient.preferences.consentList + .inboxIdState( + inboxId: fixtures.alixClient.inboxID) + XCTAssertEqual(inboxState2, .allowed) + + try await fixtures.boClient.preferences.consentList.setConsentState( + entries: [ + ConsentListEntry( + value: fixtures.alixClient.inboxID, entryType: .inbox_id, + consentType: .denied) + ]) + alixMember = try await boGroup.members.first(where: { member in + member.inboxId == fixtures.alixClient.inboxID + }) XCTAssertEqual(alixMember?.consentState, .denied) - - isInboxAllowed = try await fixtures.bobClient.contacts.isInboxAllowed(inboxId: fixtures.aliceClient.inboxID) - isInboxDenied = try await fixtures.bobClient.contacts.isInboxDenied(inboxId: fixtures.aliceClient.inboxID) - XCTAssert(!isInboxAllowed) - XCTAssert(isInboxDenied) - - try await fixtures.bobClient.contacts.allow(addresses: [fixtures.aliceClient.address]) - let isAddressAllowed = try await fixtures.bobClient.contacts.isAllowed(fixtures.aliceClient.address) - let isAddressDenied = try await fixtures.bobClient.contacts.isDenied(fixtures.aliceClient.address) - XCTAssert(isAddressAllowed) - XCTAssert(!isAddressDenied) - isInboxAllowed = try await fixtures.bobClient.contacts.isInboxAllowed(inboxId: fixtures.aliceClient.inboxID) - isInboxDenied = try await fixtures.bobClient.contacts.isInboxDenied(inboxId: fixtures.aliceClient.inboxID) - XCTAssert(isInboxAllowed) - XCTAssert(!isInboxDenied) + + let inboxState3 = try await fixtures.boClient.preferences.consentList + .inboxIdState( + inboxId: fixtures.alixClient.inboxID) + XCTAssertEqual(inboxState3, .denied) + + try await fixtures.boClient.preferences.consentList.setConsentState( + entries: [ + ConsentListEntry( + value: fixtures.alixClient.address, entryType: .address, + consentType: .allowed) + ]) + let inboxState4 = try await fixtures.boClient.preferences.consentList + .inboxIdState( + inboxId: fixtures.alixClient.inboxID) + XCTAssertEqual(inboxState4, .allowed) + let addressState = try await fixtures.boClient.preferences.consentList + .addressState(address: fixtures.alixClient.address) + XCTAssertEqual(addressState, .allowed) } - + func testCanFetchGroupById() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let boGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - try await fixtures.aliceClient.conversations.sync() - let alixGroup = try fixtures.aliceClient.findGroup(groupId: boGroup.id) + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address]) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try fixtures.alixClient.findGroup(groupId: boGroup.id) XCTAssertEqual(alixGroup?.id, boGroup.id) } func testCanFetchMessageById() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() - let boGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address]) let boMessageId = try await boGroup.send(content: "Hello") - try await fixtures.aliceClient.conversations.sync() - let alixGroup = try fixtures.aliceClient.findGroup(groupId: boGroup.id) + try await fixtures.alixClient.conversations.sync() + let alixGroup = try fixtures.alixClient.findGroup(groupId: boGroup.id) try await alixGroup?.sync() - _ = try fixtures.aliceClient.findMessage(messageId: boMessageId) + _ = try fixtures.alixClient.findMessage(messageId: boMessageId) XCTAssertEqual(alixGroup?.id, boGroup.id) } - + func testUnpublishedMessages() async throws { - let fixtures = try await localFixtures() - let boGroup = try await fixtures.bobClient.conversations.newGroup(with: [fixtures.alice.address]) - - try await fixtures.aliceClient.conversations.sync() - let alixGroup = try fixtures.aliceClient.findGroup(groupId: boGroup.id)! - let isGroupAllowed = try await fixtures.aliceClient.contacts.isGroupAllowed(groupId: boGroup.id) - XCTAssert(!isGroupAllowed) - let preparedMessageId = try await alixGroup.prepareMessage(content: "Test text") - let isGroupAllowed2 = try await fixtures.aliceClient.contacts.isGroupAllowed(groupId: boGroup.id) - XCTAssert(isGroupAllowed2) + let fixtures = try await fixtures() + let boGroup = try await fixtures.boClient.conversations.newGroup( + with: [fixtures.alix.address]) + + try await fixtures.alixClient.conversations.sync() + let alixGroup = try fixtures.alixClient.findGroup(groupId: boGroup.id)! + let isGroupAllowed = try await fixtures.alixClient.preferences + .consentList.conversationState(conversationId: boGroup.id) + XCTAssertEqual(isGroupAllowed, .unknown) + let preparedMessageId = try await alixGroup.prepareMessage( + content: "Test text") + let isGroupAllowed2 = try await fixtures.alixClient.preferences + .consentList.conversationState(conversationId: boGroup.id) + XCTAssertEqual(isGroupAllowed2, .allowed) let messageCount = try await alixGroup.messages().count XCTAssertEqual(messageCount, 1) - let messageCountPublished = try await alixGroup.messages(deliveryStatus: .published).count - let messageCountUnpublished = try await alixGroup.messages(deliveryStatus: .unpublished).count + let messageCountPublished = try await alixGroup.messages( + deliveryStatus: .published + ).count + let messageCountUnpublished = try await alixGroup.messages( + deliveryStatus: .unpublished + ).count XCTAssertEqual(messageCountPublished, 0) XCTAssertEqual(messageCountUnpublished, 1) _ = try await alixGroup.publishMessages() try await alixGroup.sync() - let messageCountPublished2 = try await alixGroup.messages(deliveryStatus: .published).count - let messageCountUnpublished2 = try await alixGroup.messages(deliveryStatus: .unpublished).count + let messageCountPublished2 = try await alixGroup.messages( + deliveryStatus: .published + ).count + let messageCountUnpublished2 = try await alixGroup.messages( + deliveryStatus: .unpublished + ).count let messageCount2 = try await alixGroup.messages().count XCTAssertEqual(messageCountPublished2, 1) XCTAssertEqual(messageCountUnpublished2, 0) @@ -889,58 +1046,65 @@ class GroupTests: XCTestCase { XCTAssertEqual(preparedMessageId, messages.first!.id) } - + func testCanSyncManyGroupsInUnderASecond() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() var groups: [Group] = [] for _ in 0..<100 { - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) groups.append(group) } - try await fixtures.bobClient.conversations.sync() - let bobGroup = try fixtures.bobClient.findGroup(groupId: groups[0].id) + try await fixtures.boClient.conversations.sync() + let boGroup = try fixtures.boClient.findGroup(groupId: groups[0].id) _ = try await groups[0].send(content: "hi") - let messageCount = try await bobGroup!.messages().count + let messageCount = try await boGroup!.messages().count XCTAssertEqual(messageCount, 0) do { let start = Date() - let numGroupsSynced = try await fixtures.bobClient.conversations.syncAllConversations() + let numGroupsSynced = try await fixtures.boClient.conversations + .syncAllConversations() let end = Date() print(end.timeIntervalSince(start)) XCTAssert(end.timeIntervalSince(start) < 1) - XCTAssert(numGroupsSynced == 100) + XCTAssert(numGroupsSynced == 100) } catch { print("Failed to list groups members: \(error)") - throw error // Rethrow the error to fail the test if group creation fails + throw error // Rethrow the error to fail the test if group creation fails } - - let messageCount2 = try await bobGroup!.messages().count + + let messageCount2 = try await boGroup!.messages().count XCTAssertEqual(messageCount2, 1) - - for aliceConv in try await fixtures.aliceClient.conversations.list() { - guard case let .group(aliceGroup) = aliceConv else { - XCTFail("failed converting conversation to group") - return - } - try await aliceGroup.removeMembers(addresses: [fixtures.bobClient.address]) - } - - // first syncAllGroups after removal still sync groups in order to process the removal - var numGroupsSynced = try await fixtures.bobClient.conversations.syncAllConversations() - XCTAssert(numGroupsSynced == 100) - - // next syncAllGroups only will sync active groups - numGroupsSynced = try await fixtures.bobClient.conversations.syncAllConversations() - XCTAssert(numGroupsSynced == 0) + + for alixConv in try await fixtures.alixClient.conversations.list() { + guard case let .group(alixGroup) = alixConv else { + XCTFail("failed converting conversation to group") + return + } + try await alixGroup.removeMembers(addresses: [ + fixtures.boClient.address + ]) + } + + // first syncAllGroups after removal still sync groups in order to process the removal + var numGroupsSynced = try await fixtures.boClient.conversations + .syncAllConversations() + XCTAssert(numGroupsSynced == 100) + + // next syncAllGroups only will sync active groups + numGroupsSynced = try await fixtures.boClient.conversations + .syncAllConversations() + XCTAssert(numGroupsSynced == 0) } - + func testCanListManyMembersInParallelInUnderASecond() async throws { - let fixtures = try await localFixtures() + let fixtures = try await fixtures() var groups: [Group] = [] for _ in 0..<100 { - let group = try await fixtures.aliceClient.conversations.newGroup(with: [fixtures.bob.address]) + let group = try await fixtures.alixClient.conversations.newGroup( + with: [fixtures.bo.address]) groups.append(group) } do { @@ -951,10 +1115,10 @@ class GroupTests: XCTestCase { XCTAssert(end.timeIntervalSince(start) < 1) } catch { print("Failed to list groups members: \(error)") - throw error // Rethrow the error to fail the test if group creation fails + throw error // Rethrow the error to fail the test if group creation fails } } - + func listMembersInParallel(groups: [Group]) async throws { await withThrowingTaskGroup(of: [Member].self) { taskGroup in for group in groups { diff --git a/Tests/XMTPTests/InvitationTests.swift b/Tests/XMTPTests/InvitationTests.swift deleted file mode 100644 index aa025d76..00000000 --- a/Tests/XMTPTests/InvitationTests.swift +++ /dev/null @@ -1,147 +0,0 @@ -// -// InvitationTests.swift -// -// -// Created by Pat Nakajima on 11/27/22. -// - -import Foundation -import XCTest -@testable import XMTPiOS -import XMTPTestHelpers - -@available(iOS 16.0, *) -class InvitationTests: XCTestCase { - - func testDeterministicInvite() async throws { - let aliceWallet = try FakeWallet.generate() - let bobWallet = try FakeWallet.generate() - - let alice = try await PrivateKeyBundleV1.generate(wallet: aliceWallet) - let bob = try await PrivateKeyBundleV1.generate(wallet: bobWallet) - - let makeInvite = { (conversationID: String) in - try InvitationV1.createDeterministic( - sender: alice.toV2(), - recipient: bob.toV2().getPublicKeyBundle(), - context: InvitationV1.Context.with { - $0.conversationID = conversationID - }) - } - - // Repeatedly making the same invite should use the same topic/keys - let original = try makeInvite("example.com/conversation-foo"); - for i in 1...10 { - let invite = try makeInvite("example.com/conversation-foo"); - XCTAssertEqual(original.topic, invite.topic); - } - - // But when the conversationId changes then it use a new topic/keys - let invite = try makeInvite("example.com/conversation-bar"); - XCTAssertNotEqual(original.topic, invite.topic); - } - - func testGenerateSealedInvitation() async throws { - let aliceWallet = try FakeWallet.generate() - let bobWallet = try FakeWallet.generate() - - let alice = try await PrivateKeyBundleV1.generate(wallet: aliceWallet) - let bob = try await PrivateKeyBundleV1.generate(wallet: bobWallet) - - let invitation = try InvitationV1.createDeterministic( - sender: alice.toV2(), - recipient: bob.toV2().getPublicKeyBundle() - ) - - let newInvitation = try SealedInvitation.createV1( - sender: try alice.toV2(), - recipient: try bob.toV2().getPublicKeyBundle(), - created: Date(), - invitation: invitation - ) - - let deserialized = try SealedInvitation(serializedData: try newInvitation.serializedData()) - - XCTAssert(!deserialized.v1.headerBytes.isEmpty, "header bytes empty") - XCTAssertEqual(newInvitation, deserialized) - - let header = newInvitation.v1.header - - // Ensure the headers haven't been mangled - XCTAssertEqual(header.sender, try alice.toV2().getPublicKeyBundle()) - XCTAssertEqual(header.recipient, try bob.toV2().getPublicKeyBundle()) - - // Ensure alice can decrypt the invitation - let aliceInvite = try newInvitation.v1.getInvitation(viewer: try alice.toV2()) - XCTAssertEqual(aliceInvite.topic, invitation.topic) - XCTAssertEqual(aliceInvite.aes256GcmHkdfSha256.keyMaterial, invitation.aes256GcmHkdfSha256.keyMaterial) - - // Ensure bob can decrypt the invitation - let bobInvite = try newInvitation.v1.getInvitation(viewer: try bob.toV2()) - XCTAssertEqual(bobInvite.topic, invitation.topic) - XCTAssertEqual(bobInvite.aes256GcmHkdfSha256.keyMaterial, invitation.aes256GcmHkdfSha256.keyMaterial) - } - - func testGeneratesKnownDeterministicTopic() async throws { - // address = 0xF56d1F3b1290204441Cb3843C2Cac1C2f5AEd690 - let aliceKeyData = Data(("0x0a8a030ac20108c192a3f7923112220a2068d2eb2ef8c50c4916b42ce638c5610e44ff4eb3ecb098" + - "c9dacf032625c72f101a940108c192a3f7923112460a440a40fc9822283078c323c9319c45e60ab4" + - "2c65f6e1744ed8c23c52728d456d33422824c98d307e8b1c86a26826578523ba15fe6f04a17fca17" + - "6664ee8017ec8ba59310011a430a410498dc2315dd45d99f5e900a071e7b56142de344540f07fbc7" + - "3a0f9a5d5df6b52eb85db06a3825988ab5e04746bc221fcdf5310a44d9523009546d4bfbfbb89cfb" + - "12c20108eb92a3f7923112220a20788be9da8e1a1a08b05f7cbf22d86980bc056b130c482fa5bd26" + - "ccb8d29b30451a940108eb92a3f7923112460a440a40a7afa25cb6f3fbb98f9e5cd92a1df1898452" + - "e0dfa1d7e5affe9eaf9b72dd14bc546d86c399768badf983f07fa7dd16eee8d793357ce6fccd6768" + - "07d87bcc595510011a430a410422931e6295c3c93a5f6f5e729dc02e1754e916cb9be16d36dc163a" + - "300931f42a0cd5fde957d75c2068e1980c5f86843daf16aba8ae57e8160b8b9f0191def09e").web3.bytesFromHex!) - let aliceKeys = try PrivateKeyBundle(serializedData: aliceKeyData).v1.toV2() - - // address = 0x3De402A325323Bb97f00cE3ad5bFAc96A11F9A34 - let bobKeyData = Data(("0x0a88030ac001088cd68df7923112220a209057f8d813314a2aae74e6c4c30f909c1c496b6037ce32" + - "a12c613558a8e961681a9201088cd68df7923112440a420a40501ae9b4f75d5bb5bae3ca4ecfda4e" + - "de9edc5a9b7fc2d56dc7325b837957c23235cc3005b46bb9ef485f106404dcf71247097ed5096355" + - "90f4b7987b833d03661a430a4104e61a7ae511567f4a2b5551221024b6932d6cdb8ecf3876ec64cf" + - "29be4291dd5428fc0301963cdf6939978846e2c35fd38fcb70c64296a929f166ef6e4e91045712c2" + - "0108b8d68df7923112220a2027707399474d417bf6aae4baa3d73b285bf728353bc3e156b0e32461" + - "ebb48f8c1a940108b8d68df7923112460a440a40fb96fa38c3f013830abb61cf6b39776e0475eb13" + - "79c66013569c3d2daecdd48c7fbee945dcdbdc5717d1f4ffd342c4d3f1b7215912829751a94e3ae1" + - "1007e0a110011a430a4104952b7158cfe819d92743a4132e2e3ae867d72f6a08292aebf471d0a7a2" + - "907f3e9947719033e20edc9ca9665874bd88c64c6b62c01928065f6069c5c80c699924").web3.bytesFromHex!) - let bobKeys = try PrivateKeyBundle(serializedData: bobKeyData) - - let aliceInvite = try InvitationV1.createDeterministic(sender: aliceKeys, recipient: bobKeys.v1.toV2().getPublicKeyBundle(), context: InvitationV1.Context.with { $0.conversationID = "test" }) - - XCTAssertEqual(aliceInvite.topic, "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto") - - let bobInvite = try InvitationV1.createDeterministic(sender: bobKeys.v1.toV2(), recipient: aliceKeys.getPublicKeyBundle(), context: InvitationV1.Context.with { $0.conversationID = "test" }) - - XCTAssertEqual(bobInvite.topic, "/xmtp/0/m-4b52be1e8567d72d0bc407debe2d3c7fca2ae93a47e58c3f9b5c5068aff80ec5/proto") - } - - func testCreatesDeterministicTopicsBidirectionally() async throws { - let aliceWallet = try FakeWallet.generate() - let bobWallet = try FakeWallet.generate() - - let alice = try await PrivateKeyBundleV1.generate(wallet: aliceWallet) - let bob = try await PrivateKeyBundleV1.generate(wallet: bobWallet) - - let aliceInvite = try InvitationV1.createDeterministic( - sender: alice.toV2(), - recipient: bob.toV2().getPublicKeyBundle() - ) - - let bobInvite = try InvitationV1.createDeterministic( - sender: bob.toV2(), - recipient: alice.toV2().getPublicKeyBundle() - ) - - let aliceSharedSecret = try alice.sharedSecret(peer: bob.toPublicKeyBundle(), myPreKey: alice.preKeys[0].publicKey, isRecipient: false) - - let bobSharedSecret = try bob.sharedSecret(peer: alice.toPublicKeyBundle(), myPreKey: bob.preKeys[0].publicKey, isRecipient: true) - - XCTAssertEqual(aliceSharedSecret.bytes, bobSharedSecret.bytes) - - XCTAssertEqual(aliceInvite.topic, bobInvite.topic) - - } -} diff --git a/Tests/XMTPTests/PrivateKeyBundleTests.swift b/Tests/XMTPTests/PrivateKeyBundleTests.swift deleted file mode 100644 index 08108469..00000000 --- a/Tests/XMTPTests/PrivateKeyBundleTests.swift +++ /dev/null @@ -1,69 +0,0 @@ -// -// PrivateKeyBundleTests.swift -// -// -// Created by Pat Nakajima on 11/29/22. -// - -import secp256k1 -import XCTest -@testable import XMTPiOS - -class PrivateKeyBundleTests: XCTestCase { - func testConversion() async throws { - let wallet = try PrivateKey.generate() - let v1 = try await PrivateKeyBundleV1.generate(wallet: wallet) - - let v2 = try v1.toV2() - - let v2PreKeyPublic = try UnsignedPublicKey(serializedData: v2.preKeys[0].publicKey.keyBytes) - XCTAssertEqual(v1.preKeys[0].publicKey.secp256K1Uncompressed.bytes, v2PreKeyPublic.secp256K1Uncompressed.bytes) - } - - func testKeyBundlesAreSigned() async throws { - let wallet = try PrivateKey.generate() - let v1 = try await PrivateKeyBundleV1.generate(wallet: wallet) - - XCTAssert(v1.identityKey.publicKey.hasSignature, "no private v1 identity key signature") - XCTAssert(v1.preKeys[0].publicKey.hasSignature, "no private v1 pre key signature") - XCTAssert(v1.toPublicKeyBundle().identityKey.hasSignature, "no public v1 identity key signature") - XCTAssert(v1.toPublicKeyBundle().preKey.hasSignature, "no public v1 pre key signature") - - let v2 = try v1.toV2() - XCTAssert(v2.identityKey.publicKey.hasSignature, "no private v2 identity key signature") - XCTAssert(v2.preKeys[0].publicKey.hasSignature, "no private v2 pre key signature") - XCTAssert(v2.getPublicKeyBundle().identityKey.hasSignature, "no public v2 identity key signature") - XCTAssert(v2.getPublicKeyBundle().preKey.hasSignature, "no public v2 pre key signature") - } - - func testSharedSecret() async throws { - let alice = try PrivateKey.generate() - let alicePrivateBundle = try await PrivateKeyBundleV1.generate(wallet: alice).toV2() - let alicePublicBundle = alicePrivateBundle.getPublicKeyBundle() - - let bob = try PrivateKey.generate() - let bobPrivateBundle = try await PrivateKeyBundleV1.generate(wallet: bob).toV2() - let bobPublicBundle = bobPrivateBundle.getPublicKeyBundle() - - let aliceSharedSecret = try alicePrivateBundle.sharedSecret(peer: bobPublicBundle, myPreKey: alicePublicBundle.preKey, isRecipient: true) - - let bobSharedSecret = try bobPrivateBundle.sharedSecret(peer: alicePublicBundle, myPreKey: bobPublicBundle.preKey, isRecipient: false) - - XCTAssertEqual(aliceSharedSecret, bobSharedSecret) - } - - func testSharedSecretMatchesWhatJSGenerates() throws { - let meBundleData = Data("0a86030ac00108a687b5d8cc3012220a20db73e1b4b5aeffb6cecd37526d842327730433e1751bceb5824d937f779797541a920108a687b5d8cc3012440a420a40d35c081d9ab59b3fb13e27cb03a225c7134bc4ce4ce51f80273481c31d803e1e4fa8ae43e7ec20b06a81b694ad28470f85fc971b8050867f5a4821c03a67f0e81a430a410443631548a55a60f06989ce1bc3fa43fdbe463ea4748dcb509e09fc58514c6e56edfac83e1fff5f382bc110fa066762f4b862db8df53be7d48268b3fdf649adc812c00108b787b5d8cc3012220a209e2631f34af8fc1ec0f75bd15ee4e110ac424300f39bff26c7a990a75a49ac641a920108b787b5d8cc3012440a420a40202a68a2e95d446511ecf22f5487b998989989adfc0a60e1ce201e0bab64d836066ccda987cda99c0e588babb8c334a820d6a6e360100ba7ba08e0e339a303681a430a4104c9733798111d89446264db365bc0dde54b5f9202eeb309eec2f18c572ce11e267fe91e184207676d7af5eaf2ad65de0881093623030f6096ea5bf3ecd252c482".web3.bytesFromHex!) - - let youBundleData = Data("0a940108c487b5d8cc3012460a440a40c51e611e662117991b19f60b6a7f6d9f08671c3d55241e959954c2e0f2ec47d15b872986d2a279ffe55df01709b000fbdcc9e85c1946876e187f90a0fd32222c10011a430a41049cccf02f766f7d4c322eeb498f2ac0283a011992fc77f9e0d5687b826aafd48d8319f48f773ec959221bf7bf7d3da4b09e59af540a633c588df2f1b6f465d6a712940108cb87b5d8cc3012460a440a40b7b0e89ce4789f6e78502357864979abe9e26cd44a36ed75578368a02cdc3bda7d56721660cb2066b76a4a6dd5a78d99df4b096cc4622a2065cf05b2f32b94be10011a430a410438f2b23a4e0f9c61e716b8cf4b23f2709d92b4feb71429a385b6878c31085384701bc787def9396b441bfb8751c042432785c352f8ee9bfb9c6cd5d6871b2d1a".web3.bytesFromHex!) - - let secretData = Data("049f4cd17426f9dfac528f400db858a9cbc87488879d6df5bea3595beaeb37415f1b24227e571dd4969406f366841e682795f284b54952a22b2dcff87971580fa604c0a97d550ce3ce5dac2e5469a2e3ece7232d80247a789044ebef0478c6911d63400a13090de6e8aeb4a1bcb878ca73b1d7eb13ab3012e564cfef74a8182467cc047d999bb077e5b223509fab7a08642c29359b8c3144ffa30002e45f09e4a515927f682eb71b68bd52f498d5d464c6bb14d3c07aefc86a1ab8e2528a21ffd41912".web3.bytesFromHex!) - - let meBundle = try PrivateKeyBundle(serializedData: meBundleData).v1.toV2() - let youBundlePublic = try SignedPublicKeyBundle(try PublicKeyBundle(serializedData: youBundleData)) - - let secret = try meBundle.sharedSecret(peer: youBundlePublic, myPreKey: meBundle.preKeys[0].publicKey, isRecipient: true) - - XCTAssertEqual(secretData, secret) - } -} diff --git a/Tests/XMTPTests/ReactionTests.swift b/Tests/XMTPTests/ReactionTests.swift index f1d6e26d..d867701c 100644 --- a/Tests/XMTPTests/ReactionTests.swift +++ b/Tests/XMTPTests/ReactionTests.swift @@ -1,132 +1,132 @@ -// -// ReactionTests.swift -// -// -// Created by Naomi Plasterer on 7/26/23. -// - import Foundation - import XCTest + @testable import XMTPiOS @available(iOS 15, *) class ReactionTests: XCTestCase { - func testCanDecodeLegacyForm() async throws { - let codec = ReactionCodec() - - // This is how clients send reactions now. - let canonicalEncoded = EncodedContent.with { - $0.type = ContentTypeReaction - $0.content = Data(""" - { - "action": "added", - "content": "smile", - "reference": "abc123", - "schema": "shortcode" - } - """.utf8) - } - - // Previously, some clients sent reactions like this. - // So we test here to make sure we can still decode them. - let legacyEncoded = EncodedContent.with { - $0.type = ContentTypeReaction - $0.parameters = [ - "action": "added", - "reference": "abc123", - "schema": "shortcode", - ] - $0.content = Data("smile".utf8) - } - - let fixtures = await fixtures() - let canonical = try codec.decode(content: canonicalEncoded, client: fixtures.aliceClient) - let legacy = try codec.decode(content: legacyEncoded, client: fixtures.aliceClient) - - XCTAssertEqual(ReactionAction.added, canonical.action) - XCTAssertEqual(ReactionAction.added, legacy.action) - XCTAssertEqual("smile", canonical.content) - XCTAssertEqual("smile", legacy.content) - XCTAssertEqual("abc123", canonical.reference) - XCTAssertEqual("abc123", legacy.reference) - XCTAssertEqual(ReactionSchema.shortcode, canonical.schema) - XCTAssertEqual(ReactionSchema.shortcode, legacy.schema) - } - - func testCanUseReactionCodec() async throws { - let fixtures = await fixtures() - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) - - fixtures.aliceClient.register(codec: ReactionCodec()) - - try await conversation.send(text: "hey alice 2 bob") - - let messageToReact = try await conversation.messages()[0] - - let reaction = Reaction( - reference: messageToReact.id, - action: .added, - content: "U+1F603", - schema: .unicode - ) - - try await conversation.send( - content: reaction, - options: .init(contentType: ContentTypeReaction) - ) - - let updatedMessages = try await conversation.messages() - - let message = try await conversation.messages()[0] - let content: Reaction = try message.content() - XCTAssertEqual("U+1F603", content.content) - XCTAssertEqual(messageToReact.id, content.reference) - XCTAssertEqual(ReactionAction.added, content.action) - XCTAssertEqual(ReactionSchema.unicode, content.schema) - } - - func testCanDecodeEmptyForm() async throws { - let codec = ReactionCodec() - - // This is how clients send reactions now. - let canonicalEncoded = EncodedContent.with { - $0.type = ContentTypeReaction - $0.content = Data(""" - { - "action": "", - "content": "smile", - "reference": "", - "schema": "" - } - """.utf8) - } - - // Previously, some clients sent reactions like this. - // So we test here to make sure we can still decode them. - let legacyEncoded = EncodedContent.with { - $0.type = ContentTypeReaction - $0.parameters = [ - "action": "", - "reference": "", - "schema": "", - ] - $0.content = Data("smile".utf8) - } - - let fixtures = await fixtures() - - let canonical = try codec.decode(content: canonicalEncoded, client: fixtures.aliceClient) - let legacy = try codec.decode(content: legacyEncoded, client: fixtures.aliceClient) - - XCTAssertEqual(ReactionAction.unknown, canonical.action) - XCTAssertEqual(ReactionAction.unknown, legacy.action) - XCTAssertEqual("smile", canonical.content) - XCTAssertEqual("smile", legacy.content) - XCTAssertEqual("", canonical.reference) - XCTAssertEqual("", legacy.reference) - XCTAssertEqual(ReactionSchema.unknown, canonical.schema) - XCTAssertEqual(ReactionSchema.unknown, legacy.schema) - } + func testCanDecodeLegacyForm() async throws { + let codec = ReactionCodec() + + // This is how clients send reactions now. + let canonicalEncoded = EncodedContent.with { + $0.type = ContentTypeReaction + $0.content = Data( + """ + { + "action": "added", + "content": "smile", + "reference": "abc123", + "schema": "shortcode" + } + """.utf8) + } + + // Previously, some clients sent reactions like this. + // So we test here to make sure we can still decode them. + let legacyEncoded = EncodedContent.with { + $0.type = ContentTypeReaction + $0.parameters = [ + "action": "added", + "reference": "abc123", + "schema": "shortcode", + ] + $0.content = Data("smile".utf8) + } + + let fixtures = try await fixtures() + let canonical = try codec.decode( + content: canonicalEncoded, client: fixtures.alixClient) + let legacy = try codec.decode( + content: legacyEncoded, client: fixtures.alixClient) + + XCTAssertEqual(ReactionAction.added, canonical.action) + XCTAssertEqual(ReactionAction.added, legacy.action) + XCTAssertEqual("smile", canonical.content) + XCTAssertEqual("smile", legacy.content) + XCTAssertEqual("abc123", canonical.reference) + XCTAssertEqual("abc123", legacy.reference) + XCTAssertEqual(ReactionSchema.shortcode, canonical.schema) + XCTAssertEqual(ReactionSchema.shortcode, legacy.schema) + } + + func testCanUseReactionCodec() async throws { + let fixtures = try await fixtures() + let conversation = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) + + fixtures.alixClient.register(codec: ReactionCodec()) + + _ = try await conversation.send(text: "hey alix 2 bo") + + let messageToReact = try await conversation.messages()[0] + + let reaction = Reaction( + reference: messageToReact.id, + action: .added, + content: "U+1F603", + schema: .unicode + ) + + try await conversation.send( + content: reaction, + options: .init(contentType: ContentTypeReaction) + ) + + _ = try await conversation.messages() + + let message = try await conversation.messages()[0] + let content: Reaction = try message.content() + XCTAssertEqual("U+1F603", content.content) + XCTAssertEqual(messageToReact.id, content.reference) + XCTAssertEqual(ReactionAction.added, content.action) + XCTAssertEqual(ReactionSchema.unicode, content.schema) + } + + func testCanDecodeEmptyForm() async throws { + let codec = ReactionCodec() + + // This is how clients send reactions now. + let canonicalEncoded = EncodedContent.with { + $0.type = ContentTypeReaction + $0.content = Data( + """ + { + "action": "", + "content": "smile", + "reference": "", + "schema": "" + } + """.utf8) + } + + // Previously, some clients sent reactions like this. + // So we test here to make sure we can still decode them. + let legacyEncoded = EncodedContent.with { + $0.type = ContentTypeReaction + $0.parameters = [ + "action": "", + "reference": "", + "schema": "", + ] + $0.content = Data("smile".utf8) + } + + let fixtures = try await fixtures() + + let canonical = try codec.decode( + content: canonicalEncoded, client: fixtures.alixClient) + let legacy = try codec.decode( + content: legacyEncoded, client: fixtures.alixClient) + + XCTAssertEqual(ReactionAction.unknown, canonical.action) + XCTAssertEqual(ReactionAction.unknown, legacy.action) + XCTAssertEqual("smile", canonical.content) + XCTAssertEqual("smile", legacy.content) + XCTAssertEqual("", canonical.reference) + XCTAssertEqual("", legacy.reference) + XCTAssertEqual(ReactionSchema.unknown, canonical.schema) + XCTAssertEqual(ReactionSchema.unknown, legacy.schema) + } } diff --git a/Tests/XMTPTests/ReadReceiptTests.swift b/Tests/XMTPTests/ReadReceiptTests.swift index 9675cc36..431a4a69 100644 --- a/Tests/XMTPTests/ReadReceiptTests.swift +++ b/Tests/XMTPTests/ReadReceiptTests.swift @@ -1,24 +1,18 @@ -// -// ReadReceiptTests.swift -// -// -// Created by Naomi Plasterer on 8/2/23. -// - import Foundation - import XCTest + @testable import XMTPiOS @available(iOS 15, *) class ReadReceiptTests: XCTestCase { func testCanUseReadReceiptCodec() async throws { - let fixtures = await fixtures() - fixtures.aliceClient.register(codec: ReadReceiptCodec()) + let fixtures = try await fixtures() + fixtures.alixClient.register(codec: ReadReceiptCodec()) - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + let conversation = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) - try await conversation.send(text: "hey alice 2 bob") + _ = try await conversation.send(text: "hey alix 2 bo") let read = ReadReceipt() @@ -27,7 +21,7 @@ class ReadReceiptTests: XCTestCase { options: .init(contentType: ContentTypeReadReceipt) ) - let updatedMessages = try await conversation.messages() + _ = try await conversation.messages() let message = try await conversation.messages()[0] let contentType: String = message.encodedContent.type.typeID diff --git a/Tests/XMTPTests/RemoteAttachmentTest.swift b/Tests/XMTPTests/RemoteAttachmentTest.swift index fb658593..b9063250 100644 --- a/Tests/XMTPTests/RemoteAttachmentTest.swift +++ b/Tests/XMTPTests/RemoteAttachmentTest.swift @@ -1,18 +1,16 @@ -// -// RemoteAttachmentTests.swift -// -// -// Created by Pat on 2/14/23. -// import Foundation - import XCTest + @testable import XMTPiOS // Fakes HTTPS urls struct TestFetcher: RemoteContentFetcher { func fetch(_ url: String) async throws -> Data { - guard let localURL = URL(string: url.replacingOccurrences(of: "https://", with: "file://")) else { + guard + let localURL = URL( + string: url.replacingOccurrences( + of: "https://", with: "file://")) + else { throw RemoteAttachmentError.invalidURL } @@ -27,52 +25,70 @@ class RemoteAttachmentTests: XCTestCase { override func setUp() async throws { // swiftlint:disable force_try - iconData = Data(base64Encoded: Data("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAACymlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj40NjA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjQ2MDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgr0TTmKAAAC5ElEQVQ4EW2TWWxMYRTHf3e2zjClVa0trVoqFRk1VKmIWhJ0JmkNETvvEtIHLwixxoM1xIOIiAjzxhBCQ9ESlRJNJEj7gJraJ63SdDrbvc53Z6xx7r253/lyzvnO/3/+n7a69KTBnyae1anJZ0nviq9pkIzppKLK+TMYbH+74Bhsobslzmv6yJQgJUHFuMiryCL+Tf8r5XcBqWxzWWhv+c6cDSPYsm4ehWPy5XSNd28j3Aw+49apMOO92aT6pRN5lf0qoJI7nvay4/JcFi+ZTiKepLPjC4ahM3VGCZVVk6iqaWWv/w5F3gEkFRyzgPxV221y8s5L6eSbocdUB25QhFUeBE6C0MWF1K6aReqqzs6aBkorBhHv0bEpwr4K5tlrhrM4MJ36K084HXhEfcjH/WvtJBM685dO5MymRyacmpWVNKx7Sdv5LrLL7FhU64ow//rJxGMJTix5QP4CF/P9Xjbv81F3wM8CWQ/1uDixqpn+aJzqtR5eSY6alMUQCIrXwuJ8PrzrokfaDTf0cnhbiPxhOQwbkcvBrZd5e/07SYl83xmhaGyBgm/az0ll3DQxulCc5fzFr7nuIs5Dotjtsm8emo61KZEobXS+iTCzaiJuGUxJTQ51u2t5H46QTKao21NL9+cgG6cNl04LCJ6+xxDsGCkDqyfPt2vgJyvdWg+LlgvWMhvNFzpwF2sEjzdzO/iCyurx+FaU45k2hicP2zgSaGLUFBlln4FNiSKnwkHT+Y/UL31sTkLXDdHCdSbIKVHp90PBWRbuH0dPJMrdo2EKSp3osQwE1b+SZ4nXzYFAI1pIw7esgv5+b0ZIBucONXJ2+3NG4mTk1AFyJ4QlxbzkWj1D/bsUg7oIfkihg0vH2nkVfoM7105untsk7UVrmL7WGLnlWSR6M3dBESem/XsbHYMsdLXERBtRU4UqaFz2QJyjbRgJaTuTqPaV/Z5V2jflObjMQbnLKW2mcSaErP8lq5QfTHkZ9teKBsUAAAAASUVORK5CYII=".utf8))! + iconData = Data( + base64Encoded: Data( + "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAhGVYSWZNTQAqAAAACAAFARIAAwAAAAEAAQAAARoABQAAAAEAAABKARsABQAAAAEAAABSASgAAwAAAAEAAgAAh2kABAAAAAEAAABaAAAAAAAAAEgAAAABAAAASAAAAAEAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAEKADAAQAAAABAAAAEAAAAADHbxzxAAAACXBIWXMAAAsTAAALEwEAmpwYAAACymlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNi4wLjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj40NjA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjQ2MDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgr0TTmKAAAC5ElEQVQ4EW2TWWxMYRTHf3e2zjClVa0trVoqFRk1VKmIWhJ0JmkNETvvEtIHLwixxoM1xIOIiAjzxhBCQ9ESlRJNJEj7gJraJ63SdDrbvc53Z6xx7r253/lyzvnO/3/+n7a69KTBnyae1anJZ0nviq9pkIzppKLK+TMYbH+74Bhsobslzmv6yJQgJUHFuMiryCL+Tf8r5XcBqWxzWWhv+c6cDSPYsm4ehWPy5XSNd28j3Aw+49apMOO92aT6pRN5lf0qoJI7nvay4/JcFi+ZTiKepLPjC4ahM3VGCZVVk6iqaWWv/w5F3gEkFRyzgPxV221y8s5L6eSbocdUB25QhFUeBE6C0MWF1K6aReqqzs6aBkorBhHv0bEpwr4K5tlrhrM4MJ36K084HXhEfcjH/WvtJBM685dO5MymRyacmpWVNKx7Sdv5LrLL7FhU64ow//rJxGMJTix5QP4CF/P9Xjbv81F3wM8CWQ/1uDixqpn+aJzqtR5eSY6alMUQCIrXwuJ8PrzrokfaDTf0cnhbiPxhOQwbkcvBrZd5e/07SYl83xmhaGyBgm/az0ll3DQxulCc5fzFr7nuIs5Dotjtsm8emo61KZEobXS+iTCzaiJuGUxJTQ51u2t5H46QTKao21NL9+cgG6cNl04LCJ6+xxDsGCkDqyfPt2vgJyvdWg+LlgvWMhvNFzpwF2sEjzdzO/iCyurx+FaU45k2hicP2zgSaGLUFBlln4FNiSKnwkHT+Y/UL31sTkLXDdHCdSbIKVHp90PBWRbuH0dPJMrdo2EKSp3osQwE1b+SZ4nXzYFAI1pIw7esgv5+b0ZIBucONXJ2+3NG4mTk1AFyJ4QlxbzkWj1D/bsUg7oIfkihg0vH2nkVfoM7105untsk7UVrmL7WGLnlWSR6M3dBESem/XsbHYMsdLXERBtRU4UqaFz2QJyjbRgJaTuTqPaV/Z5V2jflObjMQbnLKW2mcSaErP8lq5QfTHkZ9teKBsUAAAAASUVORK5CYII=" + .utf8))! } func testBasic() async throws { - let fixtures = await fixtures() - - fixtures.aliceClient.register(codec: AttachmentCodec()) - fixtures.aliceClient.register(codec: RemoteAttachmentCodec()) - - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) - let enecryptedEncodedContent = try RemoteAttachment.encodeEncrypted(content: "Hello", codec: TextCodec(), with: fixtures.aliceClient) - var remoteAttachmentContent = try RemoteAttachment(url: "https://example.com", encryptedEncodedContent: enecryptedEncodedContent) + let fixtures = try await fixtures() + + fixtures.alixClient.register(codec: AttachmentCodec()) + fixtures.alixClient.register(codec: RemoteAttachmentCodec()) + + let conversation = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) + let enecryptedEncodedContent = try RemoteAttachment.encodeEncrypted( + content: "Hello", codec: TextCodec(), with: fixtures.alixClient) + var remoteAttachmentContent = try RemoteAttachment( + url: "https://example.com", + encryptedEncodedContent: enecryptedEncodedContent) remoteAttachmentContent.filename = "hello.txt" remoteAttachmentContent.contentLength = 5 - _ = try await conversation.send(content: remoteAttachmentContent, options: .init(contentType: ContentTypeRemoteAttachment)) + _ = try await conversation.send( + content: remoteAttachmentContent, + options: .init(contentType: ContentTypeRemoteAttachment)) } func testCanUseAttachmentCodec() async throws { - let fixtures = await fixtures() - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + let fixtures = try await fixtures() + let conversation = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) - fixtures.aliceClient.register(codec: AttachmentCodec()) - fixtures.aliceClient.register(codec: RemoteAttachmentCodec()) + fixtures.alixClient.register(codec: AttachmentCodec()) + fixtures.alixClient.register(codec: RemoteAttachmentCodec()) let encryptedEncodedContent = try RemoteAttachment.encodeEncrypted( - content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), + content: Attachment( + filename: "icon.png", mimeType: "image/png", data: iconData), codec: AttachmentCodec(), - with: fixtures.aliceClient + with: fixtures.alixClient ) - let tempFileURL = URL.temporaryDirectory.appendingPathComponent(UUID().uuidString) + let tempFileURL = URL.temporaryDirectory.appendingPathComponent( + UUID().uuidString) try encryptedEncodedContent.payload.write(to: tempFileURL) // We only allow https:// urls for remote attachments, but it didn't seem worthwhile to spin up a local web server // for this, so we use the TestFetcher to swap the protocols - let fakeHTTPSFileURL = URL(string: tempFileURL.absoluteString.replacingOccurrences(of: "file://", with: "https://"))! - var content = try RemoteAttachment(url: fakeHTTPSFileURL.absoluteString, encryptedEncodedContent: encryptedEncodedContent) + let fakeHTTPSFileURL = URL( + string: tempFileURL.absoluteString.replacingOccurrences( + of: "file://", with: "https://"))! + var content = try RemoteAttachment( + url: fakeHTTPSFileURL.absoluteString, + encryptedEncodedContent: encryptedEncodedContent) content.filename = "icon.png" content.contentLength = 123 content.fetcher = TestFetcher() - try await conversation.send(content: content, options: .init(contentType: ContentTypeRemoteAttachment)) + try await conversation.send( + content: content, + options: .init(contentType: ContentTypeRemoteAttachment)) let messages = try await conversation.messages() - XCTAssertEqual(1, messages.count) + XCTAssertEqual(2, messages.count) let receivedMessage = messages[0] var remoteAttachment: RemoteAttachment = try receivedMessage.content() @@ -82,8 +98,10 @@ class RemoteAttachmentTests: XCTestCase { remoteAttachment.fetcher = TestFetcher() - let encodedContent: EncodedContent = try await remoteAttachment.content() - let attachment: Attachment = try encodedContent.decoded(with: fixtures.aliceClient) + let encodedContent: EncodedContent = + try await remoteAttachment.content() + let attachment: Attachment = try encodedContent.decoded( + with: fixtures.alixClient) XCTAssertEqual("icon.png", attachment.filename) XCTAssertEqual("image/png", attachment.mimeType) @@ -92,23 +110,29 @@ class RemoteAttachmentTests: XCTestCase { } func testCannotUseNonHTTPSUrl() async throws { - let fixtures = await fixtures() - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + let fixtures = try await fixtures() + _ = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) - - fixtures.aliceClient.register(codec: AttachmentCodec()) - fixtures.aliceClient.register(codec: RemoteAttachmentCodec()) + fixtures.alixClient.register(codec: AttachmentCodec()) + fixtures.alixClient.register(codec: RemoteAttachmentCodec()) let encryptedEncodedContent = try RemoteAttachment.encodeEncrypted( - content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), + content: Attachment( + filename: "icon.png", mimeType: "image/png", data: iconData), codec: AttachmentCodec(), - with: fixtures.aliceClient + with: fixtures.alixClient ) - let tempFileURL = URL.temporaryDirectory.appendingPathComponent(UUID().uuidString) + let tempFileURL = URL.temporaryDirectory.appendingPathComponent( + UUID().uuidString) try encryptedEncodedContent.payload.write(to: tempFileURL) - XCTAssertThrowsError(try RemoteAttachment(url: tempFileURL.absoluteString, encryptedEncodedContent: encryptedEncodedContent)) { error in + XCTAssertThrowsError( + try RemoteAttachment( + url: tempFileURL.absoluteString, + encryptedEncodedContent: encryptedEncodedContent) + ) { error in switch error as! RemoteAttachmentError { case let .invalidScheme(message): XCTAssertEqual(message, "scheme must be https") @@ -119,22 +143,28 @@ class RemoteAttachmentTests: XCTestCase { } func testVerifiesContentDigest() async throws { - let fixtures = await fixtures() - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) - + let fixtures = try await fixtures() + _ = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) let encryptedEncodedContent = try RemoteAttachment.encodeEncrypted( - content: Attachment(filename: "icon.png", mimeType: "image/png", data: iconData), + content: Attachment( + filename: "icon.png", mimeType: "image/png", data: iconData), codec: AttachmentCodec(), - with: fixtures.aliceClient + with: fixtures.alixClient ) - let tempFileURL = URL.temporaryDirectory.appendingPathComponent(UUID().uuidString) + let tempFileURL = URL.temporaryDirectory.appendingPathComponent( + UUID().uuidString) try encryptedEncodedContent.payload.write(to: tempFileURL) - let fakeHTTPSFileURL = URL(string: tempFileURL.absoluteString.replacingOccurrences(of: "file://", with: "https://"))! - var remoteAttachment = try RemoteAttachment(url: fakeHTTPSFileURL.absoluteString, encryptedEncodedContent: encryptedEncodedContent) + let fakeHTTPSFileURL = URL( + string: tempFileURL.absoluteString.replacingOccurrences( + of: "file://", with: "https://"))! + var remoteAttachment = try RemoteAttachment( + url: fakeHTTPSFileURL.absoluteString, + encryptedEncodedContent: encryptedEncodedContent) remoteAttachment.fetcher = TestFetcher() - let expect = expectation(description: "raised error") + let expect = XCTestExpectation(description: "raised error") // Tamper with content try Data([1, 2, 3, 4, 5]).write(to: tempFileURL) @@ -142,12 +172,16 @@ class RemoteAttachmentTests: XCTestCase { do { _ = try await remoteAttachment.content() } catch { - if let error = error as? RemoteAttachmentError, case let .invalidDigest(message) = error { + if let error = error as? RemoteAttachmentError, + case let .invalidDigest(message) = error + { XCTAssert(message.hasPrefix("content digest does not match")) expect.fulfill() } } + + - wait(for: [expect], timeout: 3) + await fulfillment(of: [expect], timeout: 3) } } diff --git a/Tests/XMTPTests/ReplyTests.swift b/Tests/XMTPTests/ReplyTests.swift index 3e4433c5..5fbb2920 100644 --- a/Tests/XMTPTests/ReplyTests.swift +++ b/Tests/XMTPTests/ReplyTests.swift @@ -1,23 +1,18 @@ -// -// ReplyTests.swift -// -// -// Created by Naomi Plasterer on 7/26/23. -// import Foundation - import XCTest + @testable import XMTPiOS @available(iOS 15, *) class ReplyTests: XCTestCase { func testCanUseReplyCodec() async throws { - let fixtures = await fixtures() - let conversation = try await fixtures.aliceClient.conversations.newConversation(with: fixtures.bobClient.address) + let fixtures = try await fixtures() + let conversation = try await fixtures.alixClient.conversations + .newConversation(with: fixtures.boClient.address) - fixtures.aliceClient.register(codec: ReplyCodec()) + fixtures.alixClient.register(codec: ReplyCodec()) - try await conversation.send(text: "hey alice 2 bob") + _ = try await conversation.send(text: "hey alix 2 bo") let messageToReply = try await conversation.messages()[0] @@ -32,7 +27,7 @@ class ReplyTests: XCTestCase { options: .init(contentType: ContentTypeReply) ) - let updatedMessages = try await conversation.messages() + _ = try await conversation.messages() let message = try await conversation.messages()[0] let content: Reply = try message.content() diff --git a/Tests/XMTPTests/SignatureTests.swift b/Tests/XMTPTests/SignatureTests.swift index b9cd7bde..9fa21975 100644 --- a/Tests/XMTPTests/SignatureTests.swift +++ b/Tests/XMTPTests/SignatureTests.swift @@ -1,12 +1,6 @@ -// -// SignatureTests.swift -// -// -// Created by Pat Nakajima on 11/27/22. -// - import CryptoKit import XCTest + @testable import XMTPiOS class SignatureTests: XCTestCase { @@ -14,15 +8,20 @@ class SignatureTests: XCTestCase { let digest = SHA256.hash(data: Data("Hello world".utf8)) let signingKey = try PrivateKey.generate() let signature = try await signingKey.sign(Data(digest)) - XCTAssert(try signature.verify(signedBy: signingKey.publicKey, digest: Data("Hello world".utf8))) + XCTAssert( + try signature.verify( + signedBy: signingKey.publicKey, digest: Data("Hello world".utf8) + )) } - - func testConsentProofText() { - let timestamp = UInt64(1581663600000) - let exampleAddress = "0x1234567890abcdef"; - let text = Signature.consentProofText(peerAddress: exampleAddress, timestamp: timestamp) - let expected = "XMTP : Grant inbox consent to sender\n\nCurrent Time: Fri, 14 Feb 2020 07:00:00 GMT\nFrom Address: 0x1234567890abcdef\n\nFor more info: https://xmtp.org/signatures/" - XCTAssertEqual(text, expected) - } + func testConsentProofText() { + let timestamp = UInt64(1_581_663_600_000) + let exampleAddress = "0x1234567890abcdef" + let text = Signature.consentProofText( + peerAddress: exampleAddress, timestamp: timestamp) + let expected = + "XMTP : Grant inbox consent to sender\n\nCurrent Time: Fri, 14 Feb 2020 07:00:00 GMT\nFrom Address: 0x1234567890abcdef\n\nFor more info: https://xmtp.org/signatures/" + + XCTAssertEqual(text, expected) + } } diff --git a/Tests/XMTPTests/V3ClientTests.swift b/Tests/XMTPTests/V3ClientTests.swift deleted file mode 100644 index 075fe5c4..00000000 --- a/Tests/XMTPTests/V3ClientTests.swift +++ /dev/null @@ -1,396 +0,0 @@ -// -// V3ClientTests.swift -// -// -// Created by Naomi Plasterer on 9/19/24. -// - -import LibXMTP -import XCTest -import XMTPTestHelpers - -@testable import XMTPiOS - -@available(iOS 16, *) -class V3ClientTests: XCTestCase { - // Use these fixtures to talk to the local node - struct LocalFixtures { - var alixV2: PrivateKey! - var boV3: PrivateKey! - var caroV2V3: PrivateKey! - var alixV2Client: Client! - var boV3Client: Client! - var caroV2V3Client: Client! - } - - func localFixtures() async throws -> LocalFixtures { - let key = try Crypto.secureRandomBytes(count: 32) - let alixV2 = try PrivateKey.generate() - let alixV2Client = try await Client.create( - account: alixV2, - options: .init( - api: .init(env: .local, isSecure: false) - ) - ) - let boV3 = try PrivateKey.generate() - let boV3Client = try await Client.createV3( - account: boV3, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - ) - let caroV2V3 = try PrivateKey.generate() - let caroV2V3Client = try await Client.create( - account: caroV2V3, - options: .init( - api: .init(env: .local, isSecure: false), - enableV3: true, - encryptionKey: key - ) - ) - - return .init( - alixV2: alixV2, - boV3: boV3, - caroV2V3: caroV2V3, - alixV2Client: alixV2Client, - boV3Client: boV3Client, - caroV2V3Client: caroV2V3Client - ) - } - - func testsCanCreateGroup() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.boV3Client.conversations.newGroup(with: [ - fixtures.caroV2V3.address - ]) - let members = try await group.members.map(\.inboxId).sorted() - XCTAssertEqual( - [fixtures.caroV2V3Client.inboxID, fixtures.boV3Client.inboxID] - .sorted(), members) - - await assertThrowsAsyncError( - try await fixtures.boV3Client.conversations.newGroup(with: [ - fixtures.alixV2.address - ]) - ) - } - - func testCanCreateDm() async throws { - let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.walletAddress) - let members = try await dm.members - XCTAssertEqual(members.count, 2) - - let sameDm = try await fixtures.boV3Client.findDm( - address: fixtures.caroV2V3.walletAddress) - XCTAssertEqual(sameDm?.id, dm.id) - - try await fixtures.caroV2V3Client.conversations.sync() - let caroDm = try await fixtures.caroV2V3Client.findDm( - address: fixtures.boV3Client.address) - XCTAssertEqual(caroDm?.id, dm.id) - - await assertThrowsAsyncError( - try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.alixV2.walletAddress) - ) - } - - func testCanFindConversationByTopic() async throws { - let fixtures = try await localFixtures() - - let group = try await fixtures.boV3Client.conversations.newGroup(with: [ - fixtures.caroV2V3.walletAddress - ]) - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.walletAddress) - - let sameDm = try fixtures.boV3Client.findConversationByTopic( - topic: dm.topic) - let sameGroup = try fixtures.boV3Client.findConversationByTopic( - topic: group.topic) - - XCTAssertEqual(group.id, try sameGroup?.id) - XCTAssertEqual(dm.id, try sameDm?.id) - } - - func testCanListConversations() async throws { - let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.walletAddress) - let group = try await fixtures.boV3Client.conversations.newGroup(with: [ - fixtures.caroV2V3.walletAddress - ]) - - let convoCount = try await fixtures.boV3Client.conversations - .list().count - let dmCount = try await fixtures.boV3Client.conversations.listDms().count - let groupCount = try await fixtures.boV3Client.conversations.listGroups() - .count - XCTAssertEqual(convoCount, 2) - XCTAssertEqual(dmCount, 1) - XCTAssertEqual(groupCount, 1) - - try await fixtures.caroV2V3Client.conversations.sync() - let convoCount2 = try await fixtures.caroV2V3Client.conversations.list().count - let groupCount2 = try await fixtures.caroV2V3Client.conversations - .listGroups().count - XCTAssertEqual(convoCount2, 1) - XCTAssertEqual(groupCount2, 1) - } - - func testCanListConversationsFiltered() async throws { - let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.walletAddress) - let group = try await fixtures.boV3Client.conversations.newGroup(with: [ - fixtures.caroV2V3.walletAddress - ]) - - let convoCount = try await fixtures.boV3Client.conversations - .list().count - let convoCountConsent = try await fixtures.boV3Client.conversations - .list(consentState: .allowed).count - - XCTAssertEqual(convoCount, 2) - XCTAssertEqual(convoCountConsent, 2) - - try await group.updateConsentState(state: .denied) - - let convoCountAllowed = try await fixtures.boV3Client.conversations - .list(consentState: .allowed).count - let convoCountDenied = try await fixtures.boV3Client.conversations - .list(consentState: .denied).count - - XCTAssertEqual(convoCountAllowed, 1) - XCTAssertEqual(convoCountDenied, 1) - } - - func testCanListConversationsOrder() async throws { - let fixtures = try await localFixtures() - - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.walletAddress) - let group1 = try await fixtures.boV3Client.conversations.newGroup( - with: [fixtures.caroV2V3.walletAddress]) - let group2 = try await fixtures.boV3Client.conversations.newGroup( - with: [fixtures.caroV2V3.walletAddress]) - - _ = try await dm.send(content: "Howdy") - _ = try await group2.send(content: "Howdy") - _ = try await fixtures.boV3Client.conversations.syncAllConversations() - - let conversations = try await fixtures.boV3Client.conversations - .list() - let conversationsOrdered = try await fixtures.boV3Client.conversations - .list(order: .lastMessage) - - XCTAssertEqual(conversations.count, 3) - XCTAssertEqual(conversationsOrdered.count, 3) - - XCTAssertEqual( - try conversations.map { try $0.id }, [dm.id, group1.id, group2.id]) - XCTAssertEqual( - try conversationsOrdered.map { try $0.id }, - [group2.id, dm.id, group1.id]) - } - - func testsCanSendMessages() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.boV3Client.conversations.newGroup(with: [ - fixtures.caroV2V3.address - ]) - try await group.send(content: "howdy") - let messageId = try await group.send(content: "gm") - try await group.sync() - - let groupMessages = try await group.messages() - XCTAssertEqual(groupMessages.first?.body, "gm") - XCTAssertEqual(groupMessages.first?.id, messageId) - XCTAssertEqual(groupMessages.first?.deliveryStatus, .published) - XCTAssertEqual(groupMessages.count, 3) - - try await fixtures.caroV2V3Client.conversations.sync() - let sameGroup = try await fixtures.caroV2V3Client.conversations.listGroups() - .last - try await sameGroup?.sync() - - let sameGroupMessages = try await sameGroup?.messages() - XCTAssertEqual(sameGroupMessages?.count, 2) - XCTAssertEqual(sameGroupMessages?.first?.body, "gm") - } - - func testsCanSendMessagesToDm() async throws { - let fixtures = try await localFixtures() - let dm = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.address) - try await dm.send(content: "howdy") - let messageId = try await dm.send(content: "gm") - try await dm.sync() - - let dmMessages = try await dm.messages() - XCTAssertEqual(dmMessages.first?.body, "gm") - XCTAssertEqual(dmMessages.first?.id, messageId) - XCTAssertEqual(dmMessages.first?.deliveryStatus, .published) - XCTAssertEqual(dmMessages.count, 3) - - try await fixtures.caroV2V3Client.conversations.sync() - let sameDm = try await fixtures.caroV2V3Client.findDm( - address: fixtures.boV3Client.address) - try await sameDm?.sync() - - let sameDmMessages = try await sameDm?.messages() - XCTAssertEqual(sameDmMessages?.count, 2) - XCTAssertEqual(sameDmMessages?.first?.body, "gm") - } - - func testGroupConsent() async throws { - let fixtures = try await localFixtures() - let group = try await fixtures.boV3Client.conversations.newGroup(with: [ - fixtures.caroV2V3.address - ]) - let isAllowed = try await fixtures.boV3Client.contacts.isGroupAllowed( - groupId: group.id) - XCTAssert(isAllowed) - XCTAssertEqual(try group.consentState(), .allowed) - - try await fixtures.boV3Client.contacts.denyGroups(groupIds: [group.id]) - let isDenied = try await fixtures.boV3Client.contacts.isGroupDenied( - groupId: group.id) - XCTAssert(isDenied) - XCTAssertEqual(try group.consentState(), .denied) - - try await group.updateConsentState(state: .allowed) - let isAllowed2 = try await fixtures.boV3Client.contacts.isGroupAllowed( - groupId: group.id) - XCTAssert(isAllowed2) - XCTAssertEqual(try group.consentState(), .allowed) - } - - func testCanAllowAndDenyInboxId() async throws { - let fixtures = try await localFixtures() - let boGroup = try await fixtures.boV3Client.conversations.newGroup( - with: [fixtures.caroV2V3.address]) - var isInboxAllowed = try await fixtures.boV3Client.contacts - .isInboxAllowed(inboxId: fixtures.caroV2V3.address) - var isInboxDenied = try await fixtures.boV3Client.contacts - .isInboxDenied(inboxId: fixtures.caroV2V3.address) - XCTAssert(!isInboxAllowed) - XCTAssert(!isInboxDenied) - - try await fixtures.boV3Client.contacts.allowInboxes(inboxIds: [ - fixtures.caroV2V3Client.inboxID - ]) - var caroMember = try await boGroup.members.first(where: { member in - member.inboxId == fixtures.caroV2V3Client.inboxID - }) - XCTAssertEqual(caroMember?.consentState, .allowed) - - isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed( - inboxId: fixtures.caroV2V3Client.inboxID) - XCTAssert(isInboxAllowed) - isInboxDenied = try await fixtures.boV3Client.contacts.isInboxDenied( - inboxId: fixtures.caroV2V3Client.inboxID) - XCTAssert(!isInboxDenied) - var isAddressAllowed = try await fixtures.boV3Client.contacts.isAllowed( - fixtures.caroV2V3Client.address) - XCTAssert(isAddressAllowed) - var isAddressDenied = try await fixtures.boV3Client.contacts.isDenied( - fixtures.caroV2V3Client.address) - XCTAssert(!isAddressDenied) - - try await fixtures.boV3Client.contacts.denyInboxes(inboxIds: [ - fixtures.caroV2V3Client.inboxID - ]) - caroMember = try await boGroup.members.first(where: { member in - member.inboxId == fixtures.caroV2V3Client.inboxID - }) - XCTAssertEqual(caroMember?.consentState, .denied) - - isInboxAllowed = try await fixtures.boV3Client.contacts.isInboxAllowed( - inboxId: fixtures.caroV2V3Client.inboxID) - isInboxDenied = try await fixtures.boV3Client.contacts.isInboxDenied( - inboxId: fixtures.caroV2V3Client.inboxID) - XCTAssert(!isInboxAllowed) - XCTAssert(isInboxDenied) - - try await fixtures.boV3Client.contacts.allow(addresses: [ - fixtures.alixV2.address - ]) - isAddressAllowed = try await fixtures.boV3Client.contacts.isAllowed( - fixtures.alixV2.address) - isAddressDenied = try await fixtures.boV3Client.contacts.isDenied( - fixtures.alixV2.address) - XCTAssert(isAddressAllowed) - XCTAssert(!isAddressDenied) - } - - func testCanStreamAllMessagesFromV3Users() async throws { - let fixtures = try await localFixtures() - - let expectation1 = XCTestExpectation(description: "got a conversation") - expectation1.expectedFulfillmentCount = 2 - let convo = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.address) - let group = try await fixtures.caroV2V3Client.conversations.newGroup( - with: [fixtures.boV3.address]) - try await fixtures.boV3Client.conversations.sync() - Task(priority: .userInitiated) { - for try await _ in await fixtures.boV3Client.conversations - .streamAllMessages() - { - expectation1.fulfill() - } - } - - _ = try await group.send(content: "hi") - _ = try await convo.send(content: "hi") - - await fulfillment(of: [expectation1], timeout: 3) - } - - func testCanStreamGroupsAndConversationsFromV3Users() async throws { - let fixtures = try await localFixtures() - - let expectation1 = XCTestExpectation(description: "got a conversation") - expectation1.expectedFulfillmentCount = 2 - - Task(priority: .userInitiated) { - for try await _ in await fixtures.boV3Client.conversations - .stream() - { - expectation1.fulfill() - } - } - - _ = try await fixtures.caroV2V3Client.conversations.newGroup(with: [ - fixtures.boV3.address - ]) - _ = try await fixtures.boV3Client.conversations.findOrCreateDm( - with: fixtures.caroV2V3.address) - - await fulfillment(of: [expectation1], timeout: 3) - } - - func createDms(client: Client, peers: [Client], numMessages: Int) - async throws -> [Dm] - { - var dms: [Dm] = [] - for peer in peers { - let dm = try await peer.conversations.findOrCreateDm( - with: client.address) - dms.append(dm) - for i in 0.. - - - - NSExtension - - NSExtensionPointIdentifier - com.apple.usernotifications.service - NSExtensionPrincipalClass - $(PRODUCT_MODULE_NAME).NotificationService - - - diff --git a/XMTPiOSExample/NotificationService/NotificationService.entitlements b/XMTPiOSExample/NotificationService/NotificationService.entitlements deleted file mode 100644 index a1df3603..00000000 --- a/XMTPiOSExample/NotificationService/NotificationService.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - aps-environment - development - keychain-access-groups - - $(AppIdentifierPrefix)com.xmtp.XMTPiOSExample - - - diff --git a/XMTPiOSExample/NotificationService/NotificationService.swift b/XMTPiOSExample/NotificationService/NotificationService.swift deleted file mode 100644 index 32c4cc1d..00000000 --- a/XMTPiOSExample/NotificationService/NotificationService.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// NotificationService.swift -// NotificationService -// -// Created by Pat Nakajima on 1/20/23. -// - -import UserNotifications -import XMTPiOS - -class NotificationService: UNNotificationServiceExtension { - var contentHandler: ((UNNotificationContent) -> Void)? - var bestAttemptContent: UNMutableNotificationContent? - - override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - self.contentHandler = contentHandler - bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - - do { - guard let encryptedMessage = request.content.userInfo["encryptedMessage"] as? String, - let topic = request.content.userInfo["topic"] as? String, - let encryptedMessageData = Data(base64Encoded: Data(encryptedMessage.utf8)) - else { - print("Did not get correct message data from push") - return - } - - let persistence = Persistence() - - guard let keysData = persistence.loadKeys(), - let keys = try? PrivateKeyBundle(serializedData: keysData), - let conversationContainer = try persistence.load(conversationTopic: topic) - else { - print("No keys or conversation persisted") - return - } - - Task { - let client = try await Client.from(bundle: keys) - let conversation = conversationContainer.decode(with: client) - - let envelope = XMTPiOS.Envelope.with { envelope in - envelope.message = encryptedMessageData - envelope.contentTopic = topic - } - - if let bestAttemptContent = bestAttemptContent { - let decodedMessage = try conversation.decode(envelope) - - bestAttemptContent.body = (try? decodedMessage.content()) ?? "no content" - - contentHandler(bestAttemptContent) - } - } - } catch { - print("Error receiving notification: \(error)") - } - } - - override func serviceExtensionTimeWillExpire() { - // Called just before the extension will be terminated by the system. - // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { - contentHandler(bestAttemptContent) - } - } -} diff --git a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj index bbfa15d8..325748c3 100644 --- a/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj +++ b/XMTPiOSExample/XMTPiOSExample.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ A6281995292DC825004B9117 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6281994292DC825004B9117 /* ContentView.swift */; }; A6281997292DC826004B9117 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A6281996292DC826004B9117 /* Assets.xcassets */; }; A628199B292DC826004B9117 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A628199A292DC826004B9117 /* Preview Assets.xcassets */; }; - A6494F002B6C2DF700D9FFB9 /* XMTPiOS in Frameworks */ = {isa = PBXBuildFile; productRef = A6494EFF2B6C2DF700D9FFB9 /* XMTPiOS */; }; A6557A312941166E00CC4C7B /* MessageCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6557A302941166E00CC4C7B /* MessageCellView.swift */; }; A6557A3329411F4F00CC4C7B /* NewConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6557A3229411F4F00CC4C7B /* NewConversationView.swift */; }; A65F0704297B5D4E00C3C76E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F0703297B5D4E00C3C76E /* Persistence.swift */; }; @@ -32,10 +31,6 @@ A68807152B6C53E0004340BD /* GroupDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A68807142B6C53E0004340BD /* GroupDetailView.swift */; }; A69F33CA292DD557005A5556 /* LoggedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69F33C9292DD557005A5556 /* LoggedInView.swift */; }; A69F33CC292DD568005A5556 /* QRCodeSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A69F33CB292DD568005A5556 /* QRCodeSheetView.swift */; }; - A6AE5187297B61AE006FDD0F /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = A6AE5180297B61AE006FDD0F /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - A6AE518E297B6210006FDD0F /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AE518C297B6210006FDD0F /* NotificationService.swift */; }; - A6AE5192297B6270006FDD0F /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = A65F0703297B5D4E00C3C76E /* Persistence.swift */; }; - A6AE5194297B62C8006FDD0F /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = A6AE5193297B62C8006FDD0F /* KeychainAccess */; }; A6C0F37B2AC1E321008C6AA7 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = A6C0F37A2AC1E321008C6AA7 /* Starscream */; }; A6C0F37E2AC1E34F008C6AA7 /* WalletConnect in Frameworks */ = {isa = PBXBuildFile; productRef = A6C0F37D2AC1E34F008C6AA7 /* WalletConnect */; }; A6C0F3802AC1E34F008C6AA7 /* WalletConnectModal in Frameworks */ = {isa = PBXBuildFile; productRef = A6C0F37F2AC1E34F008C6AA7 /* WalletConnectModal */; }; @@ -45,16 +40,6 @@ A6D192D0293A7B97006B49F2 /* ConversationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6D192CF293A7B97006B49F2 /* ConversationListView.swift */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - A6AE5185297B61AE006FDD0F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = A6281987292DC825004B9117 /* Project object */; - proxyType = 1; - remoteGlobalIDString = A6AE517F297B61AE006FDD0F; - remoteInfo = NotificationService; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ A65F0701297B5BCC00C3C76E /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; @@ -62,7 +47,6 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - A6AE5187297B61AE006FDD0F /* NotificationService.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -92,9 +76,6 @@ A68807142B6C53E0004340BD /* GroupDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupDetailView.swift; sourceTree = ""; }; A69F33C9292DD557005A5556 /* LoggedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggedInView.swift; sourceTree = ""; }; A69F33CB292DD568005A5556 /* QRCodeSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeSheetView.swift; sourceTree = ""; }; - A6AE5180297B61AE006FDD0F /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - A6AE518B297B61C8006FDD0F /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; }; - A6AE518C297B6210006FDD0F /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; A6C0F3832AC1E4B5008C6AA7 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; A6C0F3852AC1E549008C6AA7 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; A6D192CF293A7B97006B49F2 /* ConversationListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationListView.swift; sourceTree = ""; }; @@ -117,15 +98,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A6AE517D297B61AE006FDD0F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A6AE5194297B62C8006FDD0F /* KeychainAccess in Frameworks */, - A6494F002B6C2DF700D9FFB9 /* XMTPiOS in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -134,7 +106,6 @@ children = ( A6E774192B154D1E00F01DFF /* xmtp-ios */, A6281991292DC825004B9117 /* XMTPiOSExample */, - A6AE5181297B61AE006FDD0F /* NotificationService */, A6281990292DC825004B9117 /* Products */, A69F33C4292DC992005A5556 /* Frameworks */, ); @@ -144,7 +115,6 @@ isa = PBXGroup; children = ( A628198F292DC825004B9117 /* XMTPiOSExample.app */, - A6AE5180297B61AE006FDD0F /* NotificationService.appex */, ); name = Products; sourceTree = ""; @@ -211,15 +181,6 @@ path = Views; sourceTree = ""; }; - A6AE5181297B61AE006FDD0F /* NotificationService */ = { - isa = PBXGroup; - children = ( - A6AE518C297B6210006FDD0F /* NotificationService.swift */, - A6AE518B297B61C8006FDD0F /* NotificationService.entitlements */, - ); - path = NotificationService; - sourceTree = ""; - }; A6C0F3872AC1E54F008C6AA7 /* Extensions */ = { isa = PBXGroup; children = ( @@ -244,7 +205,6 @@ buildRules = ( ); dependencies = ( - A6AE5186297B61AE006FDD0F /* PBXTargetDependency */, ); name = XMTPiOSExample; packageProductDependencies = ( @@ -261,27 +221,6 @@ productReference = A628198F292DC825004B9117 /* XMTPiOSExample.app */; productType = "com.apple.product-type.application"; }; - A6AE517F297B61AE006FDD0F /* NotificationService */ = { - isa = PBXNativeTarget; - buildConfigurationList = A6AE5188297B61AE006FDD0F /* Build configuration list for PBXNativeTarget "NotificationService" */; - buildPhases = ( - A6AE517C297B61AE006FDD0F /* Sources */, - A6AE517D297B61AE006FDD0F /* Frameworks */, - A6AE517E297B61AE006FDD0F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = NotificationService; - packageProductDependencies = ( - A6AE5193297B62C8006FDD0F /* KeychainAccess */, - A6494EFF2B6C2DF700D9FFB9 /* XMTPiOS */, - ); - productName = NotificationService; - productReference = A6AE5180297B61AE006FDD0F /* NotificationService.appex */; - productType = "com.apple.product-type.app-extension"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -295,9 +234,6 @@ A628198E292DC825004B9117 = { CreatedOnToolsVersion = 14.1; }; - A6AE517F297B61AE006FDD0F = { - CreatedOnToolsVersion = 14.1; - }; }; }; buildConfigurationList = A628198A292DC825004B9117 /* Build configuration list for PBXProject "XMTPiOSExample" */; @@ -321,7 +257,6 @@ projectRoot = ""; targets = ( A628198E292DC825004B9117 /* XMTPiOSExample */, - A6AE517F297B61AE006FDD0F /* NotificationService */, ); }; /* End PBXProject section */ @@ -336,13 +271,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A6AE517E297B61AE006FDD0F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -396,25 +324,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - A6AE517C297B61AE006FDD0F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - A6AE518E297B6210006FDD0F /* NotificationService.swift in Sources */, - A6AE5192297B6270006FDD0F /* Persistence.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - A6AE5186297B61AE006FDD0F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = A6AE517F297B61AE006FDD0F /* NotificationService */; - targetProxy = A6AE5185297B61AE006FDD0F /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin XCBuildConfiguration section */ A628199C292DC826004B9117 /* Debug */ = { isa = XCBuildConfiguration; @@ -606,63 +517,6 @@ }; name = Release; }; - A6AE5189297B61AE006FDD0F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = FY4NZR34Z3; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NotificationService/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = NotificationService; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.xmtp.XMTPiOSExampleApp.NotificationService; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - A6AE518A297B61AE006FDD0F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = FY4NZR34Z3; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = NotificationService/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = NotificationService; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.xmtp.XMTPiOSExampleApp.NotificationService; - PRODUCT_NAME = "$(TARGET_NAME)"; - SDKROOT = iphoneos; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -684,15 +538,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - A6AE5188297B61AE006FDD0F /* Build configuration list for PBXNativeTarget "NotificationService" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - A6AE5189297B61AE006FDD0F /* Debug */, - A6AE518A297B61AE006FDD0F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -744,10 +589,6 @@ package = 6AEE396C29F330CD0027B657 /* XCRemoteSwiftPackageReference "secp256k1.swift" */; productName = secp256k1; }; - A6494EFF2B6C2DF700D9FFB9 /* XMTPiOS */ = { - isa = XCSwiftPackageProductDependency; - productName = XMTPiOS; - }; A65F0706297B5E7600C3C76E /* WalletConnectSwift */ = { isa = XCSwiftPackageProductDependency; package = A65F0705297B5E7500C3C76E /* XCRemoteSwiftPackageReference "WalletConnectSwift" */; @@ -762,11 +603,6 @@ isa = XCSwiftPackageProductDependency; productName = XMTPiOS; }; - A6AE5193297B62C8006FDD0F /* KeychainAccess */ = { - isa = XCSwiftPackageProductDependency; - package = A65F0708297B5E8600C3C76E /* XCRemoteSwiftPackageReference "KeychainAccess" */; - productName = KeychainAccess; - }; A6C0F37A2AC1E321008C6AA7 /* Starscream */ = { isa = XCSwiftPackageProductDependency; package = A6C0F3792AC1E321008C6AA7 /* XCRemoteSwiftPackageReference "Starscream" */; diff --git a/XMTPiOSExample/XMTPiOSExample/ContentView.swift b/XMTPiOSExample/XMTPiOSExample/ContentView.swift index 7d50f7e5..33e4de7f 100644 --- a/XMTPiOSExample/XMTPiOSExample/ContentView.swift +++ b/XMTPiOSExample/XMTPiOSExample/ContentView.swift @@ -31,8 +31,6 @@ struct ContentView: View { .sheet(isPresented: $isConnectingWallet) { LoginView(onConnected: { client in do { - let keysData = try client.privateKeyBundle.serializedData() - Persistence().saveKeys(keysData) self.status = .connected(client) } catch { print("Error setting up client: \(error)") @@ -46,17 +44,18 @@ struct ContentView: View { Task { do { if let keysData = Persistence().loadKeys() { - let keys = try PrivateKeyBundle(serializedData: keysData) - let client = try await Client.from( - bundle: keys, - options: .init( - api: .init(env: .local, isSecure: false), - codecs: [GroupUpdatedCodec()], - enableV3: true + if let address = Persistence().loadAddress() { + let client = try await Client.build( + address: address, + options: .init( + api: .init(env: .dev, isSecure: true), + codecs: [GroupUpdatedCodec()], + dbEncryptionKey: keysData + ) ) - ) - await MainActor.run { - self.status = .connected(client) + await MainActor.run { + self.status = .connected(client) + } } } } catch { @@ -92,18 +91,18 @@ struct ContentView: View { Task { do { let wallet = try PrivateKey.generate() + let key = try secureRandomBytes(count: 32) + Persistence().saveKeys(key) + Persistence().saveAddress(wallet.address) let client = try await Client.create( account: wallet, options: .init( - api: .init(env: .local, isSecure: false, appVersion: "XMTPTest/v1.0.0"), + api: .init(env: .dev, isSecure: true, appVersion: "XMTPTest/v1.0.0"), codecs: [GroupUpdatedCodec()], - enableV3: true + dbEncryptionKey: key ) ) - let keysData = try client.privateKeyBundle.serializedData() - Persistence().saveKeys(keysData) - await MainActor.run { self.status = .connected(client) } diff --git a/XMTPiOSExample/XMTPiOSExample/Persistence.swift b/XMTPiOSExample/XMTPiOSExample/Persistence.swift index bc46fbec..c6f0b7ff 100644 --- a/XMTPiOSExample/XMTPiOSExample/Persistence.swift +++ b/XMTPiOSExample/XMTPiOSExample/Persistence.swift @@ -28,18 +28,31 @@ struct Persistence { return nil } } + + func saveAddress(_ address: String) { + keychain[string: "address"] = address + } - func load(conversationTopic: String) throws -> ConversationContainer? { - guard let data = try keychain.getData(key(topic: conversationTopic)) else { + func loadAddress() -> String? { + do { + return try keychain.getString("address") + } catch { + print("Error loading address data: \(error)") return nil } - - let decoder = JSONDecoder() - let decoded = try decoder.decode(ConversationContainer.self, from: data) - - return decoded } +// func load(conversationTopic: String) throws -> ConversationContainer? { +// guard let data = try keychain.getData(key(topic: conversationTopic)) else { +// return nil +// } +// +// let decoder = JSONDecoder() +// let decoded = try decoder.decode(ConversationContainer.self, from: data) +// +// return decoded +// } + func save(conversation: Conversation) throws { // keychain[data: key(topic: conversation.topic)] = try JSONEncoder().encode(conversation.encodedContainer) } diff --git a/XMTPiOSExample/XMTPiOSExample/Views/ConversationDetailView.swift b/XMTPiOSExample/XMTPiOSExample/Views/ConversationDetailView.swift index 4e81cd65..188e03f9 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/ConversationDetailView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/ConversationDetailView.swift @@ -43,7 +43,7 @@ struct ConversationDetailView: View { } } } - .navigationTitle((try? conversation.peerAddress) ?? "") + .navigationTitle((try? conversation.id) ?? "") .navigationBarTitleDisplayMode(.inline) } diff --git a/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift b/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift index 99ca3171..dad6998a 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/ConversationListView.swift @@ -1,10 +1,3 @@ -// -// ConversationListView.swift -// XMTPiOSExample -// -// Created by Pat Nakajima on 12/2/22. -// - import SwiftUI import XMTPiOS @@ -12,173 +5,149 @@ struct ConversationListView: View { var client: XMTPiOS.Client @EnvironmentObject var coordinator: EnvironmentCoordinator - @State private var conversations: [ConversationOrGroup] = [] + @State private var conversations: [XMTPiOS.Conversation] = [] @State private var isShowingNewConversation = false + // Pre-sorted conversations to reduce complexity + private var sortedConversations: [XMTPiOS.Conversation] { + conversations.sorted(by: compareConversations) + } + var body: some View { List { - ForEach(conversations.sorted(by: { $0.createdAt > $1.createdAt }), id: \.id) { item in - NavigationLink(value: item) { - HStack { - switch item { - case .conversation: - Image(systemName: "person.fill") - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundStyle(.secondary) - case .group: - Image(systemName: "person.3.fill") - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundStyle(.secondary) - } - - VStack(alignment: .leading) { - switch item { - case .conversation(let conversation): - if let abbreviatedAddress = try? Util.abbreviate(address: conversation.peerAddress) { - Text(abbreviatedAddress) - } else { - Text("Unknown Address") - .foregroundStyle(.secondary) - } - case .group(let group): - let memberAddresses = try? group.members.map(\.inboxId).sorted().map { Util.abbreviate(address: $0) } - if let addresses = memberAddresses { - Text(addresses.joined(separator: ", ")) - } else { - Text("Unknown Members") - .foregroundStyle(.secondary) - } - } - - Text(item.createdAt.formatted()) - .font(.caption) - .foregroundStyle(.secondary) - } - } + ForEach(sortedConversations, id: \.id) { item in + NavigationLink(destination: destinationView(for: item)) { + conversationRow(for: item) } } } - .navigationDestination(for: ConversationOrGroup.self) { item in - switch item { - case .conversation(let conversation): - ConversationDetailView(client: client, conversation: conversation) - case .group(let group): - GroupDetailView(client: client, group: group) - } - } .navigationTitle("Conversations") - .refreshable { - await loadConversations() - } - .task { - await loadConversations() - } - .task { - do { - for try await group in try await client.conversations.streamGroups() { - conversations.insert(.group(group), at: 0) - - await add(conversations: [.group(group)]) - } - - } catch { - print("Error streaming groups: \(error)") - } - } - .task { - do { - for try await conversation in try await client.conversations.stream() { - conversations.insert(.conversation(conversation), at: 0) - - await add(conversations: [.conversation(conversation)]) - } - - } catch { - print("Error streaming conversations: \(error)") - } - } + .refreshable { await loadConversations() } + .task { await loadConversations() } + .task { await startConversationStream() } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - Button(action: { - self.isShowingNewConversation = true - }) { + Button(action: { isShowingNewConversation = true }) { Label("New Conversation", systemImage: "plus") } } } .sheet(isPresented: $isShowingNewConversation) { NewConversationView(client: client) { conversationOrGroup in - switch conversationOrGroup { - case .conversation(let conversation): - conversations.insert(.conversation(conversation), at: 0) - coordinator.path.append(conversationOrGroup) - case .group(let group): - conversations.insert(.group(group), at: 0) - coordinator.path.append(conversationOrGroup) - } + addConversation(conversationOrGroup) } } } - func loadConversations() async { - do { - let conversations = try await client.conversations.list().map { - ConversationOrGroup.conversation($0) + // Helper function to compare conversations by createdAt date + private func compareConversations(_ lhs: XMTPiOS.Conversation, _ rhs: XMTPiOS.Conversation) -> Bool { + return lhs.createdAt > rhs.createdAt + } + + // Extracted row view for each conversation + @ViewBuilder + private func conversationRow(for item: XMTPiOS.Conversation) -> some View { + HStack { + conversationIcon(for: item) + VStack(alignment: .leading) { + Text(conversationDisplayName(for: item)) + .foregroundStyle(.secondary) + Text(formattedDate(for: item.createdAt)) + .font(.caption) + .foregroundStyle(.secondary) } + } + } - try await client.conversations.sync() + // Extracted icon view for conversation type + @ViewBuilder + private func conversationIcon(for item: XMTPiOS.Conversation) -> some View { + switch item { + case .dm: + Image(systemName: "person.fill") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundStyle(.secondary) + case .group: + Image(systemName: "person.3.fill") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundStyle(.secondary) + } + } - let groups = try await client.conversations.groups().map { - ConversationOrGroup.group($0) - } + // Helper function to provide a display name based on the conversation type + private func conversationDisplayName(for item: XMTPiOS.Conversation) -> String { + switch item { + case .dm(let conversation): + return (try? Util.abbreviate(address: conversation.peerInboxId)) ?? "Unknown Address" + case .group(let group): + return (try? group.groupName()) ?? "Group Name" + } + } + + // Helper function to format the date + private func formattedDate(for date: Date) -> String { + return date.formatted() + } + + // Define destination view based on conversation type + @ViewBuilder + private func destinationView(for item: XMTPiOS.Conversation) -> some View { + switch item { + case .dm(let conversation): + ConversationDetailView(client: client, conversation: .dm(conversation)) + case .group(let group): + GroupDetailView(client: client, group: group) + } + } + // Async function to load conversations + func loadConversations() async { + do { + try await client.conversations.sync() + let loadedConversations = try await client.conversations.list() await MainActor.run { - self.conversations = conversations + groups + self.conversations = loadedConversations } - - await add(conversations: conversations) + await add(conversations: loadedConversations) } catch { print("Error loading conversations: \(error)") } } - func add(conversations: [ConversationOrGroup]) async { - for conversationOrGroup in conversations { - switch conversationOrGroup { - case .conversation(let conversation): - // Ensure we're subscribed to push notifications on these conversations - do { - let hmacKeysResult = await client.conversations.getHmacKeys() - let hmacKeys = hmacKeysResult.hmacKeys - - let result = hmacKeys[conversation.topic]?.values.map { hmacKey -> NotificationSubscriptionHmacKey in - NotificationSubscriptionHmacKey.with { sub_key in - sub_key.key = hmacKey.hmacKey - sub_key.thirtyDayPeriodsSinceEpoch = UInt32(hmacKey.thirtyDayPeriodsSinceEpoch) - } - } - - let subscription = NotificationSubscription.with { sub in - sub.hmacKeys = result ?? [] - sub.topic = conversation.topic - sub.isSilent = conversation.version == .v1 - } - try await XMTPPush.shared.subscribeWithMetadata(subscriptions: [subscription]) - } catch { - print("Error subscribing: \(error)") + // Async function to stream conversations + func startConversationStream() async { + do { + for try await conversation in try await client.conversations.stream() { + await MainActor.run { + conversations.insert(conversation, at: 0) } + await add(conversations: [conversation]) + } + } catch { + print("Error streaming conversations: \(error)") + } + } - do { - try Persistence().save(conversation: conversation) - } catch { - print("Error saving \(conversation.topic): \(error)") - } - case .group: - // Handle this in the future + // Helper function to add a conversation or group + private func addConversation(_ conversationOrGroup: XMTPiOS.Conversation) { + switch conversationOrGroup { + case .dm(let conversation): + conversations.insert(.dm(conversation), at: 0) + coordinator.path.append(conversationOrGroup) + case .group(let group): + conversations.insert(.group(group), at: 0) + coordinator.path.append(conversationOrGroup) + } + } + + func add(conversations: [XMTPiOS.Conversation]) async { + for conversationOrGroup in conversations { + switch conversationOrGroup { + case .dm, .group: return } } @@ -187,11 +156,9 @@ struct ConversationListView: View { struct ConversationListView_Previews: PreviewProvider { static var previews: some View { - VStack { - PreviewClientProvider { client in - NavigationView { - ConversationListView(client: client) - } + PreviewClientProvider { client in + NavigationStack { + ConversationListView(client: client) } } } diff --git a/XMTPiOSExample/XMTPiOSExample/Views/GroupSettingsView.swift b/XMTPiOSExample/XMTPiOSExample/Views/GroupSettingsView.swift index 83b8cbc1..b9b00a9d 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/GroupSettingsView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/GroupSettingsView.swift @@ -1,10 +1,3 @@ -// -// GroupSettingsView.swift -// XMTPiOSExample -// -// Created by Pat Nakajima on 2/6/24. -// - import SwiftUI import XMTPiOS @@ -42,9 +35,7 @@ struct GroupSettingsView: View { if client.address.lowercased() == member.lowercased() { Button("Leave", role: .destructive) { Task { - try await group.removeMembers(addresses: [client.address]) - coordinator.path = NavigationPath() - dismiss() + try? await leaveGroup() } } } else { @@ -61,34 +52,8 @@ struct GroupSettingsView: View { HStack { TextField("Add member", text: $newGroupMember) Button("Add") { - if newGroupMember.lowercased() == client.address { - self.groupError = "You cannot add yourself to a group" - return - } - - isAddingMember = true - Task { - do { - if try await self.client.canMessageV3(address: newGroupMember) { - try await group.addMembers(addresses: [newGroupMember]) - try await syncGroupMembers() - - await MainActor.run { - self.groupError = "" - self.newGroupMember = "" - self.isAddingMember = false - } - } else { - await MainActor.run { - self.groupError = "Member address not registered" - self.isAddingMember = false - } - } - } catch { - self.groupError = error.localizedDescription - self.isAddingMember = false - } + await addMember() } } .opacity(isAddingMember ? 0 : 1) @@ -100,7 +65,7 @@ struct GroupSettingsView: View { } } - if groupError != "" { + if !groupError.isEmpty { Text(groupError) .foregroundStyle(.red) .font(.subheadline) @@ -114,9 +79,48 @@ struct GroupSettingsView: View { } private func syncGroupMembers() async throws { - try? await group.sync() - try await MainActor.run { - self.groupMembers = try group.members.map(\.inboxId) + try await group.sync() + let inboxIds = try await group.members.map(\.inboxId) + await MainActor.run { + self.groupMembers = inboxIds + } + } + + private func leaveGroup() async throws { + try await group.removeMembers(addresses: [client.address]) + await MainActor.run { + coordinator.path = NavigationPath() + dismiss() + } + } + + private func addMember() async { + guard newGroupMember.lowercased() != client.address else { + groupError = "You cannot add yourself to a group" + return + } + + isAddingMember = true + do { + if try await client.canMessage(address: newGroupMember) { + try await group.addMembers(addresses: [newGroupMember]) + try await syncGroupMembers() + await MainActor.run { + groupError = "" + newGroupMember = "" + isAddingMember = false + } + } else { + await MainActor.run { + groupError = "Member address not registered" + isAddingMember = false + } + } + } catch { + await MainActor.run { + groupError = error.localizedDescription + isAddingMember = false + } } } } diff --git a/XMTPiOSExample/XMTPiOSExample/Views/LoginView.swift b/XMTPiOSExample/XMTPiOSExample/Views/LoginView.swift index d47f90c4..ebd27837 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/LoginView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/LoginView.swift @@ -161,12 +161,15 @@ struct LoginView: View { Task(priority: .high) { let signer = Signer(session: session, account: account) + let key = try secureRandomBytes(count: 32) + Persistence().saveKeys(key) + Persistence().saveAddress(signer.address) let client = try await Client.create( account: signer, options: .init( api: .init(env: .local, isSecure: false), codecs: [GroupUpdatedCodec()], - enableV3: true + dbEncryptionKey: key ) ) diff --git a/XMTPiOSExample/XMTPiOSExample/Views/NewConversationView.swift b/XMTPiOSExample/XMTPiOSExample/Views/NewConversationView.swift index e538603b..1a9c40fe 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/NewConversationView.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/NewConversationView.swift @@ -8,40 +8,9 @@ import SwiftUI import XMTPiOS -enum ConversationOrGroup: Hashable { - - case conversation(Conversation), group(XMTPiOS.Group) - - static func == (lhs: ConversationOrGroup, rhs: ConversationOrGroup) throws -> Bool { - try lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) throws { - try id.hash(into: &hasher) - } - - var id: String { - switch self { - case .conversation(let conversation): - return conversation.topic - case .group(let group): - return group.id.toHexEncodedString() - } - } - - var createdAt: Date { - switch self { - case .conversation(let conversation): - return conversation.createdAt - case .group(let group): - return group.createdAt - } - } -} - struct NewConversationView: View { var client: XMTPiOS.Client - var onCreate: (ConversationOrGroup) -> Void + var onCreate: (XMTPiOS.Conversation) -> Void @Environment(\.dismiss) var dismiss @State private var recipientAddress: String = "" @@ -84,7 +53,7 @@ struct NewConversationView: View { Task { do { - if try await self.client.canMessageV3(address: newGroupMember) { + if try await self.client.canMessage(address: newGroupMember) { await MainActor.run { self.groupError = "" self.groupMembers.append(newGroupMember) @@ -164,9 +133,9 @@ struct NewConversationView: View { let conversation = try await client.conversations.newConversation(with: address) await MainActor.run { dismiss() - onCreate(.conversation(conversation)) + onCreate(conversation) } - } catch ConversationError.recipientNotOnNetwork { + } catch ConversationError.memberNotRegistered([address]) { await MainActor.run { self.error = "Recipient is not on the XMTP network." } diff --git a/XMTPiOSExample/XMTPiOSExample/Views/PreviewClientProvider.swift b/XMTPiOSExample/XMTPiOSExample/Views/PreviewClientProvider.swift index b3ff40f7..47460154 100644 --- a/XMTPiOSExample/XMTPiOSExample/Views/PreviewClientProvider.swift +++ b/XMTPiOSExample/XMTPiOSExample/Views/PreviewClientProvider.swift @@ -42,9 +42,12 @@ struct PreviewClientProvider: View { Text("Creating client…") .task { do { - var options = ClientOptions() - options.api.env = .local - options.api.isSecure = false + let key = try secureRandomBytes(count: 32) + Persistence().saveKeys(key) + Persistence().saveAddress(wallet.address) + var options = ClientOptions(dbEncryptionKey: key) + options.api.env = .dev + options.api.isSecure = true let client = try await Client.create(account: wallet, options: options) await MainActor.run { self.client = client @@ -66,3 +69,21 @@ struct PreviewClientProvider_Previews: PreviewProvider { } } } + +func secureRandomBytes(count: Int) throws -> Data { + var bytes = [UInt8](repeating: 0, count: count) + + // Fill bytes with secure random data + let status = SecRandomCopyBytes( + kSecRandomDefault, + count, + &bytes + ) + + // A status of errSecSuccess indicates success + if status == errSecSuccess { + return Data(bytes) + } else { + fatalError("could not generate random bytes") + } +}