github.com/hernad/nomad@v1.6.112/command/var_list_test.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package command
     5  
     6  import (
     7  	"encoding/json"
     8  	"fmt"
     9  	"strings"
    10  	"testing"
    11  
    12  	"github.com/hernad/nomad/api"
    13  	"github.com/hernad/nomad/ci"
    14  	"github.com/mitchellh/cli"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func TestVarListCommand_Implements(t *testing.T) {
    19  	ci.Parallel(t)
    20  	var _ cli.Command = &VarListCommand{}
    21  }
    22  
    23  // TestVarListCommand_Offline contains all of the tests that do not require a
    24  // testServer to complete
    25  func TestVarListCommand_Offline(t *testing.T) {
    26  	ci.Parallel(t)
    27  	ui := cli.NewMockUi()
    28  	cmd := &VarListCommand{Meta: Meta{Ui: ui}}
    29  
    30  	testCases := []testVarListTestCase{
    31  		{
    32  			name:        "help",
    33  			args:        []string{"-help"},
    34  			exitCode:    1,
    35  			expectUsage: true,
    36  		},
    37  		{
    38  			name:               "bad args",
    39  			args:               []string{"some", "bad", "args"},
    40  			exitCode:           1,
    41  			expectUsageError:   true,
    42  			expectStdErrPrefix: "This command takes flags and either no arguments or one: <prefix>",
    43  		},
    44  		{
    45  			name:               "bad address",
    46  			args:               []string{"-address", "nope"},
    47  			exitCode:           1,
    48  			expectStdErrPrefix: "Error retrieving vars",
    49  		},
    50  		{
    51  			name:               "unparsable address",
    52  			args:               []string{"-address", "http://10.0.0.1:bad"},
    53  			exitCode:           1,
    54  			expectStdErrPrefix: "Error initializing client: invalid address",
    55  		},
    56  		{
    57  			name:               "missing template",
    58  			args:               []string{`-out=go-template`, "foo"},
    59  			exitCode:           1,
    60  			expectStdErrPrefix: errMissingTemplate,
    61  		},
    62  		{
    63  			name:               "unexpected_template",
    64  			args:               []string{`-out=json`, `-template="bad"`, "foo"},
    65  			exitCode:           1,
    66  			expectStdErrPrefix: errUnexpectedTemplate,
    67  		},
    68  		{
    69  			name:               "bad out",
    70  			args:               []string{`-out=bad`, "foo"},
    71  			exitCode:           1,
    72  			expectStdErrPrefix: errInvalidListOutFormat,
    73  		},
    74  	}
    75  	for _, tC := range testCases {
    76  		t.Run(tC.name, func(t *testing.T) {
    77  			tC := tC
    78  			ec := cmd.Run(tC.args)
    79  			stdOut := ui.OutputWriter.String()
    80  			errOut := ui.ErrorWriter.String()
    81  			defer resetUiWriters(ui)
    82  
    83  			require.Equal(t, tC.exitCode, ec,
    84  				"Expected exit code %v; got: %v\nstdout: %s\nstderr: %s",
    85  				tC.exitCode, ec, stdOut, errOut,
    86  			)
    87  			if tC.expectUsage {
    88  				help := cmd.Help()
    89  				require.Equal(t, help, strings.TrimSpace(stdOut))
    90  				// Test that stdout ends with a linefeed since we trim them for
    91  				// convenience in the equality tests.
    92  				require.True(t, strings.HasSuffix(stdOut, "\n"),
    93  					"stdout does not end with a linefeed")
    94  			}
    95  			if tC.expectUsageError {
    96  				require.Contains(t, errOut, commandErrorText(cmd))
    97  			}
    98  			if tC.expectStdOut != "" {
    99  				require.Equal(t, tC.expectStdOut, strings.TrimSpace(stdOut))
   100  				// Test that stdout ends with a linefeed since we trim them for
   101  				// convenience in the equality tests.
   102  				require.True(t, strings.HasSuffix(stdOut, "\n"),
   103  					"stdout does not end with a linefeed")
   104  			}
   105  			if tC.expectStdErrPrefix != "" {
   106  				require.True(t, strings.HasPrefix(errOut, tC.expectStdErrPrefix),
   107  					"Expected stderr to start with %q; got %s",
   108  					tC.expectStdErrPrefix, errOut)
   109  				// Test that stderr ends with a linefeed since we trim them for
   110  				// convenience in the equality tests.
   111  				require.True(t, strings.HasSuffix(errOut, "\n"),
   112  					"stderr does not end with a linefeed")
   113  			}
   114  		})
   115  	}
   116  }
   117  
   118  // TestVarListCommand_Online contains all of the tests that use a testServer.
   119  // They reuse the same testServer so that they can run in parallel and minimize
   120  // test startup time costs.
   121  func TestVarListCommand_Online(t *testing.T) {
   122  	ci.Parallel(t)
   123  
   124  	// Create a server
   125  	srv, client, url := testServer(t, true, nil)
   126  	defer srv.Shutdown()
   127  
   128  	ui := cli.NewMockUi()
   129  	cmd := &VarListCommand{Meta: Meta{Ui: ui}}
   130  
   131  	nsList := []string{api.DefaultNamespace, "ns1"}
   132  	pathList := []string{"a/b/c", "a/b/c/d", "z/y", "z/y/x"}
   133  	variables := setupTestVariables(client, nsList, pathList)
   134  
   135  	testTmpl := `{{ range $i, $e := . }}{{if ne $i 0}}{{print "•"}}{{end}}{{printf "%v\t%v" .Namespace .Path}}{{end}}`
   136  
   137  	pathsEqual := func(t *testing.T, expect any) testVarListJSONTestExpectFn {
   138  		out := func(t *testing.T, check any) {
   139  
   140  			expect := expect
   141  			exp, ok := expect.(NSPather)
   142  			require.True(t, ok, "expect is not an NSPather, got %T", expect)
   143  			in, ok := check.(NSPather)
   144  			require.True(t, ok, "check is not an NSPather, got %T", check)
   145  			require.ElementsMatch(t, exp.NSPaths(), in.NSPaths())
   146  		}
   147  		return out
   148  	}
   149  
   150  	hasLength := func(t *testing.T, length int) testVarListJSONTestExpectFn {
   151  		out := func(t *testing.T, check any) {
   152  
   153  			length := length
   154  			in, ok := check.(NSPather)
   155  			require.True(t, ok, "check is not an NSPather, got %T", check)
   156  			inLen := in.NSPaths().Len()
   157  			require.Equal(t, length, inLen,
   158  				"expected length of %v, got %v. \nvalues: %v",
   159  				length, inLen, in.NSPaths())
   160  		}
   161  		return out
   162  	}
   163  
   164  	testCases := []testVarListTestCase{
   165  		{
   166  			name:         "plaintext/not found",
   167  			args:         []string{"-out=table", "does/not/exist"},
   168  			expectStdOut: errNoMatchingVariables,
   169  		},
   170  		{
   171  			name: "plaintext/single variable",
   172  			args: []string{"-out=table", "a/b/c/d"},
   173  			expectStdOut: formatList([]string{
   174  				"Namespace|Path|Last Updated",
   175  				fmt.Sprintf(
   176  					"default|a/b/c/d|%s",
   177  					formatUnixNanoTime(variables.HavingPrefix("a/b/c/d")[0].ModifyTime),
   178  				),
   179  			},
   180  			),
   181  		},
   182  		{
   183  			name:         "plaintext/terse",
   184  			args:         []string{"-out=terse"},
   185  			expectStdOut: strings.Join(variables.HavingNamespace(api.DefaultNamespace).Strings(), "\n"),
   186  		},
   187  		{
   188  			name:         "plaintext/terse/prefix",
   189  			args:         []string{"-out=terse", "a/b/c"},
   190  			expectStdOut: strings.Join(variables.HavingNSPrefix(api.DefaultNamespace, "a/b/c").Strings(), "\n"),
   191  		},
   192  		{
   193  			name:               "plaintext/terse/filter",
   194  			args:               []string{"-out=terse", "-filter", "VariableMetadata.Path == \"a/b/c\""},
   195  			expectStdOut:       "a/b/c",
   196  			expectStdErrPrefix: msgWarnFilterPerformance,
   197  		},
   198  		{
   199  			name:               "plaintext/terse/paginated",
   200  			args:               []string{"-out=terse", "-per-page=1"},
   201  			expectStdOut:       "a/b/c",
   202  			expectStdErrPrefix: "Next page token",
   203  		},
   204  		{
   205  			name:         "plaintext/terse/prefix/wildcard ns",
   206  			args:         []string{"-out=terse", "-namespace", "*", "a/b/c/d"},
   207  			expectStdOut: strings.Join(variables.HavingPrefix("a/b/c/d").Strings(), "\n"),
   208  		},
   209  		{
   210  			name:               "plaintext/terse/paginated/prefix/wildcard ns",
   211  			args:               []string{"-out=terse", "-per-page=1", "-namespace", "*", "a/b/c/d"},
   212  			expectStdOut:       variables.HavingPrefix("a/b/c/d").Strings()[0],
   213  			expectStdErrPrefix: "Next page token",
   214  		},
   215  		{
   216  			name: "json/not found",
   217  			args: []string{"-out=json", "does/not/exist"},
   218  			jsonTest: &testVarListJSONTest{
   219  				jsonDest: &SVMSlice{},
   220  				expectFns: []testVarListJSONTestExpectFn{
   221  					hasLength(t, 0),
   222  				},
   223  			},
   224  		},
   225  		{
   226  			name: "json/prefix",
   227  			args: []string{"-out=json", "a"},
   228  			jsonTest: &testVarListJSONTest{
   229  				jsonDest: &SVMSlice{},
   230  				expectFns: []testVarListJSONTestExpectFn{
   231  					pathsEqual(t, variables.HavingNSPrefix(api.DefaultNamespace, "a")),
   232  				},
   233  			},
   234  		},
   235  		{
   236  			name: "json/paginated",
   237  			args: []string{"-out=json", "-per-page", "1"},
   238  			jsonTest: &testVarListJSONTest{
   239  				jsonDest: &PaginatedSVMSlice{},
   240  				expectFns: []testVarListJSONTestExpectFn{
   241  					hasLength(t, 1),
   242  				},
   243  			},
   244  		},
   245  
   246  		{
   247  			name:         "template/not found",
   248  			args:         []string{"-out=go-template", "-template", testTmpl, "does/not/exist"},
   249  			expectStdOut: "",
   250  		},
   251  		{
   252  			name:         "template/prefix",
   253  			args:         []string{"-out=go-template", "-template", testTmpl, "a/b/c/d"},
   254  			expectStdOut: "default\ta/b/c/d",
   255  		},
   256  		{
   257  			name:               "template/filter",
   258  			args:               []string{"-out=go-template", "-template", testTmpl, "-filter", "VariableMetadata.Path == \"a/b/c\""},
   259  			expectStdOut:       "default\ta/b/c",
   260  			expectStdErrPrefix: msgWarnFilterPerformance,
   261  		},
   262  		{
   263  			name:               "template/paginated",
   264  			args:               []string{"-out=go-template", "-template", testTmpl, "-per-page=1"},
   265  			expectStdOut:       "default\ta/b/c",
   266  			expectStdErrPrefix: "Next page token",
   267  		},
   268  		{
   269  			name:         "template/prefix/wildcard namespace",
   270  			args:         []string{"-namespace", "*", "-out=go-template", "-template", testTmpl, "a/b/c/d"},
   271  			expectStdOut: "default\ta/b/c/d•ns1\ta/b/c/d",
   272  		},
   273  	}
   274  	for _, tC := range testCases {
   275  		t.Run(tC.name, func(t *testing.T) {
   276  			tC := tC
   277  			// address always needs to be provided and since the test cases
   278  			// might pass a positional parameter, we need to jam it in the
   279  			// front.
   280  			tcArgs := append([]string{"-address=" + url}, tC.args...)
   281  
   282  			code := cmd.Run(tcArgs)
   283  			stdOut := ui.OutputWriter.String()
   284  			errOut := ui.ErrorWriter.String()
   285  			defer resetUiWriters(ui)
   286  
   287  			require.Equal(t, tC.exitCode, code,
   288  				"Expected exit code %v; got: %v\nstdout: %s\nstderr: %s",
   289  				tC.exitCode, code, stdOut, errOut)
   290  
   291  			if tC.expectStdOut != "" {
   292  				require.Equal(t, tC.expectStdOut, strings.TrimSpace(stdOut))
   293  
   294  				// Test that stdout ends with a linefeed since we trim them for
   295  				// convenience in the equality tests.
   296  				require.True(t, strings.HasSuffix(stdOut, "\n"),
   297  					"stdout does not end with a linefeed")
   298  			}
   299  
   300  			if tC.expectStdErrPrefix != "" {
   301  				require.True(t, strings.HasPrefix(errOut, tC.expectStdErrPrefix),
   302  					"Expected stderr to start with %q; got %s",
   303  					tC.expectStdErrPrefix, errOut)
   304  
   305  				// Test that stderr ends with a linefeed since this test only
   306  				// considers prefixes.
   307  				require.True(t, strings.HasSuffix(stdOut, "\n"),
   308  					"stderr does not end with a linefeed")
   309  			}
   310  
   311  			if tC.jsonTest != nil {
   312  				jtC := tC.jsonTest
   313  				err := json.Unmarshal([]byte(stdOut), &jtC.jsonDest)
   314  				require.NoError(t, err, "stdout: %s", stdOut)
   315  
   316  				for _, fn := range jtC.expectFns {
   317  					fn(t, jtC.jsonDest)
   318  				}
   319  			}
   320  		})
   321  	}
   322  }
   323  
   324  func resetUiWriters(ui *cli.MockUi) {
   325  	ui.ErrorWriter.Reset()
   326  	ui.OutputWriter.Reset()
   327  }
   328  
   329  type testVarListTestCase struct {
   330  	name               string
   331  	args               []string
   332  	exitCode           int
   333  	expectUsage        bool
   334  	expectUsageError   bool
   335  	expectStdOut       string
   336  	expectStdErrPrefix string
   337  	jsonTest           *testVarListJSONTest
   338  }
   339  
   340  type testVarListJSONTest struct {
   341  	jsonDest  interface{}
   342  	expectFns []testVarListJSONTestExpectFn
   343  }
   344  
   345  type testVarListJSONTestExpectFn func(*testing.T, interface{})
   346  
   347  type testSVNamespacePath struct {
   348  	Namespace string
   349  	Path      string
   350  }
   351  
   352  func setupTestVariables(c *api.Client, nsList, pathList []string) SVMSlice {
   353  
   354  	out := make(SVMSlice, 0, len(nsList)*len(pathList))
   355  
   356  	for _, ns := range nsList {
   357  		c.Namespaces().Register(&api.Namespace{Name: ns}, nil)
   358  		for _, p := range pathList {
   359  			setupTestVariable(c, ns, p, &out)
   360  		}
   361  	}
   362  
   363  	return out
   364  }
   365  
   366  func setupTestVariable(c *api.Client, ns, p string, out *SVMSlice) error {
   367  	testVar := &api.Variable{
   368  		Namespace: ns,
   369  		Path:      p,
   370  		Items:     map[string]string{"k": "v"}}
   371  	v, _, err := c.Variables().Create(testVar, &api.WriteOptions{Namespace: ns})
   372  	*out = append(*out, *v.Metadata())
   373  	return err
   374  }
   375  
   376  type NSPather interface {
   377  	Len() int
   378  	NSPaths() testSVNamespacePaths
   379  }
   380  
   381  type testSVNamespacePaths []testSVNamespacePath
   382  
   383  func (ps testSVNamespacePaths) Len() int { return len(ps) }
   384  func (ps testSVNamespacePaths) NSPaths() testSVNamespacePaths {
   385  	return ps
   386  }
   387  
   388  type SVMSlice []api.VariableMetadata
   389  
   390  func (s SVMSlice) Len() int { return len(s) }
   391  func (s SVMSlice) NSPaths() testSVNamespacePaths {
   392  
   393  	out := make(testSVNamespacePaths, len(s))
   394  	for i, v := range s {
   395  		out[i] = testSVNamespacePath{v.Namespace, v.Path}
   396  	}
   397  	return out
   398  }
   399  
   400  func (ps SVMSlice) Strings() []string {
   401  	ns := make(map[string]struct{})
   402  	outNS := make([]string, len(ps))
   403  	out := make([]string, len(ps))
   404  	for i, p := range ps {
   405  		out[i] = p.Path
   406  		outNS[i] = p.Namespace + "|" + p.Path
   407  		ns[p.Namespace] = struct{}{}
   408  	}
   409  	if len(ns) > 1 {
   410  		return strings.Split(formatList(outNS), "\n")
   411  	}
   412  	return out
   413  }
   414  
   415  func (ps *SVMSlice) HavingNamespace(ns string) SVMSlice {
   416  	return *ps.having("namespace", ns)
   417  }
   418  
   419  func (ps *SVMSlice) HavingPrefix(prefix string) SVMSlice {
   420  	return *ps.having("prefix", prefix)
   421  }
   422  
   423  func (ps *SVMSlice) HavingNSPrefix(ns, p string) SVMSlice {
   424  	return *ps.having("namespace", ns).having("prefix", p)
   425  }
   426  
   427  func (ps SVMSlice) having(field, val string) *SVMSlice {
   428  
   429  	out := make(SVMSlice, 0, len(ps))
   430  	for _, p := range ps {
   431  		if field == "namespace" && p.Namespace == val {
   432  			out = append(out, p)
   433  		}
   434  		if field == "prefix" && strings.HasPrefix(p.Path, val) {
   435  			out = append(out, p)
   436  		}
   437  	}
   438  	return &out
   439  }
   440  
   441  type PaginatedSVMSlice struct {
   442  	Data      SVMSlice
   443  	QueryMeta api.QueryMeta
   444  }
   445  
   446  func (s *PaginatedSVMSlice) Len() int { return len(s.Data) }
   447  func (s *PaginatedSVMSlice) NSPaths() testSVNamespacePaths {
   448  
   449  	out := make(testSVNamespacePaths, len(s.Data))
   450  	for i, v := range s.Data {
   451  		out[i] = testSVNamespacePath{v.Namespace, v.Path}
   452  	}
   453  	return out
   454  }
   455  
   456  type PaginatedSVQuietSlice struct {
   457  	Data      []string
   458  	QueryMeta api.QueryMeta
   459  }
   460  
   461  func (ps PaginatedSVQuietSlice) Len() int { return len(ps.Data) }
   462  func (s *PaginatedSVQuietSlice) NSPaths() testSVNamespacePaths {
   463  
   464  	out := make(testSVNamespacePaths, len(s.Data))
   465  	for i, v := range s.Data {
   466  		out[i] = testSVNamespacePath{"", v}
   467  	}
   468  	return out
   469  }