diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index cf53ad81..59305578 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -237,6 +237,12 @@ The `galasactl` tool can generate the following errors: - GAL1238E: Failed to get streams. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' - GAL1239E: Failed to get streams. Unexpected http status code {} received from the server. Error details from the server are: '{}' - GAL1240E: Failed to get streams. Unexpected http status code {} received from the server. Error details from the server are not in the json format. +- GAL12412E: Failed to delete test stream with the given name from the Galasa service +- GAL1242E: Failed to delete stream {}. Unexpected http status code {} received from the server. +- GAL1243E: Failed to delete stream {}. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} +- GAL1244E: Failed to delete stream {}. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' +- GAL1245E: Failed to delete stream {}. Unexpected http status code {} received from the server. Error details from the server are: '{}' +- GAL1246E: Failed to delete stream {}. Unexpected http status code {} received from the server. Error details from the server are not in the json format. - 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 '{}', and 'pre-release' repository is '{}' - GAL2501I: Downloaded {} artifacts to folder '{}' diff --git a/docs/generated/galasactl_streams.md b/docs/generated/galasactl_streams.md index d6b0002d..cd4d3b23 100644 --- a/docs/generated/galasactl_streams.md +++ b/docs/generated/galasactl_streams.md @@ -25,5 +25,6 @@ Parent command for managing test streams in a Galasa service ### SEE ALSO * [galasactl](galasactl.md) - CLI for Galasa +* [galasactl streams delete](galasactl_streams_delete.md) - Deletes a test stream by name * [galasactl streams get](galasactl_streams_get.md) - Gets a list of test streams diff --git a/docs/generated/galasactl_streams_delete.md b/docs/generated/galasactl_streams_delete.md new file mode 100644 index 00000000..1831065d --- /dev/null +++ b/docs/generated/galasactl_streams_delete.md @@ -0,0 +1,33 @@ +## galasactl streams delete + +Deletes a test stream by name + +### Synopsis + +Deletes a single test stream with the given name from the Galasa service + +``` +galasactl streams delete [flags] +``` + +### Options + +``` + -h, --help Displays the options for the 'streams delete' command. + --name string A mandatory field indicating the name of a test stream. +``` + +### 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. + --rate-limit-retries int The maximum number of retries that should be made when requests to the Galasa Service fail due to rate limits being exceeded. Must be a whole number. Defaults to 3 retries (default 3) + --rate-limit-retry-backoff-secs float The amount of time in seconds to wait before retrying a command if it failed due to rate limits being exceeded. Defaults to 1 second. (default 1) +``` + +### SEE ALSO + +* [galasactl streams](galasactl_streams.md) - Manages test streams in a Galasa service + diff --git a/pkg/cmd/commandCollection.go b/pkg/cmd/commandCollection.go index 0522ce55..c574ed99 100644 --- a/pkg/cmd/commandCollection.go +++ b/pkg/cmd/commandCollection.go @@ -74,6 +74,7 @@ const ( COMMAND_NAME_ROLES_GET = "roles get" COMMAND_NAME_STREAMS = "streams" COMMAND_NAME_STREAMS_GET = "streams get" + COMMAND_NAME_STREAMS_DELETE = "streams delete" ) // ----------------------------------------------------------------- @@ -519,14 +520,24 @@ func (commands *commandCollectionImpl) addStreamsCommands(factory spi.Factory, r var err error var streamsCommand spi.GalasaCommand var streamsGetCommand spi.GalasaCommand + var streamsDeleteCommand spi.GalasaCommand streamsCommand, err = NewStreamsCommand(rootCommand, commsFlagSet) if err == nil { + + commands.commandMap[streamsCommand.Name()] = streamsCommand streamsGetCommand, err = NewStreamsGetCommand(factory, streamsCommand, commsFlagSet) + if err == nil { - commands.commandMap[streamsCommand.Name()] = streamsCommand + commands.commandMap[streamsGetCommand.Name()] = streamsGetCommand + streamsDeleteCommand, err = NewStreamsDeleteCommand(factory, streamsCommand, commsFlagSet) + + if err == nil { + commands.commandMap[streamsDeleteCommand.Name()] = streamsDeleteCommand + } + } } diff --git a/pkg/cmd/streamsDelete.go b/pkg/cmd/streamsDelete.go new file mode 100644 index 00000000..90f78082 --- /dev/null +++ b/pkg/cmd/streamsDelete.go @@ -0,0 +1,149 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package cmd + +import ( + "log" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/spi" + "github.com/galasa-dev/cli/pkg/streams" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/spf13/cobra" +) + +// Objective: Allow user to do this: +// +// streams delete +type StreamsDeleteCommand struct { + cobraCommand *cobra.Command +} + +// ------------------------------------------------------------------------------------------------ +// Constructors methods +// ------------------------------------------------------------------------------------------------ +func NewStreamsDeleteCommand( + factory spi.Factory, + streamsDeleteCommand spi.GalasaCommand, + commsFlagSet GalasaFlagSet, +) (spi.GalasaCommand, error) { + + cmd := new(StreamsDeleteCommand) + err := cmd.init(factory, streamsDeleteCommand, commsFlagSet) + return cmd, err + +} + +// ------------------------------------------------------------------------------------------------ +// Public methods +// ------------------------------------------------------------------------------------------------ +func (cmd *StreamsDeleteCommand) Name() string { + return COMMAND_NAME_STREAMS_DELETE +} + +func (cmd *StreamsDeleteCommand) CobraCommand() *cobra.Command { + return cmd.cobraCommand +} + +func (cmd *StreamsDeleteCommand) Values() interface{} { + return nil +} + +// ------------------------------------------------------------------------------------------------ +// Private methods +// ------------------------------------------------------------------------------------------------ + +func (cmd *StreamsDeleteCommand) init(factory spi.Factory, streamsCommand spi.GalasaCommand, commsFlagSet GalasaFlagSet) error { + + var err error + + cmd.cobraCommand, err = cmd.createCobraCmd(factory, streamsCommand, commsFlagSet) + + return err + +} + +func (cmd *StreamsDeleteCommand) createCobraCmd( + factory spi.Factory, + streamsCommand spi.GalasaCommand, + commsFlagSet GalasaFlagSet, +) (*cobra.Command, error) { + + var err error + + commsFlagSetValues := commsFlagSet.Values().(*CommsFlagSetValues) + streamsCommandValues := streamsCommand.Values().(*StreamsCmdValues) + + streamsDeleteCobraCmd := &cobra.Command{ + Use: "delete", + Short: "Deletes a test stream by name", + Long: "Deletes a single test stream with the given name from the Galasa service", + Aliases: []string{COMMAND_NAME_STREAMS_DELETE}, + RunE: func(cobraCommand *cobra.Command, args []string) error { + return cmd.executeStreamsDelete( + factory, streamsCommand.Values().(*StreamsCmdValues), commsFlagSetValues, + ) + }, + } + + addStreamNameFlag(streamsDeleteCobraCmd, true, streamsCommandValues) + streamsCommand.CobraCommand().AddCommand(streamsDeleteCobraCmd) + + return streamsDeleteCobraCmd, err + +} + +func (cmd *StreamsDeleteCommand) executeStreamsDelete( + factory spi.Factory, + streamsCmdValues *StreamsCmdValues, + commsFlagSetValues *CommsFlagSetValues, +) error { + + var err error + + // Operations on the file system will all be relative to the current folder. + fileSystem := factory.GetFileSystem() + byteReader := factory.GetByteReader() + + err = utils.CaptureLog(fileSystem, commsFlagSetValues.logFileName) + + if err == nil { + + commsFlagSetValues.isCapturingLogs = true + + log.Println("Galasa CLI - Delete test stream from the Galasa service") + + // Get the ability to query environment variables. + env := factory.GetEnvironment() + + var galasaHome spi.GalasaHome + galasaHome, err = utils.NewGalasaHome(fileSystem, env, commsFlagSetValues.CmdParamGalasaHomePath) + if err == nil { + + var commsClient api.APICommsClient + commsClient, err = api.NewAPICommsClient( + commsFlagSetValues.bootstrap, + commsFlagSetValues.maxRetries, + commsFlagSetValues.retryBackoffSeconds, + factory, + galasaHome, + ) + + if err == nil { + deleteStreamFunc := func(apiClient *galasaapi.APIClient) error { + // Call to process the command in a unit-testable way. + return streams.DeleteStream(streamsCmdValues.name, apiClient, byteReader) + } + err = commsClient.RunAuthenticatedCommandWithRateLimitRetries(deleteStreamFunc) + } + } + } + + return err + +} diff --git a/pkg/cmd/streamsDelete_test.go b/pkg/cmd/streamsDelete_test.go new file mode 100644 index 00000000..8a89680c --- /dev/null +++ b/pkg/cmd/streamsDelete_test.go @@ -0,0 +1,80 @@ +/* + * 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 TestStreamsDeleteCommandInCommandCollectionHasName(t *testing.T) { + + factory := utils.NewMockFactory() + commands, _ := NewCommandCollection(factory) + + StreamsDeleteCommand, err := commands.GetCommand(COMMAND_NAME_STREAMS_DELETE) + assert.Nil(t, err) + + assert.Equal(t, COMMAND_NAME_STREAMS_DELETE, StreamsDeleteCommand.Name()) + assert.NotNil(t, StreamsDeleteCommand.CobraCommand()) + +} + +func TestStreamsDeleteHelpFlagSetCorrectly(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + + var args []string = []string{"streams", "delete", "--help"} + + // When... + err := Execute(factory, args) + + // Then... + // Check what the user saw is reasonable. + checkOutput("Displays the options for the 'streams delete' command.", "", factory, t) + + assert.Nil(t, err) +} + +func TestStreamsDeleteNamespaceNameFlagsReturnsOk(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + commandCollection, _ := setupTestCommandCollection(COMMAND_NAME_STREAMS_DELETE, factory, t) + + var args []string = []string{"streams", "delete", "--name", "mystream"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.Nil(t, err) + + // Check what the user saw was reasonable + checkOutput("", "", factory, t) + + assert.Nil(t, err) +} + +func TestStreamsDeleteWithoutNameFlagReturnsError(t *testing.T) { + // Given... + factory := utils.NewMockFactory() + commandCollection, _ := setupTestCommandCollection(COMMAND_NAME_STREAMS_DELETE, factory, t) + + var args []string = []string{"streams", "delete"} + + // When... + err := commandCollection.Execute(args) + + // Then... + assert.NotNil(t, err) + + checkOutput("", "Error: required flag(s) \"name\" not set", factory, t) + + assert.NotNil(t, err) +} diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 1321382a..617809b1 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -428,6 +428,14 @@ var ( GALASA_ERROR_GET_STREAMS_SERVER_REPORTED_ERROR = NewMessageType("GAL1239E: Failed to get streams. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1239, STACK_TRACE_NOT_WANTED) GALASA_ERROR_GET_STREAMS_EXPLANATION_NOT_JSON = NewMessageType("GAL1240E: Failed to get streams. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1240, STACK_TRACE_NOT_WANTED) + // Streams delete errors + GALASA_ERROR_FAILED_TO_DELETE_STREAM = NewMessageType("GAL12412E: Failed to delete test stream with the given name from the Galasa service", 1241, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_STREAMS_NO_RESPONSE_CONTENT = NewMessageType("GAL1242E: Failed to delete stream %s. Unexpected http status code %v received from the server.", 1242, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_STREAMS_RESPONSE_BODY_UNREADABLE = NewMessageType("GAL1243E: Failed to delete stream %s. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1243, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_STREAMS_UNPARSEABLE_CONTENT = NewMessageType("GAL1244E: Failed to delete stream %s. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1244, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_STREAMS_SERVER_REPORTED_ERROR = NewMessageType("GAL1245E: Failed to delete stream %s. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1245, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_STREAMS_EXPLANATION_NOT_JSON = NewMessageType("GAL1246E: Failed to delete stream %s. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1246, STACK_TRACE_NOT_WANTED) + // When getting multiple monitors... GALASA_ERROR_GET_MONITORS_REQUEST_FAILED = NewMessageType("GAL1218E: Failed to get monitors. Sending the get request to the Galasa service failed. Cause is %v", 1218, STACK_TRACE_NOT_WANTED) GALASA_ERROR_GET_MONITORS_NO_RESPONSE_CONTENT = NewMessageType("GAL1219E: Failed to get monitors. Unexpected http status code %v received from the server.", 1219, STACK_TRACE_NOT_WANTED) diff --git a/pkg/streams/streamsDelete.go b/pkg/streams/streamsDelete.go new file mode 100644 index 00000000..0af709b3 --- /dev/null +++ b/pkg/streams/streamsDelete.go @@ -0,0 +1,81 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package streams + +import ( + "context" + "log" + "net/http" + + "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" +) + +func DeleteStream(streamName string, apiClient *galasaapi.APIClient, byteReader spi.ByteReader) error { + + var err error + + streamName, err = validateStreamName(streamName) + + if err == nil { + err = deleteStreamFromRestApi(streamName, apiClient, byteReader) + } + + return err + +} + +func deleteStreamFromRestApi( + streamName string, + apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, +) error { + + var context context.Context = nil + var resp *http.Response + + restApiVersion, err := embedded.GetGalasactlRestApiVersion() + + if err == nil { + + apiCall := apiClient.StreamsAPIApi.DeleteStreamByName(context, streamName).ClientApiVersion(restApiVersion) + resp, err = apiCall.Execute() + + if resp != nil { + defer resp.Body.Close() + } + + if err != nil { + + if resp == nil { + + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_FAILED_TO_DELETE_STREAM, err.Error()) + + } else { + + err = galasaErrors.HttpResponseToGalasaError( + resp, + streamName, + byteReader, + galasaErrors.GALASA_ERROR_DELETE_STREAMS_NO_RESPONSE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_STREAMS_RESPONSE_BODY_UNREADABLE, + galasaErrors.GALASA_ERROR_DELETE_STREAMS_UNPARSEABLE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_STREAMS_SERVER_REPORTED_ERROR, + galasaErrors.GALASA_ERROR_DELETE_STREAMS_EXPLANATION_NOT_JSON, + ) + + } + + log.Printf("Test stream with name '%s', was deleted OK.\n", streamName) + } + } + + return err + +} diff --git a/pkg/streams/streamsDelete_test.go b/pkg/streams/streamsDelete_test.go new file mode 100644 index 00000000..e8ed1cc0 --- /dev/null +++ b/pkg/streams/streamsDelete_test.go @@ -0,0 +1,158 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package streams + +import ( + "fmt" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func createMockStream(name string, description string) galasaapi.Stream { + + stream := *galasaapi.NewStream() + streamMetadata := *galasaapi.NewStreamMetadata() + + streamMetadata.SetName(name) + streamMetadata.SetDescription(description) + + stream.SetMetadata(streamMetadata) + + return stream + +} + +func WriteMockStreamResponse( + t *testing.T, + writer http.ResponseWriter, + req *http.Request, + name string, + streamResultStrings []string) { + + writer.Header().Set("Content-Type", "application/json") + values := req.URL.Path + path := strings.Split(values, "/") + streamPathVar := path[2] + assert.Equal(t, streamPathVar, name) + + writer.Write([]byte(fmt.Sprintf(` + { + "metadata":{ + "name": "%s", + "description": "This is a dummy stream" + } + }`, name))) + +} + +func TestStreamDeleteAStream(t *testing.T) { + + //Given... + name := "mystream" + + deleteStreamInteraction := utils.NewHttpInteraction("/streams/"+name, http.MethodDelete) + deleteStreamInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusNoContent) + } + + interactions := []utils.HttpInteraction{ + deleteStreamInteraction, + } + + 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 := DeleteStream( + name, + apiClient, + mockByteReader) + + // Then... + assert.Nil(t, err, "DeleteStream returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") +} + +func TestStreamDeleteAnInvalidStreamNameReturnsError(t *testing.T) { + + //Given... + name := "my.stream" + + deleteStreamInteraction := utils.NewHttpInteraction("/streams/"+name, http.MethodDelete) + deleteStreamInteraction.WriteHttpResponseFunc = func(writer http.ResponseWriter, req *http.Request) { + writer.WriteHeader(http.StatusBadRequest) + } + + interactions := []utils.HttpInteraction{ + deleteStreamInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteStream( + name, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "DeleteStream returned an unexpected error") + assert.Contains(t, err.Error(), "GAL1235E") + assert.Contains(t, err.Error(), "The name provided with the --name flag cannot be empty and must only contain characters in the following ranges:") +} + +func TestStreamDeleteThrowsAnUnexpectedError(t *testing.T) { + + //Given... + name := "mystream" + + deleteStreamInteraction := utils.NewHttpInteraction("/streams/"+name, http.MethodDelete) + deleteStreamInteraction.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{ + deleteStreamInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := DeleteStream( + name, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "DeleteStream returned an unexpected error") + assert.Contains(t, err.Error(), strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, err.Error(), "GAL1245E") +}