Repository: moov-io/customers
Pull request #137 (2020-09-03 20:16:07)

Added paging using sql offset
This PR introduces the concept of a `page `query param into the customer search functionality. `count` still defaults to 20.

An example scenario:
There are 30 customers
A query with page=1 and limit =10 will return the 10 most recently created customers.
A query with page=2 and limit =10 will return the 10 most recently created customers starting from the 11th most recently created customer.
A query with page=3 and limit =10 will return the 10 most recently created customers starting from the 21th most recently created customer.
A query with page=4 and limit =10 will return no customers.


Patch:
diff --git a/client/README.md b/client/README.md
index 9cd1457..198ab7a 100644
--- a/client/README.md
+++ b/client/README.md
@@ -53,6 +53,7 @@ Class | Method | HTTP request | Description
 *CustomersApi* | [**Ping**](docs/CustomersApi.md#ping) | **Get** /ping | Ping Customers
 *CustomersApi* | [**RefreshOFACSearch**](docs/CustomersApi.md#refreshofacsearch) | **Put** /customers/{customerID}/refresh/ofac | Refresh customer OFAC search
 *CustomersApi* | [**ReplaceCustomerMetadata**](docs/CustomersApi.md#replacecustomermetadata) | **Put** /customers/{customerID}/metadata | Update customer metadata
+*CustomersApi* | [**SearchCustomers**](docs/CustomersApi.md#searchcustomers) | **Get** /customers | Get customers
 *CustomersApi* | [**UpdateCustomerStatus**](docs/CustomersApi.md#updatecustomerstatus) | **Put** /customers/{customerID}/status | Update customer status
 *CustomersApi* | [**UploadCustomerDocument**](docs/CustomersApi.md#uploadcustomerdocument) | **Post** /customers/{customerID}/documents | Upload document
 *CustomersApi* | [**ValidateAccount**](docs/CustomersApi.md#validateaccount) | **Post** /customers/{customerID}/accounts/{accountID}/validate | Validate Account
diff --git a/client/api/openapi.yaml b/client/api/openapi.yaml
index fa36ef6..02d5b00 100644
--- a/client/api/openapi.yaml
+++ b/client/api/openapi.yaml
@@ -32,6 +32,73 @@ paths:
       tags:
       - Customers
   /customers:
+    get:
+      description: Search for customers using different filter parameters
+      operationId: searchCustomers
+      parameters:
+      - description: Optional parameter for searching by customer name
+        example: jane
+        explode: false
+        in: path
+        name: query
+        required: false
+        schema:
+          type: string
+        style: simple
+      - description: Optional parameter for searching by customer email
+        example: jane@doe.com
+        explode: false
+        in: path
+        name: email
+        required: false
+        schema:
+          type: string
+        style: simple
+      - description: Optional parameter for searching by customer status
+        example: Verified
+        explode: false
+        in: path
+        name: status
+        required: false
+        schema:
+          type: string
+        style: simple
+      - description: Optional parameter for searching for customers by skipping over
+          an initial group
+        example: 10
+        explode: false
+        in: path
+        name: skip
+        required: false
+        schema:
+          type: string
+        style: simple
+      - description: Optional parameter for searching for customers by specifying
+          the amount to return
+        example: 20
+        explode: false
+        in: path
+        name: count
+        required: false
+        schema:
+          type: string
+        style: simple
+      responses:
+        "200":
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Customers'
+          description: Customers were successfully retrieved
+        "400":
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+          description: Customers were not retrieved, see error(s)
+      summary: Get customers
+      tags:
+      - Customers
     post:
       description: Create a Customer object from the given details of a human or business
       operationId: createCustomer
@@ -1179,6 +1246,10 @@ components:
       - status
       - type
       type: object
+    Customers:
+      items:
+        $ref: '#/components/schemas/Customer'
+      type: array
     CustomerType:
       description: Note if this Customer represents an individual or business
       enum:
diff --git a/client/api_customers.go b/client/api_customers.go
index 6042427..2456303 100644
--- a/client/api_customers.go
+++ b/client/api_customers.go
@@ -1657,6 +1657,115 @@ func (a *CustomersApiService) ReplaceCustomerMetadata(ctx _context.Context, cust
 	return localVarReturnValue, localVarHTTPResponse, nil
 }
 
+/*
+SearchCustomers Get customers
+Search for customers using different filter parameters
+ * @param ctx _context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
+ * @param query Optional parameter for searching by customer name
+ * @param email Optional parameter for searching by customer email
+ * @param status Optional parameter for searching by customer status
+ * @param skip Optional parameter for searching for customers by skipping over an initial group
+ * @param count Optional parameter for searching for customers by specifying the amount to return
+@return []Customer
+*/
+func (a *CustomersApiService) SearchCustomers(ctx _context.Context, query string, email string, status string, skip string, count string) ([]Customer, *_nethttp.Response, error) {
+	var (
+		localVarHTTPMethod   = _nethttp.MethodGet
+		localVarPostBody     interface{}
+		localVarFormFileName string
+		localVarFileName     string
+		localVarFileBytes    []byte
+		localVarReturnValue  []Customer
+	)
+
+	// create path and map variables
+	localVarPath := a.client.cfg.BasePath + "/customers"
+	localVarPath = strings.Replace(localVarPath, "{"+"query"+"}", _neturl.QueryEscape(parameterToString(query, "")), -1)
+
+	localVarPath = strings.Replace(localVarPath, "{"+"email"+"}", _neturl.QueryEscape(parameterToString(email, "")), -1)
+
+	localVarPath = strings.Replace(localVarPath, "{"+"status"+"}", _neturl.QueryEscape(parameterToString(status, "")), -1)
+
+	localVarPath = strings.Replace(localVarPath, "{"+"skip"+"}", _neturl.QueryEscape(parameterToString(skip, "")), -1)
+
+	localVarPath = strings.Replace(localVarPath, "{"+"count"+"}", _neturl.QueryEscape(parameterToString(count, "")), -1)
+
+	localVarHeaderParams := make(map[string]string)
+	localVarQueryParams := _neturl.Values{}
+	localVarFormParams := _neturl.Values{}
+
+	// to determine the Content-Type header
+	localVarHTTPContentTypes := []string{}
+
+	// set Content-Type header
+	localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
+	if localVarHTTPContentType != "" {
+		localVarHeaderParams["Content-Type"] = localVarHTTPContentType
+	}
+
+	// to determine the Accept header
+	localVarHTTPHeaderAccepts := []string{"application/json"}
+
+	// set Accept header
+	localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
+	if localVarHTTPHeaderAccept != "" {
+		localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
+	}
+	r, err := a.client.prepareRequest(ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, localVarFormFileName, localVarFileName, localVarFileBytes)
+	if err != nil {
+		return localVarReturnValue, nil, err
+	}
+
+	localVarHTTPResponse, err := a.client.callAPI(r)
+	if err != nil || localVarHTTPResponse == nil {
+		return localVarReturnValue, localVarHTTPResponse, err
+	}
+
+	localVarBody, err := _ioutil.ReadAll(localVarHTTPResponse.Body)
+	localVarHTTPResponse.Body.Close()
+	if err != nil {
+		return localVarReturnValue, localVarHTTPResponse, err
+	}
+
+	if localVarHTTPResponse.StatusCode >= 300 {
+		newErr := GenericOpenAPIError{
+			body:  localVarBody,
+			error: localVarHTTPResponse.Status,
+		}
+		if localVarHTTPResponse.StatusCode == 200 {
+			var v []Customer
+			err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+			if err != nil {
+				newErr.error = err.Error()
+				return localVarReturnValue, localVarHTTPResponse, newErr
+			}
+			newErr.model = v
+			return localVarReturnValue, localVarHTTPResponse, newErr
+		}
+		if localVarHTTPResponse.StatusCode == 400 {
+			var v Error
+			err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+			if err != nil {
+				newErr.error = err.Error()
+				return localVarReturnValue, localVarHTTPResponse, newErr
+			}
+			newErr.model = v
+		}
+		return localVarReturnValue, localVarHTTPResponse, newErr
+	}
+
+	err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
+	if err != nil {
+		newErr := GenericOpenAPIError{
+			body:  localVarBody,
+			error: err.Error(),
+		}
+		return localVarReturnValue, localVarHTTPResponse, newErr
+	}
+
+	return localVarReturnValue, localVarHTTPResponse, nil
+}
+
 // UpdateCustomerStatusOpts Optional parameters for the method 'UpdateCustomerStatus'
 type UpdateCustomerStatusOpts struct {
 	XRequestID optional.String
diff --git a/client/docs/CustomersApi.md b/client/docs/CustomersApi.md
index 875f7aa..6cbc1c3 100644
--- a/client/docs/CustomersApi.md
+++ b/client/docs/CustomersApi.md
@@ -19,6 +19,7 @@ Method | HTTP request | Description
 [**Ping**](CustomersApi.md#Ping) | **Get** /ping | Ping Customers
 [**RefreshOFACSearch**](CustomersApi.md#RefreshOFACSearch) | **Put** /customers/{customerID}/refresh/ofac | Refresh customer OFAC search
 [**ReplaceCustomerMetadata**](CustomersApi.md#ReplaceCustomerMetadata) | **Put** /customers/{customerID}/metadata | Update customer metadata
+[**SearchCustomers**](CustomersApi.md#SearchCustomers) | **Get** /customers | Get customers
 [**UpdateCustomerStatus**](CustomersApi.md#UpdateCustomerStatus) | **Put** /customers/{customerID}/status | Update customer status
 [**UploadCustomerDocument**](CustomersApi.md#UploadCustomerDocument) | **Post** /customers/{customerID}/documents | Upload document
 [**ValidateAccount**](CustomersApi.md#ValidateAccount) | **Post** /customers/{customerID}/accounts/{accountID}/validate | Validate Account
@@ -711,6 +712,44 @@ No authorization required
 [[Back to README]](../README.md)
 
 
+## SearchCustomers
+
+> []Customer SearchCustomers(ctx, query, email, status, skip, count)
+
+Get customers
+
+Search for customers using different filter parameters
+
+### Required Parameters
+
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc.
+**query** | **string**| Optional parameter for searching by customer name | 
+**email** | **string**| Optional parameter for searching by customer email | 
+**status** | **string**| Optional parameter for searching by customer status | 
+**skip** | **string**| Optional parameter for searching for customers by skipping over an initial group | 
+**count** | **string**| Optional parameter for searching for customers by specifying the amount to return | 
+
+### Return type
+
+[**[]Customer**](Customer.md)
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+- **Content-Type**: Not defined
+- **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints)
+[[Back to Model list]](../README.md#documentation-for-models)
+[[Back to README]](../README.md)
+
+
 ## UpdateCustomerStatus
 
 > Customer UpdateCustomerStatus(ctx, customerID, updateCustomerStatus, optional)
diff --git a/cmd/server/customer_search.go b/cmd/server/customer_search.go
index ae454fe..ae45af0 100644
--- a/cmd/server/customer_search.go
+++ b/cmd/server/customer_search.go
@@ -8,23 +8,23 @@ import (
 	"encoding/json"
 	"fmt"
 	"net/http"
-	"net/url"
-	"strconv"
 	"strings"
 
+	"github.com/go-kit/kit/log"
 	moovhttp "github.com/moov-io/base/http"
 	client "github.com/moov-io/customers/client"
 	"github.com/moov-io/customers/cmd/server/route"
-	"github.com/moov-io/customers/internal/util"
-
-	"github.com/go-kit/kit/log"
 )
 
 func searchCustomers(logger log.Logger, repo customerRepository) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
 		w = route.Responder(logger, w, r)
 
-		params := readSearchParams(r.URL)
+		params, err := readSearchParams(r)
+		if err != nil {
+			moovhttp.Problem(w, err)
+			return
+		}
 		customers, err := repo.searchCustomers(params)
 		if err != nil {
 			moovhttp.Problem(w, err)
@@ -43,19 +43,25 @@ type searchParams struct {
 	Query  string
 	Email  string
 	Status string
-	Limit  int64
+	Skip   int64
+	Count  int64
 }
 
-func readSearchParams(u *url.URL) searchParams {
+func readSearchParams(r *http.Request) (searchParams, error) {
 	params := searchParams{
-		Query:  strings.ToLower(strings.TrimSpace(u.Query().Get("query"))),
-		Email:  strings.ToLower(strings.TrimSpace(u.Query().Get("email"))),
-		Status: strings.ToLower(strings.TrimSpace(u.Query().Get("status"))),
+		Query:  strings.ToLower(strings.TrimSpace(r.URL.Query().Get("query"))),
+		Email:  strings.ToLower(strings.TrimSpace(r.URL.Query().Get("email"))),
+		Status: strings.ToLower(strings.TrimSpace(r.URL.Query().Get("status"))),
 	}
-	if limit, err := strconv.ParseInt(util.Or(u.Query().Get("limit"), "20"), 10, 32); err == nil {
-		params.Limit = limit
+	skip, count, exists, err := moovhttp.GetSkipAndCount(r)
+	if exists && err != nil {
+		return params, err
 	}
-	return params
+
+	params.Skip = int64(skip)
+	params.Count = int64(count)
+
+	return params, nil
 }
 
 func (r *sqlCustomerRepository) searchCustomers(params searchParams) ([]*client.Customer, error) {
@@ -107,5 +113,13 @@ func buildSearchQuery(params searchParams) (string, []interface{}) {
 		query += " and status like ?"
 		args = append(args, "%"+params.Status)
 	}
-	return query + " order by created_at asc limit ?;", append(args, fmt.Sprintf("%d", params.Limit))
+	query += " order by created_at asc limit ?"
+	args = append(args, fmt.Sprintf("%d", params.Count))
+
+	if params.Skip > 0 {
+		query += " offset ?"
+		args = append(args, fmt.Sprintf("%d", params.Skip))
+	}
+	query += ";"
+	return query, args
 }
diff --git a/go.mod b/go.mod
index 74a35a8..1892964 100644
--- a/go.mod
+++ b/go.mod
@@ -22,7 +22,7 @@ require (
 	github.com/mattn/go-sqlite3 v2.0.3+incompatible
 	github.com/mitchellh/mapstructure v1.3.3 // indirect
 	github.com/moov-io/ach v1.4.4
-	github.com/moov-io/base v0.11.0
+	github.com/moov-io/base v0.11.1
 	github.com/moov-io/fed v0.5.1
 	github.com/moov-io/identity v0.2.7
 	github.com/moov-io/paygate v0.8.0
diff --git a/go.sum b/go.sum
index bd68620..e053623 100644
--- a/go.sum
+++ b/go.sum
@@ -676,6 +676,8 @@ github.com/moov-io/base v0.11.0-rc1.0.20191121181647-cd3e7a9609db h1:O6qBkf9lubI
 github.com/moov-io/base v0.11.0-rc1.0.20191121181647-cd3e7a9609db/go.mod h1:oXZRZBQiO7HX+Wc7Q3jJHLfkDPxrMXRbgYhURQqfXD8=
 github.com/moov-io/base v0.11.0 h1:1dhI8qsIxsUjMmbzE52ycyy2S1z3DGUN6swGYjSmOLs=
 github.com/moov-io/base v0.11.0/go.mod h1:jEkZukoSaJ4rDFoe00S5zuoLE+p3U3BEunSEPchgSbU=
+github.com/moov-io/base v0.11.1 h1:5qGfUr3eKWFmjttK2WllKgBg0Pnzx50+JBnRQQi8Gl0=
+github.com/moov-io/base v0.11.1/go.mod h1:kiWd5d0G3Ji+V9VhCNyzgfMNfbpzBblSL+QY2n2eywE=
 github.com/moov-io/customers v0.4.0-rc4/go.mod h1:wNz/qIiG24rJrjnK3VOvzj0MbdfGznj6INO/uyntgNM=
 github.com/moov-io/customers v0.4.0-rc4.0.20200519011716-05c4c319c49b/go.mod h1:wmBTMaZ7OzoXhDtLqGngNrEaRlsk7VFLxY0kl3HzoHc=
 github.com/moov-io/customers v0.4.0-rc5.0.20200605165738-057f20d4d580/go.mod h1:2viqzwFr1cp5gXL9lhOIXl7KE1pDIA8Z4I5smZBRUSo=
@@ -910,6 +912,7 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
+github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
 github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/square/go-jose v2.5.1+incompatible/go.mod h1:7MxpAF/1WTVUu8Am+T5kNy+t0902CaLWM4Z745MkOa8=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
diff --git a/openapi.yaml b/openapi.yaml
index 2fefff1..77580b8 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -33,6 +33,56 @@ paths:
         '200':
           description: Service is running properly
   /customers:
+    get:
+      tags: [Customers]
+      summary: Get customers
+      description: Search for customers using different filter parameters
+      operationId: searchCustomers
+      parameters:
+        - name: query
+          in: path
+          description: Optional parameter for searching by customer name
+          example: jane
+          schema:
+            type: string
+          required: false
+        - name: email
+          in: path
+          description: Optional parameter for searching by customer email
+          example: jane@doe.com
+          schema:
+            type: string
+        - name: status
+          in: path
+          description: Optional parameter for searching by customer status
+          example: Verified
+          schema:
+            type: string
+        - name: skip
+          in: path
+          description: Optional parameter for searching for customers by skipping over an initial group
+          example: 10
+          schema:
+            type: string
+        - name: count
+          in: path
+          description: Optional parameter for searching for customers by specifying the amount to return
+          example: 20
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Customers were successfully retrieved
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Customers'
+        '400':
+          description: Customers were not retrieved, see error(s)
+          content:
+            application/json:
+              schema:
+                $ref: 'https://raw.githubusercontent.com/moov-io/api/master/openapi-common.yaml#/components/schemas/Error'
     post:
       tags: [Customers]
       summary: Create customer
@@ -910,6 +960,10 @@ components:
         - email
         - createdAt
         - lastModified
+    Customers:
+      type: array
+      items:
+        $ref: '#/components/schemas/Customer'
     CustomerType:
       type: string
       description: Note if this Customer represents an individual or business


Test patch:
diff --git a/cmd/server/customer_search_scope_test.go b/cmd/server/customer_search_scope_test.go
index b64e7fe..2656905 100644
--- a/cmd/server/customer_search_scope_test.go
+++ b/cmd/server/customer_search_scope_test.go
@@ -50,15 +50,8 @@ func (scope *Scope) CreateCustomers(count int) []client.Customer {
 		scope.fuzzer.Fuzz(&firstName)
 		scope.fuzzer.Fuzz(&lastName)
 		scope.fuzzer.Fuzz(&email)
-		cust, _, _ := (customerRequest{
-			FirstName: firstName,
-			LastName:  lastName,
-			Email:     email,
-		}).asCustomer(testCustomerSSNStorage(scope.t))
-		if err := scope.customerRepo.createCustomer(cust); err != nil {
-			scope.t.Error(err)
-		}
-		customers = append(customers, *cust)
+		customer := scope.CreateCustomer(firstName, lastName, email)
+		customers = append(customers, customer)
 	}
 	return customers
 }
diff --git a/cmd/server/customer_search_test.go b/cmd/server/customer_search_test.go
index 8c599a2..ea74198 100644
--- a/cmd/server/customer_search_test.go
+++ b/cmd/server/customer_search_test.go
@@ -90,7 +90,7 @@ func TestCustomerSearch__query(t *testing.T) {
 	// Query search
 	query, args := buildSearchQuery(searchParams{
 		Query: "jane doe",
-		Limit: 100,
+		Count: 100,
 	})
 	if query != "select customer_id from customers where deleted_at is null and lower(first_name) || \" \" || lower(last_name) LIKE ? order by created_at asc limit ?;" {
 		t.Errorf("unexpected query: %q", query)
@@ -126,7 +126,7 @@ func TestCustomerSearch__query(t *testing.T) {
 	query, args = buildSearchQuery(searchParams{
 		Query: "jane doe",
 		Email: "jane.doe@moov.io",
-		Limit: 25,
+		Count: 25,
 	})
 	if query != "select customer_id from customers where deleted_at is null and lower(first_name) || \" \" || lower(last_name) LIKE ? and lower(email) like ? order by created_at asc limit ?;" {
 		t.Errorf("unexpected query: %q", query)
@@ -159,14 +159,14 @@ func TestGet10MostRecentlyCreatedCustomersByDefault(t *testing.T) {
 func TestGet50MostRecentlyCreatedCustomersWhenSpecifyingLimit(t *testing.T) {
 	scope := Setup(t)
 	scope.CreateCustomers(100)
-	customers, _ := scope.GetCustomers("?limit=50")
+	customers, _ := scope.GetCustomers("?count=50")
 	scope.assert.Equal(50, len(customers))
 }
 
 func TestGet100MostRecentlyCreatedCustomersWhenSpecifyingMoreThanAvailable(t *testing.T) {
 	scope := Setup(t)
 	scope.CreateCustomers(100)
-	customers, _ := scope.GetCustomers("?limit=120")
+	customers, _ := scope.GetCustomers("?count=120")
 	scope.assert.Equal(100, len(customers))
 }
 
@@ -174,7 +174,7 @@ func TestGetCustomersWithVerifiedStatus(t *testing.T) {
 	// Create two customers. 1 with Unknown STATUS and 1 with Verified
 	scope := Setup(t)
 	scope.CreateCustomers(2)
-	customers, _ := scope.GetCustomers("?limit=120")
+	customers, _ := scope.GetCustomers("?count=120")
 	scope.assert.Equal(2, len(customers))
 	for i := 0; i < len(customers); i++ {
 		if i%2 == 0 {
@@ -186,14 +186,14 @@ func TestGetCustomersWithVerifiedStatus(t *testing.T) {
 	}
 
 	// Should have 1 Verified Status
-	customers, _ = scope.GetCustomers("?status=Verified&limit=20")
+	customers, _ = scope.GetCustomers("?status=Verified&count=20")
 	scope.assert.Equal(1, len(customers))
 	for i := 0; i < len(customers); i++ {
 		scope.assert.Equal("Verified", string(customers[i].Status))
 	}
 
 	// Should have 1 Unknown Status
-	customers, _ = scope.GetCustomers("?status=Unknown&limit=20")
+	customers, _ = scope.GetCustomers("?status=Unknown&count=20")
 	scope.assert.Equal(1, len(customers))
 	for i := 0; i < len(customers); i++ {
 		scope.assert.Equal("Unknown", string(customers[i].Status))
@@ -265,3 +265,38 @@ func TestGetCustomersByNameAndEmail(t *testing.T) {
 	customers, _ = scope.GetCustomers("?query=jim&email=jane.doe@gmail.com")
 	scope.assert.Equal(0, len(customers))
 }
+
+func TestGetCustomersUsingPaging(t *testing.T) {
+	scope := Setup(t)
+	_ = scope.CreateCustomers(30)
+
+	// Get first page of 10
+	customers, _ := scope.GetCustomers("?skip=0&count=10")
+	scope.assert.Equal(10, len(customers))
+
+	// Get second page of 10
+	customers, _ = scope.GetCustomers("?skip=10&count=10")
+	scope.assert.Equal(10, len(customers))
+
+	// Get third page of 10
+	customers, _ = scope.GetCustomers("?skip=20&count=10")
+	scope.assert.Equal(10, len(customers))
+
+	// Should be no 4th page of 10
+	customers, _ = scope.GetCustomers("?skip=30&count=10")
+	scope.assert.Equal(0, len(customers))
+}
+
+func TestGetCustomersUsingPagingFailure(t *testing.T) {
+	scope := Setup(t)
+	_ = scope.CreateCustomers(30)
+
+	customers, _ := scope.GetCustomers("?skip=123abc")
+	scope.assert.Equal(0, len(customers))
+
+	customers, _ = scope.GetCustomers("?count=123abc")
+	scope.assert.Equal(0, len(customers))
+
+	customers, _ = scope.GetCustomers("?skip=123abc&count=123abc")
+	scope.assert.Equal(0, len(customers))
+}