diff --git a/pkg/runs/runsDownload_test.go b/pkg/runs/runsDownload_test.go index fd5123e4..06d48490 100644 --- a/pkg/runs/runsDownload_test.go +++ b/pkg/runs/runsDownload_test.go @@ -12,7 +12,6 @@ import ( "net/http" "net/http/httptest" "os" - "strconv" "strings" "testing" "time" @@ -237,10 +236,7 @@ func WriteMockRasRunsResponse( writer.Header().Set("Content-Type", "application/json") values := req.URL.Query() - pageRequestedStr := values.Get("page") runNameQueryParameter := values.Get("runname") - pageRequested, _ := strconv.Atoi(pageRequestedStr) - assert.Equal(t, pageRequested, 1) assert.Equal(t, runNameQueryParameter, runName) @@ -254,9 +250,7 @@ func WriteMockRasRunsResponse( writer.Write([]byte(fmt.Sprintf(` { - "pageNumber": 1, "pageSize": 1, - "numPages": 1, "amountOfRuns": %d, "runs":[ %s ] }`, len(runResultStrings), combinedRunResultStrings))) diff --git a/pkg/runs/runsGet.go b/pkg/runs/runsGet.go index 48fe614c..d6fcc745 100644 --- a/pkg/runs/runsGet.go +++ b/pkg/runs/runsGet.go @@ -222,6 +222,7 @@ func GetRunsFromRestApi( var pageNumberWanted int32 = 1 gotAllResults := false var restApiVersion string + var pageCursor string restApiVersion, err = embedded.GetGalasactlRestApiVersion() @@ -232,7 +233,7 @@ func GetRunsFromRestApi( var runData *galasaapi.RunResults var httpResponse *http.Response log.Printf("Requesting page '%d' ", pageNumberWanted) - apicall := apiClient.ResultArchiveStoreAPIApi.GetRasSearchRuns(context).ClientApiVersion(restApiVersion) + apicall := apiClient.ResultArchiveStoreAPIApi.GetRasSearchRuns(context).ClientApiVersion(restApiVersion).IncludeCursor("true") if fromAgeMins != 0 { apicall = apicall.From(fromTime) } @@ -251,8 +252,10 @@ func GetRunsFromRestApi( if shouldGetActive { apicall = apicall.Status(activeStatusNames) } - apicall = apicall.Page(pageNumberWanted) - apicall = apicall.Sort("to:desc") + if pageCursor != "" { + apicall = apicall.Cursor(pageCursor) + } + apicall = apicall.Sort("from:desc") runData, httpResponse, err = apicall.Execute() if err != nil { @@ -271,9 +274,10 @@ func GetRunsFromRestApi( results = append(results, runsOnThisPage...) // Have we processed the last page ? - if pageNumberWanted == runData.GetNumPages() { + if !runData.HasNextCursor() || len(runsOnThisPage) < int(runData.GetPageSize()) { gotAllResults = true } else { + pageCursor = runData.GetNextCursor() pageNumberWanted++ } } diff --git a/pkg/runs/runsGet_test.go b/pkg/runs/runsGet_test.go index 16b2728b..bc7b8638 100644 --- a/pkg/runs/runsGet_test.go +++ b/pkg/runs/runsGet_test.go @@ -10,7 +10,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "strconv" "strings" "testing" @@ -84,9 +83,16 @@ const ( "contentType": "application/json" }] }` + + EMPTY_RUNS_RESPONSE = ` + { + "pageSize": 1, + "amountOfRuns": 0, + "runs":[] + }` ) -func NewRunsGetServletMock(t *testing.T, status int, runName string, runResultStrings ...string) *httptest.Server { +func NewRunsGetServletMock(t *testing.T, status int, nextPageCursors []string, pages map[string][]string, pageSize int, runName string, runResultStrings ...string) *httptest.Server { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { clientVersion := r.Header.Get("ClientApiVersion") @@ -96,7 +102,13 @@ func NewRunsGetServletMock(t *testing.T, status int, runName string, runResultSt } else if strings.Contains(r.URL.Path, "/ras/resultnames") { ConfigureServerForResultNamesEndpoint(t, w, r, status) } else { - ConfigureServerForRasRunsEndpoint(t, w, r, runName, status, runResultStrings...) + nextCursor := "" + if len(nextPageCursors) > 0 { + // Advance the expected page cursors by one + nextCursor = nextPageCursors[0] + nextPageCursors = nextPageCursors[1:] + } + ConfigureServerForRasRunsEndpoint(t, w, r, pages, nextCursor, runName, pageSize, status) } })) return server @@ -127,7 +139,16 @@ func ConfigureServerForDetailsEndpoint(t *testing.T, w http.ResponseWriter, r *h `, combinedRunResultStrings))) } -func ConfigureServerForRasRunsEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request, runName string, status int, runResultStrings ...string) { +func ConfigureServerForRasRunsEndpoint( + t *testing.T, + w http.ResponseWriter, + r *http.Request, + pages map[string][]string, + nextPageCursor string, + runName string, + pageSize int, + status int, +) { if r.URL.Path != "/ras/runs" { t.Errorf("Expected to request '/ras/runs', got: %s", r.URL.Path) } @@ -138,14 +159,24 @@ func ConfigureServerForRasRunsEndpoint(t *testing.T, w http.ResponseWriter, r *h w.WriteHeader(status) values := r.URL.Query() - pageRequestedStr := values.Get("page") runNameQueryParameter := values.Get("runname") - pageRequested, _ := strconv.Atoi(pageRequestedStr) - assert.Equal(t, pageRequested, 1) + + var pageRunsJson []string + var keyExists bool + cursorQueryParameter := values.Get("cursor") + + // Keys of the pages map correspond to page cursors, including + // an empty string key for the first request to /ras/runs + pageRunsJson, keyExists = pages[cursorQueryParameter] + assert.True(t, keyExists) + + // Subsequent requests shouldn't be made to the same page, + // so delete the page since we've visited it + delete(pages, cursorQueryParameter) assert.Equal(t, runNameQueryParameter, runName) combinedRunResultStrings := "" - for index, runResult := range runResultStrings { + for index, runResult := range pageRunsJson { if index > 0 { combinedRunResultStrings += "," } @@ -154,12 +185,11 @@ func ConfigureServerForRasRunsEndpoint(t *testing.T, w http.ResponseWriter, r *h w.Write([]byte(fmt.Sprintf(` { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, + "nextCursor": "%s", + "pageSize": %d, "amountOfRuns": %d, "runs":[ %s ] - }`, len(runResultStrings), combinedRunResultStrings))) + }`, nextPageCursor, pageSize, len(pageRunsJson), combinedRunResultStrings))) } func ConfigureServerForResultNamesEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request, status int) { @@ -208,12 +238,17 @@ func TestOutputFormatGarbageStringValidationGivesError(t *testing.T) { func TestRunsGetOfRunNameWhichExistsProducesExpectedSummary(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } + runName := "U456" age := "2d:24h" requestor := "" result := "" + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) shouldGetActive := false defer server.Close() @@ -247,11 +282,15 @@ func TestRunsGetOfRunNameWhichDoesNotExistProducesError(t *testing.T) { // Given ... age := "2d:24h" runName := "garbage" + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } requestor := "" result := "" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() mockConsole := utils.NewMockConsole() @@ -276,13 +315,17 @@ func TestRunsGetOfRunNameWhichDoesNotExistProducesError(t *testing.T) { func TestRunsGetWhereRunNameExistsTwiceProducesTwoRunResultLines(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456, RUN_U456_v2 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456, RUN_U456_v2) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() mockConsole := utils.NewMockConsole() @@ -350,13 +393,17 @@ func TestOutputFormatDetailsValidatesOk(t *testing.T) { func TestRunsGetOfRunNameWhichExistsProducesExpectedDetails(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName, RUN_U456) defer server.Close() outputFormat := "details" @@ -410,13 +457,17 @@ func TestGetFormatterNamesStringMultipleFormattersFormatsOk(t *testing.T) { func TestAPIInternalErrorIsHandledOk(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusInternalServerError, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusInternalServerError, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "details" @@ -439,13 +490,17 @@ func TestAPIInternalErrorIsHandledOk(t *testing.T) { func TestRunsGetOfRunNameWhichExistsProducesExpectedRaw(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "raw" @@ -579,14 +634,7 @@ func TestRunsGetURLQueryWithFromAndToDate(t *testing.T) { assert.NotEqualValues(t, query.Get("to"), "") w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -619,14 +667,7 @@ func TestRunsGetURLQueryJustFromAge(t *testing.T) { assert.EqualValues(t, query.Get("to"), "") w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -659,14 +700,7 @@ func TestRunsGetURLQueryWithNoRunNameAndNoFromAgeReturnsError(t *testing.T) { assert.EqualValues(t, query.Get("runname"), "") w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -700,14 +734,7 @@ func TestRunsGetURLQueryWithOlderToAgeThanFromAgeReturnsError(t *testing.T) { assert.EqualValues(t, query.Get("runname"), "U456") w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -741,14 +768,7 @@ func TestRunsGetURLQueryWithBadlyFormedFromAndToParameterReturnsError(t *testing assert.EqualValues(t, query.Get("runname"), "U456") w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -937,14 +957,7 @@ func TestRunsGetURLQueryWithRequestorNotSuppliedReturnsOK(t *testing.T) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -979,14 +992,7 @@ func TestRunsGetURLQueryWithRequestorSuppliedReturnsOK(t *testing.T) { assert.EqualValues(t, query.Get("requestor"), requestor) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -1021,14 +1027,7 @@ func TestRunsGetURLQueryWithNumericRequestorSuppliedReturnsOK(t *testing.T) { assert.Contains(t, r.URL.RawQuery, "requestor="+url.QueryEscape(requestor)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -1063,14 +1062,7 @@ func TestRunsGetURLQueryWithDashInRequestorSuppliedReturnsOK(t *testing.T) { assert.Contains(t, r.URL.RawQuery, "requestor="+url.QueryEscape(requestor)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -1105,14 +1097,7 @@ func TestRunsGetURLQueryWithAmpersandRequestorSuppliedReturnsOK(t *testing.T) { assert.Contains(t, r.URL.RawQuery, "requestor="+url.QueryEscape(requestor)) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -1148,14 +1133,7 @@ func TestRunsGetURLQueryWithSpecialCharactersRequestorSuppliedReturnsOK(t *testi w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -1175,13 +1153,17 @@ func TestRunsGetURLQueryWithSpecialCharactersRequestorSuppliedReturnsOK(t *testi func TestRunsGetURLQueryWithResultSuppliedReturnsOK(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "Passed" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "summary" @@ -1202,13 +1184,17 @@ func TestRunsGetURLQueryWithResultSuppliedReturnsOK(t *testing.T) { func TestRunsGetURLQueryWithMultipleResultSuppliedReturnsOK(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "Passed,envfail" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "summary" @@ -1230,13 +1216,17 @@ func TestRunsGetURLQueryWithMultipleResultSuppliedReturnsOK(t *testing.T) { func TestRunsGetURLQueryWithResultNotSuppliedReturnsOK(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "summary" @@ -1255,13 +1245,17 @@ func TestRunsGetURLQueryWithResultNotSuppliedReturnsOK(t *testing.T) { func TestRunsGetURLQueryWithInvalidResultSuppliedReturnsError(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "garbage" shouldGetActive := false + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "summary" @@ -1282,13 +1276,17 @@ func TestRunsGetURLQueryWithInvalidResultSuppliedReturnsError(t *testing.T) { func TestActiveAndResultAreMutuallyExclusiveShouldReturnError(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "Passed" shouldGetActive := true + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "summary" @@ -1308,13 +1306,17 @@ func TestActiveAndResultAreMutuallyExclusiveShouldReturnError(t *testing.T) { func TestActiveParameterReturnsOk(t *testing.T) { // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + nextPageCursors := []string{ "" } age := "" runName := "U456" requestor := "" result := "" shouldGetActive := true + pageSize := 100 - server := NewRunsGetServletMock(t, http.StatusOK, runName, RUN_U456) + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) defer server.Close() outputFormat := "summary" @@ -1351,14 +1353,7 @@ func TestRunsGetActiveRunsBuildsQueryCorrectly(t *testing.T) { assert.NotContains(t, r.URL.RawQuery, "status="+url.QueryEscape("finished")) w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write([]byte(` - { - "pageNumber": 1, - "pageSize": 1, - "numPages": 1, - "amountOfRuns": 0, - "runs":[] - }`)) + w.Write([]byte(EMPTY_RUNS_RESPONSE)) })) defer server.Close() @@ -1375,3 +1370,43 @@ func TestRunsGetActiveRunsBuildsQueryCorrectly(t *testing.T) { // Then ... assert.Nil(t, err) } + + +func TestRunsGetWithNextCursorGetsNextPageOfRuns(t *testing.T) { + + // Given ... + pages := make(map[string][]string, 0) + pages[""] = []string{ RUN_U456 } + pages["page2"] = []string{ RUN_U456 } + pages["page3"] = []string{} + nextPageCursors := []string{ "page2", "page3" } + + age := "" + runName := "U456" + requestor := "" + result := "" + shouldGetActive := false + pageSize := 1 + + server := NewRunsGetServletMock(t, http.StatusOK, nextPageCursors, pages, pageSize, runName) + defer server.Close() + + outputFormat := "raw" + mockConsole := utils.NewMockConsole() + + apiServerUrl := server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockTimeService := utils.NewMockTimeService() + + // When... + err := GetRuns(runName, age, requestor, result, shouldGetActive, outputFormat, mockTimeService, mockConsole, apiServerUrl, apiClient) + + // Then... + assert.Nil(t, err) + runsReturned := mockConsole.ReadText() + assert.Contains(t, runsReturned, runName) + + run := "U456|Finished|Passed|2023-05-10T06:00:13.043037Z|2023-05-10T06:00:36.159003Z|2023-05-10T06:02:53.823338Z|137664|myTestPackage.MyTestName|unitTesting|myBundleId|" + apiServerUrl + "/ras/runs/xxx876xxx/runlog\n" + expectedResults := run + run + assert.Equal(t, runsReturned, expectedResults) +}