Skip to content

Commit 42bfe6f

Browse files
authored
Merge pull request josegonzalez#450 from Iamrodos/test/add-pagination-tests
test: Add pagination tests for cursor and page-based Link headers
2 parents 6dfba7a + 5af522a commit 42bfe6f

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed

tests/test_pagination.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Tests for Link header pagination handling."""
2+
3+
import json
4+
from unittest.mock import Mock, patch
5+
6+
import pytest
7+
8+
from github_backup import github_backup
9+
10+
11+
class MockHTTPResponse:
12+
"""Mock HTTP response for paginated API calls."""
13+
14+
def __init__(self, data, link_header=None):
15+
self._content = json.dumps(data).encode("utf-8")
16+
self._link_header = link_header
17+
self._read = False
18+
self.reason = "OK"
19+
20+
def getcode(self):
21+
return 200
22+
23+
def read(self):
24+
if self._read:
25+
return b""
26+
self._read = True
27+
return self._content
28+
29+
def get_header(self, name, default=None):
30+
"""Mock method for headers.get()."""
31+
return self.headers.get(name, default)
32+
33+
@property
34+
def headers(self):
35+
headers = {"x-ratelimit-remaining": "5000"}
36+
if self._link_header:
37+
headers["Link"] = self._link_header
38+
return headers
39+
40+
41+
@pytest.fixture
42+
def mock_args():
43+
"""Mock args for retrieve_data_gen."""
44+
args = Mock()
45+
args.as_app = False
46+
args.token_fine = None
47+
args.token_classic = "fake_token"
48+
args.username = None
49+
args.password = None
50+
args.osx_keychain_item_name = None
51+
args.osx_keychain_item_account = None
52+
args.throttle_limit = None
53+
args.throttle_pause = 0
54+
return args
55+
56+
57+
def test_cursor_based_pagination(mock_args):
58+
"""Link header with 'after' cursor parameter works correctly."""
59+
60+
# Simulate issues endpoint behavior: returns cursor in Link header
61+
responses = [
62+
# Issues endpoint returns 'after' cursor parameter (not 'page')
63+
MockHTTPResponse(
64+
data=[{"issue": i} for i in range(1, 101)], # Page 1 contents
65+
link_header='<https://api.github.com/repos/owner/repo/issues?per_page=100&after=ABC123&page=2>; rel="next"',
66+
),
67+
MockHTTPResponse(
68+
data=[{"issue": i} for i in range(101, 151)], # Page 2 contents
69+
link_header=None, # No Link header - signals end of pagination
70+
),
71+
]
72+
requests_made = []
73+
74+
def mock_urlopen(request, *args, **kwargs):
75+
url = request.get_full_url()
76+
requests_made.append(url)
77+
return responses[len(requests_made) - 1]
78+
79+
with patch("github_backup.github_backup.urlopen", side_effect=mock_urlopen):
80+
results = list(
81+
github_backup.retrieve_data_gen(
82+
mock_args, "https://api.github.com/repos/owner/repo/issues"
83+
)
84+
)
85+
86+
# Verify all items retrieved and cursor was used in second request
87+
assert len(results) == 150
88+
assert len(requests_made) == 2
89+
assert "after=ABC123" in requests_made[1]
90+
91+
92+
def test_page_based_pagination(mock_args):
93+
"""Link header with 'page' parameter works correctly."""
94+
95+
# Simulate pulls/repos endpoint behavior: returns page numbers in Link header
96+
responses = [
97+
# Pulls endpoint uses traditional 'page' parameter (not cursor)
98+
MockHTTPResponse(
99+
data=[{"pull": i} for i in range(1, 101)], # Page 1 contents
100+
link_header='<https://api.github.com/repos/owner/repo/pulls?per_page=100&page=2>; rel="next"',
101+
),
102+
MockHTTPResponse(
103+
data=[{"pull": i} for i in range(101, 181)], # Page 2 contents
104+
link_header=None, # No Link header - signals end of pagination
105+
),
106+
]
107+
requests_made = []
108+
109+
def mock_urlopen(request, *args, **kwargs):
110+
url = request.get_full_url()
111+
requests_made.append(url)
112+
return responses[len(requests_made) - 1]
113+
114+
with patch("github_backup.github_backup.urlopen", side_effect=mock_urlopen):
115+
results = list(
116+
github_backup.retrieve_data_gen(
117+
mock_args, "https://api.github.com/repos/owner/repo/pulls"
118+
)
119+
)
120+
121+
# Verify all items retrieved and page parameter was used (not cursor)
122+
assert len(results) == 180
123+
assert len(requests_made) == 2
124+
assert "page=2" in requests_made[1]
125+
assert "after" not in requests_made[1]
126+
127+
128+
def test_no_link_header_stops_pagination(mock_args):
129+
"""Pagination stops when Link header is absent."""
130+
131+
# Simulate endpoint with results that fit in a single page
132+
responses = [
133+
MockHTTPResponse(
134+
data=[{"label": i} for i in range(1, 51)], # Page contents
135+
link_header=None, # No Link header - signals end of pagination
136+
)
137+
]
138+
requests_made = []
139+
140+
def mock_urlopen(request, *args, **kwargs):
141+
requests_made.append(request.get_full_url())
142+
return responses[len(requests_made) - 1]
143+
144+
with patch("github_backup.github_backup.urlopen", side_effect=mock_urlopen):
145+
results = list(
146+
github_backup.retrieve_data_gen(
147+
mock_args, "https://api.github.com/repos/owner/repo/labels"
148+
)
149+
)
150+
151+
# Verify pagination stopped after first request
152+
assert len(results) == 50
153+
assert len(requests_made) == 1

0 commit comments

Comments
 (0)