snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
This commit is contained in:
663
test/accounts_cycles_test.go
Normal file
663
test/accounts_cycles_test.go
Normal file
@@ -0,0 +1,663 @@
|
||||
// Copyright 2020-2026 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.
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats-server/v2/server"
|
||||
"github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
func TestAccountCycleService(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: help, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: help, account: A } } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cycle service import, got none")
|
||||
}
|
||||
|
||||
conf = createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: * } ]
|
||||
imports [ { service { subject: help, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: *, account: A } } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cycle service import, got none")
|
||||
}
|
||||
|
||||
conf = createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: * } ]
|
||||
imports [ { service { subject: help, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: help, account: C } } ]
|
||||
}
|
||||
C {
|
||||
exports [ { service: * } ]
|
||||
imports [ { service { subject: *, account: A } } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cycle service import, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountCycleStream(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { stream: strm } ]
|
||||
imports [ { stream { subject: strm, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { stream: strm } ]
|
||||
imports [ { stream { subject: strm, account: A } } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cyclic import, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountCycleStreamWithMapping(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { stream: * } ]
|
||||
imports [ { stream { subject: bar, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { stream: bar } ]
|
||||
imports [ { stream { subject: foo, account: A }, to: bar } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cyclic import, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountCycleNonCycleStreamWithMapping(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { stream: foo } ]
|
||||
imports [ { stream { subject: bar, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { stream: bar } ]
|
||||
imports [ { stream { subject: baz, account: C }, to: bar } ]
|
||||
}
|
||||
C {
|
||||
exports [ { stream: baz } ]
|
||||
imports [ { stream { subject: foo, account: A }, to: bar } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err != nil {
|
||||
t.Fatalf("Expected no error but got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountCycleServiceCycleWithMapping(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: a } ]
|
||||
imports [ { service { subject: b, account: B }, to: a } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: b } ]
|
||||
imports [ { service { subject: a, account: A }, to: b } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cycle service import, got none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountCycleServiceNonCycle(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: * } ]
|
||||
imports [ { service { subject: help, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: nohelp, account: C } } ]
|
||||
}
|
||||
C {
|
||||
exports [ { service: * } ]
|
||||
imports [ { service { subject: *, account: A } } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
if _, err := server.ProcessConfigFile(conf); err != nil {
|
||||
t.Fatalf("Expected no error but got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountCycleServiceNonCycleChain(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: help, account: B } } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: help, account: C } } ]
|
||||
}
|
||||
C {
|
||||
exports [ { service: help } ]
|
||||
imports [ { service { subject: help, account: D } } ]
|
||||
}
|
||||
D {
|
||||
exports [ { service: help } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
if _, err := server.ProcessConfigFile(conf); err != nil {
|
||||
t.Fatalf("Expected no error but got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
// bug: https://github.com/nats-io/nats-server/issues/1769
|
||||
func TestServiceImportReplyMatchCycle(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
port: -1
|
||||
accounts {
|
||||
A {
|
||||
users: [{user: d, pass: x}]
|
||||
imports [ {service: {account: B, subject: ">" }}]
|
||||
}
|
||||
B {
|
||||
users: [{user: x, pass: x}]
|
||||
exports [ { service: ">" } ]
|
||||
}
|
||||
}
|
||||
no_auth_user: d
|
||||
`))
|
||||
|
||||
s, opts := RunServerWithConfig(conf)
|
||||
defer s.Shutdown()
|
||||
|
||||
nc1 := clientConnectToServerWithUP(t, opts, "x", "x")
|
||||
defer nc1.Close()
|
||||
|
||||
msg := []byte("HELLO")
|
||||
_, err := nc1.Subscribe("foo", func(m *nats.Msg) {
|
||||
m.Respond(msg)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure the subscription is known by the server we're connected to.
|
||||
err = nc1.Flush()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkSubInterest(t, s, "B", "foo", time.Second)
|
||||
|
||||
nc2 := clientConnectToServer(t, s)
|
||||
defer nc2.Close()
|
||||
|
||||
resp, err := nc2.Request("foo", nil, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if resp == nil || string(resp.Data) != string(msg) {
|
||||
t.Fatalf("Wrong or empty response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceImportReplyMatchCycleMultiHops(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
port: -1
|
||||
accounts {
|
||||
A {
|
||||
users: [{user: d, pass: x}]
|
||||
imports [ {service: {account: B, subject: ">" }}]
|
||||
}
|
||||
B {
|
||||
exports [ { service: ">" } ]
|
||||
imports [ {service: {account: C, subject: ">" }}]
|
||||
}
|
||||
C {
|
||||
users: [{user: x, pass: x}]
|
||||
exports [ { service: ">" } ]
|
||||
}
|
||||
}
|
||||
no_auth_user: d
|
||||
`))
|
||||
|
||||
s, opts := RunServerWithConfig(conf)
|
||||
defer s.Shutdown()
|
||||
|
||||
nc1 := clientConnectToServerWithUP(t, opts, "x", "x")
|
||||
defer nc1.Close()
|
||||
|
||||
msg := []byte("HELLO")
|
||||
_, err := nc1.Subscribe("foo", func(m *nats.Msg) {
|
||||
m.Respond(msg)
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Ensure the subscription is known by the server we're connected to.
|
||||
err = nc1.Flush()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
checkSubInterest(t, s, "B", "foo", time.Second)
|
||||
|
||||
nc2 := clientConnectToServer(t, s)
|
||||
defer nc2.Close()
|
||||
|
||||
resp, err := nc2.Request("foo", nil, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if resp == nil || string(resp.Data) != string(msg) {
|
||||
t.Fatalf("Wrong or empty response")
|
||||
}
|
||||
}
|
||||
|
||||
// Go's stack are infinite sans memory, but not call depth. However its good to limit.
|
||||
func TestAccountCycleDepthLimit(t *testing.T) {
|
||||
var last *server.Account
|
||||
chainLen := server.MaxAccountCycleSearchDepth + 1
|
||||
|
||||
// Services
|
||||
for i := 1; i <= chainLen; i++ {
|
||||
acc := server.NewAccount(fmt.Sprintf("ACC-%d", i))
|
||||
if err := acc.AddServiceExport("*", nil); err != nil {
|
||||
t.Fatalf("Error adding service export to '*': %v", err)
|
||||
}
|
||||
if last != nil {
|
||||
err := acc.AddServiceImport(last, "foo", "foo")
|
||||
switch i {
|
||||
case chainLen:
|
||||
if err != server.ErrCycleSearchDepth {
|
||||
t.Fatalf("Expected last import to fail with '%v', but got '%v'", server.ErrCycleSearchDepth, err)
|
||||
}
|
||||
default:
|
||||
if err != nil {
|
||||
t.Fatalf("Error adding service import to 'foo': %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
last = acc
|
||||
}
|
||||
|
||||
last = nil
|
||||
|
||||
// Streams
|
||||
for i := 1; i <= chainLen; i++ {
|
||||
acc := server.NewAccount(fmt.Sprintf("ACC-%d", i))
|
||||
if err := acc.AddStreamExport("foo", nil); err != nil {
|
||||
t.Fatalf("Error adding stream export to '*': %v", err)
|
||||
}
|
||||
if last != nil {
|
||||
err := acc.AddStreamImport(last, "foo", "")
|
||||
switch i {
|
||||
case chainLen:
|
||||
if err != server.ErrCycleSearchDepth {
|
||||
t.Fatalf("Expected last import to fail with '%v', but got '%v'", server.ErrCycleSearchDepth, err)
|
||||
}
|
||||
default:
|
||||
if err != nil {
|
||||
t.Fatalf("Error adding stream import to 'foo': %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
last = acc
|
||||
}
|
||||
}
|
||||
|
||||
// Test token and partition subject mapping within an account
|
||||
func TestAccountSubjectMapping(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
port: -1
|
||||
mappings = {
|
||||
"foo.*.*" : "foo.$1.{{wildcard(2)}}.{{partition(10,1,2)}}"
|
||||
}
|
||||
`))
|
||||
|
||||
s, _ := RunServerWithConfig(conf)
|
||||
defer s.Shutdown()
|
||||
|
||||
nc1 := clientConnectToServer(t, s)
|
||||
defer nc1.Close()
|
||||
|
||||
numMessages := 100
|
||||
subjectsReceived := make(chan string)
|
||||
|
||||
msg := []byte("HELLO")
|
||||
sub1, err := nc1.Subscribe("foo.*.*.*", func(m *nats.Msg) {
|
||||
subjectsReceived <- m.Subject
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
sub1.AutoUnsubscribe(numMessages * 2)
|
||||
nc1.Flush()
|
||||
|
||||
nc2 := clientConnectToServer(t, s)
|
||||
defer nc2.Close()
|
||||
|
||||
// publish numMessages with an increasing id (should map to partition numbers with the range of 10 partitions) - twice
|
||||
for j := 0; j < 2; j++ {
|
||||
for i := 0; i < numMessages; i++ {
|
||||
err = nc2.Publish(fmt.Sprintf("foo.%d.%d", i, numMessages-i), msg)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verify all the partition numbers are in the expected range
|
||||
partitionsReceived := make([]int, numMessages)
|
||||
|
||||
for i := 0; i < numMessages; i++ {
|
||||
var subject string
|
||||
select {
|
||||
case subject = <-subjectsReceived:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timed out waiting for messages")
|
||||
}
|
||||
sTokens := strings.Split(subject, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
t1, _ := strconv.Atoi(sTokens[1])
|
||||
t2, _ := strconv.Atoi(sTokens[2])
|
||||
partitionsReceived[i], err = strconv.Atoi(sTokens[3])
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if partitionsReceived[i] > 9 || partitionsReceived[i] < 0 || t1 != i || t2 != numMessages-i {
|
||||
t.Fatalf("Error received unexpected %d.%d to partition %d", t1, t2, partitionsReceived[i])
|
||||
}
|
||||
}
|
||||
|
||||
// verify hashing is deterministic by checking it produces the same exact result twice
|
||||
for i := 0; i < numMessages; i++ {
|
||||
subject := <-subjectsReceived
|
||||
partitionNumber, err := strconv.Atoi(strings.Split(subject, ".")[3])
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if partitionsReceived[i] != partitionNumber {
|
||||
t.Fatalf("Error: same id mapped to two different partitions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// test token subject mapping within an account
|
||||
// Alice imports from Bob with subject mapping
|
||||
func TestAccountImportSubjectMapping(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
port: -1
|
||||
accounts {
|
||||
A {
|
||||
users: [{user: a, pass: x}]
|
||||
imports [ {stream: {account: B, subject: "foo.*.*"}, to : "foo.$1.{{wildcard(2)}}"}]
|
||||
}
|
||||
B {
|
||||
users: [{user: b, pass x}]
|
||||
exports [ { stream: ">" } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
|
||||
s, opts := RunServerWithConfig(conf)
|
||||
|
||||
defer s.Shutdown()
|
||||
ncA := clientConnectToServerWithUP(t, opts, "a", "x")
|
||||
defer ncA.Close()
|
||||
|
||||
numMessages := 100
|
||||
subjectsReceived := make(chan string)
|
||||
|
||||
msg := []byte("HELLO")
|
||||
sub1, err := ncA.Subscribe("foo.*.*", func(m *nats.Msg) {
|
||||
subjectsReceived <- m.Subject
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
sub1.AutoUnsubscribe(numMessages)
|
||||
ncA.Flush()
|
||||
|
||||
ncB := clientConnectToServerWithUP(t, opts, "b", "x")
|
||||
defer ncB.Close()
|
||||
|
||||
// publish numMessages with an increasing id
|
||||
|
||||
for i := 0; i < numMessages; i++ {
|
||||
err = ncB.Publish(fmt.Sprintf("foo.%d.%d", i, numMessages-i), msg)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < numMessages; i++ {
|
||||
var subject string
|
||||
select {
|
||||
case subject = <-subjectsReceived:
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatal("Timed out waiting for messages")
|
||||
}
|
||||
sTokens := strings.Split(subject, ".")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
t1, _ := strconv.Atoi(sTokens[1])
|
||||
t2, _ := strconv.Atoi(sTokens[2])
|
||||
|
||||
if t1 != i || t2 != numMessages-i {
|
||||
t.Fatalf("Error received unexpected %d.%d", t1, t2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// bug: https://github.com/nats-io/nats-server/issues/1789
|
||||
func TestAccountCycleWithRenaming(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: * } ]
|
||||
imports [ { service { subject: foo, account: B }, to: foo } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: foo } ]
|
||||
imports [ { service { subject: *, account: A }, to: "$1" } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cycle service import, got none")
|
||||
}
|
||||
|
||||
conf = createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { stream: * } ]
|
||||
imports [ { stream { subject: foo, account: B }, to: foo } ]
|
||||
}
|
||||
B {
|
||||
exports [ { stream: foo } ]
|
||||
imports [ { stream { subject: *, account: A }, to: "$1" } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected an error on cycle service import, got none")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/nats-io/nats-server/issues/5752
|
||||
func TestAccountCycleFalsePositiveSubjectMapping(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: "a.*" } ]
|
||||
imports [ { service { subject: "a.*", account: B }, to: "b.*" } ]
|
||||
}
|
||||
B {
|
||||
exports [ { service: "a.*" } ]
|
||||
imports [ { service { subject: "a.foo", account: A }, to: "c.foo" } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err != nil {
|
||||
t.Fatalf("Expected no errors on cycle service import, got error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccountCycleServiceMissedDueToFromMutation reproduces a bug where
|
||||
// checkServiceImportsForCycles mutates the `from` loop variable via
|
||||
// `from = si.from`, affecting subsequent map iterations non-deterministically.
|
||||
//
|
||||
// Cycle: E -> A -> D -> E
|
||||
// A also has a non-cyclic import from C. When C's map key is iterated first,
|
||||
// `from` is narrowed from "*" to "narrow", causing D's import to be skipped
|
||||
// (SubjectsCollide("narrow","other") is false), and the cycle is missed.
|
||||
func TestAccountCycleServiceMissedDueToFromMutation(t *testing.T) {
|
||||
// Run multiple times because Go map iteration order is non-deterministic.
|
||||
// The bug only manifests when the non-cyclic import is visited first.
|
||||
for i := 0; i < 100; i++ {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { service: * } ]
|
||||
imports [
|
||||
{ service { subject: narrow, account: C } }
|
||||
{ service { subject: other, account: D } }
|
||||
]
|
||||
}
|
||||
C {
|
||||
exports [ { service: narrow } ]
|
||||
}
|
||||
D {
|
||||
exports [ { service: other } ]
|
||||
imports [ { service { subject: other, account: E } } ]
|
||||
}
|
||||
E {
|
||||
exports [ { service: * } ]
|
||||
imports [ { service { subject: *, account: A } } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
_, err := server.ProcessConfigFile(conf)
|
||||
if err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Iteration %d: Expected cycle detection error for E->A->D->E, got: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAccountCycleStreamMissedDueToToMutation reproduces a bug where the `to`
|
||||
// parameter is mutated inside the loop in checkStreamImportsForCycles.
|
||||
// Stream imports use a slice (deterministic order), so listing the narrowing
|
||||
// import first guarantees the bug triggers every time.
|
||||
//
|
||||
// Cycle: E → A → D → E (via subject "other")
|
||||
// A also imports "narrow" from C (leaf, no cycle) declared first.
|
||||
// After processing the first import, `to` is narrowed from "*" to "narrow",
|
||||
// causing SubjectsCollide("narrow","other") to return false and miss the cycle.
|
||||
func TestAccountCycleStreamMissedDueToToMutation(t *testing.T) {
|
||||
conf := createConfFile(t, []byte(`
|
||||
accounts {
|
||||
A {
|
||||
exports [ { stream: * } ]
|
||||
imports [
|
||||
{ stream { subject: narrow, account: C }, to: narrow }
|
||||
{ stream { subject: other, account: D }, to: other }
|
||||
]
|
||||
}
|
||||
C {
|
||||
exports [ { stream: narrow } ]
|
||||
}
|
||||
D {
|
||||
exports [ { stream: other } ]
|
||||
imports [ { stream { subject: other, account: E }, to: other } ]
|
||||
}
|
||||
E {
|
||||
exports [ { stream: * } ]
|
||||
imports [ { stream { subject: *, account: A } } ]
|
||||
}
|
||||
}
|
||||
`))
|
||||
if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) {
|
||||
t.Fatalf("Expected cycle detection error for E->A->D->E, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func clientConnectToServer(t *testing.T, s *server.Server) *nats.Conn {
|
||||
t.Helper()
|
||||
nc, err := nats.Connect(s.ClientURL(),
|
||||
nats.Name("JS-TEST"),
|
||||
nats.ReconnectWait(5*time.Millisecond),
|
||||
nats.MaxReconnects(-1))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
return nc
|
||||
}
|
||||
|
||||
func clientConnectToServerWithUP(t *testing.T, opts *server.Options, user, pass string) *nats.Conn {
|
||||
curl := fmt.Sprintf("nats://%s:%s@%s:%d", user, pass, opts.Host, opts.Port)
|
||||
nc, err := nats.Connect(curl, nats.Name("JS-UP-TEST"), nats.ReconnectWait(5*time.Millisecond), nats.MaxReconnects(-1))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create client: %v", err)
|
||||
}
|
||||
return nc
|
||||
}
|
||||
Reference in New Issue
Block a user