254 lines
9.6 KiB
Swift
254 lines
9.6 KiB
Swift
// 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))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|