// Copyright 2024 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Logging import NIO import Nats import NatsServer import XCTest class CoreNatsTests: XCTestCase { static var allTests = [ ("testRtt", testRtt), ("testPublish", testPublish), ("testSuspendAndResume", testSuspendAndResume), ("testForceReconnect", testForceReconnect), ("testConnectMultipleURLsOneIsValid", testConnectMultipleURLsOneIsValid), ("testConnectMultipleURLsRetainOrder", testConnectMultipleURLsRetainOrder), ("testConnectDNSError", testConnectDNSError), ("testRetryOnFailedConnect", testRetryOnFailedConnect), ("testPublishWithReply", testPublishWithReply), ("testPublishWithReplyOnCustomInbox", testPublishWithReplyOnCustomInbox), ("testSubscribe", testSubscribe), ("testUnsubscribe", testUnsubscribe), ("testUnsubscribeAfter", testUnsubscribeAfter), ("testConnect", testConnect), ("testReconnect", testReconnect), ("testUsernameAndPassword", testUsernameAndPassword), ("testTokenAuth", testTokenAuth), ("testCredentialsAuth", testCredentialsAuth), ("testNkeyAuth", testNkeyAuth), ("testNkeyAuthFile", testNkeyAuthFile), ("testMutualTls", testMutualTls), ("testTlsFirst", testTlsFirst), ("testInvalidCertificate", testInvalidCertificate), ("testWebsocket", testWebsocket), ("testWebsocketTLS", testWebsocketTLS), ("testLameDuckMode", testLameDuckMode), ("testRequest", testRequest), ("testRequestCustomInbox", testRequestCustomInbox), ("testRequest_noResponders", testRequest_noResponders), ("testRequest_permissionDenied", testRequest_permissionDenied), ("testConcurrentChannelActiveAndRead", testConcurrentChannelActiveAndRead), ("testRequest_timeout", testRequest_timeout), ("testPublishOnClosedConnection", testPublishOnClosedConnection), ("testCloseClosedConnection", testCloseClosedConnection), ("testSuspendClosedConnection", testSuspendClosedConnection), ("testReconnectOnClosedConnection", testReconnectOnClosedConnection), ("testSubscribeMissingPermissions", testSubscribeMissingPermissions), ("testSubscribePermissionsRevoked", testSubscribePermissionsRevoked), ] var natsServer = NatsServer() override func tearDown() { super.tearDown() natsServer.stop() } func testRtt() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let rtt: TimeInterval = try await client.rtt() XCTAssertGreaterThan(rtt, 0, "should have RTT") try await client.close() } func testPublish() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let expectation = XCTestExpectation(description: "Should receive message in 5 seconsd") let iter = sub.makeAsyncIterator() Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") expectation.fulfill() } } await fulfillment(of: [expectation], timeout: 5.0) try await client.close() } func testSuspendAndResume() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let expectation = XCTestExpectation(description: "Should receive message in 5 seconsd") let iter = sub.makeAsyncIterator() Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") expectation.fulfill() } } await fulfillment(of: [expectation], timeout: 5.0) let reconnectExpectation = XCTestExpectation(description: "Should reconnect in 5 seconds") let suspendedExpectation = XCTestExpectation(description: "Should disconnect in 5 seconds") client.on([.suspended, .connected]) { event in if event.kind() == .suspended { suspendedExpectation.fulfill() } if event.kind() == .connected { reconnectExpectation.fulfill() } } try await client.suspend() await fulfillment(of: [suspendedExpectation], timeout: 5.0) try await client.resume() await fulfillment(of: [reconnectExpectation], timeout: 5.0) try await client.publish("msg".data(using: .utf8)!, subject: "test") let expectation1 = XCTestExpectation(description: "Should receive message in 5 seconsd") Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") expectation1.fulfill() } } await fulfillment(of: [expectation1], timeout: 5.0) try await client.close() } func testForceReconnect() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let expectation = XCTestExpectation(description: "Should receive message in 5 seconsd") let iter = sub.makeAsyncIterator() Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") expectation.fulfill() } } await fulfillment(of: [expectation], timeout: 5.0) let reconnectExpectation = XCTestExpectation(description: "Should reconnect in 5 seconds") let suspendedExpectation = XCTestExpectation(description: "Should disconnect in 5 seconds") client.on([.suspended, .connected]) { event in if event.kind() == .suspended { suspendedExpectation.fulfill() } if event.kind() == .connected { reconnectExpectation.fulfill() } } try await client.reconnect() await fulfillment(of: [suspendedExpectation], timeout: 5.0) await fulfillment(of: [reconnectExpectation], timeout: 5.0) try await client.publish("msg".data(using: .utf8)!, subject: "test") let expectation1 = XCTestExpectation(description: "Should receive message in 5 seconsd") Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") expectation1.fulfill() } } await fulfillment(of: [expectation1], timeout: 5.0) try await client.close() } func testConnectMultipleURLsOneIsValid() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .urls([ URL(string: natsServer.clientURL)!, URL(string: "nats://localhost:4344")!, URL(string: "nats://localhost:4343")!, ]) .build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let expectation = XCTestExpectation(description: "Should receive message in 5 seconsd") let iter = sub.makeAsyncIterator() Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") expectation.fulfill() } } await fulfillment(of: [expectation], timeout: 5.0) try await client.close() } func testConnectMultipleURLsRetainOrder() async throws { natsServer.start() let natsServer2 = NatsServer() natsServer2.start() logger.logLevel = .critical for _ in 0..<10 { let client = NatsClientOptions() .urls([URL(string: natsServer2.clientURL)!, URL(string: natsServer.clientURL)!]) .retainServersOrder() .build() try await client.connect() XCTAssertEqual(client.connectedUrl, URL(string: natsServer2.clientURL)) try await client.close() } } func testConnectDNSError() async throws { logger.logLevel = .critical let client = NatsClientOptions() .urls([URL(string: "nats://invalid:1234")!]) .build() do { try await client.connect() } catch NatsError.ConnectError.dns(_) { return } catch { XCTFail("Expeted dns lookup error; got: \(error)") } XCTFail("Expeted dns lookup error") } func testConnectNIOError() async throws { logger.logLevel = .critical let client = NatsClientOptions() .urls([URL(string: "nats://localhost:4321")!]) .build() do { // should be connection refused error try await client.connect() } catch NatsError.ConnectError.io(_) { return } catch { XCTFail("Expeted IO lookup error; got: \(error)") } XCTFail("Expeted io lookup error") } func testRetryOnFailedConnect() async throws { let client = NatsClientOptions() .url(URL(string: "nats://localhost:4321")!) .reconnectWait(1) .retryOnfailedConnect() .build() let expectation = XCTestExpectation( description: "client was not notified of connection established event") client.on(.connected) { event in expectation.fulfill() } try await client.connect() natsServer.start(port: 4321) await fulfillment(of: [expectation], timeout: 5.0) } func testPublishWithReply() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test", reply: "reply") let expectation = XCTestExpectation(description: "Should receive message in 5 seconsd") let iter = sub.makeAsyncIterator() Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") XCTAssertEqual(msg.replySubject, "reply") expectation.fulfill() } } await fulfillment(of: [expectation], timeout: 5.0) } func testPublishWithReplyOnCustomInbox() async throws { natsServer.start() logger.logLevel = .debug let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .inboxPrefix("_INBOX_foo") .build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish( "msg".data(using: .utf8)!, subject: "test", reply: client.newInbox()) let expectation = XCTestExpectation(description: "Should receive message in 5 seconds") let iter = sub.makeAsyncIterator() Task { if let msg = try await iter.next() { XCTAssertEqual(msg.subject, "test") XCTAssertTrue(msg.replySubject?.starts(with: "_INBOX_foo.") == true) expectation.fulfill() } } await fulfillment(of: [expectation], timeout: 5.0) } func testSubscribe() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let iter = sub.makeAsyncIterator() let message = try await iter.next() XCTAssertEqual(message?.payload, "msg".data(using: .utf8)!) } func testQueueGroupSubscribe() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() let sub1 = try await client.subscribe(subject: "test", queue: "queueGroup") let sub2 = try await client.subscribe(subject: "test", queue: "queueGroup") try await client.publish("msg".data(using: .utf8)!, subject: "test") try await withThrowingTaskGroup(of: NatsMessage?.self) { group in group.addTask { try await sub1.makeAsyncIterator().next() } group.addTask { try await sub2.makeAsyncIterator().next() } group.addTask { try await Task.sleep(nanoseconds: UInt64(1_000_000_000)) return nil } var msgReceived = false var timeoutReceived = false for try await result in group { if let _ = result { if msgReceived == true { XCTFail("received 2 messages") return } msgReceived = true } else { if !msgReceived { XCTFail("timeout received before getting any messages") return } timeoutReceived = true } if msgReceived && timeoutReceived { break } } group.cancelAll() try await sub1.unsubscribe() try await sub2.unsubscribe() return } } func testUnsubscribe() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let iter = sub.makeAsyncIterator() var message = try await iter.next() XCTAssertEqual(message?.payload, "msg".data(using: .utf8)!) try await client.publish("msg".data(using: .utf8)!, subject: "test") try await sub.unsubscribe() message = try await iter.next() XCTAssertNil(message) do { try await sub.unsubscribe() } catch NatsError.SubscriptionError.subscriptionClosed { return } XCTFail("Expected subscription closed error") } func testUnsubscribeAfter() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await sub.unsubscribe(after: 3) for _ in 0..<5 { try await client.publish("msg".data(using: .utf8)!, subject: "test") } var i = 0 for try await _ in sub { i += 1 } XCTAssertEqual(i, 3, "Expected 3 messages to be delivered") try await client.close() } func testConnect() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() XCTAssertNotNil(client, "Client should not be nil") } func testReconnect() async throws { natsServer.start() let port = natsServer.port! logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .reconnectWait(1) .build() try await client.connect() // Payload to publish let payload = "hello".data(using: .utf8)! var messagesReceived = 0 let sub = try! await client.subscribe(subject: "foo") // publish some messages Task { for _ in 0..<10 { try await client.publish(payload, subject: "foo") } } // make sure sub receives messages for try await _ in sub { messagesReceived += 1 if messagesReceived == 10 { break } } let expectation = XCTestExpectation( description: "client was not notified of connection established event") client.on(.connected) { event in expectation.fulfill() } // restart the server natsServer.stop() sleep(1) natsServer.start(port: port) await fulfillment(of: [expectation], timeout: 10.0) // publish more messages, sub should receive them Task { for _ in 0..<10 { try await client.publish(payload, subject: "foo") } } for try await _ in sub { messagesReceived += 1 if messagesReceived == 20 { break } } // Check if the total number of messages received matches the number sent XCTAssertEqual(20, messagesReceived, "Mismatch in the number of messages sent and received") try await client.close() } func testUsernameAndPassword() async throws { logger.logLevel = .critical let bundle = Bundle.module natsServer.start(cfg: bundle.url(forResource: "creds", withExtension: "conf")!.relativePath) let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .usernameAndPassword("derek", "s3cr3t") .maxReconnects(5) .build() try await client.connect() try await client.publish("msg".data(using: .utf8)!, subject: "test") try await client.flush() _ = try await client.subscribe(subject: "test") XCTAssertNotNil(client, "Client should not be nil") // Test if client with bad credentials throws an error let badCertsClient = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .usernameAndPassword("derek", "badpassword") .maxReconnects(5) .build() do { try await badCertsClient.connect() XCTFail("Should have thrown an error") } catch NatsError.ServerError.authorizationViolation { // success return } catch { XCTFail("Expected auth error; got: \(error)") } XCTFail("Expected error from connect") } func testTokenAuth() async throws { logger.logLevel = .critical let bundle = Bundle.module natsServer.start(cfg: bundle.url(forResource: "token", withExtension: "conf")!.relativePath) let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .token("s3cr3t") .maxReconnects(5) .build() try await client.connect() try await client.publish("msg".data(using: .utf8)!, subject: "test") try await client.flush() _ = try await client.subscribe(subject: "test") XCTAssertNotNil(client, "Client should not be nil") // Test if client with bad credentials throws an error let badCertsClient = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .token("badtoken") .maxReconnects(5) .build() do { try await badCertsClient.connect() XCTFail("Should have thrown an error") } catch NatsError.ServerError.authorizationViolation { return } catch { XCTFail("Expected auth error; got: \(error)") } XCTFail("Expected error from connect") } func testCredentialsAuth() async throws { logger.logLevel = .critical let bundle = Bundle.module natsServer.start(cfg: bundle.url(forResource: "jwt", withExtension: "conf")!.relativePath) let creds = bundle.url(forResource: "TestUser", withExtension: "creds")! let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).credentialsFile( creds ).build() try await client.connect() let subscribe = try await client.subscribe(subject: "foo").makeAsyncIterator() try await client.publish("data".data(using: .utf8)!, subject: "foo") _ = try await subscribe.next() } func testNkeyAuth() async throws { logger.logLevel = .critical let bundle = Bundle.module natsServer.start(cfg: bundle.url(forResource: "nkey", withExtension: "conf")!.relativePath) let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .nkey("SUACH75SWCM5D2JMJM6EKLR2WDARVGZT4QC6LX3AGHSWOMVAKERABBBRWM") .build() try await client.connect() let subscribe = try await client.subscribe(subject: "foo").makeAsyncIterator() try await client.publish("data".data(using: .utf8)!, subject: "foo") _ = try await subscribe.next() } func testNkeyAuthFile() async throws { logger.logLevel = .critical let bundle = Bundle.module natsServer.start(cfg: bundle.url(forResource: "nkey", withExtension: "conf")!.relativePath) let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .nkeyFile(bundle.url(forResource: "nkey", withExtension: "")!) .build() try await client.connect() let subscribe = try await client.subscribe(subject: "foo").makeAsyncIterator() try await client.publish("data".data(using: .utf8)!, subject: "foo") _ = try await subscribe.next() // Test if passing both nkey and nkeyPath throws an error let badClient = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .nkeyFile(bundle.url(forResource: "nkey", withExtension: "")!) .nkey("SUACH75SWCM5D2JMJM6EKLR2WDARVGZT4QC6LX3AGHSWOMVAKERABBBRWM") .build() do { try await badClient.connect() XCTFail("Should have thrown an error") } catch let error as NatsError.ConnectError { if case .invalidConfig(_) = error { XCTAssertEqual( error.description, "nats: invalid client configuration: cannot use both nkey and nkeyPath") return } XCTFail("Expected auth error; got: \(error)") } catch { XCTFail("Expected auth error; got: \(error)") } XCTFail("Expected error from connect") } func testMutualTls() async throws { let bundle = Bundle.module logger.logLevel = .critical let serverCert = bundle.url(forResource: "server-cert", withExtension: "pem")!.relativePath let serverKey = bundle.url(forResource: "server-key", withExtension: "pem")!.relativePath let rootCA = bundle.url(forResource: "rootCA", withExtension: "pem")!.relativePath let cfgFile = try createConfigFileFromTemplate( templateURL: bundle.url(forResource: "tls", withExtension: "conf")!, args: [serverCert, serverKey, rootCA]) natsServer.start(cfg: cfgFile.relativePath) let certsURL = bundle.url(forResource: "rootCA", withExtension: "pem")! let clientCert = bundle.url(forResource: "client-cert", withExtension: "pem")! let clientKey = bundle.url(forResource: "client-key", withExtension: "pem")! let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .requireTls() .rootCertificates(certsURL) .clientCertificate( clientCert, clientKey ) .build() try await client.connect() try await client.publish("msg".data(using: .utf8)!, subject: "test") try await client.flush() _ = try await client.subscribe(subject: "test") XCTAssertNotNil(client, "Client should not be nil") } func testTlsFirst() async throws { let bundle = Bundle.module logger.logLevel = .critical let serverCert = bundle.url(forResource: "server-cert", withExtension: "pem")!.relativePath let serverKey = bundle.url(forResource: "server-key", withExtension: "pem")!.relativePath let rootCA = bundle.url(forResource: "rootCA", withExtension: "pem")!.relativePath let cfgFile = try createConfigFileFromTemplate( templateURL: bundle.url(forResource: "tls_first", withExtension: "conf")!, args: [serverCert, serverKey, rootCA]) natsServer.start(cfg: cfgFile.relativePath) let certsURL = bundle.url(forResource: "rootCA", withExtension: "pem")! let clientCert = bundle.url(forResource: "client-cert", withExtension: "pem")! let clientKey = bundle.url(forResource: "client-key", withExtension: "pem")! let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .requireTls() .rootCertificates(certsURL) .clientCertificate( clientCert, clientKey ) .withTlsFirst() .build() try await client.connect() try await client.publish("msg".data(using: .utf8)!, subject: "test") try await client.flush() _ = try await client.subscribe(subject: "test") XCTAssertNotNil(client, "Client should not be nil") } func testInvalidCertificate() async throws { let bundle = Bundle.module logger.logLevel = .critical let serverCert = bundle.url(forResource: "server-cert", withExtension: "pem")!.relativePath let serverKey = bundle.url(forResource: "server-key", withExtension: "pem")!.relativePath let rootCA = bundle.url(forResource: "rootCA", withExtension: "pem")!.relativePath let cfgFile = try createConfigFileFromTemplate( templateURL: bundle.url(forResource: "tls", withExtension: "conf")!, args: [serverCert, serverKey, rootCA]) natsServer.start(cfg: cfgFile.relativePath) let certsURL = bundle.url(forResource: "rootCA", withExtension: "pem")! let invalidCert = bundle.url(forResource: "client-cert-invalid", withExtension: "pem")! let invalidKey = bundle.url(forResource: "client-key-invalid", withExtension: "pem")! let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .requireTls() .rootCertificates(certsURL) .clientCertificate( invalidCert, invalidKey ) .build() do { try await client.connect() } catch NatsError.ConnectError.tlsFailure(_) { return } catch { XCTFail("Expected tls error; got: \(error)") } XCTFail("Expected error from connect") } func testWebsocket() async throws { logger.logLevel = .critical let bundle = Bundle.module natsServer.start(cfg: bundle.url(forResource: "ws", withExtension: "conf")!.relativePath) let client = NatsClientOptions().url(URL(string: natsServer.clientWebsocketURL)!).build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let iter = sub.makeAsyncIterator() let message = try await iter.next() XCTAssertEqual(message?.payload, "msg".data(using: .utf8)!) try await client.close() } func testWebsocketTLS() async throws { logger.logLevel = .critical let bundle = Bundle.module let serverCert = bundle.url(forResource: "server-cert", withExtension: "pem")!.relativePath let serverKey = bundle.url(forResource: "server-key", withExtension: "pem")!.relativePath let rootCA = bundle.url(forResource: "rootCA", withExtension: "pem")!.relativePath let cfgFile = try createConfigFileFromTemplate( templateURL: bundle.url(forResource: "wss", withExtension: "conf")!, args: [serverCert, serverKey, rootCA]) natsServer.start(cfg: cfgFile.relativePath) let certsURL = bundle.url(forResource: "rootCA", withExtension: "pem")! let clientCert = bundle.url(forResource: "client-cert", withExtension: "pem")! let clientKey = bundle.url(forResource: "client-key", withExtension: "pem")! let client = NatsClientOptions() .url(URL(string: natsServer.clientWebsocketURL)!) .rootCertificates(certsURL) .clientCertificate( clientCert, clientKey ) .build() try await client.connect() let sub = try await client.subscribe(subject: "test") try await client.publish("msg".data(using: .utf8)!, subject: "test") let iter = sub.makeAsyncIterator() let message = try await iter.next() XCTAssertEqual(message?.payload, "msg".data(using: .utf8)!) try await client.close() } func testLameDuckMode() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() let expectation = XCTestExpectation( description: "client was not notified of connection established event") client.on(.lameDuckMode) { event in XCTAssertEqual(event.kind(), NatsEventKind.lameDuckMode) expectation.fulfill() } try await client.connect() natsServer.sendSignal(.lameDuckMode) await fulfillment(of: [expectation], timeout: 1.0) try await client.close() } func testRequest() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() let service = try await client.subscribe(subject: "service") Task { for try await msg in service { try await client.publish( "reply".data(using: .utf8)!, subject: msg.replySubject!, reply: "reply") } } let response = try await client.request("request".data(using: .utf8)!, subject: "service") XCTAssertEqual(response.payload, "reply".data(using: .utf8)!) try await client.close() } func testRequestCustomInbox() async throws { natsServer.start() logger.logLevel = .debug let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .inboxPrefix("_INBOX_bar.foo") .build() try await client.connect() let service = try await client.subscribe(subject: "service") Task { for try await msg in service { try await client.publish( "reply".data(using: .utf8)!, subject: msg.replySubject!, reply: "reply") } } let response = try await client.request("request".data(using: .utf8)!, subject: "service") XCTAssertEqual(response.payload, "reply".data(using: .utf8)!) try await client.close() } func testRequest_noResponders() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() do { _ = try await client.request("request".data(using: .utf8)!, subject: "service") } catch NatsError.RequestError.noResponders { try await client.close() return } XCTFail("Expected no responders") } func testRequest_timeout() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() let service = try await client.subscribe(subject: "service") Task { for try await msg in service { sleep(2) try await client.publish( "reply".data(using: .utf8)!, subject: msg.replySubject!, reply: "reply") } } do { _ = try await client.request( "request".data(using: .utf8)!, subject: "service", timeout: 1) } catch NatsError.RequestError.timeout { try await service.unsubscribe() try await client.close() return } XCTFail("Expected timeout") } func testRequest_permissionDenied() async throws { logger.logLevel = .critical let bundle = Bundle.module let templateURL = bundle.url(forResource: "permissions", withExtension: "conf")! let cfgFile = try createConfigFileFromTemplate( templateURL: templateURL, args: ["deny", "_INBOX.*"]) natsServer.start(cfg: cfgFile.relativePath) let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build() try await client.connect() do { _ = try await client.request("request".data(using: .utf8)!, subject: "service") } catch NatsError.RequestError.permissionDenied { try await client.close() return } XCTFail("Expected permission denied") } func testPublishOnClosedConnection() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let rtt: TimeInterval = try await client.rtt() XCTAssertGreaterThan(rtt, 0, "should have RTT") try await client.close() do { try await client.publish("msg".data(using: .utf8)!, subject: "test") } catch NatsError.ClientError.connectionClosed { return } catch { XCTFail("Expected connection closed error; got: \(error)") } XCTFail("Expected connection closed error") } func testCloseClosedConnection() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let rtt: TimeInterval = try await client.rtt() XCTAssertGreaterThan(rtt, 0, "should have RTT") try await client.close() do { try await client.close() } catch NatsError.ClientError.connectionClosed { return } catch { XCTFail("Expected connection closed error; got: \(error)") } XCTFail("Expected connection closed error") } func testSuspendClosedConnection() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let rtt: TimeInterval = try await client.rtt() XCTAssertGreaterThan(rtt, 0, "should have RTT") try await client.close() do { try await client.suspend() } catch NatsError.ClientError.connectionClosed { return } catch { XCTFail("Expected connection closed error; got: \(error)") } XCTFail("Expected connection closed error") } func testReconnectOnClosedConnection() async throws { natsServer.start() logger.logLevel = .critical let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let rtt: TimeInterval = try await client.rtt() XCTAssertGreaterThan(rtt, 0, "should have RTT") try await client.close() do { try await client.reconnect() } catch NatsError.ClientError.connectionClosed { return } catch { XCTFail("Expected connection closed error; got: \(error)") } XCTFail("Expected connection closed error") } func testSubscribeMissingPermissions() async throws { logger.logLevel = .critical let bundle = Bundle.module let cfgFile = try createConfigFileFromTemplate( templateURL: bundle.url(forResource: "permissions", withExtension: "conf")!, args: ["deny", "events.>"]) natsServer.start(cfg: cfgFile.relativePath) let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() var sub = try await client.subscribe(subject: "events.A") var isError = false do { for try await _ in sub { XCTFail("Expected no message)") } } catch NatsError.SubscriptionError.permissionDenied { // success isError = true } if !isError { XCTFail("Expected missing permissions error") } sub = try await client.subscribe(subject: "events.*") isError = false do { for try await _ in sub { XCTFail("Expected no message)") } } catch NatsError.SubscriptionError.permissionDenied { // success isError = true } if !isError { XCTFail("Expected missing permissions error") } } func testSubscribePermissionsRevoked() async throws { logger.logLevel = .critical let bundle = Bundle.module let templateURL = bundle.url(forResource: "permissions", withExtension: "conf")! var cfgFile = try createConfigFileFromTemplate( templateURL: templateURL, args: ["allow", "events.>"]) natsServer.start(cfg: cfgFile.relativePath) let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() let sub = try await client.subscribe(subject: "events.A") let iter = sub.makeAsyncIterator() try await client.publish("msg".data(using: .utf8)!, subject: "events.A") _ = try await iter.next() cfgFile = try createConfigFileFromTemplate( templateURL: templateURL, args: ["deny", "events.>"], destination: cfgFile) // reload config with natsServer.sendSignal(.reload) do { _ = try await iter.next() } catch NatsError.SubscriptionError.permissionDenied { // success return } XCTFail("Expected permission denied error") } /// Test race condition in concurrent subscriptions /// This test ensures that creating multiple subscriptions concurrently doesn't cause /// segmentation faults due to race conditions in the subscription map. func testConcurrentSubscriptionCreation() async throws { natsServer.start() let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() try await client.connect() // Create 10 subscriptions concurrently let tasks = (0..<10).map { i in Task { try await client.subscribe(subject: "concurrent.test.\(i)") } } // Wait for all subscriptions to complete for task in tasks { _ = try await task.value } try await client.close() } /// Test that multiple connect() calls on the same client throw an error func testMultipleConnectCallsThrowError() async throws { natsServer.start() let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .build() // First connect should succeed try await client.connect() // Second connect should throw alreadyConnected error do { try await client.connect() XCTFail("Second connect() should have thrown an error") } catch NatsError.ClientError.alreadyConnected { // Expected behavior } catch { XCTFail("Expected alreadyConnected error, got: \(error)") } try await client.close() } /// Test ByteBuffer reinitialization func testByteBufferReinitialization() async throws { natsServer.start() let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .reconnectWait(0.01) // Very short reconnect wait .maxReconnects(100) .build() try await client.connect() let sub = try await client.subscribe(subject: "test.buffer.race") // Create concurrent tasks that will stress the buffer let publishTask = Task { for i in 0..<1000 { // Send messages with varying sizes to stress buffer let payload = String(repeating: "X", count: i % 1000 + 1) try? await client.publish(payload.data(using: .utf8)!, subject: "test.buffer.race") if i % 10 == 0 { // Add small delays occasionally to change timing try? await Task.sleep(nanoseconds: 1000) } } } let reconnectTask = Task { for _ in 0..<20 { // Force reconnects while messages are being processed try? await Task.sleep(nanoseconds: 50_000_000) // 50ms try? await client.reconnect() } } // Try to consume messages during reconnect let consumeTask = Task { var count = 0 for try await _ in sub { count += 1 if count > 100 { break } } } try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { await publishTask.value } group.addTask { await reconnectTask.value } group.addTask { try await consumeTask.value } _ = try await group.waitForAll() group.cancelAll() } try await client.close() } /// Test concurrent channelActive and channelReadComplete func testConcurrentChannelActiveAndRead() async throws { natsServer.start() let client = NatsClientOptions() .url(URL(string: natsServer.clientURL)!) .reconnectWait(0.01) .maxReconnects(50) .build() try await client.connect() let sub = try await client.subscribe(subject: "test.concurrent.>") // Task 1: Rapid publishing let publishTask = Task { for i in 0..<500 { let subjects = ["test.concurrent.a", "test.concurrent.b", "test.concurrent.c"] let subject = subjects[i % subjects.count] let payload = String(repeating: "D", count: (i * 7) % 2048 + 100) try? await client.publish(payload.data(using: .utf8)!, subject: subject) } } // Task 2: Force disconnections/reconnections let reconnectTask = Task { for i in 0..<10 { try await Task.sleep(nanoseconds: 100_000_000) // 100ms try await client.reconnect() for j in 0..<10 { try? await client.publish( "RECONNECT-\(i)-\(j)".data(using: .utf8)!, subject: "test.concurrent.reconnect") } } } // Task 3: Consume messages let consumeTask = Task { var count = 0 for try await _ in sub { count += 1 if count > 200 { break } // Add occasional small delays to vary timing if count % 50 == 0 { try await Task.sleep(nanoseconds: 1_000_000) } } } // Wait for all tasks try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { await publishTask.value } group.addTask { try await reconnectTask.value } group.addTask { try await consumeTask.value } _ = try await group.waitForAll() group.cancelAll() } try await client.close() } func createConfigFileFromTemplate( templateURL: URL, args: [String], destination: URL? = nil ) throws -> URL { let templateContent = try String(contentsOf: templateURL, encoding: .utf8) let config = String(format: templateContent, arguments: args.map { $0 as CVarArg }) let tempDirectoryURL = FileManager.default.temporaryDirectory let tempFileURL: URL if let destination { tempFileURL = destination } else { tempFileURL = tempDirectoryURL.appendingPathComponent(UUID().uuidString) .appendingPathExtension("conf") } // Write the filled content to the temp file try config.write(to: tempFileURL, atomically: true, encoding: .utf8) // Return the URL of the newly created temp file return tempFileURL } }