diff --git a/client/product.go b/client/product.go index 92ebe6b..b7ab1ab 100644 --- a/client/product.go +++ b/client/product.go @@ -4,20 +4,22 @@ import ( "fmt" "net/http" "net/url" + "strconv" "strings" ) const ( - productListResourceEndpoint = "/admin/api/services.json" - productResourceEndpoint = "/admin/api/services/%d.json" - productMethodListResourceEndpoint = "/admin/api/services/%d/metrics/%d/methods.json" - productMethodResourceEndpoint = "/admin/api/services/%d/metrics/%d/methods/%d.json" - productMetricListResourceEndpoint = "/admin/api/services/%d/metrics.json" - productMetricResourceEndpoint = "/admin/api/services/%d/metrics/%d.json" - productMappingRuleListResourceEndpoint = "/admin/api/services/%d/proxy/mapping_rules.json" - productMappingRuleResourceEndpoint = "/admin/api/services/%d/proxy/mapping_rules/%d.json" - productProxyResourceEndpoint = "/admin/api/services/%d/proxy.json" - productProxyDeployResourceEndpoint = "/admin/api/services/%d/proxy/deploy.json" + productListResourceEndpoint = "/admin/api/services.json" + productResourceEndpoint = "/admin/api/services/%d.json" + productMethodListResourceEndpoint = "/admin/api/services/%d/metrics/%d/methods.json" + productMethodResourceEndpoint = "/admin/api/services/%d/metrics/%d/methods/%d.json" + productMetricListResourceEndpoint = "/admin/api/services/%d/metrics.json" + productMetricResourceEndpoint = "/admin/api/services/%d/metrics/%d.json" + productMappingRuleListResourceEndpoint = "/admin/api/services/%d/proxy/mapping_rules.json" + productMappingRuleResourceEndpoint = "/admin/api/services/%d/proxy/mapping_rules/%d.json" + productProxyResourceEndpoint = "/admin/api/services/%d/proxy.json" + productProxyDeployResourceEndpoint = "/admin/api/services/%d/proxy/deploy.json" + PRODUCTS_PER_PAGE int = 500 ) // BackendApi Read 3scale Backend @@ -109,12 +111,47 @@ func (c *ThreeScaleClient) DeleteProduct(id int64) error { return handleJsonResp(resp, http.StatusOK, nil) } -// ListProducts List existing products func (c *ThreeScaleClient) ListProducts() (*ProductList, error) { + // Keep asking until the results length is lower than "per_page" param + currentPage := 1 + productList := &ProductList{} + + allResultsPerPage := false + for next := true; next; next = allResultsPerPage { + tmpProductList, err := c.ListProductsPerPage(currentPage, PRODUCTS_PER_PAGE) + if err != nil { + return nil, err + } + + productList.Products = append(productList.Products, tmpProductList.Products...) + + allResultsPerPage = len(tmpProductList.Products) == PRODUCTS_PER_PAGE + currentPage += 1 + } + + return productList, nil + +} + +// ListProductsPerPage List existing products in a single page +// paginationValues[0] = Page in the paginated list. Defaults to 1 for the API, as the client will not send the page param. +// paginationValues[1] = Number of results per page. Default and max is 500 for the aPI, as the client will not send the per_page param. +func (c *ThreeScaleClient) ListProductsPerPage(paginationValues ...int) (*ProductList, error) { + queryValues := url.Values{} + + if len(paginationValues) > 0 { + queryValues.Add("page", strconv.Itoa(paginationValues[0])) + } + + if len(paginationValues) > 1 { + queryValues.Add("per_page", strconv.Itoa(paginationValues[1])) + } + req, err := c.buildGetReq(productListResourceEndpoint) if err != nil { return nil, err } + req.URL.RawQuery = queryValues.Encode() resp, err := c.httpClient.Do(req) if err != nil { diff --git a/client/product_test.go b/client/product_test.go index 85d1b9f..886f419 100644 --- a/client/product_test.go +++ b/client/product_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" + "strconv" "strings" "testing" ) @@ -1146,18 +1147,58 @@ func TestDeleteProduct(t *testing.T) { } func TestListProducts(t *testing.T) { + + productGenerator := func(startingIndex, n int) ProductList { + pList := ProductList{ + Products: make([]Product, 0, n), + } + + for idx := 0; idx < n; idx++ { + pList.Products = append(pList.Products, Product{ + Element: ProductItem{ID: int64(idx + startingIndex)}, + }) + } + + return pList + } + httpClient := NewTestClient(func(req *http.Request) *http.Response { + // Will serve: 3 pages + // page 1 => PRODUCTS_PER_PAGE + // page 2 => PRODUCTS_PER_PAGE + // page 3 => 51 if req.URL.Path != productListResourceEndpoint { - t.Fatalf("Path does not match. Expected [%s]; got [%s]", backendListResourceEndpoint, req.URL.Path) + t.Fatalf("Path does not match. Expected [%s]; got [%s]", productListResourceEndpoint, req.URL.Path) } if req.Method != http.MethodGet { t.Fatalf("Method does not match. Expected [%s]; got [%s]", http.MethodGet, req.Method) } + if req.URL.Query().Get("per_page") != strconv.Itoa(PRODUCTS_PER_PAGE) { + t.Fatalf("per_page param does not match. Expected [%d]; got [%s]", PRODUCTS_PER_PAGE, req.URL.Query().Get("per_page")) + } + + var list ProductList + + if req.URL.Query().Get("page") == "1" { + list = productGenerator(PRODUCTS_PER_PAGE*0, PRODUCTS_PER_PAGE) + } else if req.URL.Query().Get("page") == "2" { + list = productGenerator(PRODUCTS_PER_PAGE*1, PRODUCTS_PER_PAGE) + } else if req.URL.Query().Get("page") == "3" { + list = productGenerator(PRODUCTS_PER_PAGE*2, 51) + } else { + t.Fatalf("page param unexpected value; got [%s]", req.URL.Query().Get("page")) + } + + responseBodyBytes, err := json.Marshal(list) + if err != nil { + t.Fatal(err) + } + return &http.Response{ StatusCode: http.StatusOK, - Body: ioutil.NopCloser(bytes.NewReader(helperLoadBytes(t, "product_list_fixture.json"))), + Body: ioutil.NopCloser(bytes.NewBuffer(responseBodyBytes)), Header: make(http.Header), } }) @@ -1173,7 +1214,95 @@ func TestListProducts(t *testing.T) { t.Fatal("product list returned nil") } - if len(productList.Products) != 2 { - t.Fatalf("Then number of products does not match. Expected [%d]; got [%d]", 2, len(productList.Products)) + if len(productList.Products) != 2*PRODUCTS_PER_PAGE+51 { + t.Fatalf("Then number of products does not match. Expected [%d]; got [%d]", 2*PRODUCTS_PER_PAGE+51, len(productList.Products)) } } + +func TestListProductsPerPage(t *testing.T) { + t.Run("page and per_page params used", func(subT *testing.T) { + var ( + pageNum int = 4 + perPage int = 2 + ) + httpClient := NewTestClient(func(req *http.Request) *http.Response { + if req.URL.Path != productListResourceEndpoint { + subT.Fatalf("Path does not match. Expected [%s]; got [%s]", productListResourceEndpoint, req.URL.Path) + } + + if req.Method != http.MethodGet { + subT.Fatalf("Method does not match. Expected [%s]; got [%s]", http.MethodGet, req.Method) + } + + if req.URL.Query().Get("page") != strconv.Itoa(pageNum) { + subT.Fatalf("page param does not match. Expected [%d]; got [%s]", pageNum, req.URL.Query().Get("page")) + } + + if req.URL.Query().Get("per_page") != strconv.Itoa(perPage) { + subT.Fatalf("page param does not match. Expected [%d]; got [%s]", perPage, req.URL.Query().Get("per_page")) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(helperLoadBytes(subT, "product_list_fixture.json"))), + Header: make(http.Header), + } + }) + + credential := "someAccessToken" + c := NewThreeScale(NewTestAdminPortal(subT), credential, httpClient) + productList, err := c.ListProductsPerPage(pageNum, perPage) + if err != nil { + subT.Fatal(err) + } + + if productList == nil { + subT.Fatal("product list returned nil") + } + + if len(productList.Products) != 2 { + subT.Fatalf("Then number of products does not match. Expected [%d]; got [%d]", 2, len(productList.Products)) + } + }) + + t.Run("page and per_page params not used", func(subT *testing.T) { + httpClient := NewTestClient(func(req *http.Request) *http.Response { + if req.URL.Path != productListResourceEndpoint { + subT.Fatalf("Path does not match. Expected [%s]; got [%s]", productListResourceEndpoint, req.URL.Path) + } + + if req.Method != http.MethodGet { + subT.Fatalf("Method does not match. Expected [%s]; got [%s]", http.MethodGet, req.Method) + } + + if req.URL.Query().Get("page") != "" { + subT.Fatalf("Query param page does not match. Expected empty; got [%s]", req.URL.Query().Get("page")) + } + + if req.URL.Query().Get("per_page") != "" { + subT.Fatalf("page param does not match. Expected empty; got [%s]", req.URL.Query().Get("per_page")) + } + + return &http.Response{ + StatusCode: http.StatusOK, + Body: ioutil.NopCloser(bytes.NewReader(helperLoadBytes(subT, "product_list_fixture.json"))), + Header: make(http.Header), + } + }) + + credential := "someAccessToken" + c := NewThreeScale(NewTestAdminPortal(subT), credential, httpClient) + productList, err := c.ListProductsPerPage() + if err != nil { + subT.Fatal(err) + } + + if productList == nil { + subT.Fatal("product list returned nil") + } + + if len(productList.Products) != 2 { + subT.Fatalf("Then number of products does not match. Expected [%d]; got [%d]", 2, len(productList.Products)) + } + }) +}