Skip to content

Commit 47d8251

Browse files
committed
test: add tests for catalog entries and collectors
Add test_catalog_and_collectors.py with 28 tests covering: - Claude Code: version detection, install script, multi-method support - PHP catalog: PPA support, package manager integration - Composer: PHP dependency handling - GitHub rate limit: auth helpers, gh CLI integration - Catalog integrity: JSON validation, required fields
1 parent f7f8756 commit 47d8251

1 file changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
"""
2+
Tests for catalog entries and version collectors.
3+
4+
Covers:
5+
- Claude Code catalog: version detection, install script, multi-method support
6+
- PHP catalog: PPA support, package manager integration
7+
- Composer: PHP dependency handling
8+
- GitHub rate limit: authentication helpers, gh CLI integration
9+
"""
10+
11+
import json
12+
import os
13+
import pytest
14+
from pathlib import Path
15+
from unittest.mock import patch, MagicMock
16+
17+
18+
# Get the project root directory
19+
PROJECT_ROOT = Path(__file__).parent.parent
20+
21+
22+
class TestClaudeVersionDetection:
23+
"""Tests for Claude Code version detection fix."""
24+
25+
def test_claude_catalog_has_version_command(self):
26+
"""Test that claude.json has a version_command field."""
27+
catalog_path = PROJECT_ROOT / "catalog" / "claude.json"
28+
assert catalog_path.exists(), "claude.json catalog file should exist"
29+
30+
with open(catalog_path) as f:
31+
data = json.load(f)
32+
33+
assert "version_command" in data, "claude.json should have version_command field"
34+
assert data["version_command"], "version_command should not be empty"
35+
36+
def test_claude_catalog_version_command_format(self):
37+
"""Test that claude.json version_command checks package.json."""
38+
catalog_path = PROJECT_ROOT / "catalog" / "claude.json"
39+
40+
with open(catalog_path) as f:
41+
data = json.load(f)
42+
43+
version_cmd = data.get("version_command", "")
44+
# Should reference package.json to get version, not run claude binary
45+
assert "package.json" in version_cmd, "version_command should check package.json"
46+
assert "version" in version_cmd.lower(), "version_command should extract version"
47+
48+
def test_claude_catalog_structure(self):
49+
"""Test that claude.json has valid catalog structure."""
50+
catalog_path = PROJECT_ROOT / "catalog" / "claude.json"
51+
52+
with open(catalog_path) as f:
53+
data = json.load(f)
54+
55+
# Required fields for dedicated_script tools
56+
assert data.get("name") == "claude"
57+
assert data.get("install_method") == "dedicated_script"
58+
assert data.get("script") == "install_claude.sh"
59+
assert data.get("binary_name") == "claude"
60+
assert data.get("github_repo") == "anthropics/claude-code"
61+
62+
def test_claude_catalog_has_notes_about_install_methods(self):
63+
"""Test that claude.json documents installation methods."""
64+
catalog_path = PROJECT_ROOT / "catalog" / "claude.json"
65+
66+
with open(catalog_path) as f:
67+
data = json.load(f)
68+
69+
notes = data.get("notes", "")
70+
# Should mention native installer
71+
assert "native" in notes.lower() or "curl" in notes.lower()
72+
# Should mention Node.js version limitation
73+
assert "Node.js" in notes or "node" in notes.lower()
74+
75+
76+
class TestClaudeInstallScript:
77+
"""Tests for Claude Code install script."""
78+
79+
def test_install_script_exists(self):
80+
"""Test that install_claude.sh exists."""
81+
script_path = PROJECT_ROOT / "scripts" / "install_claude.sh"
82+
assert script_path.exists(), "install_claude.sh should exist"
83+
84+
def test_install_script_is_executable(self):
85+
"""Test that install_claude.sh is executable."""
86+
script_path = PROJECT_ROOT / "scripts" / "install_claude.sh"
87+
assert os.access(script_path, os.X_OK), "install_claude.sh should be executable"
88+
89+
def test_install_script_uses_native_installer(self):
90+
"""Test that install_claude.sh uses native installer as primary method."""
91+
script_path = PROJECT_ROOT / "scripts" / "install_claude.sh"
92+
content = script_path.read_text()
93+
94+
# Should use official native installer
95+
assert "claude.ai/install.sh" in content
96+
# Should have fallbacks
97+
assert "homebrew" in content.lower() or "brew" in content
98+
assert "npm" in content
99+
100+
def test_install_script_handles_node_version(self):
101+
"""Test that install_claude.sh checks Node.js version for npm fallback."""
102+
script_path = PROJECT_ROOT / "scripts" / "install_claude.sh"
103+
content = script_path.read_text()
104+
105+
# Should check Node.js version before npm install
106+
assert "node" in content.lower()
107+
assert "25" in content # v25+ warning
108+
109+
110+
class TestPHPCatalog:
111+
"""Tests for PHP catalog entry."""
112+
113+
def test_php_catalog_exists(self):
114+
"""Test that php.json catalog file exists."""
115+
catalog_path = PROJECT_ROOT / "catalog" / "php.json"
116+
assert catalog_path.exists(), "php.json catalog file should exist"
117+
118+
def test_php_catalog_valid_json(self):
119+
"""Test that php.json is valid JSON."""
120+
catalog_path = PROJECT_ROOT / "catalog" / "php.json"
121+
122+
with open(catalog_path) as f:
123+
data = json.load(f) # Should not raise
124+
125+
assert isinstance(data, dict)
126+
127+
def test_php_catalog_required_fields(self):
128+
"""Test that php.json has all required fields."""
129+
catalog_path = PROJECT_ROOT / "catalog" / "php.json"
130+
131+
with open(catalog_path) as f:
132+
data = json.load(f)
133+
134+
# Required fields
135+
assert data.get("name") == "php"
136+
assert data.get("category") == "php"
137+
assert data.get("install_method") == "package_manager"
138+
assert data.get("binary_name") == "php"
139+
140+
def test_php_catalog_has_ppa(self):
141+
"""Test that php.json has PPA for latest PHP on Ubuntu."""
142+
catalog_path = PROJECT_ROOT / "catalog" / "php.json"
143+
144+
with open(catalog_path) as f:
145+
data = json.load(f)
146+
147+
assert "ppa" in data, "php.json should have PPA field"
148+
assert "ondrej" in data["ppa"], "PHP PPA should be ondrej/php"
149+
150+
def test_php_catalog_has_packages(self):
151+
"""Test that php.json has package definitions for package managers."""
152+
catalog_path = PROJECT_ROOT / "catalog" / "php.json"
153+
154+
with open(catalog_path) as f:
155+
data = json.load(f)
156+
157+
assert "packages" in data, "php.json should have packages field"
158+
packages = data["packages"]
159+
160+
# Should support common package managers
161+
assert "apt" in packages, "Should have apt package"
162+
assert "brew" in packages, "Should have brew package"
163+
164+
def test_php_catalog_guide_order(self):
165+
"""Test that PHP has guide section with proper order."""
166+
catalog_path = PROJECT_ROOT / "catalog" / "php.json"
167+
168+
with open(catalog_path) as f:
169+
data = json.load(f)
170+
171+
assert "guide" in data, "php.json should have guide section"
172+
guide = data["guide"]
173+
assert "order" in guide, "guide should have order field"
174+
# PHP should come before Composer (260)
175+
assert guide["order"] < 260, "PHP should have lower order than Composer"
176+
177+
178+
class TestComposerRequiresPHP:
179+
"""Tests for Composer's PHP dependency."""
180+
181+
def test_composer_has_requires_field(self):
182+
"""Test that composer.json has requires field with PHP."""
183+
catalog_path = PROJECT_ROOT / "catalog" / "composer.json"
184+
185+
with open(catalog_path) as f:
186+
data = json.load(f)
187+
188+
assert "requires" in data, "composer.json should have requires field"
189+
assert "php" in data["requires"], "Composer should require PHP"
190+
191+
def test_composer_guide_order_after_php(self):
192+
"""Test that Composer's guide order is after PHP."""
193+
php_path = PROJECT_ROOT / "catalog" / "php.json"
194+
composer_path = PROJECT_ROOT / "catalog" / "composer.json"
195+
196+
with open(php_path) as f:
197+
php_data = json.load(f)
198+
with open(composer_path) as f:
199+
composer_data = json.load(f)
200+
201+
php_order = php_data.get("guide", {}).get("order", 0)
202+
composer_order = composer_data.get("guide", {}).get("order", 999)
203+
204+
assert composer_order > php_order, "Composer should be installed after PHP"
205+
206+
207+
class TestInstallComposerPHPCheck:
208+
"""Tests for install_composer.sh PHP dependency check."""
209+
210+
def test_install_composer_checks_php(self):
211+
"""Test that install_composer.sh checks for PHP."""
212+
script_path = PROJECT_ROOT / "scripts" / "install_composer.sh"
213+
assert script_path.exists(), "install_composer.sh should exist"
214+
215+
content = script_path.read_text()
216+
217+
# Should check for PHP
218+
assert "command -v php" in content, "Script should check for PHP"
219+
assert "Error: PHP is required" in content or "PHP is required" in content, \
220+
"Script should mention PHP requirement"
221+
222+
223+
class TestGitHubRateLimitHelpers:
224+
"""Tests for GitHub rate limit helper functions."""
225+
226+
def test_get_github_rate_limit_help_exists(self):
227+
"""Test that get_github_rate_limit_help function exists."""
228+
from cli_audit.collectors import get_github_rate_limit_help
229+
assert callable(get_github_rate_limit_help)
230+
231+
def test_get_github_rate_limit_help_content(self):
232+
"""Test that help message contains useful instructions."""
233+
from cli_audit.collectors import get_github_rate_limit_help
234+
235+
help_text = get_github_rate_limit_help()
236+
237+
# Should mention GITHUB_TOKEN
238+
assert "GITHUB_TOKEN" in help_text
239+
# Should mention gh CLI
240+
assert "gh auth" in help_text
241+
# Should include PAT creation URL
242+
assert "github.com/settings/tokens" in help_text
243+
# Should mention rate limits
244+
assert "5,000" in help_text or "5000" in help_text
245+
246+
def test_get_gh_cli_token_exists(self):
247+
"""Test that get_gh_cli_token function exists."""
248+
from cli_audit.collectors import get_gh_cli_token
249+
assert callable(get_gh_cli_token)
250+
251+
@patch("shutil.which")
252+
def test_get_gh_cli_token_no_gh(self, mock_which):
253+
"""Test get_gh_cli_token returns None when gh not installed."""
254+
from cli_audit.collectors import get_gh_cli_token
255+
256+
mock_which.return_value = None
257+
result = get_gh_cli_token()
258+
assert result is None
259+
260+
@patch("subprocess.run")
261+
@patch("shutil.which")
262+
def test_get_gh_cli_token_not_authenticated(self, mock_which, mock_run):
263+
"""Test get_gh_cli_token returns None when gh not authenticated."""
264+
from cli_audit.collectors import get_gh_cli_token
265+
266+
mock_which.return_value = "/usr/bin/gh"
267+
mock_run.return_value = MagicMock(returncode=1)
268+
269+
result = get_gh_cli_token()
270+
assert result is None
271+
272+
@patch("subprocess.run")
273+
@patch("shutil.which")
274+
def test_get_gh_cli_token_authenticated(self, mock_which, mock_run):
275+
"""Test get_gh_cli_token returns token when gh is authenticated."""
276+
from cli_audit.collectors import get_gh_cli_token
277+
278+
mock_which.return_value = "/usr/bin/gh"
279+
280+
# First call: gh auth status (success)
281+
# Second call: gh auth token (returns token)
282+
mock_run.side_effect = [
283+
MagicMock(returncode=0), # auth status
284+
MagicMock(returncode=0, stdout="ghp_testtoken123\n"), # auth token
285+
]
286+
287+
result = get_gh_cli_token()
288+
assert result == "ghp_testtoken123"
289+
290+
def test_get_github_rate_limit_returns_authenticated_field(self):
291+
"""Test that get_github_rate_limit returns authenticated field."""
292+
from cli_audit.collectors import get_github_rate_limit
293+
294+
# This is an integration test - it makes a real API call
295+
# We just check the structure is correct
296+
result = get_github_rate_limit()
297+
298+
if result: # May fail if network is unavailable
299+
assert "authenticated" in result
300+
assert "limit" in result
301+
assert "remaining" in result
302+
303+
304+
class TestGitHubRateLimitDisplay:
305+
"""Tests for rate limit display in audit.py."""
306+
307+
def test_audit_imports_help_function(self):
308+
"""Test that audit.py imports get_github_rate_limit_help."""
309+
import audit
310+
assert hasattr(audit, 'get_github_rate_limit_help') or \
311+
'get_github_rate_limit_help' in dir(audit)
312+
313+
314+
class TestCatalogIntegrity:
315+
"""Tests for overall catalog integrity."""
316+
317+
def test_all_catalog_files_valid_json(self):
318+
"""Test that all catalog files are valid JSON."""
319+
catalog_dir = PROJECT_ROOT / "catalog"
320+
321+
for json_file in catalog_dir.glob("*.json"):
322+
with open(json_file) as f:
323+
try:
324+
data = json.load(f)
325+
assert isinstance(data, dict), f"{json_file.name} should contain a dict"
326+
except json.JSONDecodeError as e:
327+
pytest.fail(f"{json_file.name} is not valid JSON: {e}")
328+
329+
def test_all_catalog_files_have_name(self):
330+
"""Test that all catalog files have a name field."""
331+
catalog_dir = PROJECT_ROOT / "catalog"
332+
333+
for json_file in catalog_dir.glob("*.json"):
334+
with open(json_file) as f:
335+
data = json.load(f)
336+
assert "name" in data, f"{json_file.name} should have 'name' field"
337+
338+
def test_all_catalog_files_have_install_method(self):
339+
"""Test that all catalog files have an install_method field."""
340+
catalog_dir = PROJECT_ROOT / "catalog"
341+
342+
for json_file in catalog_dir.glob("*.json"):
343+
with open(json_file) as f:
344+
data = json.load(f)
345+
assert "install_method" in data, f"{json_file.name} should have 'install_method' field"

0 commit comments

Comments
 (0)