From 5265b63e5b7f7276e1e41979dd14be5516b6182b Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Thu, 31 Oct 2024 13:04:50 +0000 Subject: [PATCH] Add galasactl secrets set command (#302) * feat: Add galasactl secrets delete command Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add galasactl secrets get command and formatters Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add galasactl secrets set command Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Remove duplicate console output Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * docs: Add secrets commands to README Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add description flag and validation to secrets Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * feat: Add last updated headers to secret summary output Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Allow secrets set to update description only Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * Empty commit to kick off build Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * fix: Move description to the last column in secrets get Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> * chore: Update secrets baseline Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --------- Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .secrets.baseline | 48 +- README.md | 56 + docs/generated/errors-list.md | 11 +- docs/generated/galasactl_secrets.md | 1 + docs/generated/galasactl_secrets_set.md | 39 + pkg/cmd/commandCollection.go | 7 + pkg/cmd/secretsSet.go | 205 +++ pkg/cmd/secretsSet_test.go | 146 +++ pkg/errors/errorMessage.go | 13 +- pkg/secrets/secrets.go | 12 +- pkg/secrets/secretsGet_test.go | 25 +- pkg/secrets/secretsSet.go | 223 ++++ pkg/secrets/secretsSet_test.go | 1147 +++++++++++++++++ pkg/secretsformatter/secretsFormatter.go | 2 + pkg/secretsformatter/summaryFormatter.go | 17 +- pkg/secretsformatter/summaryFormatter_test.go | 57 +- pkg/secretsformatter/yamlFormatter_test.go | 7 +- pkg/utils/httpInteractionMock.go | 9 + 18 files changed, 1976 insertions(+), 49 deletions(-) create mode 100644 docs/generated/galasactl_secrets_set.md create mode 100644 pkg/cmd/secretsSet.go create mode 100644 pkg/cmd/secretsSet_test.go create mode 100644 pkg/secrets/secretsSet.go create mode 100644 pkg/secrets/secretsSet_test.go diff --git a/.secrets.baseline b/.secrets.baseline index ff805898..f018cbc8 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -151,7 +151,7 @@ "hashed_secret": "11747ed2a3904f82931baf592443772259ea8dc1", "is_secret": false, "is_verified": false, - "line_number": 25, + "line_number": 26, "type": "Secret Keyword", "verified_result": null }, @@ -159,7 +159,7 @@ "hashed_secret": "679d55ddc3c3d0f6ea2d11275a5d084669c98d56", "is_secret": false, "is_verified": false, - "line_number": 62, + "line_number": 67, "type": "Secret Keyword", "verified_result": null }, @@ -167,7 +167,7 @@ "hashed_secret": "3b938c1150a71e71e5f1ffeadbe6475f0f6a2e36", "is_secret": false, "is_verified": false, - "line_number": 122, + "line_number": 127, "type": "Secret Keyword", "verified_result": null }, @@ -175,7 +175,43 @@ "hashed_secret": "2dfbe3ec00a96d6f711d9a70f78be17f6fd574ca", "is_secret": false, "is_verified": false, - "line_number": 284, + "line_number": 289, + "type": "Secret Keyword", + "verified_result": null + } + ], + "pkg/secrets/secretsSet.go": [ + { + "hashed_secret": "28aa91a8e751e5c49714ac040e98812f9110a1fd", + "is_secret": false, + "is_verified": false, + "line_number": 54, + "type": "Secret Keyword", + "verified_result": null + } + ], + "pkg/secrets/secretsSet_test.go": [ + { + "hashed_secret": "89e7fc0c50091804bfeb26cddefc0e701dd60fab", + "is_secret": false, + "is_verified": false, + "line_number": 316, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "edbd5e119f94badb9f99a67ac6ff4c7a5204ad61", + "is_secret": false, + "is_verified": false, + "line_number": 822, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "ea531d9e3ac1dc2beec9c298fb0026d59e4e2262", + "is_secret": false, + "is_verified": false, + "line_number": 825, "type": "Secret Keyword", "verified_result": null } @@ -195,7 +231,7 @@ "hashed_secret": "4d55af37dbbb6a42088d917caa1ca25428ec42c9", "is_secret": false, "is_verified": false, - "line_number": 44, + "line_number": 50, "type": "Secret Keyword", "verified_result": null } @@ -225,7 +261,7 @@ "hashed_secret": "679d55ddc3c3d0f6ea2d11275a5d084669c98d56", "is_secret": false, "is_verified": false, - "line_number": 29, + "line_number": 32, "type": "Secret Keyword", "verified_result": null } diff --git a/README.md b/README.md index 31172abd..158d45ef 100644 --- a/README.md +++ b/README.md @@ -657,6 +657,62 @@ galasactl secrets get --name SYSTEM1 --format yaml For a complete list of supported parameters see [here](./docs/generated/galasactl_secrets_get.md). +## secrets set + +This command can be used to create and update secrets in the Galasa Ecosystem. These secrets can then be used in Galasa tests to authenticate with test systems and perform other secure operations. The name of a secret to create or update must be provided using the `--name` flag. + +### Examples + +The `--username`, `--password`, and `--token` flags can be used in different combinations to create different types of secret. + +For example, a UsernamePassword secret can be created by supplying `--username` and `--password`: + +``` +galasactl secrets set --name SYSTEM1 --username "my-username" --password "my-password" +``` + +A UsernameToken secret can be created by supplying `--username` and `--token`: + +``` +galasactl secrets set --name SYSTEM1 --username "my-username" --token "my-token" +``` + +A Token secret can be created by supplying `--token` on its own: +``` +galasactl secrets set --name SYSTEM1 --token "my-token" +``` + +A Username secret can be created by supplying `--username` on its own: + +``` +galasactl secrets set --name SYSTEM1 --username "my-username" +``` + +Base64-encoded credentials can be supplied using the `--base64-username`, `--base64-password`, and `--base64-token` flags. + +For example, to create a UsernamePassword secret where both the username and password are base64-encoded: + +``` +galasactl secrets set --name SYSTEM1 --base64-username "my-base64-username" --base64-password "my-base64-password" +``` + +It is also possible to mix these flags with their non-encoded variants discussed previously. For example, to create a UsernameToken secret where only the token is base64-encoded: + +``` +galasactl secrets set --name SYSTEM1 --username "my-base64-username" --base64-token "my-base64-token" +``` + +Once a secret has been created, you can change the type of the secret by supplying your desired secret type using the `--type` flag. When supplying the `--type` flag, all credentials for the new secret type must be provided. To find out what secret types are supported, run `galasactl secrets set --help`. + +For example, to create a UsernamePassword secret and then change it to a Token secret: + +``` +galasactl secrets set --name SYSTEM1 --username "my-username" --password "my-password" +galasactl secrets set --name SYSTEM1 --token "my-token" --type Token +``` + +For a complete list of supported parameters see [here](./docs/generated/galasactl_secrets_set.md). + ## secrets delete This command deletes a secret with the given name from the Galasa Ecosystem's credentials store. The name of the secret to be deleted must be provided using the `--name` flag. diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index 7aa345c8..e54c7486 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -167,7 +167,7 @@ The `galasactl` tool can generate the following errors: - GAL1169E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' - GAL1170E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: '{}' - GAL1171E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. -- GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces, and must only contain characters in the Latin-1 character set. +- GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty, contain spaces or dots (.), and must only contain characters in the Latin-1 character set. - GAL1173E: An attempt to delete a secret named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} - GAL1174E: An attempt to get a secret named '{}' failed. Unexpected http status code {} received from the server. - GAL1175E: An attempt to get a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} @@ -181,6 +181,15 @@ The `galasactl` tool can generate the following errors: - GAL1183E: Failed to get secrets. Unexpected http status code {} received from the server. Error details from the server are: '{}' - GAL1184E: Failed to get secrets. Unexpected http status code {} received from the server. Error details from the server are not in the json format. - GAL1185E: Failed to get secrets. Sending the get request to the Galasa service failed. Cause is {} +- GAL1186E: Invalid secret type provided. Supported secret types are: {}. Check your provided command parameters and try again. +- GAL1187E: Failed to set a secret named '{}'. Unexpected http status code {} received from the server. +- GAL1188E: Failed to set a secret named '{}'. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} +- GAL1189E: Failed to set a secret named '{}'. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' +- GAL1190E: Failed to set a secret named '{}'. Unexpected http status code {} received from the server. Error details from the server are: '{}' +- GAL1191E: Failed to set a secret named '{}'. Unexpected http status code {} received from the server. Error details from the server are not in the json format. +- GAL1192E: Failed to set a secret named '{}'. Sending the put request to the Galasa service failed. Cause is {} +- GAL1193E: Invalid flag combination provided. --username cannot be provided with --base64-username, --password cannot be provided with --base64-password, and --token cannot be provided with --base64-token. Use the --help flag for more information, or refer to the documentation at https://galasa.dev/docs/reference/cli-commands. +- GAL1194E: Invalid secret description provided. The description provided with the --description flag cannot be an empty string, and must only contain characters in the Latin-1 character set. - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. - GAL1226E: Internal failure. Contents of gzip could be read, but not decoded. New gzip reader failed: file: {} error: {} - GAL1227E: Internal failure. Contents of gzip could not be decoded. {} error: {} diff --git a/docs/generated/galasactl_secrets.md b/docs/generated/galasactl_secrets.md index d5418926..f2d30acb 100644 --- a/docs/generated/galasactl_secrets.md +++ b/docs/generated/galasactl_secrets.md @@ -25,4 +25,5 @@ The parent command for operations to manipulate secrets in the Galasa service's * [galasactl](galasactl.md) - CLI for Galasa * [galasactl secrets delete](galasactl_secrets_delete.md) - Deletes a secret from the credentials store * [galasactl secrets get](galasactl_secrets_get.md) - Get secrets from the credentials store +* [galasactl secrets set](galasactl_secrets_set.md) - Creates or updates a secret in the credentials store diff --git a/docs/generated/galasactl_secrets_set.md b/docs/generated/galasactl_secrets_set.md new file mode 100644 index 00000000..5fee0ec9 --- /dev/null +++ b/docs/generated/galasactl_secrets_set.md @@ -0,0 +1,39 @@ +## galasactl secrets set + +Creates or updates a secret in the credentials store + +### Synopsis + +Creates or updates a secret in the credentials store + +``` +galasactl secrets set [flags] +``` + +### Options + +``` + --base64-password string a base64-encoded password to set into a secret + --base64-token string a base64-encoded token to set into a secret + --base64-username string a base64-encoded username to set into a secret + --description string the description to associate with the secret being created or updated + -h, --help Displays the options for the 'secrets set' command. + --name string A mandatory flag that identifies the secret to be created or manipulated. + --password string a password to set into a secret + --token string a token to set into a secret + --type string the desired secret type to convert an existing secret into. Supported types are: [UsernamePassword Username UsernameToken Token]. + --username string a username to set into a secret +``` + +### Options inherited from parent commands + +``` + -b, --bootstrap string Bootstrap URL. Should start with 'http://' or 'file://'. If it starts with neither, it is assumed to be a fully-qualified path. If missing, it defaults to use the 'bootstrap.properties' file in your GALASA_HOME. Example: http://example.com/bootstrap, file:///user/myuserid/.galasa/bootstrap.properties , file://C:/Users/myuserid/.galasa/bootstrap.properties + --galasahome string Path to a folder where Galasa will read and write files and configuration settings. The default is '${HOME}/.galasa'. This overrides the GALASA_HOME environment variable which may be set instead. + -l, --log string File to which log information will be sent. Any folder referred to must exist. An existing file will be overwritten. Specify "-" to log to stderr. Defaults to not logging. +``` + +### SEE ALSO + +* [galasactl secrets](galasactl_secrets.md) - Manage secrets stored in the Galasa service's credentials store + diff --git a/pkg/cmd/commandCollection.go b/pkg/cmd/commandCollection.go index 1d83c724..1ff65ec0 100644 --- a/pkg/cmd/commandCollection.go +++ b/pkg/cmd/commandCollection.go @@ -61,6 +61,7 @@ const ( COMMAND_NAME_RESOURCES_DELETE = "resources delete" COMMAND_NAME_SECRETS = "secrets" COMMAND_NAME_SECRETS_GET = "secrets get" + COMMAND_NAME_SECRETS_SET = "secrets set" COMMAND_NAME_SECRETS_DELETE = "secrets delete" COMMAND_NAME_USERS = "users" COMMAND_NAME_USERS_GET = "users get" @@ -387,6 +388,7 @@ func (commands *commandCollectionImpl) addSecretsCommands(factory spi.Factory, r var err error var secretsCommand spi.GalasaCommand var secretsGetCommand spi.GalasaCommand + var secretsSetCommand spi.GalasaCommand var secretsDeleteCommand spi.GalasaCommand secretsCommand, err = NewSecretsCmd(rootCommand) @@ -395,6 +397,10 @@ func (commands *commandCollectionImpl) addSecretsCommands(factory spi.Factory, r secretsGetCommand, err = NewSecretsGetCommand(factory, secretsCommand, rootCommand) } + if err == nil { + secretsSetCommand, err = NewSecretsSetCommand(factory, secretsCommand, rootCommand) + } + if err == nil { secretsDeleteCommand, err = NewSecretsDeleteCommand(factory, secretsCommand, rootCommand) } @@ -402,6 +408,7 @@ func (commands *commandCollectionImpl) addSecretsCommands(factory spi.Factory, r if err == nil { commands.commandMap[secretsCommand.Name()] = secretsCommand commands.commandMap[secretsGetCommand.Name()] = secretsGetCommand + commands.commandMap[secretsSetCommand.Name()] = secretsSetCommand commands.commandMap[secretsDeleteCommand.Name()] = secretsDeleteCommand } diff --git a/pkg/cmd/secretsSet.go b/pkg/cmd/secretsSet.go new file mode 100644 index 00000000..0b0f720a --- /dev/null +++ b/pkg/cmd/secretsSet.go @@ -0,0 +1,205 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "fmt" + "log" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/secrets" + "github.com/galasa-dev/cli/pkg/spi" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/spf13/cobra" +) + +type SecretsSetCmdValues struct { + secretType string + base64Username string + base64Password string + base64Token string + username string + password string + token string + description string +} + +type SecretsSetCommand struct { + values *SecretsSetCmdValues + cobraCommand *cobra.Command +} + +// ------------------------------------------------------------------------------------------------ +// Constructors methods +// ------------------------------------------------------------------------------------------------ +func NewSecretsSetCommand( + factory spi.Factory, + secretsSetCommand spi.GalasaCommand, + rootCmd spi.GalasaCommand, +) (spi.GalasaCommand, error) { + + cmd := new(SecretsSetCommand) + + err := cmd.init(factory, secretsSetCommand, rootCmd) + return cmd, err +} + +// ------------------------------------------------------------------------------------------------ +// Public methods +// ------------------------------------------------------------------------------------------------ +func (cmd *SecretsSetCommand) Name() string { + return COMMAND_NAME_SECRETS_SET +} + +func (cmd *SecretsSetCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *SecretsSetCommand) Values() interface{} { + return cmd.values +} + +// ------------------------------------------------------------------------------------------------ +// Private methods +// ------------------------------------------------------------------------------------------------ +func (cmd *SecretsSetCommand) init(factory spi.Factory, secretsCommand spi.GalasaCommand, rootCmd spi.GalasaCommand) error { + var err error + + cmd.values = &SecretsSetCmdValues{} + cmd.cobraCommand, err = cmd.createCobraCmd(factory, secretsCommand, rootCmd.Values().(*RootCmdValues)) + + return err +} + +func (cmd *SecretsSetCommand) createCobraCmd( + factory spi.Factory, + secretsCommand spi.GalasaCommand, + rootCommandValues *RootCmdValues, +) (*cobra.Command, error) { + + var err error + + secretsCommandValues := secretsCommand.Values().(*SecretsCmdValues) + secretsSetCobraCmd := &cobra.Command{ + Use: "set", + Short: "Creates or updates a secret in the credentials store", + Long: "Creates or updates a secret in the credentials store", + Aliases: []string{COMMAND_NAME_SECRETS_SET}, + RunE: func(cobraCommand *cobra.Command, args []string) error { + return cmd.executeSecretsSet(factory, secretsCommand.Values().(*SecretsCmdValues), rootCommandValues) + }, + } + + addSecretNameFlag(secretsSetCobraCmd, true, secretsCommandValues) + + usernameFlag := "username" + passwordFlag := "password" + tokenFlag := "token" + + base64UsernameFlag := "base64-username" + base64PasswordFlag := "base64-password" + base64TokenFlag := "base64-token" + + descriptionFlag := "description" + + secretsSetCobraCmd.Flags().StringVar(&cmd.values.secretType, "type", "", fmt.Sprintf("the desired secret type to convert an existing secret into. Supported types are: %v.", galasaapi.AllowedGalasaSecretTypeEnumValues)) + secretsSetCobraCmd.Flags().StringVar(&cmd.values.description, descriptionFlag, "", "the description to associate with the secret being created or updated") + secretsSetCobraCmd.Flags().StringVar(&cmd.values.username, usernameFlag, "", "a username to set into a secret") + secretsSetCobraCmd.Flags().StringVar(&cmd.values.password, passwordFlag, "", "a password to set into a secret") + secretsSetCobraCmd.Flags().StringVar(&cmd.values.token, tokenFlag, "", "a token to set into a secret") + + secretsSetCobraCmd.Flags().StringVar(&cmd.values.base64Username, base64UsernameFlag, "", "a base64-encoded username to set into a secret") + secretsSetCobraCmd.Flags().StringVar(&cmd.values.base64Password, base64PasswordFlag, "", "a base64-encoded password to set into a secret") + secretsSetCobraCmd.Flags().StringVar(&cmd.values.base64Token, base64TokenFlag, "", "a base64-encoded token to set into a secret") + + // A non-encoded credential cannot be provided alongside an encoded credential + secretsSetCobraCmd.MarkFlagsMutuallyExclusive(usernameFlag, base64UsernameFlag) + + // A password cannot be provided alongside a token (there is no secret type that allows both) + secretsSetCobraCmd.MarkFlagsMutuallyExclusive(passwordFlag, tokenFlag, base64PasswordFlag, base64TokenFlag) + + // A secret must have a name and at least one of the credentials flags + secretsSetCobraCmd.MarkFlagsOneRequired( + usernameFlag, + passwordFlag, + tokenFlag, + base64UsernameFlag, + base64PasswordFlag, + base64TokenFlag, + descriptionFlag, + ) + + secretsCommand.CobraCommand().AddCommand(secretsSetCobraCmd) + + return secretsSetCobraCmd, err +} + +func (cmd *SecretsSetCommand) executeSecretsSet( + factory spi.Factory, + secretsCmdValues *SecretsCmdValues, + rootCmdValues *RootCmdValues, +) error { + + var err error + // Operations on the file system will all be relative to the current folder. + fileSystem := factory.GetFileSystem() + + err = utils.CaptureLog(fileSystem, rootCmdValues.logFileName) + + if err == nil { + rootCmdValues.isCapturingLogs = true + + log.Println("Galasa CLI - Set secrets from the ecosystem") + + env := factory.GetEnvironment() + + var galasaHome spi.GalasaHome + galasaHome, err = utils.NewGalasaHome(fileSystem, env, rootCmdValues.CmdParamGalasaHomePath) + if err == nil { + + var urlService *api.RealUrlResolutionService = new(api.RealUrlResolutionService) + var bootstrapData *api.BootstrapData + bootstrapData, err = api.LoadBootstrap(galasaHome, fileSystem, env, secretsCmdValues.bootstrap, urlService) + if err == nil { + + var console = factory.GetStdOutConsole() + + apiServerUrl := bootstrapData.ApiServerURL + log.Printf("The API server is at '%s'\n", apiServerUrl) + + authenticator := factory.GetAuthenticator( + apiServerUrl, + galasaHome, + ) + + var apiClient *galasaapi.APIClient + apiClient, err = authenticator.GetAuthenticatedAPIClient() + + byteReader := factory.GetByteReader() + + if err == nil { + err = secrets.SetSecret( + secretsCmdValues.name, + cmd.values.username, + cmd.values.password, + cmd.values.token, + cmd.values.base64Username, + cmd.values.base64Password, + cmd.values.base64Token, + cmd.values.secretType, + cmd.values.description, + console, + apiClient, + byteReader, + ) + } + } + } + } + + return err +} diff --git a/pkg/cmd/secretsSet_test.go b/pkg/cmd/secretsSet_test.go new file mode 100644 index 00000000..2731846e --- /dev/null +++ b/pkg/cmd/secretsSet_test.go @@ -0,0 +1,146 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package cmd + +import ( + "testing" + + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestCommandListContainsSecretsSetCommand(t *testing.T) { + /// Given... + factory := utils.NewMockFactory() + commands, _ := NewCommandCollection(factory) + + // When... + secretsCommand, err := commands.GetCommand(COMMAND_NAME_SECRETS_SET) + assert.Nil(t, err) + + // Then... + assert.NotNil(t, secretsCommand) + assert.Equal(t, COMMAND_NAME_SECRETS_SET, secretsCommand.Name()) + assert.NotNil(t, secretsCommand.Values()) + assert.IsType(t, &SecretsSetCmdValues{}, secretsCommand.Values()) +} + +func TestSecretsSetHelpFlagSetCorrectly(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + + var args []string = []string{"secrets", "set", "--help"} + + // When... + err := Execute(factory, args) + + // Then... + checkOutput("Creates or updates a secret in the credentials store", "", factory, t) + + assert.Nil(t, err) +} + +func TestSecretsSetNoNameFlagProducesErrorMessage(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets", "set"} + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", `Error: required flag(s) "name" not set`, factory, t) +} + +func TestSecretsSetNonEncodedUsernameFlagWithEncodedFlagProducesErrorMessage(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets", "set", + "--name", "SYSTEM1", + "--username", "myuser", + "--base64-username", "mybase64user", + } + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", "Error: if any flags in the group [username base64-username] are set none of the others can be", factory, t) +} + +func TestSecretsSetNonEncodedPasswordFlagWithEncodedFlagProducesErrorMessage(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets", "set", + "--name", "SYSTEM1", + "--username", "myuser", + "--password", "mypassword", + "--base64-password", "my-base64-password", + } + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", "Error: if any flags in the group [password token base64-password base64-token] are set none of the others can be", factory, t) +} + +func TestSecretsSetNonEncodedTokenFlagWithEncodedFlagProducesErrorMessage(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets", "set", + "--name", "SYSTEM1", + "--username", "myuser", + "--token", "mytoken", + "--base64-token", "my-base64-token", + } + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", "Error: if any flags in the group [password token base64-password base64-token] are set none of the others can be", factory, t) +} + +func TestSecretsSetPasswordAndTokenFlagsProducesErrorMessage(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets", "set", + "--name", "SYSTEM1", + "--password", "mypassword", + "--token", "mytoken", + } + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", "Error: if any flags in the group [password token base64-password base64-token] are set none of the others can be", factory, t) +} + +func TestSecretsSetWithOnlyNameFlagProducesErrorMessage(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + var args []string = []string{"secrets", "set", "--name", "SYSTEM1" } + + // When... + err := Execute(factory, args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", "Error: at least one of the flags in the group [username password token base64-username base64-password base64-token description] is required", factory, t) +} diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 5c586d18..705eecf3 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -268,7 +268,7 @@ var ( GALASA_ERROR_DELETE_SECRET_UNPARSEABLE_CONTENT = NewMessageType("GAL1169E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1169, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_SERVER_REPORTED_ERROR = NewMessageType("GAL1170E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1170, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_EXPLANATION_NOT_JSON = NewMessageType("GAL1171E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1171, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_INVALID_SECRET_NAME = NewMessageType("GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces, and must only contain characters in the Latin-1 character set.", 1172, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_INVALID_SECRET_NAME = NewMessageType("GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty, contain spaces or dots (.), and must only contain characters in the Latin-1 character set.", 1172, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_REQUEST_FAILED = NewMessageType("GAL1173E: An attempt to delete a secret named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1173, STACK_TRACE_NOT_WANTED) GALASA_ERROR_GET_SECRET_NO_RESPONSE_CONTENT = NewMessageType("GAL1174E: An attempt to get a secret named '%s' failed. Unexpected http status code %v received from the server.", 1174, STACK_TRACE_NOT_WANTED) @@ -284,6 +284,17 @@ var ( GALASA_ERROR_GET_SECRETS_SERVER_REPORTED_ERROR = NewMessageType("GAL1183E: Failed to get secrets. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1183, STACK_TRACE_NOT_WANTED) GALASA_ERROR_GET_SECRETS_EXPLANATION_NOT_JSON = NewMessageType("GAL1184E: Failed to get secrets. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1184, STACK_TRACE_NOT_WANTED) GALASA_ERROR_GET_SECRETS_REQUEST_FAILED = NewMessageType("GAL1185E: Failed to get secrets. Sending the get request to the Galasa service failed. Cause is %v", 1185, STACK_TRACE_NOT_WANTED) + + GALASA_ERROR_INVALID_SECRET_TYPE_PROVIDED = NewMessageType("GAL1186E: Invalid secret type provided. Supported secret types are: %v. Check your provided command parameters and try again.", 1186, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SET_SECRET_NO_RESPONSE_CONTENT = NewMessageType("GAL1187E: Failed to set a secret named '%s'. Unexpected http status code %v received from the server.", 1187, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SET_SECRET_RESPONSE_BODY_UNREADABLE = NewMessageType("GAL1188E: Failed to set a secret named '%s'. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1188, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SET_SECRET_UNPARSEABLE_CONTENT = NewMessageType("GAL1189E: Failed to set a secret named '%s'. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1189, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SET_SECRET_SERVER_REPORTED_ERROR = NewMessageType("GAL1190E: Failed to set a secret named '%s'. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1190, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SET_SECRET_EXPLANATION_NOT_JSON = NewMessageType("GAL1191E: Failed to set a secret named '%s'. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1191, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SET_SECRET_REQUEST_FAILED = NewMessageType("GAL1192E: Failed to set a secret named '%s'. Sending the put request to the Galasa service failed. Cause is %v", 1192, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SET_SECRET_INVALID_FLAG_COMBINATION = NewMessageType("GAL1193E: Invalid flag combination provided. --username cannot be provided with --base64-username, --password cannot be provided with --base64-password, and --token cannot be provided with --base64-token."+SEE_COMMAND_REFERENCE, 1193, STACK_TRACE_NOT_WANTED) + + GALASA_ERROR_INVALID_SECRET_DESCRIPTION = NewMessageType("GAL1194E: Invalid secret description provided. The description provided with the --description flag cannot be an empty string, and must only contain characters in the Latin-1 character set.", 1194, STACK_TRACE_NOT_WANTED) // Warnings... GALASA_WARNING_MAVEN_NO_GALASA_OBR_REPO = NewMessageType("GAL2000W: Warning: Maven configuration file settings.xml should contain a reference to a Galasa repository so that the galasa OBR can be resolved. The official release repository is '%s', and 'pre-release' repository is '%s'", 2000, STACK_TRACE_WANTED) diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go index e73c5ebe..8a4374d8 100644 --- a/pkg/secrets/secrets.go +++ b/pkg/secrets/secrets.go @@ -16,12 +16,22 @@ func validateSecretName(secretName string) (string, error) { var err error secretName = strings.TrimSpace(secretName) - if secretName == "" || strings.ContainsAny(secretName, " \n\t") || !isLatin1(secretName) { + if secretName == "" || strings.ContainsAny(secretName, " .\n\t") || !isLatin1(secretName) { err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_SECRET_NAME) } return secretName, err } +func validateDescription(description string) (string, error) { + var err error + description = strings.TrimSpace(description) + + if description == "" || !isLatin1(description) { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_SECRET_DESCRIPTION) + } + return description, err +} + // Checks if a given string contains only characters in the Latin-1 character set (codepoints 0-255), // returning true if so, and false otherwise func isLatin1(str string) bool { diff --git a/pkg/secrets/secretsGet_test.go b/pkg/secrets/secretsGet_test.go index de3310e6..8ce3ec11 100644 --- a/pkg/secrets/secretsGet_test.go +++ b/pkg/secrets/secretsGet_test.go @@ -11,6 +11,7 @@ import ( "net/http" "strconv" "testing" + "time" "github.com/galasa-dev/cli/pkg/api" "github.com/galasa-dev/cli/pkg/galasaapi" @@ -35,6 +36,8 @@ func createMockGalasaSecret(secretName string, description string) galasaapi.Gal secretMetadata.SetName(secretName) secretMetadata.SetEncoding(DUMMY_ENCODING) secretMetadata.SetType("UsernamePassword") + secretMetadata.SetLastUpdatedBy(DUMMY_USERNAME) + secretMetadata.SetLastUpdatedTime(time.Date(2024, 01, 01, 10, 0, 0, 0, time.UTC)) if description != "" { secretMetadata.SetDescription(description) @@ -55,11 +58,13 @@ kind: GalasaSecret metadata: name: %s description: %s + lastUpdatedTime: 2024-01-01T10:00:00Z + lastUpdatedBy: %s encoding: %s type: UsernamePassword data: username: %s - password: %s`, API_VERSION, secretName, description, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) + password: %s`, API_VERSION, secretName, description, DUMMY_USERNAME, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) } func TestCanGetASecretByName(t *testing.T) { @@ -102,12 +107,12 @@ func TestCanGetASecretByName(t *testing.T) { mockByteReader) // Then... - expectedOutput := fmt.Sprintf( -`name type description -%s UsernamePassword %s + expectedOutput := +`name type last-updated(UTC) last-updated-by description +SYSTEM1 UsernamePassword 2024-01-01 10:00:00 dummy-username my SYSTEM1 secret Total:1 -`, secretName, description) +` assert.Nil(t, err, "GetSecrets returned an unexpected error") assert.Equal(t, expectedOutput, console.ReadText()) } @@ -205,13 +210,13 @@ func TestCanGetAllSecretsOk(t *testing.T) { mockByteReader) // Then... - expectedOutput := fmt.Sprintf( -`name type description -%s UsernamePassword %s -%s UsernamePassword %s + expectedOutput := +`name type last-updated(UTC) last-updated-by description +BOB UsernamePassword 2024-01-01 10:00:00 dummy-username my BOB secret +BLAH UsernamePassword 2024-01-01 10:00:00 dummy-username my BLAH secret Total:2 -`, secret1Name, description1, secret2Name, description2) +` assert.Nil(t, err, "GetSecrets returned an unexpected error") assert.Equal(t, expectedOutput, console.ReadText()) } diff --git a/pkg/secrets/secretsSet.go b/pkg/secrets/secretsSet.go new file mode 100644 index 00000000..f5865cba --- /dev/null +++ b/pkg/secrets/secretsSet.go @@ -0,0 +1,223 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package secrets + +import ( + "context" + "log" + "net/http" + "strings" + + "github.com/galasa-dev/cli/pkg/embedded" + galasaErrors "github.com/galasa-dev/cli/pkg/errors" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/spi" +) + +const ( + BASE64_ENCODING = "base64" +) + +// Creates or updates a Galasa Secret using the provided parameters into an ecosystem's credentials store +func SetSecret( + secretName string, + username string, + password string, + token string, + base64Username string, + base64Password string, + base64Token string, + secretType string, + description string, + console spi.Console, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) error { + var err error + + secretName, err = validateSecretName(secretName) + if err == nil { + log.Printf("Secret name validated OK") + if description != "" { + description, err = validateDescription(description) + } + + if err == nil { + err = validateFlagCombination(username, password, token, base64Username, base64Password, base64Token) + + if err == nil { + requestUsername := createSecretRequestUsername(username, base64Username) + requestPassword := createSecretRequestPassword(password, base64Password) + requestToken := createSecretRequestToken(token, base64Token) + + var secretTypeValue galasaapi.NullableGalasaSecretType + if secretType != "" { + secretTypeValue, err = validateSecretType(secretType) + } + + if err == nil { + secretRequest := createSecretRequest(secretName, requestUsername, requestPassword, requestToken, secretTypeValue, description) + err = sendSetSecretRequest(secretRequest, apiClient, byteReader) + } + } + } + } + log.Printf("SecretsSet exiting. err is %v\n", err) + return err +} + +func createSecretRequestUsername(username string, base64Username string) galasaapi.SecretRequestUsername { + requestUsername := *galasaapi.NewSecretRequestUsername() + + username = strings.TrimSpace(username) + base64Username = strings.TrimSpace(base64Username) + + if base64Username != "" { + requestUsername.SetValue(base64Username) + requestUsername.SetEncoding(BASE64_ENCODING) + } else if username != "" { + requestUsername.SetValue(username) + } + return requestUsername +} + +func createSecretRequestPassword(password string, base64Password string) galasaapi.SecretRequestPassword { + requestPassword := *galasaapi.NewSecretRequestPassword() + + if base64Password != "" { + requestPassword.SetValue(base64Password) + requestPassword.SetEncoding(BASE64_ENCODING) + } else if password != "" { + requestPassword.SetValue(password) + } + return requestPassword +} + +func createSecretRequestToken(token string, base64Token string) galasaapi.SecretRequestToken { + requestToken := *galasaapi.NewSecretRequestToken() + + if base64Token != "" { + requestToken.SetValue(base64Token) + requestToken.SetEncoding(BASE64_ENCODING) + } else if token != "" { + requestToken.SetValue(token) + } + return requestToken +} + +func createSecretRequest( + secretName string, + username galasaapi.SecretRequestUsername, + password galasaapi.SecretRequestPassword, + token galasaapi.SecretRequestToken, + secretType galasaapi.NullableGalasaSecretType, + description string, +) *galasaapi.SecretRequest { + secretRequest := galasaapi.NewSecretRequest() + secretRequest.SetName(secretName) + + if description != "" { + secretRequest.SetDescription(description) + } + + if secretType.IsSet() { + secretRequest.SetType(*secretType.Get()) + } + + if username.GetValue() != "" { + secretRequest.SetUsername(username) + } + + if password.GetValue() != "" { + secretRequest.SetPassword(password) + } + + if token.GetValue() != "" { + secretRequest.SetToken(token) + } + return secretRequest +} + +func sendSetSecretRequest( + secretRequest *galasaapi.SecretRequest, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) error { + var err error + var httpResponse *http.Response + var context context.Context = context.Background() + var restApiVersion string + + restApiVersion, err = embedded.GetGalasactlRestApiVersion() + secretName := secretRequest.GetName() + + if err == nil { + httpResponse, err = apiClient.SecretsAPIApi.UpdateSecret(context, secretName). + ClientApiVersion(restApiVersion). + SecretRequest(*secretRequest). + Execute() + + if httpResponse != nil { + defer httpResponse.Body.Close() + } + + if err != nil { + if httpResponse == nil { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SET_SECRET_REQUEST_FAILED, err.Error()) + } else { + err = galasaErrors.HttpResponseToGalasaError( + httpResponse, + secretName, + byteReader, + galasaErrors.GALASA_ERROR_SET_SECRET_NO_RESPONSE_CONTENT, + galasaErrors.GALASA_ERROR_SET_SECRET_RESPONSE_BODY_UNREADABLE, + galasaErrors.GALASA_ERROR_SET_SECRET_UNPARSEABLE_CONTENT, + galasaErrors.GALASA_ERROR_SET_SECRET_SERVER_REPORTED_ERROR, + galasaErrors.GALASA_ERROR_SET_SECRET_EXPLANATION_NOT_JSON, + ) + } + } + } + return err +} + +func validateSecretType(secretType string) (galasaapi.NullableGalasaSecretType, error) { + var err error + var nullableSecretType galasaapi.NullableGalasaSecretType + secretType = strings.TrimSpace(secretType) + + // Try to convert the provided type into a GalasaSecretType value + for _, supportedType := range galasaapi.AllowedGalasaSecretTypeEnumValues { + if strings.EqualFold(secretType, string(supportedType)) { + nullableSecretType = *galasaapi.NewNullableGalasaSecretType(&supportedType) + break + } + } + if !nullableSecretType.IsSet() { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_SECRET_TYPE_PROVIDED, galasaapi.AllowedGalasaSecretTypeEnumValues) + } + return nullableSecretType, err +} + +func validateFlagCombination( + username string, + password string, + token string, + base64Username string, + base64Password string, + base64Token string, +) error { + var err error + + // Make sure that a field and its base64 equivalent haven't both been provided + if (username != "" && base64Username != "") || + (password != "" && base64Password != "") || + (token != "" && base64Token != "") { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SET_SECRET_INVALID_FLAG_COMBINATION) + } + return err +} \ No newline at end of file diff --git a/pkg/secrets/secretsSet_test.go b/pkg/secrets/secretsSet_test.go new file mode 100644 index 00000000..1359c544 --- /dev/null +++ b/pkg/secrets/secretsSet_test.go @@ -0,0 +1,1147 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package secrets + +import ( + "encoding/json" + "io" + "net/http" + "strconv" + "testing" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/errors" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func readSecretRequestBody(req *http.Request) galasaapi.SecretRequest { + var secretRequest galasaapi.SecretRequest + requestBodyBytes, _ := io.ReadAll(req.Body) + defer req.Body.Close() + + _ = json.Unmarshal(requestBodyBytes, &secretRequest) + return secretRequest +} + +func TestCanCreateAUsernameSecret(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "my-username" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + createSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Empty(t, secretRequest.GetType()) + + requestUsername := secretRequest.GetUsername() + assert.Equal(t, requestUsername.GetValue(), username) + assert.Empty(t, requestUsername.GetEncoding()) + + requestPassword := secretRequest.GetPassword() + assert.Empty(t, requestPassword.GetValue()) + assert.Empty(t, requestPassword.GetEncoding()) + + requestToken := secretRequest.GetToken() + assert.Empty(t, requestToken.GetValue()) + assert.Empty(t, requestToken.GetEncoding()) + } + + createSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusCreated) + } + + interactions := []utils.HttpInteraction{ + createSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestCanCreateAUsernamePasswordSecret(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "my-username" + password := "my-password" + token := "" + base64Username := "" + base64Password := "" + base64Token := "" + secretType := "" + description := "my secret description" + + // Create the expected HTTP interactions with the API server + createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + createSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Empty(t, secretRequest.GetType()) + assert.Equal(t, secretRequest.GetDescription(), description) + + requestUsername := secretRequest.GetUsername() + assert.Equal(t, requestUsername.GetValue(), username) + assert.Empty(t, requestUsername.GetEncoding()) + + requestPassword := secretRequest.GetPassword() + assert.Equal(t, requestPassword.GetValue(), password) + assert.Empty(t, requestPassword.GetEncoding()) + + requestToken := secretRequest.GetToken() + assert.Empty(t, requestToken.GetValue()) + assert.Empty(t, requestToken.GetEncoding()) + } + + createSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusCreated) + } + + interactions := []utils.HttpInteraction{ + createSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestCanCreateAUsernameTokenSecret(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "my-username" + password := "" + token := "my-token" + base64Username := "" + base64Password := "" + base64Token := "" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + createSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Empty(t, secretRequest.GetType()) + + requestUsername := secretRequest.GetUsername() + assert.Equal(t, requestUsername.GetValue(), username) + assert.Empty(t, requestUsername.GetEncoding()) + + requestPassword := secretRequest.GetPassword() + assert.Empty(t, requestPassword.GetValue()) + assert.Empty(t, requestPassword.GetEncoding()) + + requestToken := secretRequest.GetToken() + assert.Equal(t, requestToken.GetValue(), token) + assert.Empty(t, requestToken.GetEncoding()) + } + + createSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusCreated) + } + + interactions := []utils.HttpInteraction{ + createSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestCanCreateATokenSecret(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "" + password := "" + token := "my-token" + base64Username := "" + base64Password := "" + base64Token := "" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + createSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Empty(t, secretRequest.GetType()) + + requestUsername := secretRequest.GetUsername() + assert.Empty(t, requestUsername.GetValue()) + assert.Empty(t, requestUsername.GetEncoding()) + + requestPassword := secretRequest.GetPassword() + assert.Empty(t, requestPassword.GetValue()) + assert.Empty(t, requestPassword.GetEncoding()) + + requestToken := secretRequest.GetToken() + assert.Equal(t, requestToken.GetValue(), token) + assert.Empty(t, requestToken.GetEncoding()) + } + + createSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusCreated) + } + + interactions := []utils.HttpInteraction{ + createSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestCanUpdateASecret(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "" + password := "my-new-password" + token := "" + base64Username := "" + base64Password := "" + base64Token := "" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + updateSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Empty(t, secretRequest.GetType()) + + requestUsername := secretRequest.GetUsername() + assert.Empty(t, requestUsername.GetValue()) + assert.Empty(t, requestUsername.GetEncoding()) + + requestPassword := secretRequest.GetPassword() + assert.Equal(t, requestPassword.GetValue(), password) + assert.Empty(t, requestPassword.GetEncoding()) + + requestToken := secretRequest.GetToken() + assert.Empty(t, requestToken.GetValue()) + assert.Empty(t, requestToken.GetEncoding()) + } + + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestCanUpdateAUsernamePasswordSecretInBase64Format(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "" + password := "" + token := "" + base64Username := "my-base64-username" + base64Password := "my-base64-password" + base64Token := "" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + updateSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Empty(t, secretRequest.GetType()) + + requestUsername := secretRequest.GetUsername() + assert.Equal(t, requestUsername.GetValue(), base64Username) + assert.Equal(t, requestUsername.GetEncoding(), BASE64_ENCODING) + + requestPassword := secretRequest.GetPassword() + assert.Equal(t, requestPassword.GetValue(), base64Password) + assert.Equal(t, requestPassword.GetEncoding(), BASE64_ENCODING) + + requestToken := secretRequest.GetToken() + assert.Empty(t, requestToken.GetValue()) + assert.Empty(t, requestToken.GetEncoding()) + } + + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestCanUpdateATokenSecretInBase64Format(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + updateSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Empty(t, secretRequest.GetType()) + + requestUsername := secretRequest.GetUsername() + assert.Empty(t, requestUsername.GetValue()) + assert.Empty(t, requestUsername.GetEncoding()) + + requestPassword := secretRequest.GetPassword() + assert.Empty(t, requestPassword.GetValue()) + assert.Empty(t, requestPassword.GetEncoding()) + + requestToken := secretRequest.GetToken() + assert.Equal(t, requestToken.GetValue(), base64Token) + assert.Equal(t, requestToken.GetEncoding(), BASE64_ENCODING) + } + + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestCanUpdateASecretsTypeOk(t *testing.T) { + // Given... + secretName := "SYSTEM1" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "token" + description := "my new token" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + + // Validate the request body + updateSecretInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + secretRequest := readSecretRequestBody(req) + assert.Equal(t, secretRequest.GetName(), secretName) + assert.Equal(t, secretRequest.GetType(), galasaapi.TOKEN) + + requestUsername := secretRequest.GetUsername() + assert.Empty(t, requestUsername.GetValue()) + assert.Empty(t, requestUsername.GetEncoding()) + + requestPassword := secretRequest.GetPassword() + assert.Empty(t, requestPassword.GetValue()) + assert.Empty(t, requestPassword.GetEncoding()) + + requestToken := secretRequest.GetToken() + assert.Equal(t, requestToken.GetValue(), base64Token) + assert.Equal(t, requestToken.GetEncoding(), BASE64_ENCODING) + } + + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "SetSecret returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful creation, it should be empty") +} + +func TestUpdateSecretWithNoNameThrowsError(t *testing.T) { + // Given... + secretName := "" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1172E") + assert.Contains(t, errorMsg, "Invalid secret name provided") +} + +func TestUpdateSecretWithNonLatin1NameThrowsError(t *testing.T) { + // Given... + secretName := string(rune(300)) + "NONLATIN1" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1172E") + assert.Contains(t, errorMsg, "Invalid secret name provided") +} + +func TestUpdateSecretWithNonLatin1DescriptionThrowsError(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := string(rune(256)) + " is not latin-1" + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1194E") + assert.Contains(t, errorMsg, "Invalid secret description provided") +} + +func TestUpdateSecretWithBlankDescriptionThrowsError(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := " " + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1194E") + assert.Contains(t, errorMsg, "Invalid secret description provided") +} + +func TestUpdateSecretWithUnknownTypeThrowsError(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "UNKNOWN" + description := "this should fail!" + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1186E") + assert.Contains(t, errorMsg, "Invalid secret type provided") +} + +func TestUpdateSecretWithInvalidFlagCombinationThrowsError(t *testing.T) { + // Given... + // Provide a unencoded credentials and base64-encoded ones + secretName := "MYSECRET" + username := "my-username" + password := "my-password" + token := "my-token" + base64Username := "my-base64-username" + base64Password := "my-base64-password" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1193E") + assert.Contains(t, errorMsg, "Invalid flag combination provided") +} + +func TestSetSecretFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText , secretName) + assert.Contains(t, consoleText , "GAL1187E") + assert.Contains(t, consoleText , "Unexpected http status code 500 received from the server") +} + +func TestSetSecretFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusInternalServerError) + writer.Header().Set("Content-Type", "application/notJsonOnPurpose") + writer.Write([]byte("something not json but non-zero-length.")) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText, secretName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1191E") + assert.Contains(t, consoleText, "Error details from the server are not in the json format") +} + +func TestSetSecretFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{ "this": "isBadJson because it doesnt end in a close braces" `)) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText, secretName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1189E") + assert.Contains(t, consoleText, "Error details from the server are not in a valid json format") + assert.Contains(t, consoleText, "Cause: 'unexpected end of JSON input'") +} + +func TestSetSecretFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + apiErrorCode := 5000 + apiErrorMessage := "this is an error from the API server" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + + apiError := errors.GalasaAPIError{ + Code: apiErrorCode, + Message: apiErrorMessage, + } + apiErrorBytes, _ := json.Marshal(apiError) + writer.Write(apiErrorBytes) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText, secretName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1190E") + assert.Contains(t, consoleText, apiErrorMessage) +} + +func TestSecretsSetFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Create the expected HTTP interactions with the API server + updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) + updateSecretInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{}`)) + } + + interactions := []utils.HttpInteraction{ + updateSecretInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReaderAsMock(true) + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error but it should have") + consoleText := err.Error() + assert.Contains(t, consoleText, secretName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1188E") + assert.Contains(t, consoleText, "Error details from the server could not be read") +} diff --git a/pkg/secretsformatter/secretsFormatter.go b/pkg/secretsformatter/secretsFormatter.go index acdf2a7c..878600bd 100644 --- a/pkg/secretsformatter/secretsFormatter.go +++ b/pkg/secretsformatter/secretsFormatter.go @@ -27,6 +27,8 @@ const ( HEADER_SECRET_NAME = "name" HEADER_SECRET_TYPE = "type" HEADER_SECRET_DESCRIPTION = "description" + HEADER_LAST_UPDATED_TIME = "last-updated(UTC)" + HEADER_LAST_UPDATED_BY = "last-updated-by" ) type SecretsFormatter interface { diff --git a/pkg/secretsformatter/summaryFormatter.go b/pkg/secretsformatter/summaryFormatter.go index 1c5ca849..23437731 100644 --- a/pkg/secretsformatter/summaryFormatter.go +++ b/pkg/secretsformatter/summaryFormatter.go @@ -38,7 +38,13 @@ func (*SecretSummaryFormatter) FormatSecrets(secrets []galasaapi.GalasaSecret) ( if totalSecrets > 0 { var table [][]string - var headers = []string{ HEADER_SECRET_NAME, HEADER_SECRET_TYPE, HEADER_SECRET_DESCRIPTION } + var headers = []string{ + HEADER_SECRET_NAME, + HEADER_SECRET_TYPE, + HEADER_LAST_UPDATED_TIME, + HEADER_LAST_UPDATED_BY, + HEADER_SECRET_DESCRIPTION, + } table = append(table, headers) for _, secret := range secrets { @@ -46,8 +52,15 @@ func (*SecretSummaryFormatter) FormatSecrets(secrets []galasaapi.GalasaSecret) ( name := secret.Metadata.GetName() secretType := secret.Metadata.GetType() secretDescription := secret.Metadata.GetDescription() + lastUpdatedTime := secret.Metadata.GetLastUpdatedTime() + + lastUpdatedTimeReadable := "" + if !lastUpdatedTime.IsZero() { + lastUpdatedTimeReadable = lastUpdatedTime.Format("2006-01-02 15:04:05") + } + lastUpdatedBy := secret.Metadata.GetLastUpdatedBy() - line = append(line, name, string(secretType), secretDescription) + line = append(line, name, string(secretType), lastUpdatedTimeReadable, lastUpdatedBy, secretDescription) table = append(table, line) } diff --git a/pkg/secretsformatter/summaryFormatter_test.go b/pkg/secretsformatter/summaryFormatter_test.go index 7324a640..6debbe8f 100644 --- a/pkg/secretsformatter/summaryFormatter_test.go +++ b/pkg/secretsformatter/summaryFormatter_test.go @@ -6,11 +6,11 @@ package secretsformatter import ( - "fmt" - "testing" + "testing" + "time" - "github.com/galasa-dev/cli/pkg/galasaapi" - "github.com/stretchr/testify/assert" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/stretchr/testify/assert" ) const ( @@ -20,7 +20,10 @@ const ( DUMMY_PASSWORD = "dummy-password" ) -func createMockGalasaSecretWithDescription(secretName string, description string) galasaapi.GalasaSecret { +func createMockGalasaSecretWithDescription( + secretName string, + description string, +) galasaapi.GalasaSecret { secret := *galasaapi.NewGalasaSecret() secret.SetApiVersion(API_VERSION) @@ -30,10 +33,12 @@ func createMockGalasaSecretWithDescription(secretName string, description string secretMetadata.SetName(secretName) secretMetadata.SetEncoding(DUMMY_ENCODING) secretMetadata.SetType("UsernamePassword") + secretMetadata.SetLastUpdatedBy(DUMMY_USERNAME) + secretMetadata.SetLastUpdatedTime(time.Date(2024, 01, 01, 10, 0, 0, 0, time.UTC)) - if description != "" { - secretMetadata.SetDescription(description) - } + if description != "" { + secretMetadata.SetDescription(description) + } secretData := *galasaapi.NewGalasaSecretData() secretData.SetUsername(DUMMY_USERNAME) @@ -61,8 +66,8 @@ func TestSecretSummaryFormatterNoDataReturnsTotalCountAllZeros(t *testing.T) { func TestSecretSummaryFormatterSingleDataReturnsCorrectly(t *testing.T) { // Given... formatter := NewSecretSummaryFormatter() - description := "secret for system1" - secretName := "MYSECRET" + description := "secret for system1" + secretName := "MYSECRET" secret1 := createMockGalasaSecretWithDescription(secretName, description) secrets := []galasaapi.GalasaSecret{ secret1 } @@ -71,12 +76,12 @@ func TestSecretSummaryFormatterSingleDataReturnsCorrectly(t *testing.T) { // Then... assert.Nil(t, err) - expectedFormattedOutput := fmt.Sprintf( -`name type description -%s UsernamePassword %s + expectedFormattedOutput := +`name type last-updated(UTC) last-updated-by description +MYSECRET UsernamePassword 2024-01-01 10:00:00 dummy-username secret for system1 Total:1 -`, secretName, description) +` assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) } @@ -85,12 +90,12 @@ func TestSecretSummaryFormatterMultipleDataSeperatesWithNewLine(t *testing.T) { formatter := NewSecretSummaryFormatter() secrets := make([]galasaapi.GalasaSecret, 0) - secret1Name := "SECRET1" - secret1Description := "my first secret" - secret2Name := "SECRET2" - secret2Description := "my second secret" - secret3Name := "SECRET3" - secret3Description := "my third secret" + secret1Name := "SECRET1" + secret1Description := "my first secret" + secret2Name := "SECRET2" + secret2Description := "my second secret" + secret3Name := "SECRET3" + secret3Description := "my third secret" secret1 := createMockGalasaSecretWithDescription(secret1Name, secret1Description) secret2 := createMockGalasaSecretWithDescription(secret2Name, secret2Description) @@ -102,13 +107,13 @@ func TestSecretSummaryFormatterMultipleDataSeperatesWithNewLine(t *testing.T) { // Then... assert.Nil(t, err) - expectedFormattedOutput := fmt.Sprintf( -`name type description -%s UsernamePassword %s -%s UsernamePassword %s -%s UsernamePassword %s + expectedFormattedOutput := +`name type last-updated(UTC) last-updated-by description +SECRET1 UsernamePassword 2024-01-01 10:00:00 dummy-username my first secret +SECRET2 UsernamePassword 2024-01-01 10:00:00 dummy-username my second secret +SECRET3 UsernamePassword 2024-01-01 10:00:00 dummy-username my third secret Total:3 -`, secret1Name, secret1Description, secret2Name, secret2Description, secret3Name, secret3Description) +` assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) } diff --git a/pkg/secretsformatter/yamlFormatter_test.go b/pkg/secretsformatter/yamlFormatter_test.go index 714e80a3..3f889e9f 100644 --- a/pkg/secretsformatter/yamlFormatter_test.go +++ b/pkg/secretsformatter/yamlFormatter_test.go @@ -18,15 +18,18 @@ func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { } func generateExpectedSecretYaml(secretName string) string { - return fmt.Sprintf(`apiVersion: %s + return fmt.Sprintf( +`apiVersion: %s kind: GalasaSecret metadata: name: %s + lastUpdatedTime: 2024-01-01T10:00:00Z + lastUpdatedBy: %s encoding: %s type: UsernamePassword data: username: %s - password: %s`, API_VERSION, secretName, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) + password: %s`, API_VERSION, secretName, DUMMY_USERNAME, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) } func TestSecretsYamlFormatterNoDataReturnsBlankString(t *testing.T) { diff --git a/pkg/utils/httpInteractionMock.go b/pkg/utils/httpInteractionMock.go index 6bb754fe..e28c746e 100644 --- a/pkg/utils/httpInteractionMock.go +++ b/pkg/utils/httpInteractionMock.go @@ -22,6 +22,8 @@ type HttpInteraction struct { // An override-able function to write a HTTP response for this interaction WriteHttpResponseFunc func(writer http.ResponseWriter, req *http.Request) + + ValidateRequestFunc func(t *testing.T, req *http.Request) } func NewHttpInteraction(expectedPath string, expectedHttpMethod string) HttpInteraction { @@ -35,6 +37,10 @@ func NewHttpInteraction(expectedPath string, expectedHttpMethod string) HttpInte writer.WriteHeader(http.StatusOK) } + httpInteraction.ValidateRequestFunc = func(t *testing.T, req *http.Request) { + // Do nothing... + } + return httpInteraction } @@ -42,6 +48,9 @@ func (interaction *HttpInteraction) ValidateRequest(t *testing.T, req *http.Requ assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) assert.Equal(t, interaction.ExpectedHttpMethod, req.Method, "Actual HTTP request method did not match the expected method") assert.Equal(t, interaction.ExpectedPath, req.URL.Path, "Actual request path did not match the expected path") + + // Perform additional checks based on the possibly overridden function + interaction.ValidateRequestFunc(t, req) } //-----------------------------------------------------------------------------