Skip to content

Commit 30b23ba

Browse files
committed
test: mock channel and add more tests for PushV2
1 parent 7a19f29 commit 30b23ba

File tree

4 files changed

+308
-7
lines changed

4 files changed

+308
-7
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
}

Tests/RealtimeTests/PushV2Tests.swift

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Created by Guilherme Souza on 29/07/25.
66
//
77

8+
import ConcurrencyExtras
89
import XCTest
910

1011
@testable import Realtime
@@ -56,4 +57,283 @@ final class PushV2Tests: XCTestCase {
5657

5758
XCTAssertEqual(status, .error)
5859
}
60+
61+
@MainActor
62+
func testSendWithAckDisabledReturnsOkImmediately() async {
63+
let mockSocket = MockRealtimeClient()
64+
let config = RealtimeChannelConfig(
65+
broadcast: BroadcastJoinConfig(acknowledgeBroadcasts: false, receiveOwnBroadcasts: false),
66+
presence: PresenceJoinConfig(key: "", enabled: false),
67+
isPrivate: false
68+
)
69+
let mockChannel = MockRealtimeChannel(
70+
topic: "test:channel",
71+
config: config,
72+
socket: mockSocket,
73+
logger: nil
74+
)
75+
76+
let sampleMessage = RealtimeMessageV2(
77+
joinRef: "ref1",
78+
ref: "ref2",
79+
topic: "test:channel",
80+
event: "broadcast",
81+
payload: ["data": "test"]
82+
)
83+
84+
let push = PushV2(channel: mockChannel, message: sampleMessage)
85+
let status = await push.send()
86+
87+
XCTAssertEqual(status, PushStatus.ok)
88+
XCTAssertEqual(mockSocket.pushedMessages.count, 1)
89+
XCTAssertEqual(mockSocket.pushedMessages.first?.topic, "test:channel")
90+
XCTAssertEqual(mockSocket.pushedMessages.first?.event, "broadcast")
91+
}
92+
93+
@MainActor
94+
func testSendWithAckEnabledWaitsForResponse() async {
95+
let mockSocket = MockRealtimeClient()
96+
let config = RealtimeChannelConfig(
97+
broadcast: BroadcastJoinConfig(acknowledgeBroadcasts: true, receiveOwnBroadcasts: false),
98+
presence: PresenceJoinConfig(key: "", enabled: false),
99+
isPrivate: false
100+
)
101+
let mockChannel = MockRealtimeChannel(
102+
topic: "test:channel",
103+
config: config,
104+
socket: mockSocket,
105+
logger: nil
106+
)
107+
108+
let sampleMessage = RealtimeMessageV2(
109+
joinRef: "ref1",
110+
ref: "ref2",
111+
topic: "test:channel",
112+
event: "broadcast",
113+
payload: ["data": "test"]
114+
)
115+
116+
let push = PushV2(channel: mockChannel, message: sampleMessage)
117+
118+
let sendTask = Task {
119+
await push.send()
120+
}
121+
122+
// Give push time to start waiting
123+
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
124+
125+
// Simulate receiving acknowledgment
126+
push.didReceive(status: PushStatus.ok)
127+
128+
let status = await sendTask.value
129+
XCTAssertEqual(status, PushStatus.ok)
130+
XCTAssertEqual(mockSocket.pushedMessages.count, 1)
131+
}
132+
133+
@MainActor
134+
func testChannelConfigurationForAcknowledgment() {
135+
// Test that the channel configuration is properly checked for acknowledgment settings
136+
let mockSocket = MockRealtimeClient()
137+
138+
// Test acknowledgment disabled
139+
let configAckDisabled = RealtimeChannelConfig(
140+
broadcast: BroadcastJoinConfig(acknowledgeBroadcasts: false, receiveOwnBroadcasts: false),
141+
presence: PresenceJoinConfig(key: "", enabled: false),
142+
isPrivate: false
143+
)
144+
let channelAckDisabled = MockRealtimeChannel(
145+
topic: "test:channel",
146+
config: configAckDisabled,
147+
socket: mockSocket,
148+
logger: nil
149+
)
150+
XCTAssertFalse(channelAckDisabled.config.broadcast.acknowledgeBroadcasts)
151+
152+
// Test acknowledgment enabled
153+
let configAckEnabled = RealtimeChannelConfig(
154+
broadcast: BroadcastJoinConfig(acknowledgeBroadcasts: true, receiveOwnBroadcasts: false),
155+
presence: PresenceJoinConfig(key: "", enabled: false),
156+
isPrivate: false
157+
)
158+
let channelAckEnabled = MockRealtimeChannel(
159+
topic: "test:channel",
160+
config: configAckEnabled,
161+
socket: mockSocket,
162+
logger: nil
163+
)
164+
XCTAssertTrue(channelAckEnabled.config.broadcast.acknowledgeBroadcasts)
165+
}
166+
167+
@MainActor
168+
func testSendWithAckEnabledReceivesError() async {
169+
let mockSocket = MockRealtimeClient()
170+
let config = RealtimeChannelConfig(
171+
broadcast: BroadcastJoinConfig(acknowledgeBroadcasts: true, receiveOwnBroadcasts: false),
172+
presence: PresenceJoinConfig(key: "", enabled: false),
173+
isPrivate: false
174+
)
175+
let mockChannel = MockRealtimeChannel(
176+
topic: "test:channel",
177+
config: config,
178+
socket: mockSocket,
179+
logger: nil
180+
)
181+
182+
let sampleMessage = RealtimeMessageV2(
183+
joinRef: "ref1",
184+
ref: "ref2",
185+
topic: "test:channel",
186+
event: "broadcast",
187+
payload: ["data": "test"]
188+
)
189+
190+
let push = PushV2(channel: mockChannel, message: sampleMessage)
191+
192+
let sendTask = Task {
193+
await push.send()
194+
}
195+
196+
// Give push time to start waiting
197+
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
198+
199+
// Simulate receiving error acknowledgment
200+
push.didReceive(status: PushStatus.error)
201+
202+
let status = await sendTask.value
203+
XCTAssertEqual(status, PushStatus.error)
204+
XCTAssertEqual(mockSocket.pushedMessages.count, 1)
205+
}
206+
207+
@MainActor
208+
func testDidReceiveStatusWithoutWaitingDoesNothing() {
209+
let sampleMessage = RealtimeMessageV2(
210+
joinRef: "ref1",
211+
ref: "ref2",
212+
topic: "test:channel",
213+
event: "broadcast",
214+
payload: ["data": "test"]
215+
)
216+
217+
let push = PushV2(channel: nil, message: sampleMessage)
218+
219+
// This should not crash or cause issues
220+
push.didReceive(status: PushStatus.ok)
221+
push.didReceive(status: PushStatus.error)
222+
push.didReceive(status: PushStatus.timeout)
223+
}
224+
225+
@MainActor
226+
func testMultipleDidReceiveCallsOnlyFirstMatters() async {
227+
let mockSocket = MockRealtimeClient()
228+
let config = RealtimeChannelConfig(
229+
broadcast: BroadcastJoinConfig(acknowledgeBroadcasts: true, receiveOwnBroadcasts: false),
230+
presence: PresenceJoinConfig(key: "", enabled: false),
231+
isPrivate: false
232+
)
233+
let mockChannel = MockRealtimeChannel(
234+
topic: "test:channel",
235+
config: config,
236+
socket: mockSocket,
237+
logger: nil
238+
)
239+
240+
let sampleMessage = RealtimeMessageV2(
241+
joinRef: "ref1",
242+
ref: "ref2",
243+
topic: "test:channel",
244+
event: "broadcast",
245+
payload: ["data": "test"]
246+
)
247+
248+
let push = PushV2(channel: mockChannel, message: sampleMessage)
249+
250+
let sendTask = Task {
251+
await push.send()
252+
}
253+
254+
// Give push time to start waiting
255+
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
256+
257+
// First response should be used
258+
push.didReceive(status: PushStatus.ok)
259+
260+
// Subsequent responses should be ignored
261+
push.didReceive(status: PushStatus.error)
262+
push.didReceive(status: PushStatus.timeout)
263+
264+
let status = await sendTask.value
265+
XCTAssertEqual(status, PushStatus.ok) // Should be .ok, not .error or .timeout
266+
}
267+
}
268+
269+
// MARK: - Mock Objects
270+
271+
@MainActor
272+
private final class MockRealtimeChannel: RealtimeChannelProtocol {
273+
let topic: String
274+
var config: RealtimeChannelConfig
275+
let socket: any RealtimeClientProtocol
276+
let logger: (any SupabaseLogger)?
277+
278+
init(
279+
topic: String,
280+
config: RealtimeChannelConfig,
281+
socket: any RealtimeClientProtocol,
282+
logger: (any SupabaseLogger)?
283+
) {
284+
self.topic = topic
285+
self.config = config
286+
self.socket = socket
287+
self.logger = logger
288+
}
289+
}
290+
291+
private final class MockRealtimeClient: RealtimeClientProtocol, @unchecked Sendable {
292+
private let _pushedMessages = LockIsolated<[RealtimeMessageV2]>([])
293+
private let _status = LockIsolated<RealtimeClientStatus>(.connected)
294+
let options: RealtimeClientOptions
295+
let http: any HTTPClientType = MockHTTPClient()
296+
let broadcastURL = URL(string: "https://test.supabase.co/api/broadcast")!
297+
298+
var status: RealtimeClientStatus {
299+
_status.value
300+
}
301+
302+
init(timeoutInterval: TimeInterval = 10.0) {
303+
self.options = RealtimeClientOptions(
304+
timeoutInterval: timeoutInterval
305+
)
306+
}
307+
308+
var pushedMessages: [RealtimeMessageV2] {
309+
_pushedMessages.value
310+
}
311+
312+
func connect() async {
313+
_status.setValue(.connected)
314+
}
315+
316+
func push(_ message: RealtimeMessageV2) {
317+
_pushedMessages.withValue { messages in
318+
messages.append(message)
319+
}
320+
}
321+
322+
func _getAccessToken() async -> String? {
323+
return nil
324+
}
325+
326+
func makeRef() -> String {
327+
return UUID().uuidString
328+
}
329+
330+
func _remove(_ channel: any RealtimeChannelProtocol) {
331+
// No-op for mock
332+
}
333+
}
334+
335+
private struct MockHTTPClient: HTTPClientType {
336+
func send(_ request: HTTPRequest) async throws -> HTTPResponse {
337+
return HTTPResponse(data: Data(), response: HTTPURLResponse())
338+
}
59339
}

0 commit comments

Comments
 (0)