github.com/nats-io/nsc/v2@v2.8.7-0.20240307184528-efd7023c6896/cmd/push_test.go (about)

     1  /*
     2   * Copyright 2018-2023 The NATS Authors
     3   * Licensed under the Apache License, Version 2.0 (the "License");
     4   * you may not use this file except in compliance with the License.
     5   * You may obtain a copy of the License at
     6   *
     7   * http://www.apache.org/licenses/LICENSE-2.0
     8   *
     9   * Unless required by applicable law or agreed to in writing, software
    10   * distributed under the License is distributed on an "AS IS" BASIS,
    11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12   * See the License for the specific language governing permissions and
    13   * limitations under the License.
    14   */
    15  
    16  package cmd
    17  
    18  import (
    19  	"bytes"
    20  	"fmt"
    21  	"os"
    22  	"path/filepath"
    23  	"regexp"
    24  	"runtime"
    25  	"strings"
    26  	"testing"
    27  
    28  	"github.com/nats-io/jwt/v2"
    29  	"github.com/stretchr/testify/require"
    30  )
    31  
    32  func Test_SyncOK(t *testing.T) {
    33  	_, _, okp := CreateOperatorKey(t)
    34  	as, m := RunTestAccountServerWithOperatorKP(t, okp, TasOpts{Vers: 2})
    35  	defer as.Close()
    36  
    37  	ts := NewTestStoreWithOperator(t, "T", okp)
    38  	err := ts.Store.StoreRaw(m["operator"])
    39  	require.NoError(t, err)
    40  	ts.AddAccount(t, "A")
    41  
    42  	// edit the jwt
    43  	_, _, err = ExecuteCmd(createEditAccount(), "--tag", "A")
    44  	require.NoError(t, err)
    45  
    46  	// sync the store
    47  	_, _, err = ExecuteCmd(createPushCmd(), "--account", "A")
    48  	require.NoError(t, err)
    49  
    50  	// verify the tag was stored
    51  	ac, err := ts.Store.ReadAccountClaim("A")
    52  	require.NoError(t, err)
    53  	require.Contains(t, ac.Tags, "a")
    54  }
    55  
    56  func Test_SyncNoURL(t *testing.T) {
    57  	_, _, okp := CreateOperatorKey(t)
    58  	as, m := RunTestAccountServerWithOperatorKP(t, okp, TasOpts{Vers: 2})
    59  	ts := NewTestStoreWithOperatorJWT(t, string(m["operator"]))
    60  	ts.AddAccount(t, "A")
    61  	as.Close()
    62  
    63  	// remove the account server so we cannot push
    64  	oc, err := ts.Store.ReadOperatorClaim()
    65  	require.NoError(t, err)
    66  	oc.AccountServerURL = ""
    67  	token, err := oc.Encode(okp)
    68  	require.NoError(t, err)
    69  	ts.Store.StoreClaim([]byte(token))
    70  
    71  	_, _, err = ExecuteCmd(createPushCmd(), "--account", "A")
    72  	require.Error(t, err)
    73  	require.Contains(t, err.Error(), "no account server url or nats-server url was provided by the operator jwt")
    74  }
    75  
    76  func Test_SyncNoServer(t *testing.T) {
    77  	as, m := RunTestAccountServer(t)
    78  	ts := NewTestStoreWithOperatorJWT(t, string(m["operator"]))
    79  	ts.AddAccount(t, "A")
    80  	as.Close()
    81  
    82  	_, stderr, err := ExecuteCmd(createPushCmd(), "--account", "A")
    83  	require.Error(t, err)
    84  	if runtime.GOOS == "windows" {
    85  		require.Contains(t, stderr, "connectex: No connection")
    86  	} else {
    87  		require.Contains(t, stderr, "connect: connection refused")
    88  	}
    89  }
    90  
    91  func Test_SyncManaged(t *testing.T) {
    92  	as, m := RunTestAccountServer(t)
    93  	defer as.Close()
    94  
    95  	ts := NewTestStoreWithOperatorJWT(t, string(m["operator"]))
    96  	defer ts.Done(t)
    97  
    98  	ts.AddAccount(t, "A")
    99  	ac, err := ts.Store.ReadAccountClaim("A")
   100  	require.NoError(t, err)
   101  	require.False(t, ac.IsSelfSigned())
   102  }
   103  
   104  func Test_SyncManualServer(t *testing.T) {
   105  	_, _, okp := CreateOperatorKey(t)
   106  	as, m := RunTestAccountServerWithOperatorKP(t, okp, TasOpts{Vers: 2})
   107  	defer as.Close()
   108  
   109  	// remove the account server
   110  	op, err := jwt.DecodeOperatorClaims(string(m["operator"]))
   111  	require.NoError(t, err)
   112  	op.AccountServerURL = ""
   113  	s, err := op.Encode(okp)
   114  	require.NoError(t, err)
   115  	m["operator"] = []byte(s)
   116  
   117  	ts := NewTestStoreWithOperator(t, "T", okp)
   118  	err = ts.Store.StoreRaw(m["operator"])
   119  	require.NoError(t, err)
   120  	ts.AddAccount(t, "A")
   121  
   122  	// edit the jwt
   123  	_, _, err = ExecuteCmd(createEditAccount(), "--tag", "A")
   124  	require.NoError(t, err)
   125  
   126  	// sync the store
   127  	_, _, err = ExecuteCmd(createPushCmd(), "--account", "A", "--account-jwt-server-url", as.URL)
   128  	require.NoError(t, err)
   129  
   130  	// verify the tag was stored
   131  	ac, err := ts.Store.ReadAccountClaim("A")
   132  	require.NoError(t, err)
   133  	require.Contains(t, ac.Tags, "a")
   134  }
   135  
   136  func deleteSetup(t *testing.T, del bool) (string, []string, *TestStore) {
   137  	t.Helper()
   138  
   139  	ts := NewTestStore(t, "O")
   140  	ts.AddAccount(t, "SYS")
   141  	ts.AddAccount(t, "AC1")
   142  	ts.AddAccount(t, "AC2")
   143  
   144  	_, _, err := ExecuteCmd(createEditOperatorCmd(), "--system-account", "SYS")
   145  	require.NoError(t, err)
   146  
   147  	serverconf := filepath.Join(ts.Dir, "server.conf")
   148  	_, _, err = ExecuteCmd(createServerConfigCmd(), "--nats-resolver", "--config-file", serverconf)
   149  	require.NoError(t, err)
   150  
   151  	// modify the generated file so testing becomes easier by knowing where the jwt directory is
   152  	data, err := os.ReadFile(serverconf)
   153  	require.NoError(t, err)
   154  	dir := ts.AddSubDir(t, "resolver")
   155  	data = bytes.ReplaceAll(data, []byte(`dir: './jwt'`), []byte(fmt.Sprintf(`dir: '%s'`, dir)))
   156  	data = bytes.ReplaceAll(data, []byte(`dir: '.\jwt'`), []byte(fmt.Sprintf(`dir: '%s'`, dir)))
   157  	data = bytes.ReplaceAll(data, []byte(`allow_delete: false`), []byte(fmt.Sprintf(`allow_delete: %t`, del)))
   158  	err = os.WriteFile(serverconf, data, 0660)
   159  	require.NoError(t, err)
   160  	ports := ts.RunServerWithConfig(t, serverconf)
   161  	require.NotNil(t, ports)
   162  	// only after server start as ports are not yet known in tests
   163  	_, _, err = ExecuteCmd(createEditOperatorCmd(), "--account-jwt-server-url", ports.Nats[0])
   164  	require.NoError(t, err)
   165  	_, _, err = ExecuteCmd(createPushCmd(), "--all")
   166  	require.NoError(t, err)
   167  	// test to assure AC1/AC2/SYS where pushed
   168  	filesPre, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt")
   169  	require.NoError(t, err)
   170  	require.Equal(t, len(filesPre), 3)
   171  	_, _, err = ExecuteCmd(createDeleteAccountCmd(), "--name", "AC2")
   172  	require.NoError(t, err)
   173  	// exists as nsc has a bad default account now (is not pushed, hence not in file counts)
   174  	_, _, err = ExecuteCmd(CreateAddAccountCmd(), "--name", "AC3")
   175  	require.NoError(t, err)
   176  	return dir, filesPre, ts
   177  }
   178  
   179  func Test_SyncNatsResolverDeleteNoOperatorKey(t *testing.T) {
   180  	_, _, ts := deleteSetup(t, true)
   181  	defer ts.Done(t)
   182  
   183  	opk, err := ts.OperatorKey.PublicKey()
   184  	require.NoError(t, err)
   185  	require.NoError(t, ts.KeyStore.Remove(opk))
   186  
   187  	_, stderr, err := ExecuteCmd(createPushCmd(), "--prune")
   188  	t.Log(stderr)
   189  	require.Error(t, err)
   190  }
   191  
   192  func Test_SyncNatsResolverDeleteOperatorKeyInFlag(t *testing.T) {
   193  	_, _, ts := deleteSetup(t, true)
   194  	defer ts.Done(t)
   195  
   196  	okp := ts.OperatorKey
   197  	seed, err := okp.Seed()
   198  	require.NoError(t, err)
   199  
   200  	opk, err := ts.OperatorKey.PublicKey()
   201  	require.NoError(t, err)
   202  	require.NoError(t, ts.KeyStore.Remove(opk))
   203  
   204  	cmd := createPushCmd()
   205  	HoistRootFlags(cmd)
   206  	_, _, err = ExecuteCmd(cmd, "--prune", "-K", string(seed))
   207  	require.NoError(t, err)
   208  }
   209  
   210  func Test_SyncNatsResolverDelete(t *testing.T) {
   211  	dir, filesPre, ts := deleteSetup(t, true)
   212  	defer ts.Done(t)
   213  
   214  	_, _, err := ExecuteCmd(createPushCmd(), "--prune")
   215  	require.NoError(t, err)
   216  	// test to assure AC1/SYS where pushed/pruned
   217  	filesPost, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt")
   218  	require.NoError(t, err)
   219  	require.Equal(t, 2, len(filesPost))
   220  	// assert only AC1/SYS overlap in pre/post
   221  	sameCnt := 0
   222  	for _, f1 := range filesPost {
   223  		for _, f2 := range filesPre {
   224  			if f1 == f2 {
   225  				sameCnt++
   226  				break
   227  			}
   228  		}
   229  	}
   230  	require.Equal(t, 2, sameCnt)
   231  	filesDeleted, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt.deleted")
   232  	require.NoError(t, err)
   233  	require.Equal(t, 1, len(filesDeleted))
   234  }
   235  
   236  func Test_SyncNatsResolverExplicitDelete(t *testing.T) {
   237  	dir, filesPre, ts := deleteSetup(t, true)
   238  	defer os.Remove(dir)
   239  	defer ts.Done(t)
   240  	_, _, err := ExecuteCmd(createPushCmd(), "--account-removal", "AC1")
   241  	require.NoError(t, err)
   242  	// test to assure AC1/SYS where pushed/pruned
   243  	filesPost, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt")
   244  	require.NoError(t, err)
   245  	require.Equal(t, 2, len(filesPost))
   246  	// assert only AC1/SYS overlap in pre/post
   247  	sameCnt := 0
   248  	for _, f1 := range filesPost {
   249  		for _, f2 := range filesPre {
   250  			if f1 == f2 {
   251  				sameCnt++
   252  				break
   253  			}
   254  		}
   255  	}
   256  	require.Equal(t, 2, sameCnt)
   257  	filesDeleted, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt.deleted")
   258  	require.NoError(t, err)
   259  	require.Equal(t, 1, len(filesDeleted))
   260  }
   261  
   262  func Test_SyncNatsResolverDiff(t *testing.T) {
   263  	dir, _, ts := deleteSetup(t, true)
   264  	defer os.Remove(dir)
   265  	defer ts.Done(t)
   266  	_, stdErr, err := ExecuteCmd(createPushCmd(), "--diff")
   267  	require.NoError(t, err)
   268  	require.Contains(t, stdErr, "only exists in server")
   269  	require.Contains(t, stdErr, "named AC1 exists")
   270  	require.Contains(t, stdErr, "named SYS exists")
   271  
   272  	re := regexp.MustCompile("[A-Z0-9]* named AC1 exists")
   273  	line := re.FindString(stdErr)
   274  	accId := strings.TrimSuffix(line, " named AC1 exists")
   275  
   276  	_, _, err = ExecuteCmd(createPushCmd(), "--account-removal", accId)
   277  	require.NoError(t, err)
   278  	filesDeleted, err := filepath.Glob(dir + string(os.PathSeparator) + accId + ".jwt.deleted")
   279  	require.NoError(t, err)
   280  	require.Equal(t, 1, len(filesDeleted))
   281  	_, stdErr, err = ExecuteCmd(createPushCmd(), "--diff")
   282  	require.NoError(t, err)
   283  	require.Contains(t, stdErr, "only exists in server")
   284  	require.NotContains(t, stdErr, "named AC1 exists")
   285  	require.Contains(t, stdErr, "named SYS exists")
   286  }
   287  
   288  func Test_SyncNatsResolverDeleteSYS(t *testing.T) {
   289  	dir, filesPre, ts := deleteSetup(t, true)
   290  	defer os.Remove(dir)
   291  	defer ts.Done(t)
   292  	_, _, err := ExecuteCmd(createDeleteAccountCmd(), "--name", "SYS")
   293  	require.NoError(t, err)
   294  	// exists as nsc has a bad default account now (is not pushed, hence not in file counts)
   295  	_, _, err = ExecuteCmd(CreateAddAccountCmd(), "--name", "AC4")
   296  	require.NoError(t, err)
   297  	_, _, err = ExecuteCmd(createPushCmd(), "--prune") // will fail as system acc can't be deleted
   298  	require.Error(t, err)                              // this will actually not hit the server as the system account is already deleted
   299  	filesPost, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt")
   300  	require.NoError(t, err)
   301  	require.Equal(t, 3, len(filesPost))
   302  	require.Equal(t, filesPre, filesPost)
   303  }
   304  
   305  func Test_SyncNatsResolverNoDelete(t *testing.T) {
   306  	dir, filesPre, ts := deleteSetup(t, false)
   307  	defer os.Remove(dir)
   308  	defer ts.Done(t)
   309  	_, _, err := ExecuteCmd(createPushCmd(), "--prune")
   310  	require.Error(t, err)
   311  	// test to assure that pruning did not happen
   312  	filesPost, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt")
   313  	require.NoError(t, err)
   314  	require.Equal(t, 3, len(filesPost))
   315  	require.Equal(t, filesPre, filesPost)
   316  }
   317  
   318  func Test_SyncBadUrl(t *testing.T) {
   319  	ts := NewEmptyStore(t)
   320  	defer ts.Done(t)
   321  	_, _, err := ExecuteCmd(createAddOperatorCmd(), "--name", "OP", "--sys")
   322  	require.NoError(t, err)
   323  	serverconf := filepath.Join(ts.Dir, "server.conf")
   324  	_, _, err = ExecuteCmd(createServerConfigCmd(), "--nats-resolver", "--config-file", serverconf)
   325  	require.NoError(t, err)
   326  	_, _, err = ExecuteCmd(CreateAddAccountCmd(), "--name", "AC1")
   327  	require.NoError(t, err)
   328  	// modify the generated file so testing becomes easier by knowing where the jwt directory is
   329  	data, err := os.ReadFile(serverconf)
   330  	require.NoError(t, err)
   331  	dir := ts.AddSubDir(t, "resolver")
   332  	data = bytes.ReplaceAll(data, []byte(`dir: './jwt'`), []byte(fmt.Sprintf(`dir: '%s'`, dir)))
   333  	err = os.WriteFile(serverconf, data, 0660)
   334  	require.NoError(t, err)
   335  	ports := ts.RunServerWithConfig(t, serverconf)
   336  	require.NotNil(t, ports)
   337  	// deliberately test if http push to a nats server kills it or not
   338  	badUrl := strings.ReplaceAll(ports.Nats[0], "nats://", "http://")
   339  	_, _, err = ExecuteCmd(createEditOperatorCmd(), "--account-jwt-server-url", badUrl)
   340  	require.NoError(t, err)
   341  	_, errOut, err := ExecuteCmd(createPushCmd(), "--all")
   342  	require.Error(t, err)
   343  	require.Contains(t, errOut, `Post "`+badUrl)
   344  	// Fix bad url
   345  	_, _, err = ExecuteCmd(createEditOperatorCmd(), "--account-jwt-server-url", ports.Nats[0])
   346  	require.NoError(t, err)
   347  	// Try again, thus also testing if the server is still around
   348  	// Provide explicit system account user to connect
   349  	_, _, err = ExecuteCmd(createPushCmd(), "--all", "--system-account", "SYS", "--system-user", "sys")
   350  	require.NoError(t, err)
   351  	// test to assure AC1/AC2/SYS where pushed
   352  	filesPre, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt")
   353  	require.NoError(t, err)
   354  	require.Equal(t, len(filesPre), 2)
   355  }
   356  
   357  func Test_SyncWs(t *testing.T) {
   358  	ts := NewEmptyStore(t)
   359  	defer ts.Done(t)
   360  	_, _, err := ExecuteCmd(createAddOperatorCmd(), "--name", "OP", "--sys")
   361  	require.NoError(t, err)
   362  	serverconf := filepath.Join(ts.Dir, "server.conf")
   363  	_, _, err = ExecuteCmd(createServerConfigCmd(), "--nats-resolver", "--config-file", serverconf)
   364  	require.NoError(t, err)
   365  	_, _, err = ExecuteCmd(CreateAddAccountCmd(), "--name", "AC1")
   366  	require.NoError(t, err)
   367  	// modify the generated file so testing becomes easier by knowing where the jwt directory is
   368  	data, err := os.ReadFile(serverconf)
   369  	require.NoError(t, err)
   370  	dir := ts.AddSubDir(t, "resolver")
   371  
   372  	ws := `websocket: { 
   373    port: -1 
   374    no_tls: true
   375  }`
   376  	data = append(data, ws...)
   377  	data = bytes.ReplaceAll(data, []byte(`dir: './jwt'`), []byte(fmt.Sprintf(`dir: '%s'`, dir)))
   378  	err = os.WriteFile(serverconf, data, 0660)
   379  	require.NoError(t, err)
   380  	ports := ts.RunServerWithConfig(t, serverconf)
   381  	require.NotNil(t, ports)
   382  	_, _, err = ExecuteCmd(createEditOperatorCmd(), "--account-jwt-server-url", ports.WebSocket[0])
   383  	require.NoError(t, err)
   384  	// Try again, thus also testing if the server is still around
   385  	// Provide explicit system account user to connect
   386  	_, _, err = ExecuteCmd(createPushCmd(), "--all", "--system-account", "SYS", "--system-user", "sys")
   387  	require.NoError(t, err)
   388  	// test to assure AC1/AC2/SYS where pushed
   389  	filesPre, err := filepath.Glob(dir + string(os.PathSeparator) + "*.jwt")
   390  	require.NoError(t, err)
   391  	require.Equal(t, len(filesPre), 2)
   392  }