init
Some checks failed
ci / macos (push) Has been cancelled
ci / ios (push) Has been cancelled
ci / check-linter (push) Has been cancelled

This commit is contained in:
wenzuhuai
2026-01-12 18:29:52 +08:00
commit d7bdb4f378
87 changed files with 12664 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
// 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 XCTest
@testable import Nats
class ErrorsTests: XCTestCase {
static var allTests = [
("testServerErrorPermissionsDenied", testServerErrorPermissionsDenied)
]
func testServerErrorPermissionsDenied() {
var err = NatsError.ServerError(
"Permissions Violation for Subscription to \"events.A.B.*\"]")
XCTAssertEqual(
err, NatsError.ServerError.permissionsViolation(.subscribe, "events.A.B.*", nil))
err = NatsError.ServerError("Permissions Violation for Publish to \"events.A.B.*\"")
XCTAssertEqual(
err, NatsError.ServerError.permissionsViolation(.publish, "events.A.B.*", nil))
err = NatsError.ServerError(
"Permissions Violation for Publish to \"events.A.B.*\" using queue \"q\"")
XCTAssertEqual(
err, NatsError.ServerError.permissionsViolation(.publish, "events.A.B.*", "q"))
err = NatsError.ServerError("Some other error")
XCTAssertEqual(err, NatsError.ServerError.proto("Some other error"))
}
}

View File

@@ -0,0 +1,97 @@
// 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 XCTest
@testable import Nats
class HeadersTests: XCTestCase {
static var allTests = [
("testAppend", testAppend),
("testSubscript", testSubscript),
("testInsert", testInsert),
("testSerialize", testSerialize),
("testValidNatsHeaderName", testValidNatsHeaderName),
("testDollarNatsHeaderName", testDollarNatsHeaderName),
("testInvalidNatsHeaderName", testInvalidNatsHeaderName),
(
"testInvalidNatsHeaderNameWithSpecialCharacters",
testInvalidNatsHeaderNameWithSpecialCharacters
),
]
func testAppend() {
var hm = NatsHeaderMap()
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
XCTAssertEqual(
hm.getAll(try! NatsHeaderName("foo")), [NatsHeaderValue("bar"), NatsHeaderValue("baz")])
}
func testInsert() {
var hm = NatsHeaderMap()
hm.insert(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
XCTAssertEqual(hm.getAll(try! NatsHeaderName("foo")), [NatsHeaderValue("bar")])
}
func testSerialize() {
var hm = NatsHeaderMap()
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("bar"))
hm.append(try! NatsHeaderName("foo"), NatsHeaderValue("baz"))
hm.insert(try! NatsHeaderName("bar"), NatsHeaderValue("foo"))
let expected = [
"NATS/1.0\r\nfoo:bar\r\nfoo:baz\r\nbar:foo\r\n\r\n",
"NATS/1.0\r\nbar:foo\r\nfoo:bar\r\nfoo:baz\r\n\r\n",
]
XCTAssertTrue(expected.contains(String(bytes: hm.toBytes(), encoding: .utf8)!))
}
func testValidNatsHeaderName() {
XCTAssertNoThrow(try NatsHeaderName("X-Custom-Header"))
}
func testDollarNatsHeaderName() {
XCTAssertNoThrow(try NatsHeaderName("$Dollar"))
}
func testInvalidNatsHeaderName() {
XCTAssertThrowsError(try NatsHeaderName("Invalid Header Name"))
}
func testInvalidNatsHeaderNameWithSpecialCharacters() {
XCTAssertThrowsError(try NatsHeaderName("Invalid:Header:Name"))
}
func testSubscript() {
var hm = NatsHeaderMap()
// Test setting a value
hm[try! NatsHeaderName("foo")] = NatsHeaderValue("bar")
XCTAssertEqual(hm[try! NatsHeaderName("foo")], NatsHeaderValue("bar"))
// Test updating existing value
hm[try! NatsHeaderName("foo")] = NatsHeaderValue("baz")
XCTAssertEqual(hm[try! NatsHeaderName("foo")], NatsHeaderValue("baz"))
// Test retrieving non-existing value (should be nil or default)
XCTAssertNil(hm[try! NatsHeaderName("non-existing")])
// Test removal of a value
hm[try! NatsHeaderName("foo")] = nil
XCTAssertNil(hm[try! NatsHeaderName("foo")])
}
}

View File

@@ -0,0 +1,43 @@
// 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 Foundation
import XCTest
@testable import Nats
class JwtTests: XCTestCase {
static var allTests = [
("testParseCredentialsFile", testParseCredentialsFile)
]
func testParseCredentialsFile() async throws {
logger.logLevel = .critical
let currentFile = URL(fileURLWithPath: #file)
let testDir = currentFile.deletingLastPathComponent().deletingLastPathComponent()
let resourceURL = testDir.appendingPathComponent("Integration/Resources/TestUser.creds")
let credsData = try await URLSession.shared.data(from: resourceURL).0
let nkey = String(data: JwtUtils.parseDecoratedNKey(contents: credsData)!, encoding: .utf8)
let expectedNkey = "SUACH75SWCM5D2JMJM6EKLR2WDARVGZT4QC6LX3AGHSWOMVAKERABBBRWM"
XCTAssertEqual(nkey, expectedNkey)
let jwt = String(data: JwtUtils.parseDecoratedJWT(contents: credsData)!, encoding: .utf8)
let expectedJWT =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJlZDI1NTE5LW5rZXkifQ.eyJqdGkiOiJMN1dBT1hJU0tPSUZNM1QyNEhMQ09ENzJRT1czQkNVWEdETjRKVU1SSUtHTlQ3RzdZVFRRIiwiaWF0IjoxNjUxNzkwOTgyLCJpc3MiOiJBRFRRUzdaQ0ZWSk5XNTcyNkdPWVhXNVRTQ1pGTklRU0hLMlpHWVVCQ0Q1RDc3T1ROTE9PS1pPWiIsIm5hbWUiOiJUZXN0VXNlciIsInN1YiI6IlVBRkhHNkZVRDJVVTRTREZWQUZVTDVMREZPMlhNNFdZTTc2VU5YVFBKWUpLN0VFTVlSQkhUMlZFIiwibmF0cyI6eyJwdWIiOnt9LCJzdWIiOnt9LCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjJ9fQ.bp2-Jsy33l4ayF7Ku1MNdJby4WiMKUrG-rSVYGBusAtV3xP4EdCa-zhSNUaBVIL3uYPPCQYCEoM1pCUdOnoJBg"
XCTAssertEqual(jwt, expectedJWT)
}
}

View File

@@ -0,0 +1,39 @@
// 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 XCTest
@testable import Nats
class NatsClientOptionsTests: XCTestCase {
static var allTests = [
("testDefaultInboxPrefix", testDefaultInboxPrefix),
("testCustomInboxPrefix", testCustomInboxPrefix),
]
func testDefaultInboxPrefix() {
let client = NatsClientOptions().build()
let inbox = client.newInbox()
XCTAssertTrue(inbox.hasPrefix("_INBOX."), "Default inbox prefix should be '_INBOX.'")
XCTAssertEqual(inbox.count, "_INBOX.".count + 22, "Inbox should have prefix plus NUID")
}
func testCustomInboxPrefix() {
let customPrefix = "_INBOX_abc123."
let client = NatsClientOptions().inboxPrefix(customPrefix).build()
let inbox = client.newInbox()
XCTAssertTrue(inbox.hasPrefix(customPrefix), "Inbox should use custom prefix")
XCTAssertEqual(
inbox.count, customPrefix.count + 22, "Inbox should have custom prefix plus NUID")
}
}

View File

@@ -0,0 +1,253 @@
// 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 XCTest
@testable import Nats
class ParserTests: XCTestCase {
static var allTests = [
("testParseOutMessages", testParseOutMessages)
]
override func setUp() {
super.setUp()
}
override func tearDown() {
super.tearDown()
}
func testParseOutMessages() {
struct TestCase {
let name: String
let givenChunks: [String]
let expectedOps: [ServerOp]
}
let fail: ((Int, String) -> String) = { index, name in
return "Test case: \(index)\n Input: \(name)"
}
var hm = NatsHeaderMap()
hm.append(try! NatsHeaderName("h1"), NatsHeaderValue("X"))
hm.append(try! NatsHeaderName("h1"), NatsHeaderValue("Y"))
hm.append(try! NatsHeaderName("h2"), NatsHeaderValue("Z"))
let testCases = [
TestCase(
name: "Single chunk, different operations",
givenChunks: ["MSG foo 1 5\r\nhello\r\n+OK\r\nPONG\r\n"],
expectedOps: [
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
),
.ok,
.pong,
]
),
TestCase(
name: "Chunked messages",
givenChunks: [
"MSG foo 1 5\r\nhello\r\nMSG foo 1 5\r\nwo",
"rld\r\nMSG f",
"oo 1 5\r\nhello\r\nMSG foo 1 5\r\nworld",
"\r\nMSG foo 1 5\r\nhello\r",
"\nMSG foo 1 5\r\nworld\r\nMSG foo 1 5\r\n",
"hello\r\n",
],
expectedOps: [
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
),
]
),
TestCase(
name: "Message with headers only",
givenChunks: [
"HMSG foo 1 30 30\r\nNATS/1.0\r\nh1:X\r\nh1:Y\r\nh2:Z\r\n\r\n\r\n"
],
expectedOps: [
.hMessage(
HMessageInbound(
subject: "foo", sid: 1, payload: nil, headers: hm, headersLength: 30,
length: 30)
)
]
),
TestCase(
name: "Message with headers and payload",
givenChunks: [
"HMSG foo 1 30 35\r\nNATS/1.0\r\nh1:X\r\nh1:Y\r\nh2:Z\r\n\r\nhello\r\n"
],
expectedOps: [
.hMessage(
HMessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!,
headers: hm, headersLength: 30, length: 35)
)
]
),
TestCase(
name: "Message with status and no other headers",
givenChunks: [
"HMSG foo 1 30 30\r\nNATS/1.0 503 no responders\r\n\r\n\r\n"
],
expectedOps: [
.hMessage(
HMessageInbound(
subject: "foo", sid: 1, payload: nil, headers: NatsHeaderMap(),
headersLength: 30, length: 30, status: StatusCode.noResponders,
description: "no responders"
)
)
]
),
TestCase(
name: "Message with status, headers and payload",
givenChunks: [
"HMSG foo 1 48 53\r\nNATS/1.0 503 no responders\r\nh1:X\r\nh1:Y\r\nh2:Z\r\n\r\nhello\r\n"
],
expectedOps: [
.hMessage(
HMessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!,
headers: hm, headersLength: 48, length: 53,
status: StatusCode.noResponders,
description: "no responders")
)
]
),
TestCase(
name: "With empty lines",
givenChunks: [
"MSG foo 1 5\r\nhello\r\nMSG foo 1 5\r\nwo",
"",
"",
"rld\r\nMSG f",
"oo 1 5\r\nhello\r\n",
],
expectedOps: [
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "world".data(using: .utf8)!, length: 5)
),
.message(
MessageInbound(
subject: "foo", sid: 1, payload: "hello".data(using: .utf8)!, length: 5)
),
]
),
TestCase(
name: "With crlf in payload",
givenChunks: [
"MSG foo 1 7\r\nhe\r\nllo\r\n"
],
expectedOps: [
.message(
MessageInbound(
subject: "foo", sid: 1, payload: Data("he\r\nllo".utf8), length: 7))
]
),
]
for (tn, tc) in testCases.enumerated() {
logger.logLevel = .critical
var ops = [ServerOp]()
var prevRemainder: Data?
for chunk in tc.givenChunks {
var chunkData = Data(chunk.utf8)
if let prevRemainder {
chunkData.prepend(prevRemainder)
}
let res = try! chunkData.parseOutMessages()
prevRemainder = res.remainder
ops.append(contentsOf: res.ops)
}
XCTAssertEqual(ops.count, tc.expectedOps.count)
for (i, op) in ops.enumerated() {
switch op {
case .ok:
if case .ok = tc.expectedOps[i] {
} else {
XCTFail(fail(tn, tc.name))
}
case .info(let info):
if case .info(let expectedInfo) = tc.expectedOps[i] {
XCTAssertEqual(info, expectedInfo, fail(tn, tc.name))
} else {
XCTFail(fail(tn, tc.name))
}
case .ping:
if case .ping = tc.expectedOps[i] {
} else {
XCTFail(fail(tn, tc.name))
}
case .pong:
if case .pong = tc.expectedOps[i] {
} else {
XCTFail(fail(tn, tc.name))
}
case .error(_):
if case .error(_) = tc.expectedOps[i] {
} else {
XCTFail(fail(tn, tc.name))
}
case .message(let msg):
if case .message(let expectedMessage) = tc.expectedOps[i] {
XCTAssertEqual(msg, expectedMessage, fail(tn, tc.name))
} else {
XCTFail(fail(tn, tc.name))
}
case .hMessage(let msg):
if case .hMessage(let expectedMessage) = tc.expectedOps[i] {
XCTAssertEqual(msg, expectedMessage, fail(tn, tc.name))
} else {
XCTFail(fail(tn, tc.name))
}
}
}
}
}
}