Skip to content

Commit

Permalink
test(realtime): add realtime integration tests (#333)
Browse files Browse the repository at this point in the history
* Move to ObservationTokenTests

* test(realtime): add integration tests for realtime v2

* Rename schema to key_value_storage

* chore: move realtime integration tests to IntegrationTests target

* test: comment out failing test

* test: fix optional type for receivedMessage

* chore: fix dot-env target

* chore: fix load env script

* test: use mock web socket

* chore: swift format
  • Loading branch information
grdsdev authored Apr 10, 2024
1 parent aca50a5 commit 3b74720
Show file tree
Hide file tree
Showing 21 changed files with 499 additions and 45 deletions.
3 changes: 0 additions & 3 deletions .env

This file was deleted.

4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Get these from your API settings: https://supabase.com/dashboard/project/_/settings/api

SUPABASE_URL=https://mysupabasereference.supabase.co
SUPABASE_ANON_KEY=my.supabase.anon.key
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,6 @@ iOSInjectionProject/


# Environment
.env.local
.env
Secrets.swift
Tests/IntegrationTests/Environment.swift
DotEnv.swift
23 changes: 17 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@ PLATFORM_WATCHOS = watchOS Simulator,name=Apple Watch Series 9 (41mm)
SCHEME ?= Supabase
PLATFORM ?= iOS Simulator,name=iPhone 15 Pro

set-env:
@source scripts/setenv.sh > Tests/IntegrationTests/Environment.swift
export SECRETS
define SECRETS
enum DotEnv {
static let SUPABASE_URL = "$(SUPABASE_URL)"
static let SUPABASE_ANON_KEY = "$(SUPABASE_ANON_KEY)"
}
endef

test-all: set-env
load-env:
@. ./scripts/load_env.sh

dot-env:
@echo "$$SECRETS" > Tests/IntegrationTests/DotEnv.swift

test-all: dot-env
set -o pipefail && \
xcodebuild test \
-skipMacroValidation \
Expand All @@ -19,7 +30,7 @@ test-all: set-env
-testPlan AllTests \
-destination platform="$(PLATFORM)" | xcpretty

test-library: set-env
test-library: dot-env
set -o pipefail && \
xcodebuild test \
-skipMacroValidation \
Expand All @@ -28,8 +39,8 @@ test-library: set-env
-derivedDataPath /tmp/derived-data \
-destination platform="$(PLATFORM)" | xcpretty

test-integration: set-env
@set -o pipefail && \
test-integration: dot-env
set -o pipefail && \
xcodebuild test \
-skipMacroValidation \
-workspace supabase-swift.xcworkspace \
Expand Down
2 changes: 2 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ let package = Package(
"Auth",
"TestHelpers",
"PostgREST",
"Realtime",
]
),
.target(
Expand Down Expand Up @@ -127,6 +128,7 @@ let package = Package(
name: "RealtimeTests",
dependencies: [
"Realtime",
"PostgREST",
"TestHelpers",
.product(name: "CustomDump", package: "swift-custom-dump"),
]
Expand Down
1 change: 1 addition & 0 deletions Sources/PostgREST/PostgrestClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ConcurrencyExtras
import Foundation

public typealias PostgrestError = _Helpers.PostgrestError
public typealias AnyJSON = _Helpers.AnyJSON

#if canImport(FoundationNetworking)
import FoundationNetworking
Expand Down
2 changes: 2 additions & 0 deletions Sources/Realtime/V2/RealtimeClientV2.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import Foundation
let NSEC_PER_SEC: UInt64 = 1000000000
#endif

public typealias JSONObject = _Helpers.JSONObject

public actor RealtimeClientV2 {
public struct Configuration: Sendable {
var url: URL
Expand Down
14 changes: 14 additions & 0 deletions Sources/TestHelpers/AsyncSequence.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// AsyncSequence.swift
//
//
// Created by Guilherme Souza on 04/04/24.
//

import Foundation

extension AsyncSequence {
package func collect() async rethrows -> [Element] {
try await reduce(into: [Element]()) { $0.append($1) }
}
}
4 changes: 2 additions & 2 deletions Tests/IntegrationTests/AuthClientIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import XCTest
final class AuthClientIntegrationTests: XCTestCase {
let authClient = AuthClient(
configuration: AuthClient.Configuration(
url: URL(string: "\(Environment.SUPABASE_URL)/auth/v1")!,
url: URL(string: "\(DotEnv.SUPABASE_URL)/auth/v1")!,
headers: [
"apikey": Environment.SUPABASE_ANON_KEY,
"apikey": DotEnv.SUPABASE_ANON_KEY,
],
localStorage: InMemoryLocalStorage(),
logger: nil
Expand Down
4 changes: 2 additions & 2 deletions Tests/IntegrationTests/PostgrestIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ struct User: Codable, Hashable {
@available(iOS 15.0.0, macOS 12.0.0, tvOS 13.0, *)
final class IntegrationTests: XCTestCase {
let client = PostgrestClient(
url: URL(string: "\(Environment.SUPABASE_URL)/rest/v1")!,
url: URL(string: "\(DotEnv.SUPABASE_URL)/rest/v1")!,
headers: [
"Apikey": Environment.SUPABASE_ANON_KEY,
"Apikey": DotEnv.SUPABASE_ANON_KEY,
],
logger: nil
)
Expand Down
244 changes: 244 additions & 0 deletions Tests/IntegrationTests/RealtimeIntegrationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
//
// RealtimeIntegrationTests.swift
//
//
// Created by Guilherme Souza on 27/03/24.
//

import ConcurrencyExtras
import CustomDump
import PostgREST
@testable import Realtime
import Supabase
import TestHelpers
import XCTest

struct Logger: SupabaseLogger {
func log(message: SupabaseLogMessage) {
print(message.description)
}
}

final class RealtimeIntegrationTests: XCTestCase {
let realtime = RealtimeClientV2(
config: RealtimeClientV2.Configuration(
url: URL(string: "\(DotEnv.SUPABASE_URL)/realtime/v1")!,
apiKey: DotEnv.SUPABASE_ANON_KEY,
logger: Logger()
)
)

let db = PostgrestClient(
url: URL(string: "\(DotEnv.SUPABASE_URL)/rest/v1")!,
headers: [
"apikey": DotEnv.SUPABASE_ANON_KEY,
],
logger: Logger()
)

func testBroadcast() async throws {
let expectation = expectation(description: "receivedBroadcastMessages")
expectation.expectedFulfillmentCount = 3

let channel = await realtime.channel("integration") {
$0.broadcast.receiveOwnBroadcasts = true
}

let receivedMessages = LockIsolated<[JSONObject]>([])

Task {
for await message in await channel.broadcast(event: "test") {
receivedMessages.withValue {
$0.append(message)
}
expectation.fulfill()
}
}

await Task.megaYield()

await channel.subscribe()

struct Message: Codable {
var value: Int
}

try await channel.broadcast(event: "test", message: Message(value: 1))
try await channel.broadcast(event: "test", message: Message(value: 2))
try await channel.broadcast(event: "test", message: ["value": 3, "another_value": 42])

await fulfillment(of: [expectation], timeout: 0.5)

XCTAssertNoDifference(
receivedMessages.value,
[
[
"event": "test",
"payload": [
"value": 1,
],
"type": "broadcast",
],
[
"event": "test",
"payload": [
"value": 2,
],
"type": "broadcast",
],
[
"event": "test",
"payload": [
"value": 3,
"another_value": 42,
],
"type": "broadcast",
],
]
)

await channel.unsubscribe()
}

func testPresence() async throws {
let channel = await realtime.channel("integration") {
$0.broadcast.receiveOwnBroadcasts = true
}

let expectation = expectation(description: "presenceChange")
expectation.expectedFulfillmentCount = 4

let receivedPresenceChanges = LockIsolated<[any PresenceAction]>([])

Task {
for await presence in await channel.presenceChange() {
receivedPresenceChanges.withValue {
$0.append(presence)
}
expectation.fulfill()
}
}

await Task.megaYield()

await channel.subscribe()

struct UserState: Codable, Equatable {
let email: String
}

try await channel.track(UserState(email: "[email protected]"))
try await channel.track(["email": "[email protected]"])

await channel.untrack()

await fulfillment(of: [expectation], timeout: 0.5)

let joins = try receivedPresenceChanges.value.map { try $0.decodeJoins(as: UserState.self) }
let leaves = try receivedPresenceChanges.value.map { try $0.decodeLeaves(as: UserState.self) }
XCTAssertNoDifference(
joins,
[
[], // This is the first PRESENCE_STATE event.
[UserState(email: "[email protected]")],
[UserState(email: "[email protected]")],
[],
]
)

XCTAssertNoDifference(
leaves,
[
[], // This is the first PRESENCE_STATE event.
[],
[UserState(email: "[email protected]")],
[UserState(email: "[email protected]")],
]
)

await channel.unsubscribe()
}

// FIXME: Test getting stuck
// func testPostgresChanges() async throws {
// let channel = await realtime.channel("db-changes")
//
// let receivedInsertActions = Task {
// await channel.postgresChange(InsertAction.self, schema: "public").prefix(1).collect()
// }
//
// let receivedUpdateActions = Task {
// await channel.postgresChange(UpdateAction.self, schema: "public").prefix(1).collect()
// }
//
// let receivedDeleteActions = Task {
// await channel.postgresChange(DeleteAction.self, schema: "public").prefix(1).collect()
// }
//
// let receivedAnyActionsTask = Task {
// await channel.postgresChange(AnyAction.self, schema: "public").prefix(3).collect()
// }
//
// await Task.megaYield()
// await channel.subscribe()
//
// struct Entry: Codable, Equatable {
// let key: String
// let value: AnyJSON
// }
//
// let key = try await (
// db.from("key_value_storage")
// .insert(["key": AnyJSON.string(UUID().uuidString), "value": "value1"]).select().single()
// .execute().value as Entry
// ).key
// try await db.from("key_value_storage").update(["value": "value2"]).eq("key", value: key)
// .execute()
// try await db.from("key_value_storage").delete().eq("key", value: key).execute()
//
// let insertedEntries = try await receivedInsertActions.value.map {
// try $0.decodeRecord(
// as: Entry.self,
// decoder: JSONDecoder()
// )
// }
// let updatedEntries = try await receivedUpdateActions.value.map {
// try $0.decodeRecord(
// as: Entry.self,
// decoder: JSONDecoder()
// )
// }
// let deletedEntryIds = await receivedDeleteActions.value.compactMap {
// $0.oldRecord["key"]?.stringValue
// }
//
// XCTAssertNoDifference(insertedEntries, [Entry(key: key, value: "value1")])
// XCTAssertNoDifference(updatedEntries, [Entry(key: key, value: "value2")])
// XCTAssertNoDifference(deletedEntryIds, [key])
//
// let receivedAnyActions = await receivedAnyActionsTask.value
// XCTAssertEqual(receivedAnyActions.count, 3)
//
// if case let .insert(action) = receivedAnyActions[0] {
// let record = try action.decodeRecord(as: Entry.self, decoder: JSONDecoder())
// XCTAssertNoDifference(record, Entry(key: key, value: "value1"))
// } else {
// XCTFail("Expected a `AnyAction.insert` on `receivedAnyActions[0]`")
// }
//
// if case let .update(action) = receivedAnyActions[1] {
// let record = try action.decodeRecord(as: Entry.self, decoder: JSONDecoder())
// XCTAssertNoDifference(record, Entry(key: key, value: "value2"))
// } else {
// XCTFail("Expected a `AnyAction.update` on `receivedAnyActions[1]`")
// }
//
// if case let .delete(action) = receivedAnyActions[2] {
// XCTAssertNoDifference(key, action.oldRecord["key"]?.stringValue)
// } else {
// XCTFail("Expected a `AnyAction.delete` on `receivedAnyActions[2]`")
// }
//
// await channel.unsubscribe()
// }
}
2 changes: 1 addition & 1 deletion Tests/RealtimeTests/CallbackManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ final class CallbackManagerTests: XCTestCase {

let jsonObject = try JSONObject(message)

let receivedMessage = LockIsolated(JSONObject?.none)
let receivedMessage = LockIsolated<JSONObject?>(nil)
callbackManager.addBroadcastCallback(event: event) {
receivedMessage.setValue($0)
}
Expand Down
Loading

0 comments on commit 3b74720

Please sign in to comment.