main
py 148 lines 4.9 KB
Raw
1 """Tests for scripts/topic_paths.py — topic-aware data directory resolution."""
2
3 from __future__ import annotations
4
5 import pytest
6
7 from scripts.topic_paths import (
8 DATA_ROOT,
9 DEFAULT_TOPIC,
10 analyzed_dir,
11 cache_dir,
12 ensure_dirs,
13 load_topic_id,
14 metrics_dir,
15 raw_dir,
16 snapshots_dir,
17 )
18
19
20 class TestDefaultTopic:
21 """When topic is None or 'general', use legacy flat paths."""
22
23 def test_raw_dir_none(self):
24 assert raw_dir(None) == DATA_ROOT / "raw"
25
26 def test_raw_dir_general(self):
27 assert raw_dir("general") == DATA_ROOT / "raw"
28
29 def test_analyzed_dir_default(self):
30 assert analyzed_dir() == DATA_ROOT / "analyzed"
31
32 def test_metrics_dir_default(self):
33 assert metrics_dir() == DATA_ROOT / "metrics"
34
35 def test_snapshots_dir_default(self):
36 assert snapshots_dir() == DATA_ROOT / "snapshots"
37
38 def test_cache_dir_default(self):
39 assert cache_dir() == DATA_ROOT / "cache"
40
41
42 class TestNamespacedTopic:
43 """When a specific topic is given, paths include the topic subdirectory."""
44
45 def test_raw_dir_ai_ml(self):
46 assert raw_dir("ai-ml") == DATA_ROOT / "raw" / "ai-ml"
47
48 def test_analyzed_dir_rust(self):
49 assert analyzed_dir("rust") == DATA_ROOT / "analyzed" / "rust"
50
51 def test_metrics_dir_topic(self):
52 assert metrics_dir("web-dev") == DATA_ROOT / "metrics" / "web-dev"
53
54 def test_snapshots_dir_topic(self):
55 assert snapshots_dir("ai-ml") == DATA_ROOT / "snapshots" / "ai-ml"
56
57 def test_cache_dir_topic(self):
58 assert cache_dir("rust") == DATA_ROOT / "cache" / "rust"
59
60 def test_case_normalization(self):
61 assert raw_dir("AI-ML") == DATA_ROOT / "raw" / "ai-ml"
62
63 def test_whitespace_stripped(self):
64 assert raw_dir(" rust ") == DATA_ROOT / "raw" / "rust"
65
66
67 class TestEnsureDirs:
68 """ensure_dirs creates all required directories."""
69
70 def test_creates_all_dirs(self, tmp_path, monkeypatch):
71 monkeypatch.setattr("scripts.topic_paths.DATA_ROOT", tmp_path / "data")
72 ensure_dirs("ai-ml")
73 for subdir in ("raw", "analyzed", "metrics", "snapshots", "cache"):
74 assert (tmp_path / "data" / subdir / "ai-ml").is_dir()
75
76 def test_creates_flat_dirs_for_general(self, tmp_path, monkeypatch):
77 monkeypatch.setattr("scripts.topic_paths.DATA_ROOT", tmp_path / "data")
78 ensure_dirs(None)
79 for subdir in ("raw", "analyzed", "metrics", "snapshots", "cache"):
80 assert (tmp_path / "data" / subdir).is_dir()
81
82
83 class TestLoadTopicId:
84 """load_topic_id reads the topic.id from YAML config."""
85
86 def test_reads_valid_config(self, tmp_path):
87 config = tmp_path / "topic.yml"
88 config.write_text("topic:\n id: rust\n name: Rust\n")
89 assert load_topic_id(config) == "rust"
90
91 def test_missing_file_returns_default(self, tmp_path):
92 assert load_topic_id(tmp_path / "nope.yml") == DEFAULT_TOPIC
93
94 def test_malformed_yaml_returns_default(self, tmp_path):
95 config = tmp_path / "bad.yml"
96 config.write_text("{{invalid yaml")
97 assert load_topic_id(config) == DEFAULT_TOPIC
98
99 def test_missing_topic_key_returns_default(self, tmp_path):
100 config = tmp_path / "empty.yml"
101 config.write_text("scoring:\n min_stars: 10\n")
102 assert load_topic_id(config) == DEFAULT_TOPIC
103
104
105 class TestTopicIdValidation:
106 """_resolve must reject topic IDs that could cause path traversal."""
107
108 def test_dotdot_raises(self):
109 from scripts.topic_paths import DATA_ROOT, _resolve
110
111 with pytest.raises(ValueError, match="Invalid topic ID"):
112 _resolve(DATA_ROOT / "raw", "../../../etc")
113
114 def test_absolute_path_raises(self):
115 from scripts.topic_paths import DATA_ROOT, _resolve
116
117 with pytest.raises(ValueError, match="Invalid topic ID"):
118 _resolve(DATA_ROOT / "raw", "/etc/passwd")
119
120 def test_slash_in_id_raises(self):
121 from scripts.topic_paths import DATA_ROOT, _resolve
122
123 with pytest.raises(ValueError, match="Invalid topic ID"):
124 _resolve(DATA_ROOT / "raw", "valid/subdir")
125
126 def test_null_byte_raises(self):
127 from scripts.topic_paths import DATA_ROOT, _resolve
128
129 with pytest.raises(ValueError, match="Invalid topic ID"):
130 _resolve(DATA_ROOT / "raw", "evil\x00byte")
131
132 def test_leading_hyphen_raises(self):
133 from scripts.topic_paths import DATA_ROOT, _resolve
134
135 with pytest.raises(ValueError, match="Invalid topic ID"):
136 _resolve(DATA_ROOT / "raw", "-bad")
137
138 def test_valid_hyphenated_id_passes(self):
139 from scripts.topic_paths import DATA_ROOT, _resolve
140
141 result = _resolve(DATA_ROOT / "raw", "ai-ml")
142 assert result == DATA_ROOT / "raw" / "ai-ml"
143
144 def test_valid_underscore_id_passes(self):
145 from scripts.topic_paths import DATA_ROOT, _resolve
146
147 result = _resolve(DATA_ROOT / "raw", "rust_2026")
148 assert result == DATA_ROOT / "raw" / "rust_2026"