Skip to content

Commit

Permalink
test(realtime): add tests for connection and disconnection
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev committed Nov 24, 2023
1 parent 4872f10 commit a780a02
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 23 deletions.
22 changes: 22 additions & 0 deletions Sources/Realtime/Dependencies.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Dependencies.swift
//
//
// Created by Guilherme Souza on 24/11/23.
//

import Foundation

enum Dependencies {
static var timeoutTimer: () -> TimeoutTimerProtocol = {
TimeoutTimer()
}

static var heartbeatTimer: (
_ timeInterval: TimeInterval,
_ queue: DispatchQueue,
_ leeway: DispatchTimeInterval
) -> HeartbeatTimerProtocol = {
HeartbeatTimer(timeInterval: $0, queue: $1, leeway: $2)
}
}
7 changes: 6 additions & 1 deletion Sources/Realtime/HeartbeatTimer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ import Foundation
queue but guarantees thread safety.
*/

class HeartbeatTimer {
protocol HeartbeatTimerProtocol {
func start(eventHandler: @escaping () -> Void)
func stop()
}

class HeartbeatTimer: HeartbeatTimerProtocol {
// ----------------------------------------------------------------------

// MARK: - Dependencies
Expand Down
3 changes: 1 addition & 2 deletions Sources/Realtime/PhoenixTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import Foundation
/**
Defines a `Socket`'s Transport layer.
*/
// sourcery: AutoMockable
public protocol PhoenixTransport {
/// The current `ReadyState` of the `Transport` layer
var readyState: PhoenixTransportReadyState { get }
Expand Down Expand Up @@ -67,7 +66,7 @@ public protocol PhoenixTransport {

// ----------------------------------------------------------------------
/// Delegate to receive notifications of events that occur in the `Transport` layer
public protocol PhoenixTransportDelegate {
public protocol PhoenixTransportDelegate: AnyObject {
/**
Notified when the `Transport` opens.

Expand Down
20 changes: 8 additions & 12 deletions Sources/Realtime/RealtimeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,13 @@ public class RealtimeClient: PhoenixTransportDelegate {
var ref: UInt64 = .min // 0 (max: 18,446,744,073,709,551,615)

/// Timer that triggers sending new Heartbeat messages
var heartbeatTimer: HeartbeatTimer?
var heartbeatTimer: HeartbeatTimerProtocol?

/// Ref counter for the last heartbeat that was sent
var pendingHeartbeatRef: String?

/// Timer to use when attempting to reconnect
var reconnectTimer: TimeoutTimer
var reconnectTimer: TimeoutTimerProtocol

/// Close status
var closeStatus: CloseStatus = .unknown
Expand Down Expand Up @@ -209,7 +209,7 @@ public class RealtimeClient: PhoenixTransportDelegate {
vsn: vsn
)

reconnectTimer = TimeoutTimer()
reconnectTimer = Dependencies.timeoutTimer()
reconnectTimer.callback = { [weak self] in
self?.logItems("Socket attempting to reconnect")
self?.teardown(reason: "reconnection")
Expand Down Expand Up @@ -277,14 +277,6 @@ public class RealtimeClient: PhoenixTransportDelegate {
// Reset the close status when attempting to connect
closeStatus = .unknown

// We need to build this right before attempting to connect as the
// parameters could be built upon demand and change over time
endpointUrl = RealtimeClient.buildEndpointUrl(
url: url,
params: params,
vsn: vsn
)

connection = transport(endpointUrl)
connection?.delegate = self
// self.connection?.disableSSLCertValidation = disableSSLCertValidation
Expand Down Expand Up @@ -737,7 +729,11 @@ public class RealtimeClient: PhoenixTransportDelegate {
// Do not start up the heartbeat timer if skipHeartbeat is true
guard !skipHeartbeat else { return }

heartbeatTimer = HeartbeatTimer(timeInterval: heartbeatInterval, leeway: heartbeatLeeway)
heartbeatTimer = Dependencies.heartbeatTimer(
heartbeatInterval,
Defaults.heartbeatQueue,
heartbeatLeeway
)
heartbeatTimer?.start(eventHandler: { [weak self] in
self?.sendHeartbeat()
})
Expand Down
12 changes: 10 additions & 2 deletions Sources/Realtime/TimeoutTimer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@

import Foundation

class TimeoutTimer {
protocol TimeoutTimerProtocol {
var callback: @Sendable () -> Void { get set }
var timerCalculation: @Sendable (Int) -> TimeInterval { get set }

func reset()
func scheduleTimeout()
}

class TimeoutTimer: TimeoutTimerProtocol {
/// Callback to be informed when the underlying Timer fires
var callback: @Sendable () -> Void = {}

Expand Down Expand Up @@ -93,7 +101,7 @@ class TimeoutTimer {
/// Wrapper class around a DispatchQueue. Allows for providing a fake clock
/// during tests.
class TimerQueue {
// Can be overriden in tests
// Can be overridden in tests
static var main = TimerQueue()

func queue(timeInterval: TimeInterval, execute: DispatchWorkItem) {
Expand Down
140 changes: 134 additions & 6 deletions Tests/RealtimeTests/RealtimeClientTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ConcurrencyExtras
import XCTest
@_spi(Internal) import _Helpers
@testable import Realtime
Expand Down Expand Up @@ -104,16 +105,143 @@ final class RealtimeClientTests: XCTestCase {

XCTAssertEqual(resultUrl.query, "vsn=1.0")
}

func testConnect() throws {
let (_, sut, _) = makeSUT()

XCTAssertNil(sut.connection, "connection should be nil before calling connect method.")

sut.connect()
XCTAssertEqual(sut.closeStatus, .unknown)

let connection = try XCTUnwrap(sut.connection as? PhoenixTransportMock)
XCTAssertIdentical(connection.delegate, sut)

XCTAssertEqual(connection.connectHeaders, sut.headers)

// Given readyState = .open
connection.readyState = .open

// When calling connect
sut.connect()

// Verify that transport's connect was called only once (first connect call).
XCTAssertEqual(connection.connectCallCount, 1)
}

func testDisconnect() async throws {
let timeoutTimer = TimeoutTimerMock()
Dependencies.timeoutTimer = { timeoutTimer }

let heartbeatTimer = HeartbeatTimerMock()
Dependencies.heartbeatTimer = { _, _, _ in
heartbeatTimer
}

let (_, sut, transport) = makeSUT()

let expectation = expectation(description: "onClose")
let onCloseReceivedParams = LockIsolated<(Int, String?)?>(nil)
sut.onClose { code, reason in
onCloseReceivedParams.setValue((code, reason))
expectation.fulfill()
}

sut.connect()

XCTAssertEqual(sut.closeStatus, .unknown)
sut.disconnect(code: .normal, reason: "test")

XCTAssertEqual(sut.closeStatus, .clean)

XCTAssertEqual(timeoutTimer.resetCallCount, 2)

XCTAssertNil(sut.connection)
XCTAssertNil(transport.delegate)
XCTAssertEqual(transport.disconnectCallCount, 1)
XCTAssertEqual(transport.disconnectCode, 1000)
XCTAssertEqual(transport.disconnectReason, "test")

await fulfillment(of: [expectation])

let (code, reason) = try XCTUnwrap(onCloseReceivedParams.value)
XCTAssertEqual(code, 1000)
XCTAssertEqual(reason, "test")

XCTAssertEqual(heartbeatTimer.stopCallCount, 1)
}
}

final class PhoenixTransportMock: PhoenixTransport {
var readyState: Realtime.PhoenixTransportReadyState = .closed
class PhoenixTransportMock: PhoenixTransport {
var readyState: PhoenixTransportReadyState = .closed
var delegate: PhoenixTransportDelegate?

var delegate: Realtime.PhoenixTransportDelegate?
private(set) var connectCallCount = 0
private(set) var disconnectCallCount = 0
private(set) var sendCallCount = 0

func connect(with _: [String: String]) {}
private(set) var connectHeaders: [String: String]?
private(set) var disconnectCode: Int?
private(set) var disconnectReason: String?
private(set) var sendData: Data?

func disconnect(code _: Int, reason _: String?) {}
func connect(with headers: [String: String]) {
connectCallCount += 1
connectHeaders = headers

func send(data _: Data) {}
delegate?.onOpen(response: nil)
}

func disconnect(code: Int, reason: String?) {
disconnectCallCount += 1
disconnectCode = code
disconnectReason = reason

delegate?.onClose(code: code, reason: reason)
}

func send(data: Data) {
sendCallCount += 1
sendData = data

delegate?.onMessage(message: data)
}
}

class TimeoutTimerMock: TimeoutTimerProtocol {
var callback: @Sendable () -> Void = {}
var timerCalculation: @Sendable (Int) -> TimeInterval = { _ in 0.0 }

private(set) var resetCallCount = 0
private(set) var scheduleTimeoutCallCount = 0

func reset() {
resetCallCount += 1
}

func scheduleTimeout() {
scheduleTimeoutCallCount += 1
}
}

class HeartbeatTimerMock: HeartbeatTimerProtocol {
private(set) var startCallCount = 0
private(set) var stopCallCount = 0
private var eventHandler: (() -> Void)?

func start(eventHandler: @escaping () -> Void) {
startCallCount += 1
self.eventHandler = eventHandler
// You can add additional assertions or logging here based on your testing needs
}

func stop() {
stopCallCount += 1
// You can add additional assertions or logging here based on your testing needs
}

// Helper method to simulate the timer firing an event
func simulateTimerEvent() {
eventHandler?()
}
}

0 comments on commit a780a02

Please sign in to comment.