diff --git a/README.md b/README.md index c9e00991..7bf1641a 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,20 @@ galasactl runs get --name C1234 --format badFormatterName ``` For a complete list of supported parameters see [here](./docs/generated/galasactl_runs_get.md). +## runs delete + +This command deletes a test run from an ecosystem's RAS. The name of the test run to delete can be provided to delete it along with any associated artifacts that have been stored. + +### Examples + +A run named "C1234" can be deleted using the following command: + +``` +galasactl runs delete --name C1234 +``` + +A complete list of supported parameters for the `runs delete` command is available [here](./docs/generated/galasactl_runs_delete.md) + ## runs download This command downloads all artifacts for a test run that are stored in an ecosystem's RAS. diff --git a/pkg/cmd/factory.go b/pkg/cmd/factory.go index 148a364e..f8d2aca3 100644 --- a/pkg/cmd/factory.go +++ b/pkg/cmd/factory.go @@ -60,3 +60,7 @@ func (factory *RealFactory) GetAuthenticator(apiServerUrl string, galasaHome spi jwtCache := auth.NewJwtCache(factory.GetFileSystem(), galasaHome, factory.GetTimeService()) return auth.NewAuthenticator(apiServerUrl, factory.GetFileSystem(), galasaHome, factory.GetTimeService(), factory.GetEnvironment(), jwtCache) } + +func (*RealFactory) GetByteReader() spi.ByteReader { + return utils.NewByteReader() +} diff --git a/pkg/cmd/runsDelete.go b/pkg/cmd/runsDelete.go index 9b2a3257..44d5e6aa 100644 --- a/pkg/cmd/runsDelete.go +++ b/pkg/cmd/runsDelete.go @@ -135,6 +135,8 @@ func (cmd *RunsDeleteCommand) executeRunsDelete( var apiClient *galasaapi.APIClient apiClient, err = authenticator.GetAuthenticatedAPIClient() + byteReader := factory.GetByteReader() + if err == nil { // Call to process the command in a unit-testable way. err = runs.RunsDelete( @@ -143,6 +145,7 @@ func (cmd *RunsDeleteCommand) executeRunsDelete( apiServerUrl, apiClient, timeService, + byteReader, ) } } diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index f0fb696a..44c28c9d 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -7,7 +7,6 @@ package runs import ( "context" - "io" "log" "net/http" @@ -27,6 +26,7 @@ func RunsDelete( apiServerUrl string, apiClient *galasaapi.APIClient, timeService spi.TimeService, + byteReader spi.ByteReader, ) error { var err error @@ -52,7 +52,7 @@ func RunsDelete( if len(runs) == 0 { err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND, runName) } else { - err = deleteRuns(runs, apiClient) + err = deleteRuns(runs, apiClient, byteReader) } } @@ -67,6 +67,7 @@ func RunsDelete( func deleteRuns( runs []galasaapi.Run, apiClient *galasaapi.APIClient, + byteReader spi.ByteReader, ) error { var err error @@ -93,6 +94,7 @@ func deleteRuns( err = httpResponseToGalasaError( httpResponse, runName, + byteReader, galasaErrors.GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT, galasaErrors.GALASA_ERROR_DELETE_RUNS_RESPONSE_PAYLOAD_UNREADABLE, galasaErrors.GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT, @@ -116,6 +118,7 @@ func deleteRuns( func httpResponseToGalasaError( response *http.Response, identifier string, + byteReader spi.ByteReader, errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, errorMsgUnableToReadResponseBody *galasaErrors.MessageType, errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, @@ -136,7 +139,7 @@ func httpResponseToGalasaError( if contentType != "application/json" { err = galasaErrors.NewGalasaError(errorMsgResponseContentTypeNotJson, identifier, statusCode) } else { - responseBodyBytes, err = io.ReadAll(response.Body) + responseBodyBytes, err = byteReader.ReadAll(response.Body) if err != nil { err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) } else { diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index eb7b4aff..e0017e2f 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -61,6 +61,7 @@ func TestCanDeleteARun(t *testing.T) { apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -68,7 +69,8 @@ func TestCanDeleteARun(t *testing.T) { console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... assert.Nil(t, err, "RunsDelete returned an unexpected error") @@ -94,6 +96,7 @@ func TestDeleteNonExistantRunDisplaysError(t *testing.T) { apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -101,7 +104,8 @@ func TestDeleteNonExistantRunDisplaysError(t *testing.T) { console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... assert.NotNil(t, err, "RunsDelete did not return an error but it should have") @@ -144,6 +148,7 @@ func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *test apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -151,10 +156,11 @@ func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *test console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText , runName) assert.Contains(t, consoleText , "GAL1159E") @@ -195,6 +201,7 @@ func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrec apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -202,10 +209,11 @@ func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrec console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText, runName) assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) @@ -248,6 +256,7 @@ func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCo apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -255,10 +264,11 @@ func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCo console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText, runName) assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) @@ -310,6 +320,7 @@ func TestRunsDeleteFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *test apiServerUrl := server.Server.URL apiClient := api.InitialiseAPI(apiServerUrl) mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReader() // When... err := RunsDelete( @@ -317,13 +328,71 @@ func TestRunsDeleteFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *test console, apiServerUrl, apiClient, - mockTimeService) + mockTimeService, + mockByteReader) // Then... - assert.NotNil(t, err, "RunsDelete returned an unexpected error") + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") consoleText := console.ReadText() assert.Contains(t, consoleText, runName) assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) assert.Contains(t, consoleText, "GAL1162E") assert.Contains(t, consoleText, apiErrorMessage) } + + +func TestRunsDeleteFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *testing.T) { + // Given... + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) + + // Create the expected HTTP interactions with the API server + getRunsInteraction := utils.NewHttpInteraction("/ras/runs", http.MethodGet) + getRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } + + deleteRunsInteraction := utils.NewHttpInteraction("/ras/runs/" + runId, http.MethodDelete) + deleteRunsInteraction.WriteHttpResponseLambda = func(writer http.ResponseWriter, req *http.Request) { + writer.Header().Set("Content-Type", "application/json") + writer.WriteHeader(http.StatusInternalServerError) + writer.Write([]byte(`{}`)) + } + + interactions := []utils.HttpInteraction{ + getRunsInteraction, + deleteRunsInteraction, + } + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + mockByteReader := utils.NewMockByteReaderAsMock(true) + + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService, + mockByteReader) + + // Then... + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText, runName) + assert.Contains(t, consoleText, strconv.Itoa(http.StatusInternalServerError)) + assert.Contains(t, consoleText, "GAL1160E") + assert.Contains(t, consoleText, "GAL1160E") + assert.Contains(t, consoleText, "Error details from the server could not be read") +} diff --git a/pkg/spi/byteReader.go b/pkg/spi/byteReader.go new file mode 100644 index 00000000..2c6a22db --- /dev/null +++ b/pkg/spi/byteReader.go @@ -0,0 +1,13 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package spi + +import "io" + +// An interface to allow for mocking out "io" package reading-related methods +type ByteReader interface { + ReadAll(reader io.Reader) ([]byte, error) +} diff --git a/pkg/spi/factory.go b/pkg/spi/factory.go index 54084ea2..5ef9b5ae 100644 --- a/pkg/spi/factory.go +++ b/pkg/spi/factory.go @@ -24,4 +24,5 @@ type Factory interface { GetStdErrConsole() Console GetTimeService() TimeService GetAuthenticator(apiServerUrl string, galasaHome GalasaHome) Authenticator + GetByteReader() ByteReader } diff --git a/pkg/utils/byteReader.go b/pkg/utils/byteReader.go new file mode 100644 index 00000000..5a8d22be --- /dev/null +++ b/pkg/utils/byteReader.go @@ -0,0 +1,24 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package utils + +import ( + "io" + + "github.com/galasa-dev/cli/pkg/spi" +) + +// Implementation of a byte reader to allow mocking out methods from the io package +type ByteReaderImpl struct { +} + +func NewByteReader() spi.ByteReader { + return new(ByteReaderImpl) +} + +func (*ByteReaderImpl) ReadAll(reader io.Reader) ([]byte, error) { + return io.ReadAll(reader) +} diff --git a/pkg/utils/byteReaderMock.go b/pkg/utils/byteReaderMock.go new file mode 100644 index 00000000..7c65c206 --- /dev/null +++ b/pkg/utils/byteReaderMock.go @@ -0,0 +1,39 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package utils + +import ( + "errors" + "io" + + "github.com/galasa-dev/cli/pkg/spi" +) + +// Mock implementation of a byte reader to allow for simulating failed read operations +type MockByteReader struct { + throwReadError bool +} + +func NewMockByteReaderAsMock(throwReadError bool) *MockByteReader { + return &MockByteReader{ + throwReadError: throwReadError, + } +} + +func NewMockByteReader() spi.ByteReader { + return NewMockByteReaderAsMock(false) +} + +func (mockReader *MockByteReader) ReadAll(reader io.Reader) ([]byte, error) { + var err error + var bytes []byte + if mockReader.throwReadError { + err = errors.New("simulating a read failure") + } else { + bytes, err = io.ReadAll(reader) + } + return bytes, err +} diff --git a/pkg/utils/factoryMock.go b/pkg/utils/factoryMock.go index 56b043d2..bb1443e0 100644 --- a/pkg/utils/factoryMock.go +++ b/pkg/utils/factoryMock.go @@ -18,6 +18,7 @@ type MockFactory struct { StdErrConsole spi.Console TimeService spi.TimeService Authenticator spi.Authenticator + ByteReader spi.ByteReader } func NewMockFactory() *MockFactory { @@ -72,3 +73,10 @@ func (factory *MockFactory) GetAuthenticator(apiServerUrl string, galasaHome spi } return factory.Authenticator } + +func (factory *MockFactory) GetByteReader() spi.ByteReader { + if factory.ByteReader == nil { + factory.ByteReader = NewMockByteReader() + } + return factory.ByteReader +}