get.pme.sh/pnats@v0.0.0-20240304004023-26bb5a137ed0/test/accounts_cycles_test.go (about) 1 // Copyright 2020 The NATS Authors 2 // Licensed under the Apache License, Version 2.0 (the "License"); 3 // you may not use this file except in compliance with the License. 4 // You may obtain a copy of the License at 5 // 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package test 15 16 import ( 17 "fmt" 18 "strconv" 19 "strings" 20 "testing" 21 "time" 22 23 "get.pme.sh/pnats/server" 24 "github.com/nats-io/nats.go" 25 ) 26 27 func TestAccountCycleService(t *testing.T) { 28 conf := createConfFile(t, []byte(` 29 accounts { 30 A { 31 exports [ { service: help } ] 32 imports [ { service { subject: help, account: B } } ] 33 } 34 B { 35 exports [ { service: help } ] 36 imports [ { service { subject: help, account: A } } ] 37 } 38 } 39 `)) 40 41 if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) { 42 t.Fatalf("Expected an error on cycle service import, got none") 43 } 44 45 conf = createConfFile(t, []byte(` 46 accounts { 47 A { 48 exports [ { service: * } ] 49 imports [ { service { subject: help, account: B } } ] 50 } 51 B { 52 exports [ { service: help } ] 53 imports [ { service { subject: *, account: A } } ] 54 } 55 } 56 `)) 57 58 if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) { 59 t.Fatalf("Expected an error on cycle service import, got none") 60 } 61 62 conf = createConfFile(t, []byte(` 63 accounts { 64 A { 65 exports [ { service: * } ] 66 imports [ { service { subject: help, account: B } } ] 67 } 68 B { 69 exports [ { service: help } ] 70 imports [ { service { subject: help, account: C } } ] 71 } 72 C { 73 exports [ { service: * } ] 74 imports [ { service { subject: *, account: A } } ] 75 } 76 } 77 `)) 78 79 if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) { 80 t.Fatalf("Expected an error on cycle service import, got none") 81 } 82 } 83 84 func TestAccountCycleStream(t *testing.T) { 85 conf := createConfFile(t, []byte(` 86 accounts { 87 A { 88 exports [ { stream: strm } ] 89 imports [ { stream { subject: strm, account: B } } ] 90 } 91 B { 92 exports [ { stream: strm } ] 93 imports [ { stream { subject: strm, account: A } } ] 94 } 95 } 96 `)) 97 if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) { 98 t.Fatalf("Expected an error on cyclic import, got none") 99 } 100 } 101 102 func TestAccountCycleStreamWithMapping(t *testing.T) { 103 conf := createConfFile(t, []byte(` 104 accounts { 105 A { 106 exports [ { stream: * } ] 107 imports [ { stream { subject: bar, account: B } } ] 108 } 109 B { 110 exports [ { stream: bar } ] 111 imports [ { stream { subject: foo, account: A }, to: bar } ] 112 } 113 } 114 `)) 115 if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) { 116 t.Fatalf("Expected an error on cyclic import, got none") 117 } 118 } 119 120 func TestAccountCycleNonCycleStreamWithMapping(t *testing.T) { 121 conf := createConfFile(t, []byte(` 122 accounts { 123 A { 124 exports [ { stream: foo } ] 125 imports [ { stream { subject: bar, account: B } } ] 126 } 127 B { 128 exports [ { stream: bar } ] 129 imports [ { stream { subject: baz, account: C }, to: bar } ] 130 } 131 C { 132 exports [ { stream: baz } ] 133 imports [ { stream { subject: foo, account: A }, to: bar } ] 134 } 135 } 136 `)) 137 if _, err := server.ProcessConfigFile(conf); err != nil { 138 t.Fatalf("Expected no error but got %s", err) 139 } 140 } 141 142 func TestAccountCycleServiceCycleWithMapping(t *testing.T) { 143 conf := createConfFile(t, []byte(` 144 accounts { 145 A { 146 exports [ { service: a } ] 147 imports [ { service { subject: b, account: B }, to: a } ] 148 } 149 B { 150 exports [ { service: b } ] 151 imports [ { service { subject: a, account: A }, to: b } ] 152 } 153 } 154 `)) 155 if _, err := server.ProcessConfigFile(conf); err == nil || !strings.Contains(err.Error(), server.ErrImportFormsCycle.Error()) { 156 t.Fatalf("Expected an error on cycle service import, got none") 157 } 158 } 159 160 func TestAccountCycleServiceNonCycle(t *testing.T) { 161 conf := createConfFile(t, []byte(` 162 accounts { 163 A { 164 exports [ { service: * } ] 165 imports [ { service { subject: help, account: B } } ] 166 } 167 B { 168 exports [ { service: help } ] 169 imports [ { service { subject: nohelp, account: C } } ] 170 } 171 C { 172 exports [ { service: * } ] 173 imports [ { service { subject: *, account: A } } ] 174 } 175 } 176 `)) 177 178 if _, err := server.ProcessConfigFile(conf); err != nil { 179 t.Fatalf("Expected no error but got %s", err) 180 } 181 } 182 183 func TestAccountCycleServiceNonCycleChain(t *testing.T) { 184 conf := createConfFile(t, []byte(` 185 accounts { 186 A { 187 exports [ { service: help } ] 188 imports [ { service { subject: help, account: B } } ] 189 } 190 B { 191 exports [ { service: help } ] 192 imports [ { service { subject: help, account: C } } ] 193 } 194 C { 195 exports [ { service: help } ] 196 imports [ { service { subject: help, account: D } } ] 197 } 198 D { 199 exports [ { service: help } ] 200 } 201 } 202 `)) 203 204 if _, err := server.ProcessConfigFile(conf); err != nil { 205 t.Fatalf("Expected no error but got %s", err) 206 } 207 } 208 209 // bug: https://github.com/nats-io/nats-server/issues/1769 210 func TestServiceImportReplyMatchCycle(t *testing.T) { 211 conf := createConfFile(t, []byte(` 212 port: -1 213 accounts { 214 A { 215 users: [{user: d, pass: x}] 216 imports [ {service: {account: B, subject: ">" }}] 217 } 218 B { 219 users: [{user: x, pass: x}] 220 exports [ { service: ">" } ] 221 } 222 } 223 no_auth_user: d 224 `)) 225 226 s, opts := RunServerWithConfig(conf) 227 defer s.Shutdown() 228 229 nc1 := clientConnectToServerWithUP(t, opts, "x", "x") 230 defer nc1.Close() 231 232 msg := []byte("HELLO") 233 nc1.Subscribe("foo", func(m *nats.Msg) { 234 m.Respond(msg) 235 }) 236 237 nc2 := clientConnectToServer(t, s) 238 defer nc2.Close() 239 240 resp, err := nc2.Request("foo", nil, time.Second) 241 if err != nil { 242 t.Fatalf("Unexpected error: %v", err) 243 } 244 if resp == nil || string(resp.Data) != string(msg) { 245 t.Fatalf("Wrong or empty response") 246 } 247 } 248 249 func TestServiceImportReplyMatchCycleMultiHops(t *testing.T) { 250 conf := createConfFile(t, []byte(` 251 port: -1 252 accounts { 253 A { 254 users: [{user: d, pass: x}] 255 imports [ {service: {account: B, subject: ">" }}] 256 } 257 B { 258 exports [ { service: ">" } ] 259 imports [ {service: {account: C, subject: ">" }}] 260 } 261 C { 262 users: [{user: x, pass: x}] 263 exports [ { service: ">" } ] 264 } 265 } 266 no_auth_user: d 267 `)) 268 269 s, opts := RunServerWithConfig(conf) 270 defer s.Shutdown() 271 272 nc1 := clientConnectToServerWithUP(t, opts, "x", "x") 273 defer nc1.Close() 274 275 msg := []byte("HELLO") 276 nc1.Subscribe("foo", func(m *nats.Msg) { 277 m.Respond(msg) 278 }) 279 280 nc2 := clientConnectToServer(t, s) 281 defer nc2.Close() 282 283 resp, err := nc2.Request("foo", nil, time.Second) 284 if err != nil { 285 t.Fatalf("Unexpected error: %v", err) 286 } 287 if resp == nil || string(resp.Data) != string(msg) { 288 t.Fatalf("Wrong or empty response") 289 } 290 } 291 292 // Go's stack are infinite sans memory, but not call depth. However its good to limit. 293 func TestAccountCycleDepthLimit(t *testing.T) { 294 var last *server.Account 295 chainLen := server.MaxAccountCycleSearchDepth + 1 296 297 // Services 298 for i := 1; i <= chainLen; i++ { 299 acc := server.NewAccount(fmt.Sprintf("ACC-%d", i)) 300 if err := acc.AddServiceExport("*", nil); err != nil { 301 t.Fatalf("Error adding service export to '*': %v", err) 302 } 303 if last != nil { 304 err := acc.AddServiceImport(last, "foo", "foo") 305 switch i { 306 case chainLen: 307 if err != server.ErrCycleSearchDepth { 308 t.Fatalf("Expected last import to fail with '%v', but got '%v'", server.ErrCycleSearchDepth, err) 309 } 310 default: 311 if err != nil { 312 t.Fatalf("Error adding service import to 'foo': %v", err) 313 } 314 } 315 } 316 last = acc 317 } 318 319 last = nil 320 321 // Streams 322 for i := 1; i <= chainLen; i++ { 323 acc := server.NewAccount(fmt.Sprintf("ACC-%d", i)) 324 if err := acc.AddStreamExport("foo", nil); err != nil { 325 t.Fatalf("Error adding stream export to '*': %v", err) 326 } 327 if last != nil { 328 err := acc.AddStreamImport(last, "foo", "") 329 switch i { 330 case chainLen: 331 if err != server.ErrCycleSearchDepth { 332 t.Fatalf("Expected last import to fail with '%v', but got '%v'", server.ErrCycleSearchDepth, err) 333 } 334 default: 335 if err != nil { 336 t.Fatalf("Error adding stream import to 'foo': %v", err) 337 } 338 } 339 } 340 last = acc 341 } 342 } 343 344 // Test token and partition subject mapping within an account 345 func TestAccountSubjectMapping(t *testing.T) { 346 conf := createConfFile(t, []byte(` 347 port: -1 348 mappings = { 349 "foo.*.*" : "foo.$1.{{wildcard(2)}}.{{partition(10,1,2)}}" 350 } 351 `)) 352 353 s, _ := RunServerWithConfig(conf) 354 defer s.Shutdown() 355 356 nc1 := clientConnectToServer(t, s) 357 defer nc1.Close() 358 359 numMessages := 100 360 subjectsReceived := make(chan string) 361 362 msg := []byte("HELLO") 363 sub1, err := nc1.Subscribe("foo.*.*.*", func(m *nats.Msg) { 364 subjectsReceived <- m.Subject 365 }) 366 if err != nil { 367 t.Fatalf("Unexpected error: %v", err) 368 } 369 sub1.AutoUnsubscribe(numMessages * 2) 370 nc1.Flush() 371 372 nc2 := clientConnectToServer(t, s) 373 defer nc2.Close() 374 375 // publish numMessages with an increasing id (should map to partition numbers with the range of 10 partitions) - twice 376 for j := 0; j < 2; j++ { 377 for i := 0; i < numMessages; i++ { 378 err = nc2.Publish(fmt.Sprintf("foo.%d.%d", i, numMessages-i), msg) 379 if err != nil { 380 t.Fatalf("Unexpected error: %v", err) 381 } 382 } 383 } 384 385 // verify all the partition numbers are in the expected range 386 partitionsReceived := make([]int, numMessages) 387 388 for i := 0; i < numMessages; i++ { 389 var subject string 390 select { 391 case subject = <-subjectsReceived: 392 case <-time.After(5 * time.Second): 393 t.Fatal("Timed out waiting for messages") 394 } 395 sTokens := strings.Split(subject, ".") 396 if err != nil { 397 t.Fatalf("Unexpected error: %v", err) 398 } 399 t1, _ := strconv.Atoi(sTokens[1]) 400 t2, _ := strconv.Atoi(sTokens[2]) 401 partitionsReceived[i], err = strconv.Atoi(sTokens[3]) 402 if err != nil { 403 t.Fatalf("Unexpected error: %v", err) 404 } 405 406 if partitionsReceived[i] > 9 || partitionsReceived[i] < 0 || t1 != i || t2 != numMessages-i { 407 t.Fatalf("Error received unexpected %d.%d to partition %d", t1, t2, partitionsReceived[i]) 408 } 409 } 410 411 // verify hashing is deterministic by checking it produces the same exact result twice 412 for i := 0; i < numMessages; i++ { 413 subject := <-subjectsReceived 414 partitionNumber, err := strconv.Atoi(strings.Split(subject, ".")[3]) 415 if err != nil { 416 t.Fatalf("Unexpected error: %v", err) 417 } 418 if partitionsReceived[i] != partitionNumber { 419 t.Fatalf("Error: same id mapped to two different partitions") 420 } 421 } 422 } 423 424 // test token subject mapping within an account 425 // Alice imports from Bob with subject mapping 426 func TestAccountImportSubjectMapping(t *testing.T) { 427 conf := createConfFile(t, []byte(` 428 port: -1 429 accounts { 430 A { 431 users: [{user: a, pass: x}] 432 imports [ {stream: {account: B, subject: "foo.*.*"}, to : "foo.$1.{{wildcard(2)}}"}] 433 } 434 B { 435 users: [{user: b, pass x}] 436 exports [ { stream: ">" } ] 437 } 438 } 439 `)) 440 441 s, opts := RunServerWithConfig(conf) 442 443 defer s.Shutdown() 444 ncA := clientConnectToServerWithUP(t, opts, "a", "x") 445 defer ncA.Close() 446 447 numMessages := 100 448 subjectsReceived := make(chan string) 449 450 msg := []byte("HELLO") 451 sub1, err := ncA.Subscribe("foo.*.*", func(m *nats.Msg) { 452 subjectsReceived <- m.Subject 453 }) 454 if err != nil { 455 t.Fatalf("Unexpected error: %v", err) 456 } 457 sub1.AutoUnsubscribe(numMessages) 458 ncA.Flush() 459 460 ncB := clientConnectToServerWithUP(t, opts, "b", "x") 461 defer ncB.Close() 462 463 // publish numMessages with an increasing id 464 465 for i := 0; i < numMessages; i++ { 466 err = ncB.Publish(fmt.Sprintf("foo.%d.%d", i, numMessages-i), msg) 467 if err != nil { 468 t.Fatalf("Unexpected error: %v", err) 469 } 470 } 471 472 for i := 0; i < numMessages; i++ { 473 var subject string 474 select { 475 case subject = <-subjectsReceived: 476 case <-time.After(1 * time.Second): 477 t.Fatal("Timed out waiting for messages") 478 } 479 sTokens := strings.Split(subject, ".") 480 if err != nil { 481 t.Fatalf("Unexpected error: %v", err) 482 } 483 t1, _ := strconv.Atoi(sTokens[1]) 484 t2, _ := strconv.Atoi(sTokens[2]) 485 486 if t1 != i || t2 != numMessages-i { 487 t.Fatalf("Error received unexpected %d.%d", t1, t2) 488 } 489 } 490 } 491 492 func clientConnectToServer(t *testing.T, s *server.Server) *nats.Conn { 493 t.Helper() 494 nc, err := nats.Connect(s.ClientURL(), 495 nats.Name("JS-TEST"), 496 nats.ReconnectWait(5*time.Millisecond), 497 nats.MaxReconnects(-1)) 498 if err != nil { 499 t.Fatalf("Failed to create client: %v", err) 500 } 501 return nc 502 } 503 504 func clientConnectToServerWithUP(t *testing.T, opts *server.Options, user, pass string) *nats.Conn { 505 curl := fmt.Sprintf("nats://%s:%s@%s:%d", user, pass, opts.Host, opts.Port) 506 nc, err := nats.Connect(curl, nats.Name("JS-UP-TEST"), nats.ReconnectWait(5*time.Millisecond), nats.MaxReconnects(-1)) 507 if err != nil { 508 t.Fatalf("Failed to create client: %v", err) 509 } 510 return nc 511 }