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 }