forked from github/spec-kit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_github_http.py
More file actions
191 lines (155 loc) 路 7.78 KB
/
Copy pathtest_github_http.py
File metadata and controls
191 lines (155 loc) 路 7.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""Tests for GitHub-authenticated HTTP request helpers."""
import io
import json
import os
from contextlib import contextmanager
from unittest.mock import MagicMock, patch
import pytest
from specify_cli._github_http import (
build_github_request,
resolve_github_release_asset_api_url,
)
class TestBuildGitHubRequest:
"""Tests for build_github_request() URL validation and auth handling."""
# --- URL Validation Tests ---
def test_empty_url_raises_value_error(self):
"""build_github_request() must reject an empty string URL."""
with pytest.raises(ValueError, match="url must not be empty"):
build_github_request("")
def test_whitespace_url_raises_value_error(self):
"""build_github_request() must reject a whitespace-only URL."""
with pytest.raises(ValueError, match="url must not be empty"):
build_github_request(" ")
def test_non_http_url_raises_value_error(self):
"""build_github_request() must reject URLs without http/https scheme."""
with pytest.raises(ValueError, match="url must start with http"):
build_github_request("not-a-url")
def test_ftp_url_raises_value_error(self):
"""build_github_request() must reject ftp:// URLs."""
with pytest.raises(ValueError, match="url must start with http"):
build_github_request("ftp://github.com/file.zip")
# --- Valid URL Tests ---
def test_valid_https_url_returns_request(self):
"""build_github_request() must return a Request for a valid https URL."""
req = build_github_request("https://github.com/github/spec-kit")
assert req.full_url == "https://github.com/github/spec-kit"
def test_valid_http_url_returns_request(self):
"""build_github_request() must accept http:// URLs."""
req = build_github_request("http://example.com/file")
assert req.full_url == "http://example.com/file"
# --- Auth Header Tests ---
def test_github_token_added_for_github_host(self):
"""Authorization header is set when GITHUB_TOKEN is present."""
with patch.dict(os.environ, {"GITHUB_TOKEN": "test-token", "GH_TOKEN": ""}):
req = build_github_request("https://github.com/github/spec-kit")
assert req.get_header("Authorization") == "Bearer test-token"
def test_gh_token_used_as_fallback(self):
"""GH_TOKEN is used when GITHUB_TOKEN is absent."""
with patch.dict(os.environ, {"GITHUB_TOKEN": "", "GH_TOKEN": "fallback-token"}):
req = build_github_request("https://github.com/github/spec-kit")
assert req.get_header("Authorization") == "Bearer fallback-token"
def test_no_auth_header_for_non_github_host(self):
"""Authorization header must NOT be set for non-GitHub URLs."""
with patch.dict(os.environ, {"GITHUB_TOKEN": "test-token"}):
req = build_github_request("https://example.com/file")
assert req.get_header("Authorization") is None
def test_no_auth_header_when_no_token(self):
"""No Authorization header when no token is set in environment."""
with patch.dict(os.environ, {}, clear=True):
req = build_github_request("https://github.com/github/spec-kit")
assert req.get_header("Authorization") is None
def test_missing_hostname_raises_value_error(self):
"""build_github_request() must reject URLs with valid scheme but no hostname."""
with pytest.raises(ValueError, match="url must include a hostname"):
build_github_request("http://")
class TestResolveGitHubReleaseAssetApiUrl:
"""Tests for resolve_github_release_asset_api_url()."""
def _make_open_url_fn(self, release_json):
"""Create a fake open_url_fn that returns release JSON."""
@contextmanager
def fake_open(url, timeout=None, extra_headers=None):
resp = MagicMock()
resp.read.return_value = json.dumps(release_json).encode()
yield resp
return fake_open
def test_returns_none_for_non_github_url(self):
"""Non-GitHub URLs should return None."""
result = resolve_github_release_asset_api_url(
"https://example.com/file.zip", lambda *a, **kw: None
)
assert result is None
def test_returns_none_for_non_release_github_url(self):
"""GitHub URLs that aren't release downloads return None."""
result = resolve_github_release_asset_api_url(
"https://github.com/org/repo/archive/refs/tags/v1.zip",
lambda *a, **kw: None,
)
assert result is None
def test_passthrough_for_existing_api_asset_url(self):
"""Already-resolved REST API asset URLs are returned as-is."""
url = "https://api.github.com/repos/org/repo/releases/assets/12345"
result = resolve_github_release_asset_api_url(url, lambda *a, **kw: None)
assert result == url
def test_resolves_browser_url_to_api_url(self):
"""Browser release URL resolves to REST API asset URL."""
release_json = {
"assets": [
{"name": "pack.zip", "url": "https://api.github.com/repos/org/repo/releases/assets/99"}
]
}
result = resolve_github_release_asset_api_url(
"https://github.com/org/repo/releases/download/v1.0/pack.zip",
self._make_open_url_fn(release_json),
)
assert result == "https://api.github.com/repos/org/repo/releases/assets/99"
def test_returns_none_when_asset_not_found(self):
"""Returns None when the release exists but asset name doesn't match."""
release_json = {"assets": [{"name": "other.zip", "url": "https://api.github.com/repos/org/repo/releases/assets/1"}]}
result = resolve_github_release_asset_api_url(
"https://github.com/org/repo/releases/download/v1/missing.zip",
self._make_open_url_fn(release_json),
)
assert result is None
def test_returns_none_on_network_error(self):
"""Returns None when the API request fails."""
import urllib.error
@contextmanager
def failing_open(url, timeout=None, extra_headers=None):
raise urllib.error.URLError("network error")
yield # noqa: unreachable
result = resolve_github_release_asset_api_url(
"https://github.com/org/repo/releases/download/v1/pack.zip",
failing_open,
)
assert result is None
def test_tag_with_special_characters_is_url_encoded(self):
"""Tags with reserved characters (e.g. '/') are encoded in the API URL."""
captured_urls = []
@contextmanager
def capturing_open(url, timeout=None, extra_headers=None):
captured_urls.append(url)
resp = MagicMock()
resp.read.return_value = json.dumps({"assets": []}).encode()
yield resp
resolve_github_release_asset_api_url(
"https://github.com/org/repo/releases/download/feature%2Fv1/pack.zip",
capturing_open,
)
# The tag "feature/v1" (decoded from %2F) must be re-encoded as "feature%2Fv1"
assert len(captured_urls) == 1
assert "releases/tags/feature%2Fv1" in captured_urls[0]
def test_tag_with_hash_is_url_encoded(self):
"""Tags with '#' character are properly encoded."""
captured_urls = []
@contextmanager
def capturing_open(url, timeout=None, extra_headers=None):
captured_urls.append(url)
resp = MagicMock()
resp.read.return_value = json.dumps({"assets": []}).encode()
yield resp
resolve_github_release_asset_api_url(
"https://github.com/org/repo/releases/download/v1%23beta/pack.zip",
capturing_open,
)
assert len(captured_urls) == 1
assert "releases/tags/v1%23beta" in captured_urls[0]