forked from yusufkaraaslan/Skill_Seekers
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_rate_limit_handler.py
More file actions
300 lines (236 loc) · 10.6 KB
/
test_rate_limit_handler.py
File metadata and controls
300 lines (236 loc) · 10.6 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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
"""
Tests for Rate Limit Handler
Tests the smart rate limit detection and handling system.
"""
from datetime import datetime, timedelta
from unittest.mock import Mock, patch
import pytest
from skill_seekers.cli.config_manager import ConfigManager
from skill_seekers.cli.rate_limit_handler import (
RateLimitError,
RateLimitHandler,
create_github_headers,
)
class TestRateLimitHandler:
"""Test RateLimitHandler functionality."""
def test_create_headers_no_token(self):
"""Test header creation without token."""
headers = create_github_headers(None)
assert headers == {}
def test_create_headers_with_token(self):
"""Test header creation with token."""
token = "ghp_test123"
headers = create_github_headers(token)
assert headers == {"Authorization": "token ghp_test123"}
def test_init_without_token(self):
"""Test initialization without token."""
handler = RateLimitHandler(token=None, interactive=True)
assert handler.token is None
assert handler.interactive is True
assert handler.strategy == "prompt"
def test_init_with_token(self):
"""Test initialization with token."""
handler = RateLimitHandler(token="ghp_test", interactive=False)
assert handler.token == "ghp_test"
assert handler.interactive is False
@patch("skill_seekers.cli.rate_limit_handler.get_config_manager")
def test_init_with_config_strategy(self, mock_get_config):
"""Test initialization pulls strategy from config."""
mock_config = Mock()
mock_config.config = {
"rate_limit": {
"auto_switch_profiles": True,
"show_countdown": True,
"default_timeout_minutes": 30,
}
}
mock_config.get_rate_limit_strategy.return_value = "wait"
mock_config.get_timeout_minutes.return_value = 45
mock_get_config.return_value = mock_config
handler = RateLimitHandler(token="ghp_test", interactive=True)
assert handler.strategy == "wait"
assert handler.timeout_minutes == 45
def test_extract_rate_limit_info(self):
"""Test extracting rate limit info from response headers."""
handler = RateLimitHandler()
# Create mock response
mock_response = Mock()
reset_time = int((datetime.now() + timedelta(minutes=30)).timestamp())
mock_response.headers = {
"X-RateLimit-Limit": "5000",
"X-RateLimit-Remaining": "100",
"X-RateLimit-Reset": str(reset_time),
}
info = handler.extract_rate_limit_info(mock_response)
assert info["limit"] == 5000
assert info["remaining"] == 100
assert info["reset_timestamp"] == reset_time
assert isinstance(info["reset_time"], datetime)
@patch("builtins.input", return_value="n")
def test_check_upfront_no_token_declined(self, mock_input):
"""Test upfront check with no token, user declines."""
handler = RateLimitHandler(token=None, interactive=True)
result = handler.check_upfront()
assert result is False
mock_input.assert_called_once()
@patch("builtins.input", return_value="y")
def test_check_upfront_no_token_accepted(self, mock_input):
"""Test upfront check with no token, user accepts."""
handler = RateLimitHandler(token=None, interactive=True)
result = handler.check_upfront()
assert result is True
mock_input.assert_called_once()
def test_check_upfront_no_token_non_interactive(self):
"""Test upfront check with no token in non-interactive mode."""
handler = RateLimitHandler(token=None, interactive=False)
result = handler.check_upfront()
# Should proceed without prompting
assert result is True
@patch("requests.get")
@patch("skill_seekers.cli.rate_limit_handler.get_config_manager")
def test_check_upfront_with_token_good_status(self, mock_get_config, mock_get):
"""Test upfront check with token and good rate limit status."""
# Mock config
mock_config = Mock()
mock_config.config = {
"rate_limit": {
"auto_switch_profiles": False,
"show_countdown": True,
"default_timeout_minutes": 30,
}
}
mock_config.get_rate_limit_strategy.return_value = "prompt"
mock_config.get_timeout_minutes.return_value = 30
mock_get_config.return_value = mock_config
# Mock rate limit check
reset_time = int((datetime.now() + timedelta(minutes=60)).timestamp())
mock_response = Mock()
mock_response.json.return_value = {
"rate": {"limit": 5000, "remaining": 4500, "reset": reset_time}
}
mock_response.raise_for_status = Mock()
mock_get.return_value = mock_response
handler = RateLimitHandler(token="ghp_test", interactive=True)
result = handler.check_upfront()
assert result is True
def test_check_response_not_rate_limited(self):
"""Test check_response with normal 200 response."""
handler = RateLimitHandler(interactive=True)
mock_response = Mock()
mock_response.status_code = 200
result = handler.check_response(mock_response)
assert result is True
def test_check_response_other_403(self):
"""Test check_response with 403 but not rate limit."""
handler = RateLimitHandler(interactive=True)
mock_response = Mock()
mock_response.status_code = 403
mock_response.json.return_value = {"message": "Forbidden - not rate limit"}
result = handler.check_response(mock_response)
assert result is True
@patch("skill_seekers.cli.rate_limit_handler.get_config_manager")
def test_non_interactive_fail_strategy(self, mock_get_config):
"""Test non-interactive mode with fail strategy raises error."""
mock_config = Mock()
mock_config.config = {
"rate_limit": {
"auto_switch_profiles": False,
"show_countdown": True,
"default_timeout_minutes": 30,
}
}
mock_config.get_rate_limit_strategy.return_value = "fail"
mock_config.get_timeout_minutes.return_value = 30
mock_get_config.return_value = mock_config
handler = RateLimitHandler(token="ghp_test", interactive=False)
reset_time = datetime.now() + timedelta(minutes=30)
rate_info = {"limit": 5000, "remaining": 0, "reset_time": reset_time}
with pytest.raises(RateLimitError):
handler.handle_rate_limit(rate_info)
class TestConfigManagerIntegration:
"""Test ConfigManager integration with rate limit handler."""
def test_config_manager_creates_default_config(self, tmp_path, monkeypatch):
"""Test that ConfigManager creates default config structure."""
# Override config paths for testing
config_dir = tmp_path / ".config" / "skill-seekers"
progress_dir = tmp_path / ".local" / "share" / "skill-seekers" / "progress"
# Monkey patch the class variables
monkeypatch.setattr(ConfigManager, "CONFIG_DIR", config_dir)
monkeypatch.setattr(ConfigManager, "CONFIG_FILE", config_dir / "config.json")
monkeypatch.setattr(ConfigManager, "PROGRESS_DIR", progress_dir)
config = ConfigManager()
# Check directories created
assert config.config_dir.exists()
assert config.progress_dir.exists()
# Check default config structure
assert "github" in config.config
assert "rate_limit" in config.config
assert "resume" in config.config
assert "api_keys" in config.config
# Check rate limit defaults
assert config.config["rate_limit"]["default_timeout_minutes"] == 30
assert config.config["rate_limit"]["auto_switch_profiles"] is True
def test_add_and_retrieve_github_profile(self, tmp_path, monkeypatch):
"""Test adding and retrieving GitHub profiles."""
config_dir = tmp_path / ".config" / "skill-seekers"
monkeypatch.setattr(ConfigManager, "CONFIG_DIR", config_dir)
monkeypatch.setattr(ConfigManager, "CONFIG_FILE", config_dir / "config.json")
monkeypatch.setattr(
ConfigManager,
"PROGRESS_DIR",
tmp_path / ".local" / "share" / "skill-seekers" / "progress",
)
config = ConfigManager()
# Add a profile
config.add_github_profile(
name="test-profile",
token="ghp_test123",
description="Test profile",
rate_limit_strategy="wait",
timeout_minutes=45,
set_as_default=True,
)
# Retrieve token
token = config.get_github_token(profile_name="test-profile")
assert token == "ghp_test123"
# Check it's default
profiles = config.list_github_profiles()
assert len(profiles) == 1
assert profiles[0]["is_default"] is True
assert profiles[0]["name"] == "test-profile"
def test_get_next_profile(self, tmp_path, monkeypatch):
"""Test profile switching."""
# Use separate tmp directory for this test
test_dir = tmp_path / "test_switching"
config_dir = test_dir / ".config" / "skill-seekers"
monkeypatch.setattr(ConfigManager, "CONFIG_DIR", config_dir)
monkeypatch.setattr(ConfigManager, "CONFIG_FILE", config_dir / "config.json")
monkeypatch.setattr(
ConfigManager,
"PROGRESS_DIR",
test_dir / ".local" / "share" / "skill-seekers" / "progress",
)
monkeypatch.setattr(ConfigManager, "WELCOME_FLAG", config_dir / ".welcomed")
config = ConfigManager()
# Ensure clean state
config.config["github"]["profiles"] = {}
# Add two profiles
config.add_github_profile("profile1", "ghp_token1", set_as_default=True)
config.add_github_profile("profile2", "ghp_token2", set_as_default=False)
# Verify we have exactly 2 profiles
profiles = config.list_github_profiles()
assert len(profiles) == 2
# Get next profile after profile1
next_data = config.get_next_profile("ghp_token1")
assert next_data is not None
name, token = next_data
assert name == "profile2"
assert token == "ghp_token2"
# Get next profile after profile2 (should wrap to profile1)
next_data = config.get_next_profile("ghp_token2")
assert next_data is not None
name, token = next_data
assert name == "profile1"
assert token == "ghp_token1"
if __name__ == "__main__":
pytest.main([__file__, "-v"])