// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "encoding/json" "fmt" "os" "regexp" "strings" "testing" "github.com/hashicorp/nomad/api" "github.com/hashicorp/nomad/ci" "github.com/mitchellh/cli" "github.com/posener/complete" "github.com/shoenig/test/must" "github.com/stretchr/testify/require" ) func TestVarPutCommand_Implements(t *testing.T) { ci.Parallel(t) var _ cli.Command = &VarPutCommand{} } func TestVarPutCommand_Fails(t *testing.T) { ci.Parallel(t) t.Run("bad_args", func(t *testing.T) { ci.Parallel(t) ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} code := cmd.Run([]string{"-bad-flag"}) out := ui.ErrorWriter.String() require.Equal(t, 1, code, "expected exit code 1, got: %d") require.Contains(t, out, commandErrorText(cmd), "expected help output, got: %s", out) }) t.Run("bad_address", func(t *testing.T) { ci.Parallel(t) ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} code := cmd.Run([]string{"-address=nope", "foo", "-"}) out := ui.ErrorWriter.String() require.Equal(t, 1, code, "expected exit code 1, got: %d") require.Contains(t, out, "Error creating variable", "expected error creating variable, got: %s", out) }) t.Run("missing_template", func(t *testing.T) { ci.Parallel(t) ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} code := cmd.Run([]string{`-out=go-template`, "foo", "-"}) out := strings.TrimSpace(ui.ErrorWriter.String()) require.Equal(t, 1, code, "expected exit code 1, got: %d", code) require.Equal(t, errMissingTemplate+"\n"+commandErrorText(cmd), out) }) t.Run("unexpected_template", func(t *testing.T) { ci.Parallel(t) ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} code := cmd.Run([]string{`-out=json`, `-template="bad"`, "foo", "-"}) out := strings.TrimSpace(ui.ErrorWriter.String()) require.Equal(t, 1, code, "expected exit code 1, got: %d", code) require.Equal(t, errUnexpectedTemplate+"\n"+commandErrorText(cmd), out) }) t.Run("bad_in", func(t *testing.T) { ci.Parallel(t) ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} code := cmd.Run([]string{`-in=bad`, "foo", "-"}) out := strings.TrimSpace(ui.ErrorWriter.String()) require.Equal(t, 1, code, "expected exit code 1, got: %d", code) require.Equal(t, errInvalidInFormat+"\n"+commandErrorText(cmd), out) }) t.Run("wildcard_namespace", func(t *testing.T) { ci.Parallel(t) ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} code := cmd.Run([]string{`-namespace=*`, "foo", "-"}) out := strings.TrimSpace(ui.ErrorWriter.String()) require.Equal(t, 1, code, "expected exit code 1, got: %d", code) require.Equal(t, errWildcardNamespaceNotAllowed, out) }) } func TestVarPutCommand_GoodJson(t *testing.T) { ci.Parallel(t) // Create a server srv, client, url := testServer(t, true, nil) defer srv.Shutdown() ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} // Get the variable code := cmd.Run([]string{"-address=" + url, "-out=json", "test/var", "k1=v1", "k2=v2"}) require.Equal(t, 0, code, "expected exit 0, got: %d; %v", code, ui.ErrorWriter.String()) t.Cleanup(func() { _, _ = client.Variables().Delete("test/var", nil) }) var outVar api.Variable b := ui.OutputWriter.Bytes() err := json.Unmarshal(b, &outVar) require.NoError(t, err, "error unmarshaling json: %v\nb: %s", err, b) require.Equal(t, "default", outVar.Namespace) require.Equal(t, "test/var", outVar.Path) require.Equal(t, api.VariableItems{"k1": "v1", "k2": "v2"}, outVar.Items) } func TestVarPutCommand_FlagsWithSpec(t *testing.T) { ci.Parallel(t) // Create a server srv, _, url := testServer(t, true, nil) defer srv.Shutdown() ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} // Create a temporary file and ensure the proper cleanup is run once the // test ends. osFile, err := os.CreateTemp("", "nomad-cli-var-put-test-*.hcl") must.NoError(t, err) t.Cleanup(func() { _ = osFile.Close() _ = os.Remove(osFile.Name()) }) // Write out a partial spec that includes the namespace and variable path. _, err = osFile.Write([]byte("path = \"path/to/variable\"\nnamespace = \"default\"")) must.NoError(t, err) // Create the variables, ensure we clean it up, and check the command // response. code := cmd.Run([]string{"-address=" + url, "@" + osFile.Name(), "k1=v1", "k2=v2"}) must.Zero(t, code) must.StrContains(t, ui.OutputWriter.String(), "path/to/variable") must.StrContains(t, ui.OutputWriter.String(), "\"k1\": \"v1\"") must.StrContains(t, ui.OutputWriter.String(), "\"k2\": \"v2\"") } func TestVarPutCommand_AutocompleteArgs(t *testing.T) { ci.Parallel(t) srv, client, url := testServer(t, true, nil) defer srv.Shutdown() ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui, flagAddress: url}} // Create a var sv := testVariable() _, _, err := client.Variables().Create(sv, nil) require.NoError(t, err) args := complete.Args{Last: "t"} predictor := cmd.AutocompleteArgs() res := predictor.Predict(args) require.Equal(t, 1, len(res)) require.Equal(t, sv.Path, res[0]) } func TestVarPutCommand_KeyWarning(t *testing.T) { // Extract invalid characters from warning message. r := regexp.MustCompile(`contains characters \[(.*)\]`) tcs := []struct { name string goodKeys []string badKeys []string badChars []string }{ { name: "simple", goodKeys: []string{"simple"}, }, { name: "hasDot", badKeys: []string{"has.Dot"}, badChars: []string{`"."`}, }, { name: "unicode_letters", goodKeys: []string{"世界"}, }, { name: "unicode_numbers", goodKeys: []string{"٣٢١"}, }, { name: "two_good", goodKeys: []string{"aardvark", "beagle"}, }, { name: "one_good_one_bad", goodKeys: []string{"aardvark"}, badKeys: []string{"bad.key"}, badChars: []string{`"."`}, }, { name: "one_good_two_bad", goodKeys: []string{"aardvark"}, badKeys: []string{"bad.key", "also-bad"}, badChars: []string{`"."`, `"-"`}, }, { name: "repeated_bad_char", goodKeys: []string{"aardvark"}, badKeys: []string{"bad.key", "also.bad"}, badChars: []string{`"."`, `"."`}, }, { name: "repeated_bad_char_same_key", goodKeys: []string{"aardvark"}, badKeys: []string{"bad.key."}, badChars: []string{`"."`}, }, { name: "dont_escape", goodKeys: []string{"aardvark"}, badKeys: []string{"bad\\key"}, badChars: []string{`"\"`}, }, } ci.Parallel(t) _, _, url := testServer(t, false, nil) for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { tc := tc // capture test case ci.Parallel(t) // make the subtests parallel keys := append(tc.goodKeys, tc.badKeys...) // combine keys into a single slice for i, k := range keys { keys[i] = k + "=value" // Make each key into a k=v pair; value is not part of test } ui := cli.NewMockUi() cmd := &VarPutCommand{Meta: Meta{Ui: ui}} args := append([]string{"-address=" + url, "-force", "-out=json", "test/var"}, keys...) code := cmd.Run(args) errOut := ui.ErrorWriter.String() must.Eq(t, 0, code) // the command should always succeed badKeysLen := len(tc.badKeys) switch badKeysLen { case 0: must.Eq(t, "", errOut) // cases with no bad keys shouldn't put anything to stderr return case 1: must.StrContains(t, errOut, "1 warning:") // header should be singular default: must.StrContains(t, errOut, fmt.Sprintf("%d warnings:", badKeysLen)) // header should be plural } for _, k := range tc.badKeys { must.StrContains(t, errOut, k) // every bad key should appear in the warning output } if len(tc.badChars) > 0 { invalid := r.FindAllStringSubmatch(errOut, -1) for i, k := range tc.badChars { must.Eq(t, invalid[i][1], k) // every bad char should appear in the warning output } } for _, k := range tc.goodKeys { must.StrNotContains(t, errOut, k) // good keys should not be emitted } }) } }