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 }