diff --git a/customer.go b/customer.go index a63fe7f1..1e7a6057 100644 --- a/customer.go +++ b/customer.go @@ -18,6 +18,7 @@ const ( // See: https://help.shopify.com/api/reference/customer type CustomerService interface { List(context.Context, interface{}) ([]Customer, error) + ListAll(context.Context, interface{}) ([]Customer, error) ListWithPagination(ctx context.Context, options interface{}) ([]Customer, *Pagination, error) Count(context.Context, interface{}) (int, error) Get(context.Context, uint64, interface{}) (*Customer, error) @@ -111,6 +112,29 @@ func (s *CustomerServiceOp) List(ctx context.Context, options interface{}) ([]Cu return resource.Customers, err } +// ListAll Lists all customers, iterating over pages +func (s *CustomerServiceOp) ListAll(ctx context.Context, options interface{}) ([]Customer, error) { + collector := []Customer{} + + for { + entities, pagination, err := s.ListWithPagination(ctx, options) + + if err != nil { + return collector, err + } + + collector = append(collector, entities...) + + if pagination.NextPageOptions == nil { + break + } + + options = pagination.NextPageOptions + } + + return collector, nil +} + // ListWithPagination lists customers and return pagination to retrieve next/previous results. func (s *CustomerServiceOp) ListWithPagination(ctx context.Context, options interface{}) ([]Customer, *Pagination, error) { path := fmt.Sprintf("%s.json", customersBasePath) diff --git a/customer_test.go b/customer_test.go index e676cce7..016a8b07 100644 --- a/customer_test.go +++ b/customer_test.go @@ -32,6 +32,118 @@ func TestCustomerList(t *testing.T) { } } +func TestCustomerListAll(t *testing.T) { + setup() + defer teardown() + + listURL := fmt.Sprintf("https://fooshop.myshopify.com/%s/customers.json", client.pathPrefix) + + cases := []struct { + name string + expectedCustomers []Customer + expectedRequestURLs []string + expectedLinkHeaders []string + expectedBodies []string + expectedErr error + }{ + { + name: "Pulls the next page", + expectedRequestURLs: []string{ + listURL, + fmt.Sprintf("%s?page_info=pg2", listURL), + }, + expectedLinkHeaders: []string{ + `; rel="next"`, + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"customers": [{"id":1},{"id":2}]}`, + `{"customers": [{"id":3},{"id":4}]}`, + }, + expectedCustomers: []Customer{{Id: 1}, {Id: 2}, {Id: 3}, {Id: 4}}, + expectedErr: nil, + }, + { + name: "Stops when there is not a next page", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"customers": [{"id":1}]}`, + }, + expectedCustomers: []Customer{{Id: 1}}, + expectedErr: nil, + }, + { + name: "Returns errors when required", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"customers": []}`, + }, + expectedCustomers: []Customer{}, + expectedErr: errors.New("page_info is missing"), + }, + } + + for i, c := range cases { + t.Run(c.name, func(t *testing.T) { + if len(c.expectedRequestURLs) != len(c.expectedLinkHeaders) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected link headers (%d)", + len(c.expectedRequestURLs), + len(c.expectedLinkHeaders), + ) + + return + } + + if len(c.expectedRequestURLs) != len(c.expectedBodies) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected bodies (%d)", + len(c.expectedRequestURLs), + len(c.expectedBodies), + ) + + return + } + + for i := range c.expectedRequestURLs { + response := &http.Response{ + StatusCode: 200, + Body: httpmock.NewRespBodyFromString(c.expectedBodies[i]), + Header: http.Header{ + "Link": {c.expectedLinkHeaders[i]}, + }, + } + + httpmock.RegisterResponder("GET", c.expectedRequestURLs[i], httpmock.ResponderFromResponse(response)) + } + + customers, err := client.Customer.ListAll(context.Background(), nil) + if !reflect.DeepEqual(customers, c.expectedCustomers) { + t.Errorf("test %d Customer.ListAll orders returned %+v, expected %+v", i, customers, c.expectedCustomers) + } + + if (c.expectedErr != nil || err != nil) && err.Error() != c.expectedErr.Error() { + t.Errorf( + "test %d Customer.ListAll err returned %+v, expected %+v", + i, + err, + c.expectedErr, + ) + } + }) + } +} + func TestCustomerListWithPagination(t *testing.T) { setup() defer teardown() diff --git a/order.go b/order.go index d4ad7bff..1b158cfb 100644 --- a/order.go +++ b/order.go @@ -19,6 +19,7 @@ const ( // See: https://help.shopify.com/api/reference/order type OrderService interface { List(context.Context, interface{}) ([]Order, error) + ListAll(context.Context, interface{}) ([]Order, error) ListWithPagination(context.Context, interface{}) ([]Order, *Pagination, error) Count(context.Context, interface{}) (int, error) Get(context.Context, uint64, interface{}) (*Order, error) @@ -538,6 +539,29 @@ func (s *OrderServiceOp) List(ctx context.Context, options interface{}) ([]Order return orders, nil } +// ListAll Lists all orders, iterating over pages +func (s *OrderServiceOp) ListAll(ctx context.Context, options interface{}) ([]Order, error) { + collector := []Order{} + + for { + entities, pagination, err := s.ListWithPagination(ctx, options) + + if err != nil { + return collector, err + } + + collector = append(collector, entities...) + + if pagination.NextPageOptions == nil { + break + } + + options = pagination.NextPageOptions + } + + return collector, nil +} + func (s *OrderServiceOp) ListWithPagination(ctx context.Context, options interface{}) ([]Order, *Pagination, error) { path := fmt.Sprintf("%s.json", ordersBasePath) resource := new(OrdersResource) diff --git a/order_risk.go b/order_risk.go index 4e0cb254..66d35201 100644 --- a/order_risk.go +++ b/order_risk.go @@ -15,6 +15,7 @@ const ( // See: https://shopify.dev/docs/api/admin-rest/2023-10/resources/order-risk type OrderRiskService interface { List(context.Context, uint64, interface{}) ([]OrderRisk, error) + ListAll(context.Context, uint64, interface{}) ([]OrderRisk, error) ListWithPagination(context.Context, uint64, interface{}) ([]OrderRisk, *Pagination, error) Get(context.Context, uint64, uint64, interface{}) (*OrderRisk, error) Create(context.Context, uint64, OrderRisk) (*OrderRisk, error) @@ -79,6 +80,29 @@ func (s *OrderRiskServiceOp) List(ctx context.Context, orderId uint64, options i return orders, nil } +// ListAll Lists all OrderRisk, iterating over pages +func (s *OrderRiskServiceOp) ListAll(ctx context.Context, orderId uint64, options interface{}) ([]OrderRisk, error) { + collector := []OrderRisk{} + + for { + entities, pagination, err := s.ListWithPagination(ctx, orderId, options) + + if err != nil { + return collector, err + } + + collector = append(collector, entities...) + + if pagination.NextPageOptions == nil { + break + } + + options = pagination.NextPageOptions + } + + return collector, nil +} + func (s *OrderRiskServiceOp) ListWithPagination(ctx context.Context, orderId uint64, options interface{}) ([]OrderRisk, *Pagination, error) { path := fmt.Sprintf("%s/%d/%s.json", ordersRiskBasePath, orderId, ordersRiskResourceName) resource := new(OrdersRisksResource) diff --git a/order_risk_test.go b/order_risk_test.go index 68e09197..73cce1b5 100644 --- a/order_risk_test.go +++ b/order_risk_test.go @@ -31,6 +31,118 @@ func TestOrderRiskListError(t *testing.T) { } } +func TestOrderRiskListAll(t *testing.T) { + setup() + defer teardown() + + listURL := fmt.Sprintf("https://fooshop.myshopify.com/%s/orders/450789469/risks.json", client.pathPrefix) + + cases := []struct { + name string + expectedOrderRisks []OrderRisk + expectedRequestURLs []string + expectedLinkHeaders []string + expectedBodies []string + expectedErr error + }{ + { + name: "Pulls the next page", + expectedRequestURLs: []string{ + listURL, + fmt.Sprintf("%s?page_info=pg2", listURL), + }, + expectedLinkHeaders: []string{ + `; rel="next"`, + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"risks": [{"id":1},{"id":2}]}`, + `{"risks": [{"id":3},{"id":4}]}`, + }, + expectedOrderRisks: []OrderRisk{{Id: 1}, {Id: 2}, {Id: 3}, {Id: 4}}, + expectedErr: nil, + }, + { + name: "Stops when there is not a next page", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"risks": [{"id":1}]}`, + }, + expectedOrderRisks: []OrderRisk{{Id: 1}}, + expectedErr: nil, + }, + { + name: "Returns errors when required", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"risks": []}`, + }, + expectedOrderRisks: []OrderRisk{}, + expectedErr: errors.New("page_info is missing"), + }, + } + + for i, c := range cases { + t.Run(c.name, func(t *testing.T) { + if len(c.expectedRequestURLs) != len(c.expectedLinkHeaders) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected link headers (%d)", + len(c.expectedRequestURLs), + len(c.expectedLinkHeaders), + ) + + return + } + + if len(c.expectedRequestURLs) != len(c.expectedBodies) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected bodies (%d)", + len(c.expectedRequestURLs), + len(c.expectedBodies), + ) + + return + } + + for i := range c.expectedRequestURLs { + response := &http.Response{ + StatusCode: 200, + Body: httpmock.NewRespBodyFromString(c.expectedBodies[i]), + Header: http.Header{ + "Link": {c.expectedLinkHeaders[i]}, + }, + } + + httpmock.RegisterResponder("GET", c.expectedRequestURLs[i], httpmock.ResponderFromResponse(response)) + } + + risks, err := client.OrderRisk.ListAll(context.Background(), 450789469, nil) + if !reflect.DeepEqual(risks, c.expectedOrderRisks) { + t.Errorf("test %d OrderRisk.ListAll orders returned %+v, expected %+v", i, risks, c.expectedOrderRisks) + } + + if (c.expectedErr != nil || err != nil) && err.Error() != c.expectedErr.Error() { + t.Errorf( + "test %d OrderRisk.ListAll err returned %+v, expected %+v", + i, + err, + c.expectedErr, + ) + } + }) + } +} + func TestOrderRiskListWithPagination(t *testing.T) { setup() defer teardown() diff --git a/order_test.go b/order_test.go index d3594531..61a0b8c7 100644 --- a/order_test.go +++ b/order_test.go @@ -34,6 +34,118 @@ func TestOrderListError(t *testing.T) { } } +func TestOrderListAll(t *testing.T) { + setup() + defer teardown() + + listURL := fmt.Sprintf("https://fooshop.myshopify.com/%s/orders.json", client.pathPrefix) + + cases := []struct { + name string + expectedOrders []Order + expectedRequestURLs []string + expectedLinkHeaders []string + expectedBodies []string + expectedErr error + }{ + { + name: "Pulls the next page", + expectedRequestURLs: []string{ + listURL, + fmt.Sprintf("%s?page_info=pg2", listURL), + }, + expectedLinkHeaders: []string{ + `; rel="next"`, + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"orders": [{"id":1},{"id":2}]}`, + `{"orders": [{"id":3},{"id":4}]}`, + }, + expectedOrders: []Order{{Id: 1}, {Id: 2}, {Id: 3}, {Id: 4}}, + expectedErr: nil, + }, + { + name: "Stops when there is not a next page", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"orders": [{"id":1}]}`, + }, + expectedOrders: []Order{{Id: 1}}, + expectedErr: nil, + }, + { + name: "Returns errors when required", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"orders": []}`, + }, + expectedOrders: []Order{}, + expectedErr: errors.New("page_info is missing"), + }, + } + + for i, c := range cases { + t.Run(c.name, func(t *testing.T) { + if len(c.expectedRequestURLs) != len(c.expectedLinkHeaders) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected link headers (%d)", + len(c.expectedRequestURLs), + len(c.expectedLinkHeaders), + ) + + return + } + + if len(c.expectedRequestURLs) != len(c.expectedBodies) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected bodies (%d)", + len(c.expectedRequestURLs), + len(c.expectedBodies), + ) + + return + } + + for i := range c.expectedRequestURLs { + response := &http.Response{ + StatusCode: 200, + Body: httpmock.NewRespBodyFromString(c.expectedBodies[i]), + Header: http.Header{ + "Link": {c.expectedLinkHeaders[i]}, + }, + } + + httpmock.RegisterResponder("GET", c.expectedRequestURLs[i], httpmock.ResponderFromResponse(response)) + } + + orders, err := client.Order.ListAll(context.Background(), nil) + if !reflect.DeepEqual(orders, c.expectedOrders) { + t.Errorf("test %d Order.ListAll orders returned %+v, expected %+v", i, orders, c.expectedOrders) + } + + if (c.expectedErr != nil || err != nil) && err.Error() != c.expectedErr.Error() { + t.Errorf( + "test %d Order.ListAll err returned %+v, expected %+v", + i, + err, + c.expectedErr, + ) + } + }) + } +} + func TestOrderListWithPagination(t *testing.T) { setup() defer teardown() diff --git a/payments_transactions.go b/payments_transactions.go index d15cb61a..81451687 100644 --- a/payments_transactions.go +++ b/payments_transactions.go @@ -12,6 +12,7 @@ const paymentsTransactionsBasePath = "shopify_payments/balance/transactions" // See: https://shopify.dev/docs/api/admin-rest/2023-01/resources/transactions type PaymentsTransactionsService interface { List(context.Context, interface{}) ([]PaymentsTransactions, error) + ListAll(context.Context, interface{}) ([]PaymentsTransactions, error) ListWithPagination(context.Context, interface{}) ([]PaymentsTransactions, *Pagination, error) Get(context.Context, uint64, interface{}) (*PaymentsTransactions, error) } @@ -88,6 +89,29 @@ func (s *PaymentsTransactionsServiceOp) List(ctx context.Context, options interf return PaymentsTransactions, nil } +// ListAll Lists all PaymentsTransactions, iterating over pages +func (s *PaymentsTransactionsServiceOp) ListAll(ctx context.Context, options interface{}) ([]PaymentsTransactions, error) { + collector := []PaymentsTransactions{} + + for { + entities, pagination, err := s.ListWithPagination(ctx, options) + + if err != nil { + return collector, err + } + + collector = append(collector, entities...) + + if pagination.NextPageOptions == nil { + break + } + + options = pagination.NextPageOptions + } + + return collector, nil +} + func (s *PaymentsTransactionsServiceOp) ListWithPagination(ctx context.Context, options interface{}) ([]PaymentsTransactions, *Pagination, error) { path := fmt.Sprintf("%s.json", paymentsTransactionsBasePath) resource := new(PaymentsTransactionsResource) diff --git a/payments_transactions_test.go b/payments_transactions_test.go index 17b16e1f..48e1d2d9 100644 --- a/payments_transactions_test.go +++ b/payments_transactions_test.go @@ -112,6 +112,118 @@ func TestPaymentsTransactionsListError(t *testing.T) { } } +func TestPaymentsTransactionsListAll(t *testing.T) { + setup() + defer teardown() + + listURL := fmt.Sprintf("https://fooshop.myshopify.com/%s/shopify_payments/balance/transactions.json", client.pathPrefix) + + cases := []struct { + name string + expectedPaymentsTransactionss []PaymentsTransactions + expectedRequestURLs []string + expectedLinkHeaders []string + expectedBodies []string + expectedErr error + }{ + { + name: "Pulls the next page", + expectedRequestURLs: []string{ + listURL, + fmt.Sprintf("%s?page_info=pg2", listURL), + }, + expectedLinkHeaders: []string{ + `; rel="next"`, + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"transactions": [{"id":1},{"id":2}]}`, + `{"transactions": [{"id":3},{"id":4}]}`, + }, + expectedPaymentsTransactionss: []PaymentsTransactions{{Id: 1}, {Id: 2}, {Id: 3}, {Id: 4}}, + expectedErr: nil, + }, + { + name: "Stops when there is not a next page", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"transactions": [{"id":1}]}`, + }, + expectedPaymentsTransactionss: []PaymentsTransactions{{Id: 1}}, + expectedErr: nil, + }, + { + name: "Returns errors when required", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"transactions": []}`, + }, + expectedPaymentsTransactionss: []PaymentsTransactions{}, + expectedErr: errors.New("page_info is missing"), + }, + } + + for i, c := range cases { + t.Run(c.name, func(t *testing.T) { + if len(c.expectedRequestURLs) != len(c.expectedLinkHeaders) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected link headers (%d)", + len(c.expectedRequestURLs), + len(c.expectedLinkHeaders), + ) + + return + } + + if len(c.expectedRequestURLs) != len(c.expectedBodies) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected bodies (%d)", + len(c.expectedRequestURLs), + len(c.expectedBodies), + ) + + return + } + + for i := range c.expectedRequestURLs { + response := &http.Response{ + StatusCode: 200, + Body: httpmock.NewRespBodyFromString(c.expectedBodies[i]), + Header: http.Header{ + "Link": {c.expectedLinkHeaders[i]}, + }, + } + + httpmock.RegisterResponder("GET", c.expectedRequestURLs[i], httpmock.ResponderFromResponse(response)) + } + + transactions, err := client.PaymentsTransactions.ListAll(context.Background(), nil) + if !reflect.DeepEqual(transactions, c.expectedPaymentsTransactionss) { + t.Errorf("test %d PaymentsTransactions.ListAll orders returned %+v, expected %+v", i, transactions, c.expectedPaymentsTransactionss) + } + + if (c.expectedErr != nil || err != nil) && err.Error() != c.expectedErr.Error() { + t.Errorf( + "test %d PaymentsTransactions.ListAll err returned %+v, expected %+v", + i, + err, + c.expectedErr, + ) + } + }) + } +} + func TestPaymentsTransactionsListWithPagination(t *testing.T) { setup() defer teardown() diff --git a/payouts.go b/payouts.go index 38a124d5..79d15d5b 100644 --- a/payouts.go +++ b/payouts.go @@ -14,6 +14,7 @@ const payoutsBasePath = "shopify_payments/payouts" // See: https://shopify.dev/docs/api/admin-rest/2023-01/resources/payouts type PayoutsService interface { List(context.Context, interface{}) ([]Payout, error) + ListAll(context.Context, interface{}) ([]Payout, error) ListWithPagination(context.Context, interface{}) ([]Payout, *Pagination, error) Get(context.Context, uint64, interface{}) (*Payout, error) } @@ -75,6 +76,29 @@ func (s *PayoutsServiceOp) List(ctx context.Context, options interface{}) ([]Pay return payouts, nil } +// ListAll Lists all payouts, iterating over pages +func (s *PayoutsServiceOp) ListAll(ctx context.Context, options interface{}) ([]Payout, error) { + collector := []Payout{} + + for { + entities, pagination, err := s.ListWithPagination(ctx, options) + + if err != nil { + return collector, err + } + + collector = append(collector, entities...) + + if pagination.NextPageOptions == nil { + break + } + + options = pagination.NextPageOptions + } + + return collector, nil +} + func (s *PayoutsServiceOp) ListWithPagination(ctx context.Context, options interface{}) ([]Payout, *Pagination, error) { path := fmt.Sprintf("%s.json", payoutsBasePath) resource := new(PayoutsResource) diff --git a/payouts_test.go b/payouts_test.go index e741e09a..509a5708 100644 --- a/payouts_test.go +++ b/payouts_test.go @@ -65,6 +65,118 @@ func TestPayoutsListError(t *testing.T) { } } +func TestPayoutListAll(t *testing.T) { + setup() + defer teardown() + + listURL := fmt.Sprintf("https://fooshop.myshopify.com/%s/shopify_payments/payouts.json", client.pathPrefix) + + cases := []struct { + name string + expectedPayouts []Payout + expectedRequestURLs []string + expectedLinkHeaders []string + expectedBodies []string + expectedErr error + }{ + { + name: "Pulls the next page", + expectedRequestURLs: []string{ + listURL, + fmt.Sprintf("%s?page_info=pg2", listURL), + }, + expectedLinkHeaders: []string{ + `; rel="next"`, + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"payouts": [{"id":1},{"id":2}]}`, + `{"payouts": [{"id":3},{"id":4}]}`, + }, + expectedPayouts: []Payout{{Id: 1}, {Id: 2}, {Id: 3}, {Id: 4}}, + expectedErr: nil, + }, + { + name: "Stops when there is not a next page", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"payouts": [{"id":1}]}`, + }, + expectedPayouts: []Payout{{Id: 1}}, + expectedErr: nil, + }, + { + name: "Returns errors when required", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"payouts": []}`, + }, + expectedPayouts: []Payout{}, + expectedErr: errors.New("page_info is missing"), + }, + } + + for i, c := range cases { + t.Run(c.name, func(t *testing.T) { + if len(c.expectedRequestURLs) != len(c.expectedLinkHeaders) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected link headers (%d)", + len(c.expectedRequestURLs), + len(c.expectedLinkHeaders), + ) + + return + } + + if len(c.expectedRequestURLs) != len(c.expectedBodies) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected bodies (%d)", + len(c.expectedRequestURLs), + len(c.expectedBodies), + ) + + return + } + + for i := range c.expectedRequestURLs { + response := &http.Response{ + StatusCode: 200, + Body: httpmock.NewRespBodyFromString(c.expectedBodies[i]), + Header: http.Header{ + "Link": {c.expectedLinkHeaders[i]}, + }, + } + + httpmock.RegisterResponder("GET", c.expectedRequestURLs[i], httpmock.ResponderFromResponse(response)) + } + + payouts, err := client.Payouts.ListAll(context.Background(), nil) + if !reflect.DeepEqual(payouts, c.expectedPayouts) { + t.Errorf("test %d Payout.ListAll orders returned %+v, expected %+v", i, payouts, c.expectedPayouts) + } + + if (c.expectedErr != nil || err != nil) && err.Error() != c.expectedErr.Error() { + t.Errorf( + "test %d Payout.ListAll err returned %+v, expected %+v", + i, + err, + c.expectedErr, + ) + } + }) + } +} + func TestPayoutsListWithPagination(t *testing.T) { setup() defer teardown() diff --git a/product.go b/product.go index 95f63e28..9fb948fa 100644 --- a/product.go +++ b/product.go @@ -20,6 +20,7 @@ var linkRegex = regexp.MustCompile(`^ *<([^>]+)>; rel="(previous|next)" *$`) // See: https://help.shopify.com/api/reference/product type ProductService interface { List(context.Context, interface{}) ([]Product, error) + ListAll(context.Context, interface{}) ([]Product, error) ListWithPagination(context.Context, interface{}) ([]Product, *Pagination, error) Count(context.Context, interface{}) (int, error) Get(context.Context, uint64, interface{}) (*Product, error) @@ -129,6 +130,29 @@ func (s *ProductServiceOp) List(ctx context.Context, options interface{}) ([]Pro return products, nil } +// ListAll Lists all products, iterating over pages +func (s *ProductServiceOp) ListAll(ctx context.Context, options interface{}) ([]Product, error) { + collector := []Product{} + + for { + entities, pagination, err := s.ListWithPagination(ctx, options) + + if err != nil { + return collector, err + } + + collector = append(collector, entities...) + + if pagination.NextPageOptions == nil { + break + } + + options = pagination.NextPageOptions + } + + return collector, nil +} + // ListWithPagination lists products and return pagination to retrieve next/previous results. func (s *ProductServiceOp) ListWithPagination(ctx context.Context, options interface{}) ([]Product, *Pagination, error) { path := fmt.Sprintf("%s.json", productsBasePath) diff --git a/product_listing.go b/product_listing.go index 679bd3e6..86362d5b 100644 --- a/product_listing.go +++ b/product_listing.go @@ -13,6 +13,7 @@ const productListingBasePath = "product_listings" // See: https://shopify.dev/docs/admin-api/rest/reference/sales-channels/productlisting type ProductListingService interface { List(context.Context, interface{}) ([]ProductListing, error) + ListAll(context.Context, interface{}) ([]ProductListing, error) ListWithPagination(context.Context, interface{}) ([]ProductListing, *Pagination, error) Count(context.Context, interface{}) (int, error) Get(context.Context, uint64, interface{}) (*ProductListing, error) @@ -83,6 +84,29 @@ func (s *ProductListingServiceOp) List(ctx context.Context, options interface{}) return products, nil } +// ListAll Lists all products, iterating over pages +func (s *ProductListingServiceOp) ListAll(ctx context.Context, options interface{}) ([]ProductListing, error) { + collector := []ProductListing{} + + for { + entities, pagination, err := s.ListWithPagination(ctx, options) + + if err != nil { + return collector, err + } + + collector = append(collector, entities...) + + if pagination.NextPageOptions == nil { + break + } + + options = pagination.NextPageOptions + } + + return collector, nil +} + // ListWithPagination lists products and return pagination to retrieve next/previous results. func (s *ProductListingServiceOp) ListWithPagination(ctx context.Context, options interface{}) ([]ProductListing, *Pagination, error) { path := fmt.Sprintf("%s.json", productListingBasePath) diff --git a/product_listing_test.go b/product_listing_test.go index a341482d..3fab1aeb 100644 --- a/product_listing_test.go +++ b/product_listing_test.go @@ -58,6 +58,118 @@ func TestProductListingListError(t *testing.T) { } } +func TestProductListingListAll(t *testing.T) { + setup() + defer teardown() + + listURL := fmt.Sprintf("https://fooshop.myshopify.com/%s/product_listings.json", client.pathPrefix) + + cases := []struct { + name string + expectedProductListings []ProductListing + expectedRequestURLs []string + expectedLinkHeaders []string + expectedBodies []string + expectedErr error + }{ + { + name: "Pulls the next page", + expectedRequestURLs: []string{ + listURL, + fmt.Sprintf("%s?page_info=pg2", listURL), + }, + expectedLinkHeaders: []string{ + `; rel="next"`, + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"product_listings": [{"product_id":1},{"product_id":2}]}`, + `{"product_listings": [{"product_id":3},{"product_id":4}]}`, + }, + expectedProductListings: []ProductListing{{Id: 1}, {Id: 2}, {Id: 3}, {Id: 4}}, + expectedErr: nil, + }, + { + name: "Stops when there is not a next page", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"product_listings": [{"product_id":1}]}`, + }, + expectedProductListings: []ProductListing{{Id: 1}}, + expectedErr: nil, + }, + { + name: "Returns errors when required", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"product_listings": []}`, + }, + expectedProductListings: []ProductListing{}, + expectedErr: errors.New("page_info is missing"), + }, + } + + for i, c := range cases { + t.Run(c.name, func(t *testing.T) { + if len(c.expectedRequestURLs) != len(c.expectedLinkHeaders) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected link headers (%d)", + len(c.expectedRequestURLs), + len(c.expectedLinkHeaders), + ) + + return + } + + if len(c.expectedRequestURLs) != len(c.expectedBodies) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected bodies (%d)", + len(c.expectedRequestURLs), + len(c.expectedBodies), + ) + + return + } + + for i := range c.expectedRequestURLs { + response := &http.Response{ + StatusCode: 200, + Body: httpmock.NewRespBodyFromString(c.expectedBodies[i]), + Header: http.Header{ + "Link": {c.expectedLinkHeaders[i]}, + }, + } + + httpmock.RegisterResponder("GET", c.expectedRequestURLs[i], httpmock.ResponderFromResponse(response)) + } + + productListings, err := client.ProductListing.ListAll(context.Background(), nil) + if !reflect.DeepEqual(productListings, c.expectedProductListings) { + t.Errorf("test %d ProductListing.ListAll orders returned %+v, expected %+v", i, productListings, c.expectedProductListings) + } + + if (c.expectedErr != nil || err != nil) && err.Error() != c.expectedErr.Error() { + t.Errorf( + "test %d ProductListing.ListAll err returned %+v, expected %+v", + i, + err, + c.expectedErr, + ) + } + }) + } +} + func TestProductListingListWithPagination(t *testing.T) { setup() defer teardown() diff --git a/product_test.go b/product_test.go index 5ebcf1e9..fef61863 100644 --- a/product_test.go +++ b/product_test.go @@ -82,6 +82,118 @@ func TestProductListError(t *testing.T) { } } +func TestProductListAll(t *testing.T) { + setup() + defer teardown() + + listURL := fmt.Sprintf("https://fooshop.myshopify.com/%s/products.json", client.pathPrefix) + + cases := []struct { + name string + expectedProducts []Product + expectedRequestURLs []string + expectedLinkHeaders []string + expectedBodies []string + expectedErr error + }{ + { + name: "Pulls the next page", + expectedRequestURLs: []string{ + listURL, + fmt.Sprintf("%s?page_info=pg2", listURL), + }, + expectedLinkHeaders: []string{ + `; rel="next"`, + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"products": [{"id":1},{"id":2}]}`, + `{"products": [{"id":3},{"id":4}]}`, + }, + expectedProducts: []Product{{Id: 1}, {Id: 2}, {Id: 3}, {Id: 4}}, + expectedErr: nil, + }, + { + name: "Stops when there is not a next page", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"products": [{"id":1}]}`, + }, + expectedProducts: []Product{{Id: 1}}, + expectedErr: nil, + }, + { + name: "Returns errors when required", + expectedRequestURLs: []string{ + listURL, + }, + expectedLinkHeaders: []string{ + `; rel="previous"`, + }, + expectedBodies: []string{ + `{"products": []}`, + }, + expectedProducts: []Product{}, + expectedErr: errors.New("page_info is missing"), + }, + } + + for i, c := range cases { + t.Run(c.name, func(t *testing.T) { + if len(c.expectedRequestURLs) != len(c.expectedLinkHeaders) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected link headers (%d)", + len(c.expectedRequestURLs), + len(c.expectedLinkHeaders), + ) + + return + } + + if len(c.expectedRequestURLs) != len(c.expectedBodies) { + t.Errorf( + "test case must have the same number of expected request urls (%d) as expected bodies (%d)", + len(c.expectedRequestURLs), + len(c.expectedBodies), + ) + + return + } + + for i := range c.expectedRequestURLs { + response := &http.Response{ + StatusCode: 200, + Body: httpmock.NewRespBodyFromString(c.expectedBodies[i]), + Header: http.Header{ + "Link": {c.expectedLinkHeaders[i]}, + }, + } + + httpmock.RegisterResponder("GET", c.expectedRequestURLs[i], httpmock.ResponderFromResponse(response)) + } + + products, err := client.Product.ListAll(context.Background(), nil) + if !reflect.DeepEqual(products, c.expectedProducts) { + t.Errorf("test %d Product.ListAll orders returned %+v, expected %+v", i, products, c.expectedProducts) + } + + if (c.expectedErr != nil || err != nil) && err.Error() != c.expectedErr.Error() { + t.Errorf( + "test %d Product.ListAll err returned %+v, expected %+v", + i, + err, + c.expectedErr, + ) + } + }) + } +} + func TestProductListWithPagination(t *testing.T) { setup() defer teardown()