Skip to content

Commit 771ee18

Browse files
authored
test: add comprehensive Realtime test coverage (#755)
1 parent be133fe commit 771ee18

File tree

11 files changed

+1711
-8
lines changed

11 files changed

+1711
-8
lines changed

Sources/Realtime/PushV2.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ public enum PushStatus: String, Sendable {
1616

1717
@MainActor
1818
final class PushV2 {
19-
private weak var channel: RealtimeChannelV2?
19+
private weak var channel: (any RealtimeChannelProtocol)?
2020
let message: RealtimeMessageV2
2121

2222
private var receivedContinuation: CheckedContinuation<PushStatus, Never>?
2323

24-
init(channel: RealtimeChannelV2?, message: RealtimeMessageV2) {
24+
init(channel: (any RealtimeChannelProtocol)?, message: RealtimeMessageV2) {
2525
self.channel = channel
2626
self.message = message
2727
}

Sources/Realtime/RealtimeChannelV2.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ public struct RealtimeChannelConfig: Sendable {
2424
public var isPrivate: Bool
2525
}
2626

27-
public final class RealtimeChannelV2: Sendable {
27+
protocol RealtimeChannelProtocol: AnyObject, Sendable {
28+
@MainActor var config: RealtimeChannelConfig { get }
29+
var topic: String { get }
30+
var logger: (any SupabaseLogger)? { get }
31+
32+
var socket: any RealtimeClientProtocol { get }
33+
}
34+
35+
public final class RealtimeChannelV2: Sendable, RealtimeChannelProtocol {
2836
struct MutableState {
2937
var clientChanges: [PostgresJoinConfig] = []
3038
var joinRef: String?
@@ -39,7 +47,7 @@ public final class RealtimeChannelV2: Sendable {
3947
@MainActor var config: RealtimeChannelConfig
4048

4149
let logger: (any SupabaseLogger)?
42-
let socket: RealtimeClientV2
50+
let socket: any RealtimeClientProtocol
4351

4452
@MainActor var joinRef: String? { mutableState.joinRef }
4553

@@ -70,7 +78,7 @@ public final class RealtimeChannelV2: Sendable {
7078
init(
7179
topic: String,
7280
config: RealtimeChannelConfig,
73-
socket: RealtimeClientV2,
81+
socket: any RealtimeClientProtocol,
7482
logger: (any SupabaseLogger)?
7583
) {
7684
self.topic = topic

Sources/Realtime/RealtimeClientV2.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,20 @@ import Foundation
1616
typealias WebSocketTransport = @Sendable (_ url: URL, _ headers: [String: String]) async throws ->
1717
any WebSocket
1818

19-
public final class RealtimeClientV2: Sendable {
19+
protocol RealtimeClientProtocol: AnyObject, Sendable {
20+
var status: RealtimeClientStatus { get }
21+
var options: RealtimeClientOptions { get }
22+
var http: any HTTPClientType { get }
23+
var broadcastURL: URL { get }
24+
25+
func connect() async
26+
func push(_ message: RealtimeMessageV2)
27+
func _getAccessToken() async -> String?
28+
func makeRef() -> String
29+
func _remove(_ channel: any RealtimeChannelProtocol)
30+
}
31+
32+
public final class RealtimeClientV2: Sendable, RealtimeClientProtocol {
2033
struct MutableState {
2134
var accessToken: String?
2235
var ref = 0
@@ -320,7 +333,7 @@ public final class RealtimeClientV2: Sendable {
320333
}
321334
}
322335

323-
func _remove(_ channel: RealtimeChannelV2) {
336+
func _remove(_ channel: any RealtimeChannelProtocol) {
324337
mutableState.withValue {
325338
$0.channels[channel.topic] = nil
326339
}

Supabase.xcworkspace/xcshareddata/xcschemes/Functions.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
33
LastUpgradeVersion = "1510"
4-
version = "1.7">
4+
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"
77
buildImplicitDependencies = "YES">
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// ExportsTests.swift
3+
// Supabase
4+
//
5+
// Created by Guilherme Souza on 29/07/25.
6+
//
7+
8+
import XCTest
9+
10+
@testable import Realtime
11+
12+
final class ExportsTests: XCTestCase {
13+
func testHelperImportIsAccessible() {
14+
// Test that the Helpers module is properly exported
15+
// This is a simple validation that the @_exported import works
16+
17+
// Test that we can access JSONObject from Helpers via Realtime
18+
let jsonObject: JSONObject = [:]
19+
XCTAssertNotNil(jsonObject)
20+
21+
// Test that we can access AnyJSON from Helpers via Realtime
22+
let anyJSON: AnyJSON = .string("test")
23+
XCTAssertEqual(anyJSON, .string("test"))
24+
25+
// Test that we can access ObservationToken from Helpers via Realtime
26+
let token = ObservationToken {
27+
// Empty cleanup
28+
}
29+
XCTAssertNotNil(token)
30+
}
31+
}
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
//
2+
// PostgresActionTests.swift
3+
// Supabase
4+
//
5+
// Created by Guilherme Souza on 29/07/25.
6+
//
7+
8+
import XCTest
9+
10+
@testable import Realtime
11+
12+
final class PostgresActionTests: XCTestCase {
13+
private let sampleMessage = RealtimeMessageV2(
14+
joinRef: nil,
15+
ref: nil,
16+
topic: "test:table",
17+
event: "postgres_changes",
18+
payload: [:]
19+
)
20+
21+
private let sampleColumns = [
22+
Column(name: "id", type: "int8"),
23+
Column(name: "name", type: "text"),
24+
Column(name: "email", type: "text"),
25+
]
26+
27+
private let sampleDate = Date(timeIntervalSince1970: 1_722_246_000) // Fixed timestamp for consistency
28+
29+
func testColumnEquality() {
30+
let column1 = Column(name: "id", type: "int8")
31+
let column2 = Column(name: "id", type: "int8")
32+
let column3 = Column(name: "email", type: "text")
33+
34+
XCTAssertEqual(column1, column2)
35+
XCTAssertNotEqual(column1, column3)
36+
}
37+
38+
func testInsertActionEventType() {
39+
XCTAssertEqual(InsertAction.eventType, .insert)
40+
}
41+
42+
func testInsertActionProperties() {
43+
let record: JSONObject = ["id": .string("123"), "name": .string("John")]
44+
let insertAction = InsertAction(
45+
columns: sampleColumns,
46+
commitTimestamp: sampleDate,
47+
record: record,
48+
rawMessage: sampleMessage
49+
)
50+
51+
XCTAssertEqual(insertAction.columns, sampleColumns)
52+
XCTAssertEqual(insertAction.commitTimestamp, sampleDate)
53+
XCTAssertEqual(insertAction.record, record)
54+
XCTAssertEqual(insertAction.rawMessage.topic, "test:table")
55+
}
56+
57+
func testUpdateActionEventType() {
58+
XCTAssertEqual(UpdateAction.eventType, .update)
59+
}
60+
61+
func testUpdateActionProperties() {
62+
let record: JSONObject = ["id": .string("123"), "name": .string("John Updated")]
63+
let oldRecord: JSONObject = ["id": .string("123"), "name": .string("John")]
64+
65+
let updateAction = UpdateAction(
66+
columns: sampleColumns,
67+
commitTimestamp: sampleDate,
68+
record: record,
69+
oldRecord: oldRecord,
70+
rawMessage: sampleMessage
71+
)
72+
73+
XCTAssertEqual(updateAction.columns, sampleColumns)
74+
XCTAssertEqual(updateAction.commitTimestamp, sampleDate)
75+
XCTAssertEqual(updateAction.record, record)
76+
XCTAssertEqual(updateAction.oldRecord, oldRecord)
77+
XCTAssertEqual(updateAction.rawMessage.topic, "test:table")
78+
}
79+
80+
func testDeleteActionEventType() {
81+
XCTAssertEqual(DeleteAction.eventType, .delete)
82+
}
83+
84+
func testDeleteActionProperties() {
85+
let oldRecord: JSONObject = ["id": .string("123"), "name": .string("John")]
86+
87+
let deleteAction = DeleteAction(
88+
columns: sampleColumns,
89+
commitTimestamp: sampleDate,
90+
oldRecord: oldRecord,
91+
rawMessage: sampleMessage
92+
)
93+
94+
XCTAssertEqual(deleteAction.columns, sampleColumns)
95+
XCTAssertEqual(deleteAction.commitTimestamp, sampleDate)
96+
XCTAssertEqual(deleteAction.oldRecord, oldRecord)
97+
XCTAssertEqual(deleteAction.rawMessage.topic, "test:table")
98+
}
99+
100+
func testAnyActionEventType() {
101+
XCTAssertEqual(AnyAction.eventType, .all)
102+
}
103+
104+
func testAnyActionInsertCase() {
105+
let record: JSONObject = ["id": .string("123"), "name": .string("John")]
106+
let insertAction = InsertAction(
107+
columns: sampleColumns,
108+
commitTimestamp: sampleDate,
109+
record: record,
110+
rawMessage: sampleMessage
111+
)
112+
113+
let anyAction = AnyAction.insert(insertAction)
114+
XCTAssertEqual(anyAction.rawMessage.topic, "test:table")
115+
116+
if case let .insert(wrappedAction) = anyAction {
117+
XCTAssertEqual(wrappedAction.record, record)
118+
} else {
119+
XCTFail("Expected insert case")
120+
}
121+
}
122+
123+
func testAnyActionUpdateCase() {
124+
let record: JSONObject = ["id": .string("123"), "name": .string("John Updated")]
125+
let oldRecord: JSONObject = ["id": .string("123"), "name": .string("John")]
126+
127+
let updateAction = UpdateAction(
128+
columns: sampleColumns,
129+
commitTimestamp: sampleDate,
130+
record: record,
131+
oldRecord: oldRecord,
132+
rawMessage: sampleMessage
133+
)
134+
135+
let anyAction = AnyAction.update(updateAction)
136+
XCTAssertEqual(anyAction.rawMessage.topic, "test:table")
137+
138+
if case let .update(wrappedAction) = anyAction {
139+
XCTAssertEqual(wrappedAction.record, record)
140+
XCTAssertEqual(wrappedAction.oldRecord, oldRecord)
141+
} else {
142+
XCTFail("Expected update case")
143+
}
144+
}
145+
146+
func testAnyActionDeleteCase() {
147+
let oldRecord: JSONObject = ["id": .string("123"), "name": .string("John")]
148+
149+
let deleteAction = DeleteAction(
150+
columns: sampleColumns,
151+
commitTimestamp: sampleDate,
152+
oldRecord: oldRecord,
153+
rawMessage: sampleMessage
154+
)
155+
156+
let anyAction = AnyAction.delete(deleteAction)
157+
XCTAssertEqual(anyAction.rawMessage.topic, "test:table")
158+
159+
if case let .delete(wrappedAction) = anyAction {
160+
XCTAssertEqual(wrappedAction.oldRecord, oldRecord)
161+
} else {
162+
XCTFail("Expected delete case")
163+
}
164+
}
165+
166+
func testAnyActionEquality() {
167+
let record: JSONObject = ["id": .string("123")]
168+
let insertAction1 = InsertAction(
169+
columns: sampleColumns,
170+
commitTimestamp: sampleDate,
171+
record: record,
172+
rawMessage: sampleMessage
173+
)
174+
let insertAction2 = InsertAction(
175+
columns: sampleColumns,
176+
commitTimestamp: sampleDate,
177+
record: record,
178+
rawMessage: sampleMessage
179+
)
180+
181+
let anyAction1 = AnyAction.insert(insertAction1)
182+
let anyAction2 = AnyAction.insert(insertAction2)
183+
184+
XCTAssertEqual(anyAction1, anyAction2)
185+
}
186+
187+
func testDecodeRecord() throws {
188+
struct TestRecord: Codable, Equatable {
189+
let id: String
190+
let name: String
191+
let email: String?
192+
}
193+
194+
let record: JSONObject = [
195+
"id": .string("123"),
196+
"name": .string("John"),
197+
"email": .string("john@example.com"),
198+
]
199+
200+
let insertAction = InsertAction(
201+
columns: sampleColumns,
202+
commitTimestamp: sampleDate,
203+
record: record,
204+
rawMessage: sampleMessage
205+
)
206+
207+
let decoder = JSONDecoder()
208+
let decodedRecord = try insertAction.decodeRecord(as: TestRecord.self, decoder: decoder)
209+
210+
let expectedRecord = TestRecord(id: "123", name: "John", email: "john@example.com")
211+
XCTAssertEqual(decodedRecord, expectedRecord)
212+
}
213+
214+
func testDecodeOldRecord() throws {
215+
struct TestRecord: Codable, Equatable {
216+
let id: String
217+
let name: String
218+
}
219+
220+
let record: JSONObject = ["id": .string("123"), "name": .string("John Updated")]
221+
let oldRecord: JSONObject = ["id": .string("123"), "name": .string("John")]
222+
223+
let updateAction = UpdateAction(
224+
columns: sampleColumns,
225+
commitTimestamp: sampleDate,
226+
record: record,
227+
oldRecord: oldRecord,
228+
rawMessage: sampleMessage
229+
)
230+
231+
let decoder = JSONDecoder()
232+
let decodedOldRecord = try updateAction.decodeOldRecord(as: TestRecord.self, decoder: decoder)
233+
234+
let expectedOldRecord = TestRecord(id: "123", name: "John")
235+
XCTAssertEqual(decodedOldRecord, expectedOldRecord)
236+
}
237+
238+
func testDecodeRecordError() {
239+
struct TestRecord: Codable {
240+
let id: Int // This will cause decode error since we're passing string
241+
let name: String
242+
}
243+
244+
let record: JSONObject = [
245+
"id": .string("not-a-number"), // This should cause decoding to fail
246+
"name": .string("John"),
247+
]
248+
249+
let insertAction = InsertAction(
250+
columns: sampleColumns,
251+
commitTimestamp: sampleDate,
252+
record: record,
253+
rawMessage: sampleMessage
254+
)
255+
256+
let decoder = JSONDecoder()
257+
XCTAssertThrowsError(try insertAction.decodeRecord(as: TestRecord.self, decoder: decoder))
258+
}
259+
}

0 commit comments

Comments
 (0)