main
py 252 lines 8.27 KB
Raw
1 from __future__ import annotations
2
3 import json
4 import sys
5 import time
6 from pathlib import Path
7
8 import pytest
9
10 sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
11
12 from plugins._oauth.helpers.providers import github_copilot as copilot
13 from plugins._oauth.helpers.providers.base import GITHUB_COPILOT_PROVIDER_ID
14 from plugins._oauth.helpers.state import pop_device_attempt
15
16
17 def test_normalize_enterprise_domain_defaults_blank_to_github_dot_com():
18 assert copilot.normalize_enterprise_domain("") == "github.com"
19
20
21 @pytest.mark.parametrize(
22 ("value", "expected"),
23 [
24 ("github.example.com", "github.example.com"),
25 ("https://github.example.com/org", "github.example.com"),
26 ],
27 )
28 def test_normalize_enterprise_domain_accepts_domain_or_url(value, expected):
29 assert copilot.normalize_enterprise_domain(value) == expected
30
31
32 def test_normalize_enterprise_domain_rejects_invalid_url():
33 with pytest.raises(ValueError, match="Invalid GitHub Enterprise"):
34 copilot.normalize_enterprise_domain("http://")
35
36
37 def test_copilot_base_url_from_token_uses_proxy_endpoint():
38 token = "tid=1;exp=9999999999;proxy-ep=proxy.individual.githubcopilot.com;sku=monthly"
39
40 assert copilot.copilot_base_url_from_token(token, "") == "https://api.individual.githubcopilot.com"
41
42
43 def test_copilot_base_url_from_token_falls_back_for_malicious_proxy_endpoint():
44 token = "tid=1;exp=9999999999;proxy-ep=evil.example.com;sku=monthly"
45
46 assert copilot.copilot_base_url_from_token(token, "") == "https://api.individual.githubcopilot.com"
47
48
49 def test_poll_pending_and_slow_down_do_not_complete(monkeypatch):
50 provider = copilot.GitHubCopilotOAuthProvider()
51 pending = provider._store_device_attempt_for_test("github.com", "device-pending", "PENDING", 5)
52 slowed = provider._store_device_attempt_for_test("github.com", "device-slow", "SLOW", 5)
53
54 def fake_poll(domain, device_code):
55 if device_code == "device-slow":
56 return {"error": "slow_down"}
57 return {"error": "authorization_pending"}
58
59 monkeypatch.setattr(copilot, "_post_device_poll", fake_poll)
60
61 try:
62 pending_result = provider.poll_login({"attempt_id": pending.attempt_id})
63 slowed_result = provider.poll_login({"attempt_id": slowed.attempt_id})
64
65 assert pending_result.ok is True
66 assert pending_result.completed is False
67 assert pending_result.interval == 5
68 assert slowed_result.ok is True
69 assert slowed_result.completed is False
70 assert slowed_result.interval == 10
71 finally:
72 pop_device_attempt(pending.attempt_id)
73 pop_device_attempt(slowed.attempt_id)
74
75
76 def test_poll_success_stores_copilot_credentials(tmp_path, monkeypatch):
77 provider = copilot.GitHubCopilotOAuthProvider()
78 auth_path = tmp_path / "auth.json"
79 attempt = provider._store_device_attempt_for_test("github.com", "device-ok", "OK", 5)
80
81 monkeypatch.setattr(provider, "auth_path", lambda: auth_path)
82 monkeypatch.setattr(
83 copilot,
84 "_post_device_poll",
85 lambda domain, device_code: {"access_token": "github-access-token"},
86 )
87 monkeypatch.setattr(
88 copilot,
89 "refresh_copilot_token",
90 lambda refresh, domain: {
91 "provider": GITHUB_COPILOT_PROVIDER_ID,
92 "type": "oauth",
93 "refresh": refresh,
94 "access": "copilot-access-token",
95 "expires": 9_999_999_000,
96 "enterprise_domain": "",
97 "base_url": "https://api.individual.githubcopilot.com",
98 },
99 )
100 monkeypatch.setattr(
101 copilot,
102 "enable_known_models",
103 lambda token, domain: {"attempted": 8, "enabled": 8, "failed": []},
104 )
105
106 result = provider.poll_login({"attempt_id": attempt.attempt_id})
107
108 assert result.ok is True
109 assert result.completed is True
110 saved = json.loads(auth_path.read_text(encoding="utf-8"))
111 assert saved["provider"] == GITHUB_COPILOT_PROVIDER_ID
112 assert saved["refresh"] == "github-access-token"
113 assert saved["access"] == "copilot-access-token"
114 assert saved["base_url"] == "https://api.individual.githubcopilot.com"
115
116
117 def test_models_returns_curated_list_without_network(monkeypatch):
118 provider = copilot.GitHubCopilotOAuthProvider()
119 monkeypatch.setattr(provider, "read_auth", lambda: {})
120
121 models = provider.models()
122
123 assert models[:3] == ["gpt-5.2", "claude-sonnet-4.5", "claude-opus-4.5"]
124 assert "grok-code-fast-1" in models
125
126
127 def test_ensure_fresh_auth_refreshes_expired_credentials(tmp_path, monkeypatch):
128 provider = copilot.GitHubCopilotOAuthProvider()
129 auth_path = tmp_path / "auth.json"
130 provider.write_auth = lambda data: copilot.write_private_json(auth_path, data)
131 provider.read_auth = lambda: copilot.read_json_file(auth_path)
132 provider.write_auth(
133 {
134 "provider": GITHUB_COPILOT_PROVIDER_ID,
135 "type": "oauth",
136 "refresh": "github-refresh-token",
137 "access": "expired-access-token",
138 "expires": 1,
139 "enterprise_domain": "",
140 "base_url": "https://api.individual.githubcopilot.com",
141 "models_warning": "existing warning",
142 }
143 )
144
145 monkeypatch.setattr(
146 copilot,
147 "refresh_copilot_token",
148 lambda refresh, domain: {
149 "provider": GITHUB_COPILOT_PROVIDER_ID,
150 "type": "oauth",
151 "refresh": refresh,
152 "access": "fresh-access-token",
153 "expires": int(time.time() * 1000) + 3_600_000,
154 "enterprise_domain": "",
155 "base_url": "https://api.individual.githubcopilot.com",
156 },
157 )
158
159 auth = provider.ensure_fresh_auth()
160
161 assert auth["refresh"] == "github-refresh-token"
162 assert auth["access"] == "fresh-access-token"
163 assert auth["models_warning"] == "existing warning"
164 saved = json.loads(auth_path.read_text(encoding="utf-8"))
165 assert saved["access"] == "fresh-access-token"
166
167
168 def test_models_uses_refreshed_auth_without_live_network(monkeypatch):
169 provider = copilot.GitHubCopilotOAuthProvider()
170 calls = []
171
172 monkeypatch.setattr(
173 provider,
174 "ensure_fresh_auth",
175 lambda: {
176 "access": "fresh-access-token",
177 "base_url": "https://api.individual.githubcopilot.com",
178 },
179 )
180
181 class FakeResponse:
182 ok = True
183
184 def json(self):
185 return {"data": [{"id": "fresh-model"}]}
186
187 class FakeRequests:
188 @staticmethod
189 def get(url, headers, timeout):
190 calls.append((url, headers, timeout))
191 return FakeResponse()
192
193 monkeypatch.setitem(sys.modules, "requests", FakeRequests)
194
195 assert provider.models() == ["fresh-model"]
196 assert calls[0][1]["Authorization"] == "Bearer fresh-access-token"
197
198
199 def test_models_does_not_send_bearer_token_to_malicious_base_url(monkeypatch):
200 provider = copilot.GitHubCopilotOAuthProvider()
201 calls = []
202
203 monkeypatch.setattr(
204 provider,
205 "ensure_fresh_auth",
206 lambda: {
207 "access": "fresh-access-token",
208 "base_url": "https://evil.example.com/v1",
209 },
210 )
211
212 class FakeResponse:
213 ok = True
214
215 def json(self):
216 return {"data": [{"id": "safe-model"}]}
217
218 class FakeRequests:
219 @staticmethod
220 def get(url, headers, timeout):
221 calls.append((url, headers, timeout))
222 return FakeResponse()
223
224 monkeypatch.setitem(sys.modules, "requests", FakeRequests)
225
226 assert provider.models() == ["safe-model"]
227 assert calls[0][0] == "https://api.individual.githubcopilot.com/models"
228 assert not calls[0][0].startswith("https://evil.example.com")
229 assert calls[0][1]["Authorization"] == "Bearer fresh-access-token"
230
231
232 def test_refresh_copilot_token_normalizes_malicious_proxy_endpoint(monkeypatch):
233 class FakeResponse:
234 ok = True
235 status_code = 200
236
237 def json(self):
238 return {
239 "token": "tid=1;exp=9999999999;proxy-ep=evil.example.com;sku=monthly",
240 "expires_at": int(time.time()) + 3600,
241 }
242
243 class FakeRequests:
244 @staticmethod
245 def get(url, headers, timeout):
246 return FakeResponse()
247
248 monkeypatch.setitem(sys.modules, "requests", FakeRequests)
249
250 auth = copilot.refresh_copilot_token("github-refresh-token", "")
251
252 assert auth["base_url"] == "https://api.individual.githubcopilot.com"