Skip to content

Commit

Permalink
test: create test cases to test closing and reconnecting WebSocket
Browse files Browse the repository at this point in the history
  • Loading branch information
pokryfka committed Oct 26, 2023
1 parent 5255993 commit 7f2b53f
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 20 deletions.
6 changes: 3 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ let package = Package(
name: "swift-graphql",
platforms: [
.iOS(.v15),
.macOS(.v10_15),
.tvOS(.v13),
.watchOS(.v6)
.macOS(.v12),
.tvOS(.v15),
.watchOS(.v8)
],
products: [
// SwiftGraphQL
Expand Down
11 changes: 7 additions & 4 deletions Sources/GraphQLWebSocket/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public class GraphQLWebSocket: WebSocketDelegate {
private var socket: WebSocket?

/// Holds information about the connection health and what the client is doing about it.
private var health: Health = Health.notconnected
private(set) var health: Health = Health.notconnected

private enum Health: Equatable {
enum Health: Equatable {

/// Connection is healthy and client can communicate with the server.
case acknowledged
Expand Down Expand Up @@ -124,7 +124,7 @@ public class GraphQLWebSocket: WebSocketDelegate {
/// query identifier that the client used to identify the subscription.
///
/// - NOTE: We also use pipelines to tell how many ongoing connections the client is managing.
private var pipelines = [String: AnyCancellable]()
private(set) var pipelines = [String: AnyCancellable]()

// MARK: - Initializer

Expand Down Expand Up @@ -280,7 +280,7 @@ public class GraphQLWebSocket: WebSocketDelegate {

/// Correctly closes the connection with the server.
private func close(code: UInt16) {
self.config.logger.debug("Connection with the server closed (\(code))!")
self.config.logger.debug("Connection with the server closed (\(code))!") // TODO: no, its not!

// The server shouldn't reconnect, we tell all listenerss to stop listening.
guard shouldRetryToConnect(code: code) else {
Expand Down Expand Up @@ -492,6 +492,9 @@ public class GraphQLWebSocket: WebSocketDelegate {

/// Correctly closes the connection with the server.
public func close(code: CloseCode = .normalClosure) {
// NOTE: this does NOT close connection if there are not complete subscriptions
// TODO: does it make sense to expose it? this is confusing, add `force` flag at the very least?
// TODO: does it make sense to allow client set closeCode?
close(code: code.rawValue)
}

Expand Down
142 changes: 129 additions & 13 deletions Tests/GraphQLWebSocketTests/ClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import GraphQL
@testable import GraphQLWebSocket
import XCTest

// NOTE: start local test server before running the tests


@available(macOS 12, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final class ClientTests: XCTestCase {

private var cancellables = Set<AnyCancellable>()

/// Returns the execution arguments for the counter subscription.
private func count(from: Int, to: Int) -> ExecutionArgs {
let args = ExecutionArgs(
Expand Down Expand Up @@ -38,44 +34,164 @@ final class ClientTests: XCTestCase {
let request = URLRequest(url: URL(string: "ws://127.0.0.1:4000/graphql")!)
let client = GraphQLWebSocket(request: request)

var cancellables = Set<AnyCancellable>()

client.onEvent()
.compactMap({ msg -> Error? in
.compactMap { msg -> Error? in
switch msg {
case .error(let err):
return err
default:
return nil
}
})
.sink { err in
}
.sink { _ in
XCTFail()
}
.store(in: &self.cancellables)
.store(in: &cancellables)

var xs = [Int]()
var ys = [Int]()

// We parallely check two GraphQL subscriptions.
client.subscribe(self.count(from: 10, to: 5))
.sink { completion in
.sink { _ in
xsexpect.fulfill()
} receiveValue: { result in
xs.append(self.decode(result))
}
.store(in: &self.cancellables)
.store(in: &cancellables)

client.subscribe(self.count(from: 100, to: 105))
.sink { completion in
.sink { _ in
ysexpect.fulfill()
} receiveValue: { result in
ys.append(self.decode(result))
}
.store(in: &self.cancellables)
.store(in: &cancellables)

waitForExpectations(timeout: 5)

XCTAssertEqual([10, 9, 8, 7, 6], xs)
XCTAssertEqual([100, 101, 102, 103, 104], ys)
}

func testWebSocketCloseAfterComplete() throws {
let subComplete = expectation(description: "subscription complete")

let request = URLRequest(url: URL(string: "ws://127.0.0.1:4000/graphql")!)
let config = GraphQLWebSocketConfiguration()
config.logger.logLevel = .debug
config.behaviour = .eager
let client = GraphQLWebSocket(request: request, config: config)

XCTAssertEqual(client.health, .connecting)

var cancellables = Set<AnyCancellable>()

let conClosed = expectation(description: "connection closed")
client.onEvent()
.sink { event in
switch event {
case .closed:
conClosed.fulfill()
default:
break
}
}
.store(in: &cancellables)

// start a subscription which will be commpleted within 5 seconds
client.subscribe(self.count(from: 10, to: 8))
.sink { _ in
XCTAssertEqual(client.health, .acknowledged)
subComplete.fulfill()
} receiveValue: { _ in
XCTAssertEqual(client.health, .acknowledged)
}
.store(in: &cancellables)

// wait for the subscriptions to complete
wait(for: [subComplete], timeout: 5)

XCTAssertEqual(client.health, .acknowledged)

// wait for socket to be closed
// note that the current implementation will not allow closing
// if there are not complete subscriptions
XCTAssertTrue(client.pipelines.isEmpty)
client.close()

wait(for: [conClosed], timeout: 5)

XCTAssertEqual(client.health, .notconnected)
}

func testWebSocketReconnect() throws {
let subComplete = expectation(description: "subscription complete")

let request = URLRequest(url: URL(string: "ws://127.0.0.1:4000/graphql")!)
let config = GraphQLWebSocketConfiguration()
config.logger.logLevel = .debug
config.behaviour = .eager
let client = GraphQLWebSocket(request: request, config: config)

XCTAssertEqual(client.health, .connecting)

var cancellables = Set<AnyCancellable>()

let conClosed = expectation(description: "connection closed")
client.onEvent()
.sink { event in
switch event {
case .closed:
conClosed.fulfill()
default:
break
}
}
.store(in: &cancellables)

// start a subscription which will be commpleted within 5 seconds
client.subscribe(self.count(from: 10, to: 8))
.sink { _ in
XCTAssertEqual(client.health, .acknowledged)
subComplete.fulfill()
} receiveValue: { _ in
XCTAssertEqual(client.health, .acknowledged)
}
.store(in: &cancellables)

// wait for the subscriptions to complete
wait(for: [subComplete], timeout: 5)

XCTAssertEqual(client.health, .acknowledged)

// wait for socket to be closed
// note that the current implementation will not allow closing
// if there are not complete subscriptions
XCTAssertTrue(client.pipelines.isEmpty)
client.close()

wait(for: [conClosed], timeout: 5)

XCTAssertEqual(client.health, .notconnected)

// start new subscription which will try to reconnect, it should complate withing 5 seconds
let sub2Complete = expectation(description: "subscription2 complete")
client.subscribe(self.count(from: 10, to: 8))
.sink { _ in
XCTAssertEqual(client.health, .acknowledged)
sub2Complete.fulfill()
} receiveValue: { _ in
XCTAssertEqual(client.health, .acknowledged)
}
.store(in: &cancellables)

// wait for the subscriptions to complete
wait(for: [sub2Complete], timeout: 5)

XCTAssertEqual(client.health, .acknowledged)
XCTAssertTrue(client.pipelines.isEmpty)
}
}

0 comments on commit 7f2b53f

Please sign in to comment.