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,418 @@
// 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 JetStream
import Logging
import Nats
import NatsServer
import XCTest
class ConsumerTests: XCTestCase {
static var allTests = [
("testFetchWithDefaultOptions", testFetchWithDefaultOptions),
("testFetchConsumerDeleted", testFetchConsumerDeleted),
("testFetchExpires", testFetchExpires),
("testFetchInvalidIdleHeartbeat", testFetchInvalidIdleHeartbeat),
("testAck", testAck),
("testNak", testNak),
("testNakWithDelay", testNakWithDelay),
("testTerm", testTerm),
]
var natsServer = NatsServer()
override func tearDown() {
super.tearDown()
natsServer.stop()
}
func testFetchWithDefaultOptions() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
let payload = "hello".data(using: .utf8)!
// publish some messages on stream
for _ in 1...100 {
let ack = try await ctx.publish("foo.A", message: payload)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 100)
let batch = try await consumer.fetch(batch: 30)
var i = 0
for try await msg in batch {
try await msg.ack()
XCTAssertEqual(msg.payload, payload)
i += 1
}
XCTAssertEqual(i, 30)
}
func testFetchConsumerDeleted() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
let payload = "hello".data(using: .utf8)!
// publish some messages on stream
for _ in 1...10 {
let ack = try await ctx.publish("foo.A", message: payload)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 10)
let batch = try await consumer.fetch(batch: 30)
sleep(1)
try await stream.deleteConsumer(name: "cons")
var i = 0
do {
for try await msg in batch {
try await msg.ack()
XCTAssertEqual(msg.payload, payload)
i += 1
}
} catch JetStreamError.FetchError.consumerDeleted {
XCTAssertEqual(i, 10)
return
}
XCTFail("should get consumer deleted")
}
func testFetchExpires() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
let payload = "hello".data(using: .utf8)!
// publish some messages on stream
for _ in 1...10 {
let ack = try await ctx.publish("foo.A", message: payload)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 10)
let batch = try await consumer.fetch(batch: 30, expires: 1)
var i = 0
for try await msg in batch {
try await msg.ack()
XCTAssertEqual(msg.payload, payload)
i += 1
}
XCTAssertEqual(i, 10)
}
func testFetchInvalidIdleHeartbeat() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
let batch = try await consumer.fetch(batch: 30, expires: 1, idleHeartbeat: 2)
do {
for try await _ in batch {}
} catch JetStreamError.FetchError.badRequest {
// success
return
}
XCTFail("should get bad request")
}
func testFetchMissingHeartbeat() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
let payload = "hello".data(using: .utf8)!
// publish some messages on stream
for _ in 1...10 {
let ack = try await ctx.publish("foo.A", message: payload)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 10)
try await stream.deleteConsumer(name: "cons")
let batch = try await consumer.fetch(batch: 30, idleHeartbeat: 1)
do {
for try await _ in batch {}
} catch JetStreamError.FetchError.noHeartbeatReceived {
return
} catch JetStreamError.FetchError.noResponders {
// This is also expected when the consumer has been deleted
return
}
XCTFail("should get missing heartbeats or no responders error")
}
func testAck() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
// create a consumer with 500ms ack wait
let consumer = try await stream.createConsumer(
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
// publish some messages on stream
for i in 0..<100 {
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 100)
var batch = try await consumer.fetch(batch: 10)
var i = 0
for try await msg in batch {
try await msg.ack()
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(i)")
i += 1
}
XCTAssertEqual(i, 10)
// now wait 1 second and make sure the messages are not re-delivered
sleep(1)
batch = try await consumer.fetch(batch: 10)
for try await msg in batch {
try await msg.ack()
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(i)")
i += 1
}
XCTAssertEqual(i, 20)
}
func testNak() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
// create a consumer with 500ms ack wait
let consumer = try await stream.createConsumer(
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
// publish some messages on stream
for i in 0..<10 {
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 10)
var batch = try await consumer.fetch(batch: 1)
var iter = batch.makeAsyncIterator()
var msg = try await iter.next()!
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
var meta = try msg.metadata()
XCTAssertEqual(meta.streamSequence, 1)
XCTAssertEqual(meta.consumerSequence, 1)
try await msg.ack(ackType: .nak())
// now fetch the message again, it should be redelivered
batch = try await consumer.fetch(batch: 1)
iter = batch.makeAsyncIterator()
msg = try await iter.next()!
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
meta = try msg.metadata()
XCTAssertEqual(meta.streamSequence, 1)
XCTAssertEqual(meta.consumerSequence, 2)
try await msg.ack()
}
func testNakWithDelay() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
// create a consumer with 500ms ack wait
let consumer = try await stream.createConsumer(cfg: ConsumerConfig(name: "cons"))
// publish some messages on stream
for i in 0..<10 {
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 10)
var batch = try await consumer.fetch(batch: 1)
var iter = batch.makeAsyncIterator()
var msg = try await iter.next()!
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
var meta = try msg.metadata()
XCTAssertEqual(meta.streamSequence, 1)
XCTAssertEqual(meta.consumerSequence, 1)
try await msg.ack(ackType: .nak(delay: 0.5))
// now fetch the next message immediately, it should be the next message
batch = try await consumer.fetch(batch: 1)
iter = batch.makeAsyncIterator()
msg = try await iter.next()!
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(1)")
meta = try msg.metadata()
XCTAssertEqual(meta.streamSequence, 2)
XCTAssertEqual(meta.consumerSequence, 2)
try await msg.ack()
// wait a second, the first message should be redelivered at this point
sleep(1)
batch = try await consumer.fetch(batch: 1)
iter = batch.makeAsyncIterator()
msg = try await iter.next()!
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
meta = try msg.metadata()
XCTAssertEqual(meta.streamSequence, 1)
XCTAssertEqual(meta.consumerSequence, 3)
}
func testTerm() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let streamCfg = StreamConfig(name: "test", subjects: ["foo.*"])
let stream = try await ctx.createStream(cfg: streamCfg)
// create a consumer with 500ms ack wait
let consumer = try await stream.createConsumer(
cfg: ConsumerConfig(name: "cons", ackWait: NanoTimeInterval(0.5)))
// publish some messages on stream
for i in 0..<10 {
let ack = try await ctx.publish("foo.A", message: "\(i)".data(using: .utf8)!)
_ = try await ack.wait()
}
let info = try await stream.info()
XCTAssertEqual(info.state.messages, 10)
var batch = try await consumer.fetch(batch: 1)
var iter = batch.makeAsyncIterator()
var msg = try await iter.next()!
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(0)")
var meta = try msg.metadata()
XCTAssertEqual(meta.streamSequence, 1)
XCTAssertEqual(meta.consumerSequence, 1)
try await msg.ack(ackType: .term())
// wait 1s, the first message should not be redelivered (even though we are past ack wait)
sleep(1)
batch = try await consumer.fetch(batch: 1)
iter = batch.makeAsyncIterator()
msg = try await iter.next()!
XCTAssertEqual(String(decoding: msg.payload!, as: UTF8.self), "\(1)")
meta = try msg.metadata()
XCTAssertEqual(meta.streamSequence, 2)
XCTAssertEqual(meta.consumerSequence, 2)
try await msg.ack()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,69 @@
//
// File.swift
//
//
// Created by Piotr Piotrowski on 05/06/2024.
//
import Logging
import NIO
import Nats
import NatsServer
import XCTest
@testable import JetStream
class RequestTests: XCTestCase {
static var allTests = [
("testRequest", testRequest)
]
var natsServer = NatsServer()
override func tearDown() {
super.tearDown()
natsServer.stop()
}
func testRequest() async throws {
let bundle = Bundle.module
natsServer.start(
cfg: bundle.url(forResource: "jetstream", withExtension: "conf")!.relativePath)
logger.logLevel = .critical
let client = NatsClientOptions().url(URL(string: natsServer.clientURL)!).build()
try await client.connect()
let ctx = JetStreamContext(client: client)
let stream = """
{
"name": "FOO",
"subjects": ["foo"]
}
"""
let data = stream.data(using: .utf8)!
_ = try await client.request(data, subject: "$JS.API.STREAM.CREATE.FOO")
let info: Response<AccountInfo> = try await ctx.request("INFO", message: Data())
guard case .success(let info) = info else {
XCTFail("request should be successful")
return
}
XCTAssertEqual(info.streams, 1)
let badInfo: Response<AccountInfo> = try await ctx.request(
"STREAM.INFO.BAD", message: Data())
guard case .error(let jetStreamAPIResponse) = badInfo else {
XCTFail("should get error")
return
}
XCTAssertEqual(ErrorCode.streamNotFound, jetStreamAPIResponse.error.errorCode)
}
}

View File

@@ -0,0 +1,3 @@
jetstream: {
domain: ABC
}

View File

@@ -0,0 +1,25 @@
jetstream: {
max_mem_store: 64MiB,
max_file_store: 10GiB
}
no_auth_user: pp
accounts {
JS {
jetstream: enabled
users: [
{
user: pp,
password: foo
}
]
}
NO_JS {
users: [
{
user: nojs,
password: foo
}
]
}
}

View File

@@ -0,0 +1,17 @@
jetstream: enabled
accounts: {
A: {
users: [ {user: a, password: a} ]
jetstream: enabled
exports: [
{service: '$JS.API.>' }
]
},
I: {
jetstream: disabled
users: [ {user: i, password: i} ]
imports: [
{service: {account: A, subject: '$JS.API.>'}, to: 'fromA.>' }
]
}
}

View File

@@ -0,0 +1,119 @@
// 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 JetStream
@testable import Nats
class JetStreamMessageTests: XCTestCase {
static var allTests = [
("testValidOldFormatMessage", testValidOldFormatMessage),
("testValidNewFormatMessage", testValidNewFormatMessage),
("testMissingTokens", testMissingTokens),
("testInvalidTokenValues", testInvalidTokenValues),
("testInvalidPrefix", testInvalidPrefix),
("testNoReplySubject", testNoReplySubject),
]
func testValidOldFormatMessage() async throws {
let replySubject = "$JS.ACK.myStream.myConsumer.10.20.30.1234567890.5"
let natsMessage = NatsMessage(
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
status: nil, description: nil)
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
let metadata = try jetStreamMessage.metadata()
XCTAssertNil(metadata.domain)
XCTAssertNil(metadata.accountHash)
XCTAssertEqual(metadata.stream, "myStream")
XCTAssertEqual(metadata.consumer, "myConsumer")
XCTAssertEqual(metadata.delivered, 10)
XCTAssertEqual(metadata.streamSequence, 20)
XCTAssertEqual(metadata.consumerSequence, 30)
XCTAssertEqual(metadata.timestamp, "1234567890")
XCTAssertEqual(metadata.pending, 5)
}
func testValidNewFormatMessage() async throws {
let replySubject = "$JS.ACK.domain.accountHash123.myStream.myConsumer.10.20.30.1234567890.5"
let natsMessage = NatsMessage(
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
status: nil, description: nil)
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
let metadata = try jetStreamMessage.metadata()
XCTAssertEqual(metadata.domain, "domain")
XCTAssertEqual(metadata.accountHash, "accountHash123")
XCTAssertEqual(metadata.stream, "myStream")
XCTAssertEqual(metadata.consumer, "myConsumer")
XCTAssertEqual(metadata.delivered, 10)
XCTAssertEqual(metadata.streamSequence, 20)
XCTAssertEqual(metadata.consumerSequence, 30)
XCTAssertEqual(metadata.timestamp, "1234567890")
XCTAssertEqual(metadata.pending, 5)
}
func testMissingTokens() async throws {
let replySubject = "$JS.ACK.myStream.myConsumer"
let natsMessage = NatsMessage(
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
status: nil, description: nil)
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
do {
_ = try jetStreamMessage.metadata()
} catch JetStreamError.MessageMetadataError.invalidTokenNum {
return
}
}
func testInvalidTokenValues() async throws {
let replySubject = "$JS.ACK.myStream.myConsumer.invalid.20.30.1234567890.5"
let natsMessage = NatsMessage(
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
status: nil, description: nil)
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
do {
_ = try jetStreamMessage.metadata()
} catch JetStreamError.MessageMetadataError.invalidTokenValue {
return
}
}
func testInvalidPrefix() async throws {
let replySubject = "$JS.WRONG.myStream.myConsumer.10.20.30.1234567890.5"
let natsMessage = NatsMessage(
payload: nil, subject: "", replySubject: replySubject, length: 0, headers: nil,
status: nil, description: nil)
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
do {
_ = try jetStreamMessage.metadata()
} catch JetStreamError.MessageMetadataError.invalidPrefix {
return
}
}
func testNoReplySubject() async throws {
let natsMessage = NatsMessage(
payload: nil, subject: "", replySubject: nil, length: 0, headers: nil, status: nil,
description: nil)
let jetStreamMessage = JetStreamMessage(message: natsMessage, client: NatsClient())
do {
_ = try jetStreamMessage.metadata()
} catch JetStreamError.MessageMetadataError.noReplyInMessage {
return
}
}
}