Skip to content

Commit 1c2bf47

Browse files
Add some basic integration tests
It’s unlikely that we’re going to have a working unified test suite before the beta release, so here are some very basic smoke tests just to give us a _little bit_ of confidence that things are kind of working and that we don’t introduce major regressions. Would be good to have a way of separating these from the unit tests so that they don’t slow them down, but can figure that out later; I don’t have loads of time to spend on this at the moment.
1 parent 9454e40 commit 1c2bf47

File tree

8 files changed

+154
-2
lines changed

8 files changed

+154
-2
lines changed

.github/workflows/check.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ jobs:
1717

1818
steps:
1919
- uses: actions/checkout@v4
20+
with:
21+
submodules: true
2022

2123
# This step can be removed once the runners’ default version of Xcode is 16 or above
2224
- uses: maxim-lobanov/setup-xcode@v1
@@ -42,6 +44,8 @@ jobs:
4244
runs-on: macos-15
4345
steps:
4446
- uses: actions/checkout@v4
47+
with:
48+
submodules: true
4549

4650
# This step can be removed once the runners’ default version of Xcode is 16 or above
4751
- uses: maxim-lobanov/setup-xcode@v1
@@ -59,6 +63,8 @@ jobs:
5963
matrix: ${{ steps.generation-step.outputs.matrix }}
6064
steps:
6165
- uses: actions/checkout@v4
66+
with:
67+
submodules: true
6268

6369
# This step can be removed once the runners’ default version of Xcode is 16 or above
6470
- uses: maxim-lobanov/setup-xcode@v1
@@ -78,6 +84,8 @@ jobs:
7884

7985
steps:
8086
- uses: actions/checkout@v4
87+
with:
88+
submodules: true
8189
- uses: maxim-lobanov/setup-xcode@v1
8290
with:
8391
xcode-version: ${{ matrix.tooling.xcodeVersion }}
@@ -97,6 +105,8 @@ jobs:
97105

98106
steps:
99107
- uses: actions/checkout@v4
108+
with:
109+
submodules: true
100110
- uses: maxim-lobanov/setup-xcode@v1
101111
with:
102112
xcode-version: ${{ matrix.tooling.xcodeVersion }}
@@ -115,6 +125,8 @@ jobs:
115125

116126
steps:
117127
- uses: actions/checkout@v4
128+
with:
129+
submodules: true
118130
- uses: maxim-lobanov/setup-xcode@v1
119131
with:
120132
xcode-version: ${{ matrix.tooling.xcodeVersion }}

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "Tests/AblyChatTests/ably-common"]
2+
path = Tests/AblyChatTests/ably-common
3+
url = https://github.com/ably/ably-common

.prettierignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
# Don’t try and format the asset catalogue JSON files, which are managed by Xcode
22
*.xcassets/
3+
4+
# Submodules
5+
Tests/AblyChatTests/ably-common

CONTRIBUTING.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99
## Setup
1010

11-
1. `mint bootstrap` — this will take quite a long time (~5 minutes on my machine) the first time you run it
12-
2. `npm install`
11+
1. `git submodule update --init`
12+
2. `mint bootstrap` — this will take quite a long time (~5 minutes on my machine) the first time you run it
13+
3. `npm install`
1314

1415
## Running the tests
1516

@@ -25,6 +26,7 @@ To check formatting and code quality, run `swift run BuildTool lint`. Run with `
2526
## Development guidelines
2627

2728
- The aim of the [example app](README.md#example-app) is that it demonstrate all of the core functionality of the SDK. So if you add a new feature, try to add something to the example app to demonstrate this feature.
29+
- If you add a new feature, try to extend the `IntegrationTests` tests to perform a smoke test of its core functionality.
2830
- We should aim to make it easy for consumers of the SDK to be able to mock out the SDK in the tests for their own code. A couple of things that will aid with this:
2931
- Describe the SDK’s functionality via protocols (when doing so would still be sufficiently idiomatic to Swift).
3032
- When defining a `struct` that is emitted by the public API of the library, make sure to define a public memberwise initializer so that users can create one to be emitted by their mocks. (There is no way to make Swift’s autogenerated memberwise initializer public, so you will need to write one yourself. In Xcode, you can do this by clicking at the start of the type declaration and doing Editor → Refactor → Generate Memberwise Initializer.)

Package.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ let package = Package(
5858
name: "AsyncAlgorithms",
5959
package: "swift-async-algorithms"
6060
),
61+
],
62+
resources: [
63+
.copy("ably-common"),
6164
]
6265
),
6366
.executableTarget(
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Foundation
2+
3+
/// Provides the ``createAPIKey()`` function to create an API key for the Ably sandbox environment.
4+
enum Sandbox {
5+
private struct TestApp: Codable {
6+
var keys: [Key]
7+
8+
struct Key: Codable {
9+
var keyStr: String
10+
}
11+
}
12+
13+
enum Error: Swift.Error {
14+
case badResponseStatus(Int)
15+
}
16+
17+
private static func loadAppCreationRequestBody() async throws -> Data {
18+
let testAppSetupFileURL = Bundle.module.url(
19+
forResource: "test-app-setup",
20+
withExtension: "json",
21+
subdirectory: "ably-common/test-resources"
22+
)!
23+
24+
let (data, _) = try await URLSession.shared.data(for: .init(url: testAppSetupFileURL))
25+
// swiftlint:disable:next force_cast
26+
let dictionary = try JSONSerialization.jsonObject(with: data) as! [String: Any]
27+
return try JSONSerialization.data(withJSONObject: dictionary["post_apps"]!)
28+
}
29+
30+
static func createAPIKey() async throws -> String {
31+
var request = URLRequest(url: .init(string: "https://sandbox-rest.ably.io/apps")!)
32+
request.httpMethod = "POST"
33+
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
34+
request.httpBody = try await loadAppCreationRequestBody()
35+
36+
let (data, response) = try await URLSession.shared.data(for: request)
37+
38+
// swiftlint:disable:next force_cast
39+
let statusCode = (response as! HTTPURLResponse).statusCode
40+
41+
guard (200 ..< 300).contains(statusCode) else {
42+
throw Error.badResponseStatus(statusCode)
43+
}
44+
45+
let testApp = try JSONDecoder().decode(TestApp.self, from: data)
46+
47+
// From JS chat repo at 7985ab7 — "The key we need to use is the one at index 5, which gives enough permissions to interact with Chat and Channels"
48+
return testApp.keys[5].keyStr
49+
}
50+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Ably
2+
import AblyChat
3+
import Testing
4+
5+
/// Some very basic integration tests, just to check that things are kind of working.
6+
///
7+
/// It would be nice to give this a time limit, but unfortunately the `timeLimit` trait is only available on iOS 16 etc and above. CodeRabbit suggested writing a timeout function myself and wrapping the contents of the test in it, but I didn’t have time to try understanding its suggested code, so it can wait.
8+
@Suite
9+
struct IntegrationTests {
10+
private static func createSandboxRealtime(apiKey: String) -> ARTRealtime {
11+
let realtimeOptions = ARTClientOptions(key: apiKey)
12+
realtimeOptions.environment = "sandbox"
13+
realtimeOptions.clientId = UUID().uuidString
14+
15+
return ARTRealtime(options: realtimeOptions)
16+
}
17+
18+
private static func createSandboxChatClient(apiKey: String) -> DefaultChatClient {
19+
let realtime = createSandboxRealtime(apiKey: apiKey)
20+
return DefaultChatClient(realtime: realtime, clientOptions: nil)
21+
}
22+
23+
@Test
24+
func basicIntegrationTest() async throws {
25+
let apiKey = try await Sandbox.createAPIKey()
26+
27+
// (1) Create a couple of chat clients — one for sending and one for receiving
28+
let txClient = Self.createSandboxChatClient(apiKey: apiKey)
29+
let rxClient = Self.createSandboxChatClient(apiKey: apiKey)
30+
31+
// (2) Fetch a room
32+
let roomID = "basketball"
33+
let txRoom = try await txClient.rooms.get(roomID: roomID, options: .init())
34+
let rxRoom = try await rxClient.rooms.get(roomID: roomID, options: .init())
35+
36+
// (3) Subscribe to room status
37+
let rxRoomStatusSubscription = await rxRoom.onStatusChange(bufferingPolicy: .unbounded)
38+
39+
// (4) Attach the room so we can receive messages on it
40+
try await rxRoom.attach()
41+
42+
// (5) Check that we received an ATTACHED status change as a result of attaching the room
43+
_ = try #require(await rxRoomStatusSubscription.first { $0.current == .attached })
44+
#expect(await rxRoom.status == .attached)
45+
46+
// (6) Send a message before subscribing to messages, so that later on we can check history works.
47+
48+
// Create a throwaway subscription and wait for it to receive a message. This is to make sure that rxRoom has seen the message that we send here, so that the first message we receive on the subscription created in (7) is that which we’ll send in (8), and not that which we send here.
49+
let throwawayRxMessageSubscription = try await rxRoom.messages.subscribe(bufferingPolicy: .unbounded)
50+
51+
// Send the message
52+
let txMessageBeforeRxSubscribe = try await txRoom.messages.send(params: .init(text: "Hello from txRoom, before rxRoom subscribe"))
53+
54+
// Wait for rxRoom to see the message we just sent
55+
let throwawayRxMessage = try #require(await throwawayRxMessageSubscription.first { _ in true })
56+
#expect(throwawayRxMessage == txMessageBeforeRxSubscribe)
57+
58+
// (7) Subscribe to messages
59+
let rxMessageSubscription = try await rxRoom.messages.subscribe(bufferingPolicy: .unbounded)
60+
61+
// (8) Now that we’re subscribed to messages, send a message on the other client and check that we receive it on the subscription
62+
let txMessageAfterRxSubscribe = try await txRoom.messages.send(params: .init(text: "Hello from txRoom, after rxRoom subscribe"))
63+
let rxMessageFromSubscription = try #require(await rxMessageSubscription.first { _ in true })
64+
#expect(rxMessageFromSubscription == txMessageAfterRxSubscribe)
65+
66+
// (9) Fetch historical messages from before subscribing, and check we get txMessageBeforeRxSubscribe
67+
let rxMessagesBeforeSubscribing = try await rxMessageSubscription.getPreviousMessages(params: .init())
68+
try #require(rxMessagesBeforeSubscribing.items.count == 1)
69+
#expect(rxMessagesBeforeSubscribing.items[0] == txMessageBeforeRxSubscribe)
70+
71+
// (10) Detach the room
72+
try await rxRoom.detach()
73+
74+
// (11) Check that we received a DETACHED status change as a result of detaching the room
75+
_ = try #require(await rxRoomStatusSubscription.first { $0.current == .detached })
76+
#expect(await rxRoom.status == .detached)
77+
}
78+
}

Tests/AblyChatTests/ably-common

Submodule ably-common added at 60fd9cf

0 commit comments

Comments
 (0)