Skip to content

Commit 43c0735

Browse files
fix: pagination with next_page_url (#110)
* fix: pagination with next_page_urls * add more unit tests
1 parent f4ff21f commit 43c0735

File tree

285 files changed

+2754
-1413
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

285 files changed

+2754
-1413
lines changed

client/mock_client.go

Lines changed: 78 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/page_util.go

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"log"
88
"reflect"
99
"regexp"
10+
"strings"
1011
)
1112

1213
//Takes a limit on the max number of records to read and a max pageSize and calculates the max number of pages to read.
@@ -28,16 +29,16 @@ func ReadLimits(pageSize *int, limit *int) int {
2829
}
2930
}
3031

31-
func GetNext(response interface{}, curRecord *int, limit *int, getNextPage func(nextPageUri string) (interface{}, error)) (interface{}, error) {
32-
nextPageUri, err := getNextPageUri(response, curRecord, limit)
32+
func GetNext(baseUrl string, response interface{}, curRecord *int, limit *int, getNextPage func(nextPageUri string) (interface{}, error)) (interface{}, error) {
33+
nextPageUrl, err := getNextPageAddress(baseUrl, response, curRecord, limit)
3334
if err != nil {
3435
return nil, err
3536
}
3637

37-
return getNextPage(nextPageUri)
38+
return getNextPage(nextPageUrl)
3839
}
3940

40-
func GetPayload(response interface{}) ([]interface{}, string, error) {
41+
func GetPayload(baseUrl string, response interface{}) ([]interface{}, string, error) {
4142
payload := toMap(response)
4243
var data [][]interface{}
4344
for _, v := range payload {
@@ -56,25 +57,29 @@ func GetPayload(response interface{}) ([]interface{}, string, error) {
5657
}
5758

5859
if len(data) == 1 {
59-
return data[0], getNextPageUrl(payload), nil
60+
return data[0], getNextPageUrl(baseUrl, payload), nil
6061
}
6162
return nil, "", errors.New("could not retrieve payload from response")
6263
}
6364

6465
func toMap(s interface{}) map[string]interface{} {
6566
var payload map[string]interface{}
66-
test, err := json.Marshal(s)
67-
if err != nil {
68-
log.Print("Map creation error: ", err)
67+
test, errMarshal := json.Marshal(s)
68+
if errMarshal != nil {
69+
return nil
70+
}
71+
72+
errUnmarshal := json.Unmarshal(test, &payload)
73+
if errUnmarshal != nil {
74+
log.Print("Map creation error: ", errUnmarshal)
6975
return nil
7076
}
71-
_ = json.Unmarshal(test, &payload)
7277
return payload
7378
}
7479

75-
func getNextPageUri(response interface{}, curRecord *int, limit *int) (string, error) {
76-
//get just the non metadata info and the next page uri
77-
payload, nextPageUri, err := GetPayload(response)
80+
func getNextPageAddress(baseUrl string, response interface{}, curRecord *int, limit *int) (string, error) {
81+
//get just the non metadata info and the next page url
82+
payload, nextPageUrl, err := GetPayload(baseUrl, response)
7883
if err != nil {
7984
return "", err
8085
}
@@ -91,20 +96,21 @@ func getNextPageUri(response interface{}, curRecord *int, limit *int) (string, e
9196
if remaining > 0 {
9297
pageSize := min(len(payload), remaining)
9398
re := regexp.MustCompile(`PageSize=\d+`)
94-
nextPageUri = re.ReplaceAllString(nextPageUri, fmt.Sprintf("PageSize=%d", pageSize))
99+
nextPageUrl = re.ReplaceAllString(nextPageUrl, fmt.Sprintf("PageSize=%d", pageSize))
95100
}
96101
}
97102

98-
return nextPageUri, err
103+
return nextPageUrl, err
99104
}
100105

101-
func getNextPageUrl(payload map[string]interface{}) string {
106+
func getNextPageUrl(baseUrl string, payload map[string]interface{}) string {
102107
if payload != nil && payload["meta"] != nil && payload["meta"].(map[string]interface{})["next_page_url"] != nil {
103108
return payload["meta"].(map[string]interface{})["next_page_url"].(string)
104109
}
105110

106111
if payload != nil && payload["next_page_uri"] != nil {
107-
return payload["next_page_uri"].(string)
112+
// remove any leading and trailing '/'
113+
return fmt.Sprintf("%s/%s", strings.Trim(baseUrl, "/"), strings.Trim(payload["next_page_uri"].(string), "/"))
108114
}
109115

110116
return ""

client/page_util_test.go

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
package client
22

33
import (
4+
"bytes"
5+
"encoding/json"
6+
"io/ioutil"
7+
"net/http"
8+
"net/url"
49
"testing"
510

11+
"github.com/golang/mock/gomock"
12+
613
"github.com/stretchr/testify/assert"
714
)
815

9-
func TestReadLimits(t *testing.T) {
16+
func TestPageUtil_ReadLimits(t *testing.T) {
1017
assert.Equal(t, 5, ReadLimits(nil, setLimit(5)))
1118
assert.Equal(t, 5, ReadLimits(setPageSize(10), setLimit(5)))
1219
assert.Equal(t, 1000, ReadLimits(nil, setLimit(5000)))
@@ -21,3 +28,195 @@ func setLimit(limit int) *int {
2128
func setPageSize(pageSize int) *int {
2229
return &pageSize
2330
}
31+
32+
func TestPageUtil_GetNextPageUri(t *testing.T) {
33+
payload := map[string]interface{}{
34+
"next_page_uri": "/2010-04-01/Accounts/ACXX/IncomingPhoneNumbers.json?PageSize=50&Page=1",
35+
"page_size": 50,
36+
}
37+
baseUrl := "https://api.twilio.com/"
38+
nextPageUrl := getNextPageUrl(baseUrl, payload)
39+
assert.Equal(t, "https://api.twilio.com/2010-04-01/Accounts/ACXX/IncomingPhoneNumbers.json?PageSize=50&Page=1", nextPageUrl)
40+
41+
payload["next_page_uri"] = "2010-04-01/Accounts/ACXX/IncomingPhoneNumbers.json?PageSize=50&Page=1"
42+
baseUrl = "https://api.twilio.com"
43+
nextPageUrl = getNextPageUrl(baseUrl, payload)
44+
assert.Equal(t, "https://api.twilio.com/2010-04-01/Accounts/ACXX/IncomingPhoneNumbers.json?PageSize=50&Page=1", nextPageUrl)
45+
46+
payload = map[string]interface{}{}
47+
nextPageUrl = getNextPageUrl(baseUrl, payload)
48+
assert.Equal(t, "", nextPageUrl)
49+
}
50+
51+
func TestPageUtil_GetNextPageUrl(t *testing.T) {
52+
payload := map[string]interface{}{
53+
"meta": map[string]interface{}{
54+
"next_page_url": "https://api.twilio.com/2010-04-01/Accounts/ACXX/IncomingPhoneNumbers.json?PageSize=50&Page=1",
55+
"page_size": 50,
56+
},
57+
}
58+
59+
nextPageUrl := getNextPageUrl("https://apitest.twilio.com", payload)
60+
assert.Equal(t, "https://api.twilio.com/2010-04-01/Accounts/ACXX/IncomingPhoneNumbers.json?PageSize=50&Page=1", nextPageUrl)
61+
}
62+
63+
func getTestClient(t *testing.T) *MockBaseClient {
64+
mockCtrl := gomock.NewController(t)
65+
testClient := NewMockBaseClient(mockCtrl)
66+
testClient.EXPECT().AccountSid().DoAndReturn(func() string {
67+
return "AC222222222222222222222222222222"
68+
}).AnyTimes()
69+
70+
testClient.EXPECT().SendRequest(
71+
gomock.Any(),
72+
gomock.Any(),
73+
gomock.Any(),
74+
gomock.Any()).
75+
DoAndReturn(func(method string, rawURL string, data url.Values,
76+
headers map[string]interface{}) (*http.Response, error) {
77+
response := map[string]interface{}{
78+
"end": 4,
79+
"first_page_uri": "/2010-04-01/Accounts/ACXX/Messages.json?From=9999999999&PageNumber=&To=4444444444&PageSize=2&Page=0",
80+
"messages": []map[string]interface{}{
81+
{
82+
"direction": "outbound-api",
83+
"from": "4444444444",
84+
"to": "9999999999",
85+
"body": "Message 0",
86+
"status": "delivered",
87+
},
88+
{
89+
"direction": "outbound-api",
90+
"from": "4444444444",
91+
"to": "9999999999",
92+
"body": "Message 1",
93+
"status": "delivered",
94+
},
95+
},
96+
"uri": "/2010-04-01/Accounts/ACXX/Messages.json?From=9999999999&PageNumber=&To=4444444444&PageSize=2&Page=0&PageToken=dummy",
97+
"page_size": 5,
98+
"start": 0,
99+
"next_page_uri": "/2010-04-01/Accounts/ACXX/Messages.json?From=9999999999&PageNumber=&To=4444444444&PageSize=2&Page=1&PageToken=PASMXX",
100+
"page": 0,
101+
}
102+
103+
resp, _ := json.Marshal(response)
104+
105+
return &http.Response{
106+
Body: ioutil.NopCloser(bytes.NewReader(resp)),
107+
}, nil
108+
},
109+
)
110+
111+
return testClient
112+
}
113+
114+
type testResponse struct {
115+
End int `json:"end,omitempty"`
116+
FirstPageUri string `json:"first_page_uri,omitempty"`
117+
Messages []testMessage `json:"messages,omitempty"`
118+
NextPageUri string `json:"next_page_uri,omitempty"`
119+
Page int `json:"page,omitempty"`
120+
PageSize int `json:"page_size,omitempty"`
121+
PreviousPageUri string `json:"previous_page_uri,omitempty"`
122+
Start int `json:"start,omitempty"`
123+
Uri string `json:"uri,omitempty"`
124+
}
125+
126+
type testMessage struct {
127+
// The message text
128+
Body *string `json:"body,omitempty"`
129+
// The direction of the message
130+
Direction *string `json:"direction,omitempty"`
131+
// The phone number that initiated the message
132+
From *string `json:"from,omitempty"`
133+
// The status of the message
134+
Status *string `json:"status,omitempty"`
135+
// The phone number that received the message
136+
To *string `json:"to,omitempty"`
137+
}
138+
139+
func getSomething(nextPageUrl string) (interface{}, error) {
140+
return nextPageUrl, nil
141+
}
142+
143+
func TestPageUtil_GetNext(t *testing.T) {
144+
testClient := getTestClient(t)
145+
baseUrl := "https://api.twilio.com"
146+
response, _ := testClient.SendRequest("get", "", nil, nil) //nolint:bodyclose
147+
ps := &testResponse{}
148+
_ = json.NewDecoder(response.Body).Decode(ps)
149+
150+
curRecord := 0
151+
limit := 10
152+
153+
nextPageUrl, err := GetNext(baseUrl, ps, &curRecord, &limit, getSomething)
154+
assert.Equal(t, "https://api.twilio.com/2010-04-01/Accounts/ACXX/Messages.json?From=9999999999&PageNumber=&To=4444444444&PageSize=2&Page=1&PageToken=PASMXX", nextPageUrl)
155+
assert.Nil(t, err)
156+
157+
curRecord = 15
158+
nextPageUrl, err = GetNext(baseUrl, ps, &curRecord, &limit, getSomething)
159+
assert.Empty(t, nextPageUrl)
160+
assert.Nil(t, err)
161+
}
162+
163+
func TestPageUtil_GetNextWithErr(t *testing.T) {
164+
nextPageUrl, err := GetNext("baseUrl", nil, nil, nil, getSomething)
165+
assert.Nil(t, nextPageUrl)
166+
assert.NotNil(t, err)
167+
}
168+
169+
func TestPageUtil_ToMap(t *testing.T) {
170+
testMap := toMap("invalid")
171+
assert.Nil(t, testMap)
172+
173+
valid := testResponse{
174+
End: 0,
175+
FirstPageUri: "first",
176+
Messages: nil,
177+
NextPageUri: "next",
178+
Page: 0,
179+
PageSize: 0,
180+
PreviousPageUri: "previous",
181+
Start: 0,
182+
Uri: "uri",
183+
}
184+
testMap = toMap(valid)
185+
assert.NotNil(t, testMap)
186+
}
187+
188+
func TestPageUtil_GetNextPageAddress(t *testing.T) {
189+
nextPageAddress, _ := getNextPageAddress("baseUrl", nil, nil, nil)
190+
assert.Empty(t, nextPageAddress)
191+
}
192+
193+
func TestPageUtil_GetPayload(t *testing.T) {
194+
response := map[string]interface{}{
195+
"end": 4,
196+
"first_page_uri": "/2010-04-01/Accounts/ACXX/Messages.json?From=9999999999&PageNumber=&To=4444444444&PageSize=2&Page=0",
197+
"messages": []map[string]interface{}{
198+
{
199+
"direction": "outbound-api",
200+
"from": "4444444444",
201+
"to": "9999999999",
202+
"body": "Message 0",
203+
"status": "delivered",
204+
},
205+
},
206+
"messages2": []map[string]interface{}{
207+
{
208+
"direction": "outbound-api",
209+
"from": "4444444444",
210+
"to": "9999999999",
211+
"body": "Message 0",
212+
"status": "delivered",
213+
},
214+
},
215+
}
216+
217+
payload, nextPageUrl, err := GetPayload("baseUrl", response)
218+
assert.Nil(t, payload)
219+
assert.Empty(t, nextPageUrl)
220+
assert.NotNil(t, err)
221+
assert.Equal(t, "payload contains more than 1 record of type array", err.Error())
222+
}

0 commit comments

Comments
 (0)