// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package command import ( "encoding/json" "io" "strings" "testing" "github.com/go-test/deep" "github.com/hashicorp/vault/api" "github.com/mitchellh/cli" ) func testKVMetadataPatchCommand(tb testing.TB) (*cli.MockUi, *KVMetadataPatchCommand) { tb.Helper() ui := cli.NewMockUi() return ui, &KVMetadataPatchCommand{ BaseCommand: &BaseCommand{ UI: ui, }, } } func kvMetadataPatchWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) { t.Helper() return retryKVCommand(t, func() (int, string) { ui, cmd := testKVMetadataPatchCommand(t) cmd.client = client if stdin != nil { cmd.testStdin = stdin } code := cmd.Run(args) combined := ui.OutputWriter.String() + ui.ErrorWriter.String() return code, combined }) } func kvMetadataPutWithRetry(t *testing.T, client *api.Client, args []string, stdin *io.PipeReader) (int, string) { t.Helper() return retryKVCommand(t, func() (int, string) { ui, cmd := testKVMetadataPutCommand(t) cmd.client = client if stdin != nil { cmd.testStdin = stdin } code := cmd.Run(args) combined := ui.OutputWriter.String() + ui.ErrorWriter.String() return code, combined }) } func TestKvMetadataPatchCommand_EmptyArgs(t *testing.T) { client, closer := testVaultServer(t) defer closer() if err := client.Sys().Mount("kv/", &api.MountInput{ Type: "kv-v2", }); err != nil { t.Fatalf("kv-v2 mount error: %#v", err) } args := make([]string, 0) code, combined := kvMetadataPatchWithRetry(t, client, args, nil) expectedCode := 1 expectedOutput := "Not enough arguments" if code != expectedCode { t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", expectedCode, code, args) } if !strings.Contains(combined, expectedOutput) { t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args) } } func TestKvMetadataPatchCommand_Flags(t *testing.T) { t.Parallel() cases := []struct { name string args []string out string code int expectedUpdates map[string]interface{} }{ { "cas_required_success", []string{"-cas-required=true"}, "Success!", 0, map[string]interface{}{ "cas_required": true, }, }, { "cas_required_invalid", []string{"-cas-required=12345"}, "invalid boolean value", 1, map[string]interface{}{}, }, { "custom_metadata_success", []string{"-custom-metadata=baz=ghi"}, "Success!", 0, map[string]interface{}{ "custom_metadata": map[string]interface{}{ "foo": "abc", "bar": "def", "baz": "ghi", }, }, }, { "remove-custom_metadata", []string{"-custom-metadata=baz=ghi", "-remove-custom-metadata=foo"}, "Success!", 0, map[string]interface{}{ "custom_metadata": map[string]interface{}{ "bar": "def", "baz": "ghi", }, }, }, { "remove-custom_metadata-multiple", []string{"-custom-metadata=baz=ghi", "-remove-custom-metadata=foo", "-remove-custom-metadata=bar"}, "Success!", 0, map[string]interface{}{ "custom_metadata": map[string]interface{}{ "baz": "ghi", }, }, }, { "delete_version_after_success", []string{"-delete-version-after=5s"}, "Success!", 0, map[string]interface{}{ "delete_version_after": "5s", }, }, { "delete_version_after_invalid", []string{"-delete-version-after=false"}, "invalid duration", 1, map[string]interface{}{}, }, { "max_versions_success", []string{"-max-versions=10"}, "Success!", 0, map[string]interface{}{ "max_versions": json.Number("10"), }, }, { "max_versions_invalid", []string{"-max-versions=false"}, "invalid syntax", 1, map[string]interface{}{}, }, { "multiple_flags_success", []string{"-max-versions=20", "-custom-metadata=baz=123"}, "Success!", 0, map[string]interface{}{ "max_versions": json.Number("20"), "custom_metadata": map[string]interface{}{ "foo": "abc", "bar": "def", "baz": "123", }, }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { client, closer := testVaultServer(t) defer closer() basePath := t.Name() + "/" secretPath := basePath + "my-secret" metadataPath := basePath + "metadata/" + "my-secret" if err := client.Sys().Mount(basePath, &api.MountInput{ Type: "kv-v2", }); err != nil { t.Fatalf("kv-v2 mount error: %#v", err) } putArgs := []string{"-cas-required=true", "-custom-metadata=foo=abc", "-custom-metadata=bar=def", secretPath} code, combined := kvMetadataPutWithRetry(t, client, putArgs, nil) if code != 0 { t.Fatalf("initial metadata put failed, code: %d, output: %s", code, combined) } initialMetadata, err := client.Logical().Read(metadataPath) if err != nil { t.Fatalf("metadata read failed, err: %#v", err) } patchArgs := append(tc.args, secretPath) code, combined = kvMetadataPatchWithRetry(t, client, patchArgs, nil) if !strings.Contains(combined, tc.out) { t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", tc.out, combined, patchArgs) } if code != tc.code { t.Fatalf("expected code to be %d but was %d for patch cmd with args %#v", tc.code, code, patchArgs) } patchedMetadata, err := client.Logical().Read(metadataPath) if err != nil { t.Fatalf("metadata read failed, err: %#v", err) } for k, v := range patchedMetadata.Data { var expectedVal interface{} if inputVal, ok := tc.expectedUpdates[k]; ok { expectedVal = inputVal } else { expectedVal = initialMetadata.Data[k] } if diff := deep.Equal(expectedVal, v); len(diff) > 0 { t.Fatalf("patched %q mismatch, diff: %#v", k, diff) } } }) } } func TestKvMetadataPatchCommand_CasWarning(t *testing.T) { client, closer := testVaultServer(t) defer closer() basePath := "kv/" if err := client.Sys().Mount(basePath, &api.MountInput{ Type: "kv-v2", }); err != nil { t.Fatalf("kv-v2 mount error: %#v", err) } secretPath := basePath + "my-secret" args := []string{"-cas-required=true", secretPath} code, combined := kvMetadataPutWithRetry(t, client, args, nil) if code != 0 { t.Fatalf("metadata put failed, code: %d, output: %s", code, combined) } casConfig := map[string]interface{}{ "cas_required": true, } _, err := client.Logical().Write(basePath+"config", casConfig) if err != nil { t.Fatalf("config write failed, err: #%v", err) } args = []string{"-cas-required=false", secretPath} code, combined = kvMetadataPatchWithRetry(t, client, args, nil) if code != 0 { t.Fatalf("expected code to be 0 but was %d for patch cmd with args %#v", code, args) } expectedOutput := "\"cas_required\" set to false, but is mandated by backend config" if !strings.Contains(combined, expectedOutput) { t.Fatalf("expected output to be %q but was %q for patch cmd with args %#v", expectedOutput, combined, args) } }