diff --git a/readme/api_registry_test.go b/readme/api_registry_test.go index 4b50b9e..14244a5 100644 --- a/readme/api_registry_test.go +++ b/readme/api_registry_test.go @@ -31,7 +31,7 @@ func Test_APIRegistry_Get(t *testing.T) { t.Run("when API responds with 404", func(t *testing.T) { // Arrange - expect := testdata.APISpecResponseVersionEmtpy + expect := testdata.APISpecResponseVersionEmpty gock.New(TestClient.APIURL). Get(readme.APIRegistryEndpoint + "/invalid"). Reply(404). diff --git a/readme/api_specification.go b/readme/api_specification.go index d2f8353..8569339 100644 --- a/readme/api_specification.go +++ b/readme/api_specification.go @@ -101,47 +101,18 @@ type APISpecificationSaved struct { // // API Reference: https://docs.readme.com/reference/getapispecification func (c APISpecificationClient) GetAll(options ...RequestOptions) ([]APISpecification, *APIResponse, error) { - var specifications []APISpecification - var apiResponse *APIResponse - var err error - hasNextPage := false - - // Initialize pagination counter. - page := 1 - if len(options) > 0 { - if options[0].Page != 0 { - page = options[0].Page - } + var results []APISpecification + opts := parseRequestOptions(options) + apiResponse, err := c.client.fetchAllPages(APISpecificationEndpoint, opts, &results) + if err != nil { + return results, apiResponse, fmt.Errorf("unable to retrieve specifications: %w", err) } - for { - var specPaginatedResult []APISpecification - - apiRequest := &APIRequest{ - Method: "GET", - Endpoint: APISpecificationEndpoint, - UseAuth: true, - OkStatusCode: []int{200}, - Response: &specPaginatedResult, - } - if len(options) > 0 { - apiRequest.RequestOptions = options[0] - } - - apiResponse, hasNextPage, err = c.client.paginatedRequest(apiRequest, page) - if err != nil { - return specifications, apiResponse, fmt.Errorf("unable to retrieve specifications: %w", err) - } - specifications = append(specifications, specPaginatedResult...) - - if !hasNextPage { - break - } - - page = page + 1 + if len(results) == 0 { + return nil, apiResponse, nil } - return specifications, apiResponse, nil + return results, apiResponse, nil } // Get a single API specification with a provided ID. diff --git a/readme/api_specification_test.go b/readme/api_specification_test.go index 57a1b1a..1eb5dcf 100644 --- a/readme/api_specification_test.go +++ b/readme/api_specification_test.go @@ -68,7 +68,7 @@ func Test_APISpecification_GetAll(t *testing.T) { Reply(400). AddHeader("Link", `<`+apiSpecEndpointPaginated+`&page=2>; rel="next", <>; rel="prev", <>; rel="last"`). AddHeader("x-total-count", "1"). - JSON(testdata.APISpecResponseVersionEmtpy.APIErrorResponse) + JSON(testdata.APISpecResponseVersionEmpty.APIErrorResponse) defer gock.Off() // Act @@ -272,7 +272,7 @@ func Test_APISpecification_Get(t *testing.T) { gock.New(TestClient.APIURL). Get(readme.APISpecificationEndpoint). Reply(400). - JSON(testdata.APISpecResponseVersionEmtpy.APIErrorResponse) + JSON(testdata.APISpecResponseVersionEmpty.APIErrorResponse) defer gock.Off() expect := "unable to retrieve API specifications" @@ -442,7 +442,7 @@ func Test_APISpecification_Delete(t *testing.T) { t.Run("when called with invalid ID and API response with 400", func(t *testing.T) { // Arrange - expect := testdata.APISpecResponseVersionEmtpy.APIErrorResponse + expect := testdata.APISpecResponseVersionEmpty.APIErrorResponse gock.New(TestClient.APIURL). Delete(readme.APISpecificationEndpoint + "/0123456789"). Reply(400). diff --git a/readme/category.go b/readme/category.go index 21d56b7..a4ff1a0 100644 --- a/readme/category.go +++ b/readme/category.go @@ -104,7 +104,7 @@ type Category struct { type CategoryParams struct { // Title is a *required* short title for the category. This is what will show in the sidebar. Title string `json:"title"` - // Type is tye type of category, which can be "reference" or "guide". + // Type is the type of category, which can be "reference" or "guide". Type string `json:"type"` } @@ -186,47 +186,18 @@ func validCategoryType(categoryType string) bool { // // API Reference: https://docs.readme.com/reference/getcategories func (c CategoryClient) GetAll(options ...RequestOptions) ([]Category, *APIResponse, error) { - var err error - var categories []Category - var apiResponse *APIResponse - hasNextPage := false - - // Initialize pagination counter. - page := 1 - if len(options) > 0 { - if options[0].Page != 0 { - page = options[0].Page - } + var results []Category + opts := parseRequestOptions(options) + apiResponse, err := c.client.fetchAllPages(CategoryEndpoint, opts, &results) + if err != nil { + return results, apiResponse, fmt.Errorf("unable to retrieve categories: %w", err) } - for { - var paginatedResult []Category - - apiRequest := &APIRequest{ - Method: "GET", - Endpoint: CategoryEndpoint, - UseAuth: true, - OkStatusCode: []int{200}, - Response: &paginatedResult, - } - if len(options) > 0 { - apiRequest.RequestOptions = options[0] - } - - apiResponse, hasNextPage, err = c.client.paginatedRequest(apiRequest, page) - if err != nil { - return categories, apiResponse, fmt.Errorf("unable to retrieve categories: %w", err) - } - categories = append(categories, paginatedResult...) - - if !hasNextPage { - break - } - - page = page + 1 + if len(results) == 0 { + return nil, apiResponse, nil } - return categories, apiResponse, nil + return results, apiResponse, nil } // Get a single category on ReadMe.com. diff --git a/readme/category_test.go b/readme/category_test.go index 9018ebc..e5edfea 100644 --- a/readme/category_test.go +++ b/readme/category_test.go @@ -9,99 +9,6 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_Category_GetAll(t *testing.T) { - // Arrange - gock.New(TestClient.APIURL). - Get(readme.CategoryEndpoint). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "3"). - JSON(testdata.Categories) - defer gock.Off() - - // Act - got, _, err := TestClient.Category.GetAll(readme.RequestOptions{Page: 1}) - - // Assert - assert.NoError(t, err, "it does not return an error") - assert.Equal(t, testdata.Categories, got, "it returns expected data") - assert.True(t, gock.IsDone(), "it makes the expected API call") -} - -func Test_Category_GetAll_Pagination_Invalid(t *testing.T) { - t.Run("when pagination header is invalid", func(t *testing.T) { - // Arrange - var expect []readme.Category - gock.New(TestClient.APIURL). - Get(readme.CategoryEndpoint). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "90"). - JSON(expect) - defer gock.Off() - - // Act - got, _, err := TestClient.Category.GetAll(readme.RequestOptions{PerPage: 6, Page: 15}) - - // Asert - assert.NoError(t, err, "it does not return an error") - assert.Equal(t, expect, got, "returns nil []Category") - assert.True(t, gock.IsDone(), "it makes the expected API call") - }) - - t.Run("when page >= (totalCount / perPage)", func(t *testing.T) { - // Arrange - var expect []readme.Category - gock.New(TestClient.APIURL). - Get(readme.CategoryEndpoint). - MatchParam("page", "14"). - MatchParam("perPage", "6"). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "90"). - JSON(expect) - gock.New(TestClient.APIURL). - Get(readme.CategoryEndpoint). - MatchParam("page", "15"). - MatchParam("perPage", "6"). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "90"). - JSON(expect) - defer gock.Off() - - // Act - got, apiResponse, err := TestClient.Category.GetAll(readme.RequestOptions{PerPage: 6, Page: 14}) - - // Assert - assert.NoError(t, err, "it does not return an error") - assert.Equal(t, "/categories?perPage=6&page=15", apiResponse.Request.Endpoint, "it returns expected endpoint") - assert.Equal(t, expect, got, "it returns expected []Category response") - assert.True(t, gock.IsDone(), "it makes the expected API call") - }) - - t.Run("when total count header is not a number", func(t *testing.T) { - // Arrange - var expect []readme.Category - gock.New(TestClient.APIURL). - Get(readme.CategoryEndpoint). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "x"). - JSON(expect) - defer gock.Off() - - // Act - got, _, err := TestClient.Category.GetAll(readme.RequestOptions{PerPage: 6, Page: 15}) - - // Assert - assert.Error(t, err, "it returns an error") - assert.ErrorContains(t, err, "unable to parse 'x-total-count' header", "it returns the expected error") - assert.Equal(t, expect, got, "it returns nil []Category") - assert.True(t, gock.IsDone(), "it makes the expected API call") - }) -} - func Test_Category_Get(t *testing.T) { // Arrange expect := testdata.Categories[0] diff --git a/readme/changelog.go b/readme/changelog.go index 0e74e2d..f8c746f 100644 --- a/readme/changelog.go +++ b/readme/changelog.go @@ -99,23 +99,18 @@ func validChangelogType(changelogType string) bool { // // API Reference: https://docs.readme.com/main/reference/getchangelogs func (c ChangelogClient) GetAll(options ...RequestOptions) ([]Changelog, *APIResponse, error) { - var response []Changelog - - apiRequest := &APIRequest{ - Method: "GET", - Endpoint: ChangelogEndpoint, - UseAuth: true, - OkStatusCode: []int{200}, - Response: &response, + var results []Changelog + opts := parseRequestOptions(options) + apiResponse, err := c.client.fetchAllPages(ChangelogEndpoint, opts, &results) + if err != nil { + return nil, apiResponse, fmt.Errorf("unable to retrieve changelogs: %w", err) } - if len(options) > 0 { - apiRequest.RequestOptions = options[0] + if len(results) == 0 { + return nil, apiResponse, nil } - apiResponse, err := c.client.APIRequest(apiRequest) - - return response, apiResponse, err + return results, apiResponse, err } // Get retrieves a single changelog from ReadMe. diff --git a/readme/changelog_test.go b/readme/changelog_test.go index 2a8aa27..a9d821a 100644 --- a/readme/changelog_test.go +++ b/readme/changelog_test.go @@ -28,21 +28,47 @@ func Test_Changelog_Get(t *testing.T) { } func Test_Changelog_GetAll(t *testing.T) { - // Arrange - expect := testdata.Changelogs - gock.New(TestClient.APIURL). - Get(readme.ChangelogEndpoint). - Reply(200). - JSON(expect) - defer gock.Off() - - // Act - got, _, err := TestClient.Changelog.GetAll(readme.RequestOptions{Page: 1}) - - // Assert - assert.NoError(t, err, "it does not return an error") - assert.Equal(t, expect, got, "it returns slice of Changelog structs") - assert.True(t, gock.IsDone(), "it makes the expected API call") + testCases := []struct { + name string + page int + expected []readme.Changelog + }{ + { + name: "when called with no page", + page: 1, + expected: testdata.Changelogs, + }, + { + name: "when called with page 2", + page: 2, + expected: testdata.Changelogs, + }, + { + name: "when there are no changelogs", + page: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Arrange + gock.New(TestClient.APIURL). + Get(readme.ChangelogEndpoint). + Reply(200). + AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). + AddHeader("x-total-count", "1"). + JSON(tc.expected) + defer gock.Off() + + // Act + got, _, err := TestClient.Changelog.GetAll(readme.RequestOptions{Page: 1}) + + // Assert + assert.NoError(t, err, "it does not return an error") + assert.Equal(t, tc.expected, got, "it returns slice of Changelog structs") + assert.True(t, gock.IsDone(), "it makes the expected API call") + }) + } } func Test_Changelog_Create(t *testing.T) { diff --git a/readme/custom_page.go b/readme/custom_page.go index 3ac338d..6da13a2 100644 --- a/readme/custom_page.go +++ b/readme/custom_page.go @@ -86,47 +86,18 @@ type CustomPageParams struct { // // API Reference: https://docs.readme.com/main/reference/getcustompages func (c CustomPageClient) GetAll(options ...RequestOptions) ([]CustomPage, *APIResponse, error) { - var customPages []CustomPage - var apiResponse *APIResponse - var err error - hasNextPage := false - - // Initialize pagination counter. - page := 1 - if len(options) > 0 { - if options[0].Page != 0 { - page = options[0].Page - } + var results []CustomPage + opts := parseRequestOptions(options) + apiResponse, err := c.client.fetchAllPages(CustomPageEndpoint, opts, &results) + if err != nil { + return nil, apiResponse, err } - for { - var paginatedResult []CustomPage - - apiRequest := &APIRequest{ - Method: "GET", - Endpoint: CustomPageEndpoint, - UseAuth: true, - OkStatusCode: []int{200}, - Response: &paginatedResult, - } - if len(options) > 0 { - apiRequest.RequestOptions = options[0] - } - - apiResponse, hasNextPage, err = c.client.paginatedRequest(apiRequest, page) - if err != nil { - return customPages, apiResponse, fmt.Errorf("unable to retrieve custom pages: %w", err) - } - customPages = append(customPages, paginatedResult...) - - if !hasNextPage { - break - } - - page = page + 1 + if len(results) == 0 { + return nil, apiResponse, nil } - return customPages, apiResponse, nil + return results, apiResponse, nil } // Get a single custom page's data from ReadMe. diff --git a/readme/custom_page_test.go b/readme/custom_page_test.go index 788a121..7f53e37 100644 --- a/readme/custom_page_test.go +++ b/readme/custom_page_test.go @@ -1,6 +1,7 @@ package readme_test import ( + "fmt" "testing" "github.com/h2non/gock" @@ -10,82 +11,116 @@ import ( ) func Test_CustomPages_GetAll(t *testing.T) { - t.Run("when called with valid parameters and API responds with 200", func(t *testing.T) { - // Arrange - expect := testdata.CustomPages - gock.New(TestClient.APIURL). - Get(readme.CustomPageEndpoint). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "3"). - JSON(expect) - defer gock.Off() - - reqOpts := readme.RequestOptions{PerPage: 100, Page: 1} - - // Act - got, _, err := TestClient.CustomPage.GetAll(reqOpts) - - // Assert - assert.NoError(t, err, "it does not return an error") - assert.Equal(t, expect, got, "it returns expected []CustomPage struct") - assert.True(t, gock.IsDone(), "it makes the expected API call") - }) + tests := []struct { + name string + reqOpts readme.RequestOptions + mockResponseStatus int + mockHeaders map[string]string + mockResponseBody interface{} + expectError bool + expectedErrorMsg string + expectedResult []readme.CustomPage + expectedEndpoint string + }{ + { + name: "valid parameters with 200 response", + reqOpts: readme.RequestOptions{ + PerPage: 100, + Page: 1, + }, + mockResponseStatus: 200, + mockHeaders: map[string]string{ + "Link": `; rel="next", <>; rel="prev", <>; rel="last"`, + "x-total-count": "3", + }, + mockResponseBody: testdata.CustomPages, + expectError: false, + expectedResult: testdata.CustomPages, + }, + { + name: "invalid pagination header with large page number", + reqOpts: readme.RequestOptions{ + PerPage: 6, + Page: 16, + }, + mockResponseStatus: 200, + mockHeaders: map[string]string{ + "Link": `; rel="next", <>; rel="prev", <>; rel="last"`, + "x-total-count": "90", + }, + mockResponseBody: testdata.CustomPages, + expectError: false, + expectedResult: testdata.CustomPages, + expectedEndpoint: "/custompages?perPage=6&page=16", + }, + { + name: "invalid x-total-count header", + reqOpts: readme.RequestOptions{ + PerPage: 6, + Page: 15, + }, + mockResponseStatus: 200, + mockHeaders: map[string]string{ + "Link": `; rel="next", <>; rel="prev", <>; rel="last"`, + "x-total-count": "x", + }, + mockResponseBody: []readme.CustomPage{}, + expectError: true, + expectedErrorMsg: "unable to parse 'x-total-count' header:", + expectedResult: nil, + }, + { + name: "when there are no custom pages", + reqOpts: readme.RequestOptions{ + PerPage: 6, + Page: 1, + }, + mockResponseStatus: 200, + mockHeaders: map[string]string{ + "Link": `; rel="next", <>; rel="prev", <>; rel="last"`, + "x-total-count": "0", + }, + mockResponseBody: []readme.CustomPage{}, + expectError: false, + expectedResult: nil, + }, + } - t.Run("when response pagination header is invalid", func(t *testing.T) { - t.Run("when page >= (totalCount / perPage)", func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { // Arrange - expect := testdata.CustomPages - gock.New(TestClient.APIURL). + resp := gock.New(TestClient.APIURL). Get(readme.CustomPageEndpoint). - MatchParam("page", "16"). - MatchParam("perPage", "6"). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "90"). - JSON(expect) + MatchParam("page", fmt.Sprintf("%d", tt.reqOpts.Page)). + MatchParam("perPage", fmt.Sprintf("%d", tt.reqOpts.PerPage)). + Reply(tt.mockResponseStatus) + for key, value := range tt.mockHeaders { + resp.AddHeader(key, value) + } + resp.JSON(tt.mockResponseBody) defer gock.Off() - reqOpts := readme.RequestOptions{PerPage: 6, Page: 16} - // Act - got, apiResponse, err := TestClient.CustomPage.GetAll(reqOpts) + got, apiResponse, err := TestClient.CustomPage.GetAll(tt.reqOpts) // Assert - assert.NoError(t, err, "it does not return an error") - assert.Equal(t, "/custompages?perPage=6&page=16", - apiResponse.Request.Endpoint, - "it requests with the expected pagination query parameters") - assert.Equal(t, expect, got, "it returns expected []CustomPage") - assert.True(t, gock.IsDone(), "it makes the expected API call") - }) - - t.Run("when pagination x-total-count header is invalid", func(t *testing.T) { - // Arrange - var expect []readme.CustomPage - gock.New(TestClient.APIURL). - Get(readme.CustomPageEndpoint). - MatchParam("page", "15"). - MatchParam("perPage", "6"). - Reply(200). - AddHeader("Link", `; rel="next", <>; rel="prev", <>; rel="last"`). - AddHeader("x-total-count", "x"). - JSON(expect) - defer gock.Off() + if tt.expectError { + assert.Error(t, err, "it returns an error") + if tt.expectedErrorMsg != "" { + assert.ErrorContains(t, err, tt.expectedErrorMsg, "it returns the expected error") + } + } else { + assert.NoError(t, err, "it does not return an error") + assert.Equal(t, tt.expectedResult, got, "it returns expected []CustomPage struct") + } + + if tt.expectedEndpoint != "" && apiResponse != nil { + assert.Equal(t, tt.expectedEndpoint, apiResponse.Request.Endpoint, "it requests with the expected pagination query parameters") + } - reqOpts := readme.RequestOptions{PerPage: 6, Page: 15} - - // Act - got, _, err := TestClient.CustomPage.GetAll(reqOpts) - - // Assert - assert.Error(t, err, "it returns an error") - assert.ErrorContains(t, err, "unable to parse 'x-total-count' header:", - "it returns the expected error") - assert.Equal(t, expect, got, "it returns nil []CustomPage") assert.True(t, gock.IsDone(), "it makes the expected API call") }) - }) + } } func Test_CustomPages_Get(t *testing.T) { diff --git a/readme/doc.go b/readme/doc.go index ff498eb..d2f8671 100644 --- a/readme/doc.go +++ b/readme/doc.go @@ -218,7 +218,7 @@ type DocParams struct { Title string `json:"title"` // Type of the page. The available types all show up under the /docs/ URL path of your docs // project (also known as the "guides" section). Can be "basic" (most common), "error" (page - // desribing an API error), or "link" (page that redirects to an external link). + // describing an API error), or "link" (page that redirects to an external link). Type string `json:"type,omitempty"` } diff --git a/readme/readme.go b/readme/readme.go index 84fcc43..d41f708 100644 --- a/readme/readme.go +++ b/readme/readme.go @@ -382,6 +382,56 @@ func (c *Client) paginatedRequest(apiRequest *APIRequest, page int) (*APIRespons return apiResponse, true, nil } +// fetchAllPages retrieves all pages of data from a paginated endpoint. +func (c *Client) fetchAllPages( + endpoint string, + options *RequestOptions, + result interface{}, +) (*APIResponse, error) { + hasNextPage := false + page := 1 + var apiResponse *APIResponse + var err error + + if options != nil && options.Page != 0 { + page = options.Page + } + + for { + apiRequest := &APIRequest{ + Method: "GET", + Endpoint: endpoint, + UseAuth: true, + OkStatusCode: []int{200}, + Response: result, + } + if options != nil { + apiRequest.RequestOptions = *options + } + + apiResponse, hasNextPage, err = c.paginatedRequest(apiRequest, page) + if err != nil { + return apiResponse, fmt.Errorf("unable to retrieve data: %w", err) + } + + if !hasNextPage { + break + } + page++ + } + + return apiResponse, nil +} + +// parseRequestOptions is a helper function to parse the RequestOptions slice +// and return the first element as a *RequestOptions struct. +func parseRequestOptions(options []RequestOptions) *RequestOptions { + if len(options) > 0 { + return &options[0] + } + return nil +} + // HasNextPage checks if a "next" link is provided in the "links" response header for pagination, // indicating the request has a next page. // diff --git a/tests/testdata/api_specification.go b/tests/testdata/api_specification.go index 21cc4db..0203194 100644 --- a/tests/testdata/api_specification.go +++ b/tests/testdata/api_specification.go @@ -24,7 +24,7 @@ var APISpecifications = []readme.APISpecification{ }, } -var APISpecResponseVersionEmtpy = readme.APIResponse{ +var APISpecResponseVersionEmpty = readme.APIResponse{ APIErrorResponse: readme.APIErrorResponse{ Error: "VERSION_EMPTY", Message: "string",