1- """Tests for HTTP 451 (DMCA takedown) handling."""
1+ """Tests for HTTP 451 (DMCA takedown) and HTTP 403 (TOS) handling."""
22
3+ import io
34import json
4- from unittest .mock import Mock , patch
5+ from unittest .mock import patch
6+ from urllib .error import HTTPError
57
68import pytest
79
810from github_backup import github_backup
911
1012
13+ def _make_http_error (code , body_bytes , msg = "Error" , headers = None ):
14+ """Create an HTTPError with a readable body (like a real urllib response)."""
15+ if headers is None :
16+ headers = {"x-ratelimit-remaining" : "5000" }
17+ return HTTPError (
18+ url = "https://api.github.com/repos/test/repo" ,
19+ code = code ,
20+ msg = msg ,
21+ hdrs = headers ,
22+ fp = io .BytesIO (body_bytes ),
23+ )
24+
25+
1126class TestHTTP451Exception :
1227 """Test suite for HTTP 451 DMCA takedown exception handling."""
1328
1429 def test_repository_unavailable_error_raised (self , create_args ):
1530 """HTTP 451 should raise RepositoryUnavailableError with DMCA URL."""
1631 args = create_args ()
1732
18- mock_response = Mock ()
19- mock_response .getcode .return_value = 451
20-
2133 dmca_data = {
2234 "message" : "Repository access blocked" ,
2335 "block" : {
@@ -26,66 +38,151 @@ def test_repository_unavailable_error_raised(self, create_args):
2638 "html_url" : "https://github.com/github/dmca/blob/master/2024/11/2024-11-04-source-code.md" ,
2739 },
2840 }
29- mock_response .read .return_value = json .dumps (dmca_data ).encode ("utf-8" )
30- mock_response .headers = {"x-ratelimit-remaining" : "5000" }
31- mock_response .reason = "Unavailable For Legal Reasons"
32-
33- with patch (
34- "github_backup.github_backup.make_request_with_retry" ,
35- return_value = mock_response ,
36- ):
41+ body = json .dumps (dmca_data ).encode ("utf-8" )
42+
43+ def mock_urlopen (* a , ** kw ):
44+ raise _make_http_error (451 , body , msg = "Unavailable For Legal Reasons" )
45+
46+ with patch ("github_backup.github_backup.urlopen" , side_effect = mock_urlopen ):
3747 with pytest .raises (github_backup .RepositoryUnavailableError ) as exc_info :
3848 github_backup .retrieve_data (
3949 args , "https://api.github.com/repos/test/dmca/issues"
4050 )
4151
4252 assert (
43- exc_info .value .dmca_url
53+ exc_info .value .legal_url
4454 == "https://github.com/github/dmca/blob/master/2024/11/2024-11-04-source-code.md"
4555 )
4656 assert "451" in str (exc_info .value )
4757
48- def test_repository_unavailable_error_without_dmca_url (self , create_args ):
58+ def test_repository_unavailable_error_without_legal_url (self , create_args ):
4959 """HTTP 451 without DMCA details should still raise exception."""
5060 args = create_args ()
5161
52- mock_response = Mock ()
53- mock_response .getcode .return_value = 451
54- mock_response .read .return_value = b'{"message": "Blocked"}'
55- mock_response .headers = {"x-ratelimit-remaining" : "5000" }
56- mock_response .reason = "Unavailable For Legal Reasons"
62+ def mock_urlopen (* a , ** kw ):
63+ raise _make_http_error (451 , b'{"message": "Blocked"}' )
5764
58- with patch (
59- "github_backup.github_backup.make_request_with_retry" ,
60- return_value = mock_response ,
61- ):
65+ with patch ("github_backup.github_backup.urlopen" , side_effect = mock_urlopen ):
6266 with pytest .raises (github_backup .RepositoryUnavailableError ) as exc_info :
6367 github_backup .retrieve_data (
6468 args , "https://api.github.com/repos/test/dmca/issues"
6569 )
6670
67- assert exc_info .value .dmca_url is None
71+ assert exc_info .value .legal_url is None
6872 assert "451" in str (exc_info .value )
6973
7074 def test_repository_unavailable_error_with_malformed_json (self , create_args ):
7175 """HTTP 451 with malformed JSON should still raise exception."""
7276 args = create_args ()
7377
74- mock_response = Mock ()
75- mock_response .getcode .return_value = 451
76- mock_response .read .return_value = b"invalid json {"
77- mock_response .headers = {"x-ratelimit-remaining" : "5000" }
78- mock_response .reason = "Unavailable For Legal Reasons"
78+ def mock_urlopen (* a , ** kw ):
79+ raise _make_http_error (451 , b"invalid json {" )
7980
80- with patch (
81- "github_backup.github_backup.make_request_with_retry" ,
82- return_value = mock_response ,
83- ):
81+ with patch ("github_backup.github_backup.urlopen" , side_effect = mock_urlopen ):
8482 with pytest .raises (github_backup .RepositoryUnavailableError ):
8583 github_backup .retrieve_data (
8684 args , "https://api.github.com/repos/test/dmca/issues"
8785 )
8886
8987
88+ class TestHTTP403TOS :
89+ """Test suite for HTTP 403 TOS violation handling."""
90+
91+ def test_403_tos_raises_repository_unavailable (self , create_args ):
92+ """HTTP 403 (non-rate-limit) should raise RepositoryUnavailableError."""
93+ args = create_args ()
94+
95+ tos_data = {
96+ "message" : "Repository access blocked" ,
97+ "block" : {
98+ "reason" : "tos" ,
99+ "html_url" : "https://github.com/contact/tos-violation" ,
100+ },
101+ }
102+ body = json .dumps (tos_data ).encode ("utf-8" )
103+
104+ def mock_urlopen (* a , ** kw ):
105+ raise _make_http_error (
106+ 403 , body , msg = "Forbidden" ,
107+ headers = {"x-ratelimit-remaining" : "5000" },
108+ )
109+
110+ with patch ("github_backup.github_backup.urlopen" , side_effect = mock_urlopen ):
111+ with pytest .raises (github_backup .RepositoryUnavailableError ) as exc_info :
112+ github_backup .retrieve_data (
113+ args , "https://api.github.com/repos/test/blocked/issues"
114+ )
115+
116+ assert exc_info .value .legal_url == "https://github.com/contact/tos-violation"
117+ assert "403" in str (exc_info .value )
118+
119+ def test_403_permission_denied_not_converted (self , create_args ):
120+ """HTTP 403 without 'block' in body should propagate as HTTPError, not RepositoryUnavailableError."""
121+ args = create_args ()
122+
123+ body = json .dumps ({"message" : "Must have admin rights to Repository." }).encode ("utf-8" )
124+
125+ def mock_urlopen (* a , ** kw ):
126+ raise _make_http_error (
127+ 403 , body , msg = "Forbidden" ,
128+ headers = {"x-ratelimit-remaining" : "5000" },
129+ )
130+
131+ with patch ("github_backup.github_backup.urlopen" , side_effect = mock_urlopen ):
132+ with pytest .raises (HTTPError ) as exc_info :
133+ github_backup .retrieve_data (
134+ args , "https://api.github.com/repos/test/private/issues"
135+ )
136+
137+ assert exc_info .value .code == 403
138+
139+ def test_403_rate_limit_not_converted (self , create_args ):
140+ """HTTP 403 with rate limit exhausted should NOT become RepositoryUnavailableError."""
141+ args = create_args ()
142+
143+ call_count = 0
144+
145+ def mock_urlopen (* a , ** kw ):
146+ nonlocal call_count
147+ call_count += 1
148+ raise _make_http_error (
149+ 403 , b'{"message": "rate limit"}' , msg = "Forbidden" ,
150+ headers = {"x-ratelimit-remaining" : "0" },
151+ )
152+
153+ with patch ("github_backup.github_backup.urlopen" , side_effect = mock_urlopen ):
154+ with patch (
155+ "github_backup.github_backup.calculate_retry_delay" , return_value = 0
156+ ):
157+ with pytest .raises (HTTPError ) as exc_info :
158+ github_backup .retrieve_data (
159+ args , "https://api.github.com/repos/test/ratelimit/issues"
160+ )
161+
162+ assert exc_info .value .code == 403
163+ # Should have retried (not raised immediately as RepositoryUnavailableError)
164+ assert call_count > 1
165+
166+
167+ class TestRetrieveRepositoriesUnavailable :
168+ """Test that retrieve_repositories handles RepositoryUnavailableError gracefully."""
169+
170+ def test_unavailable_repo_returns_empty_list (self , create_args ):
171+ """retrieve_repositories should return [] when the repo is unavailable."""
172+ args = create_args (repository = "blocked-repo" )
173+
174+ def mock_urlopen (* a , ** kw ):
175+ raise _make_http_error (
176+ 451 ,
177+ json .dumps ({"message" : "Blocked" , "block" : {"html_url" : "https://example.com/dmca" }}).encode ("utf-8" ),
178+ msg = "Unavailable For Legal Reasons" ,
179+ )
180+
181+ with patch ("github_backup.github_backup.urlopen" , side_effect = mock_urlopen ):
182+ repos = github_backup .retrieve_repositories (args , {"login" : None })
183+
184+ assert repos == []
185+
186+
90187if __name__ == "__main__" :
91188 pytest .main ([__file__ , "-v" ])
0 commit comments