github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/command/var_list_test.go (about)

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