diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index 0d7bffad..a22a2879 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -152,8 +152,13 @@ The `galasactl` tool can generate the following errors: - GAL1154E: The provided token ID, '{}', does not match formatting requirements. The token ID can contain any character in the 'a'-'z', 'A'-'Z', '0'-'9', '-' (dash), or '_' (underscore) ranges only. - GAL1155E: The id provided by the --id field cannot be an empty string. - GAL1156E: '{}' is not supported as a valid value. Valid values are 'me'. -- GAL1157E: An attempt to delete a run failed. Cause is {} -- GAL1158E: An attempt to delete a run failed. The server responded with an error. Cause is {} +- GAL1157E: An attempt to delete a run named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} +- GAL1159E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. +- GAL1160E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} +- GAL1161E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' +- GAL1162E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: {} +- GAL1163E: The run named '{}' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete +- GAL1164E: An attempt to delete a run named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. - 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/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index 0fbf41c5..1b0fb002 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -250,9 +250,16 @@ var ( GALASA_ERROR_MISSING_USER_LOGIN_ID_FLAG = NewMessageType("GAL1155E: The id provided by the --id field cannot be an empty string.", 1155, STACK_TRACE_NOT_WANTED) GALASA_ERROR_LOGIN_ID_NOT_SUPPORTED = NewMessageType("GAL1156E: '%s' is not supported as a valid value. Valid values are 'me'.", 1156, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_RUN_FAILED = NewMessageType("GAL1157E: An attempt to delete a run named '%s' failed. Cause is %s", 1157, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1158E: An attempt to delete a run named '%s' failed. The server responded with an error. Cause is %s", 1158, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT = NewMessageType("GAL1159E: Failed to delete a run named '%s'. The server responded with an unexpected status code '%v' and did not contain any content. Cause is %s", 1159, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_DELETE_RUNS_BADLY_FORMATTED = NewMessageType("GAL1159E: Failed to delete a run named '%s'. The server responded with an unexpected status code '%v' and did not contain any content. Cause is %s", 1159, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_SERVER_DELETE_RUNS_FAILED = NewMessageType("GAL1157E: An attempt to delete a run named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1157, STACK_TRACE_NOT_WANTED) + + // 4 related but slightly different errors, when an HTTP response arrives from the Galasa server, and we can/can't parse the payload to get the message details out. + GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT = NewMessageType("GAL1159E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server.", 1159, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_RESPONSE_PAYLOAD_UNREADABLE = NewMessageType("GAL1160E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server could not be read. Cause: %s", 1160, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT = NewMessageType("GAL1161E: An attempt to delete a run 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'", 1161, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_SERVER_REPORTED_ERROR = NewMessageType("GAL1162E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: %s", 1162, STACK_TRACE_NOT_WANTED) + + GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND = NewMessageType("GAL1163E: The run named '%s' could not be deleted because it was not found by the Galasa service. Try listing runs using 'galasactl runs get' to identify the one you wish to delete", 1163, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_DELETE_RUNS_EXPLANATION_NOT_JSON = NewMessageType("GAL1164E: An attempt to delete a run named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1164, 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/errors/galasaAPIError.go b/pkg/errors/galasaAPIError.go index 4b756744..eb04bef8 100644 --- a/pkg/errors/galasaAPIError.go +++ b/pkg/errors/galasaAPIError.go @@ -20,7 +20,15 @@ type GalasaAPIError struct { // the reason for the failure. // NOTE: when this function is called ensure that the calling function has the `defer resp.Body.Close()` // called in order to ensure that the response body is closed when the function completes -func GetApiErrorFromResponse(body []byte) (*GalasaAPIError, error){ +func GetApiErrorFromResponse(body []byte) (*GalasaAPIError, error) { + return GetApiErrorFromResponseBytes(body, func(marshallingError error) error{ + err := NewGalasaError(GALASA_ERROR_UNABLE_TO_READ_RESPONSE_BODY, marshallingError) + return err + }, + ) +} + +func GetApiErrorFromResponseBytes(body []byte, marshallingErrorLambda func(marshallingError error) error) (*GalasaAPIError, error) { var err error apiError := new(GalasaAPIError) @@ -28,8 +36,8 @@ func GetApiErrorFromResponse(body []byte) (*GalasaAPIError, error){ err = json.Unmarshal(body, &apiError) if err != nil { - log.Printf("GetApiErrorFromResponse FAIL - %v", err) - err = NewGalasaError(GALASA_ERROR_UNABLE_TO_READ_RESPONSE_BODY, err.Error()) + log.Printf("GetApiErrorFromResponseBytes failed to unmarshal bytes into a galasa api error structure. %v", err.Error()) + err = marshallingErrorLambda(err) } return apiError, err } diff --git a/pkg/runs/runsDelete.go b/pkg/runs/runsDelete.go index 85d8889d..184ba4c8 100644 --- a/pkg/runs/runsDelete.go +++ b/pkg/runs/runsDelete.go @@ -48,8 +48,15 @@ func RunsDelete( runs, err = GetRunsFromRestApi(runName, requestorParameter, resultParameter, fromAgeHours, toAgeHours, shouldGetActive, timeService, apiClient) if err == nil { - err = deleteRuns(runs, apiClient) - } else { + + if len(runs) == 0 { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUN_NOT_FOUND, runName) + } else { + err = deleteRuns(runs, apiClient) + } + } + + if err != nil { console.WriteString(err.Error()) } } @@ -72,7 +79,7 @@ func deleteRuns( if err == nil { for _, run := range runs { runId := run.GetRunId() - // runName := *run.GetTestStructure().RunName + runName := *run.GetTestStructure().RunName apicall := apiClient.ResultArchiveStoreAPIApi.DeleteRasRunById(context, runId).ClientApiVersion(restApiVersion) httpResponse, err = apicall.Execute() @@ -83,13 +90,15 @@ func deleteRuns( // We never got a response, error sending it or something ? err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_SERVER_DELETE_RUNS_FAILED, err.Error()) } else { - // err = convertToGalasaError( - // httpResponse, - // runName, - // GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT, - // GALASA_ERROR_DELETE_RUN_FAILED, - - // ) + err = httpResponseToGalasaError( + httpResponse, + runName, + galasaErrors.GALASA_ERROR_DELETE_RUNS_NO_RESPONSE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_RUNS_RESPONSE_PAYLOAD_UNREADABLE, + galasaErrors.GALASA_ERROR_DELETE_RUNS_UNPARSEABLE_CONTENT, + galasaErrors.GALASA_ERROR_DELETE_RUNS_SERVER_REPORTED_ERROR, + galasaErrors.GALASA_ERROR_DELETE_RUNS_EXPLANATION_NOT_JSON, + ) } } @@ -104,13 +113,14 @@ func deleteRuns( return err } -func convertToGalasaError( +func httpResponseToGalasaError( response *http.Response, identifier string, errorMsgUnexpectedStatusCodeNoResponseBody *galasaErrors.MessageType, errorMsgUnableToReadResponseBody *galasaErrors.MessageType, - errorMsgReceivedFromApiServer *galasaErrors.MessageType, errorMsgResponsePayloadInWrongFormat *galasaErrors.MessageType, + errorMsgReceivedFromApiServer *galasaErrors.MessageType, + errorMsgResponseContentTypeNotJson *galasaErrors.MessageType, ) error { defer response.Body.Close() var err error @@ -121,23 +131,30 @@ func convertToGalasaError( log.Printf("Failed - HTTP response - status code: '%v'\n", statusCode) err = galasaErrors.NewGalasaError(errorMsgUnexpectedStatusCodeNoResponseBody, identifier, statusCode) } else { - - responseBodyBytes, err = io.ReadAll(response.Body) - if err != nil { - err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) + + contentType := response.Header.Get("Content-Type") + if contentType != "application/json" { + err = galasaErrors.NewGalasaError(errorMsgResponseContentTypeNotJson, identifier, statusCode) } else { - - var errorFromServer *galasaErrors.GalasaAPIError - errorFromServer, err = galasaErrors.GetApiErrorFromResponse(responseBodyBytes) - + responseBodyBytes, err = io.ReadAll(response.Body) if err != nil { - //unable to parse response into api error. It should have been json. - log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) - err = galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode) + err = galasaErrors.NewGalasaError(errorMsgUnableToReadResponseBody, identifier, statusCode, err.Error()) } else { - // server returned galasa api error structure we understand. - log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) - err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, errorFromServer.Message) + + var errorFromServer *galasaErrors.GalasaAPIError + errorFromServer, err = galasaErrors.GetApiErrorFromResponseBytes( + responseBodyBytes, + func (marshallingError error) error { + log.Printf("Failed - HTTP response - status code: '%v' payload in response is not json: '%v' \n", statusCode, string(responseBodyBytes)) + return galasaErrors.NewGalasaError(errorMsgResponsePayloadInWrongFormat, identifier, statusCode, marshallingError) + }, + ) + + if err == nil { + // server returned galasa api error structure we understand. + log.Printf("Failed - HTTP response - status code: '%v' server responded with error message: '%v' \n", statusCode, errorMsgReceivedFromApiServer) + err = galasaErrors.NewGalasaError(errorMsgReceivedFromApiServer, identifier, errorFromServer.Message) + } } } } diff --git a/pkg/runs/runsDelete_test.go b/pkg/runs/runsDelete_test.go index 7e5dbc36..a6debbe3 100644 --- a/pkg/runs/runsDelete_test.go +++ b/pkg/runs/runsDelete_test.go @@ -9,6 +9,7 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strconv" "testing" "github.com/galasa-dev/cli/pkg/api" @@ -17,13 +18,25 @@ import ( "github.com/stretchr/testify/assert" ) -func NewRunsDeleteServletMock( - t *testing.T, - runName string, - runId string, - runResultJsonStrings []string, - deleteRunStatusCode int, -) *httptest.Server { +func createMockRun(runName string, runId string) galasaapi.Run { + run := *galasaapi.NewRun() + run.SetRunId(runId) + testStructure := *galasaapi.NewTestStructure() + testStructure.SetRunName(runName) + + run.SetTestStructure(testStructure) + return run +} + +func TestCanDeleteARun(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) server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { @@ -31,47 +44,87 @@ func NewRunsDeleteServletMock( acceptHeader := req.Header.Get("Accept") if req.URL.Path == "/ras/runs" { assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsResponse(t, writer, req, runName, runResultJsonStrings) + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) } else if req.URL.Path == "/ras/runs/"+runId { - assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) - WriteMockRasRunsDeleteResponse(t, writer, req, runName, deleteRunStatusCode) + writer.WriteHeader(http.StatusNoContent) } })) - return server -} + console := utils.NewMockConsole() + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() -func WriteMockRasRunsDeleteResponse( - t *testing.T, - writer http.ResponseWriter, - req *http.Request, - runName string, - statusCode int) { + // When... + err := RunsDelete( + runName, + console, + apiServerUrl, + apiClient, + mockTimeService) - writer.WriteHeader(statusCode) - + // Then... + assert.Nil(t, err, "RunsDelete returned an unexpected error") + assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") } -func createMockRun(runName string) galasaapi.Run { - run := *galasaapi.NewRun() - run.SetRunId(runName) - testStructure := *galasaapi.NewTestStructure() - testStructure.SetRunName(runName) +func TestDeleteNonExistantRunDisplaysError(t *testing.T) { + // Given... + nonExistantRunName := "runDoesNotExist123" - run.SetTestStructure(testStructure) - return run + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + acceptHeader := req.Header.Get("Accept") + if req.URL.Path == "/ras/runs" { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsResponse(t, writer, req, nonExistantRunName, []string{}) + } else { + assert.Fail(t, "An unexpected http request was issued to the test case.") + } + })) + + console := utils.NewMockConsole() + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := RunsDelete( + nonExistantRunName, + console, + apiServerUrl, + apiClient, + mockTimeService) + + // Then... + assert.NotNil(t, err, "RunsDelete did not return an error but it should have") + consoleOutputText := console.ReadText() + assert.Contains(t, consoleOutputText, nonExistantRunName) + assert.Contains(t, consoleOutputText, "GAL1163E") + assert.Contains(t, consoleOutputText, "The run named 'runDoesNotExist123' could not be deleted") } -func TestCanDeleteARun(t *testing.T) { +func TestRunsDeleteFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { // Given... runName := "J20" + runId := "J234567890" // Create the mock run to be deleted - runToDelete := createMockRun(runName) + runToDelete := createMockRun(runName, runId) runToDeleteBytes, _ := json.Marshal(runToDelete) runToDeleteJson := string(runToDeleteBytes) - server := NewRunsDeleteServletMock(t, runName, runName, []string{ runToDeleteJson }, 204) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + acceptHeader := req.Header.Get("Accept") + if req.URL.Path == "/ras/runs" { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } else if req.URL.Path == "/ras/runs/"+runId { + writer.WriteHeader(http.StatusInternalServerError) + } + })) console := utils.NewMockConsole() apiServerUrl := server.URL @@ -87,20 +140,35 @@ func TestCanDeleteARun(t *testing.T) { mockTimeService) // Then... - assert.Nil(t, err, "RunsDelete returned an unexpected error") - assert.Empty(t, console.ReadText(), "The console was written to on a successful deletion, it should be empty") + assert.NotNil(t, err, "RunsDelete returned an unexpected error") + consoleText := console.ReadText() + assert.Contains(t, consoleText , runName) + assert.Contains(t, consoleText , "GAL1159E") } -func TestDeleteNonExistantRunDisplaysError(t *testing.T) { +func TestRunsDeleteFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrectMessage(t *testing.T) { // Given... - nonExistantRunName := "run-does-not-exist" - - existingRunName := "J20" - existingRun := createMockRun(existingRunName) - existingRunBytes, _ := json.Marshal(existingRun) - existingRunJson := string(existingRunBytes) + runName := "J20" + runId := "J234567890" + + // Create the mock run to be deleted + runToDelete := createMockRun(runName, runId) + runToDeleteBytes, _ := json.Marshal(runToDelete) + runToDeleteJson := string(runToDeleteBytes) - server := NewRunsDeleteServletMock(t, nonExistantRunName, nonExistantRunName, []string{ existingRunJson }, 404) + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + + assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) + acceptHeader := req.Header.Get("Accept") + if req.URL.Path == "/ras/runs" { + assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) + WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) + } else if req.URL.Path == "/ras/runs/"+runId { + writer.WriteHeader(http.StatusInternalServerError) + writer.Header().Set("Content-Type", "application/notJsonOnPurpose") + writer.Write([]byte("something not json but non-zero-length.")) + } + })) console := utils.NewMockConsole() apiServerUrl := server.URL @@ -109,12 +177,64 @@ func TestDeleteNonExistantRunDisplaysError(t *testing.T) { // When... err := RunsDelete( - nonExistantRunName, + runName, console, apiServerUrl, apiClient, mockTimeService) // Then... - assert.NotNil(t, err, "RunsDelete did not return an error but it should have") + 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, "GAL1164E") + assert.Contains(t, consoleText, "Error details from the server are not in the json format") } + + +// func TestRunsDeleteFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCorrectMessage(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) + +// server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, req *http.Request) { + +// assert.NotEmpty(t, req.Header.Get("ClientApiVersion")) +// acceptHeader := req.Header.Get("Accept") +// if req.URL.Path == "/ras/runs" { +// assert.Equal(t, "application/json", acceptHeader, "Expected Accept: application/json header, got: %s", acceptHeader) +// WriteMockRasRunsResponse(t, writer, req, runName, []string{ runToDeleteJson }) +// } else if req.URL.Path == "/ras/runs/"+runId { +// writer.WriteHeader(http.StatusInternalServerError) +// writer.Header().Add("Content-Type", "application/json") +// writer.Write([]byte(`{ "this", "isBadJson because it doesnt end in a close braces" `)) +// } +// })) + +// console := utils.NewMockConsole() +// apiServerUrl := server.URL +// apiClient := api.InitialiseAPI(apiServerUrl) +// mockTimeService := utils.NewMockTimeService() + +// // When... +// err := RunsDelete( +// runName, +// console, +// apiServerUrl, +// apiClient, +// mockTimeService) + +// // 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, "Gxxxx") +// assert.Contains(t, consoleText, "Error details from the server are not in the json format") +// }