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  }