| | @@ -0,0 +1,269 @@ |
| 1 | +"""Tests for navegador.enrichment.rails — RailsEnricher."""
|
| 2 | +
|
| 3 | +from unittest.mock import MagicMock, call
|
| 4 | +
|
| 5 | +import pytest
|
| 6 | +
|
| 7 | +from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
|
| 8 | +from navegador.enrichment.rails import RailsEnricher
|
| 9 | +from navegador.graph.store import GraphStore
|
| 10 | +
|
| 11 | +
|
| 12 | +# ── Helpers ───────────────────────────────────────────────────────────────────
|
| 13 | +
|
| 14 | +
|
| 15 | +def _mock_store(result_set=None):
|
| 16 | + """Return a GraphStore backed by a mock FalkorDB graph."""
|
| 17 | + client = MagicMock()
|
| 18 | + graph = MagicMock()
|
| 19 | + graph.query.return_value = MagicMock(result_set=result_set)
|
| 20 | + client.select_graph.return_value = graph
|
| 21 | + return GraphStore(client)
|
| 22 | +
|
| 23 | +
|
| 24 | +def _store_with_side_effect(side_effect):
|
| 25 | + """Return a GraphStore whose graph.query uses a side_effect callable."""
|
| 26 | + client = MagicMock()
|
| 27 | + graph = MagicMock()
|
| 28 | + graph.query.side_effect = side_effect
|
| 29 | + client.select_graph.return_value = graph
|
| 30 | + return GraphStore(client)
|
| 31 | +
|
| 32 | +
|
| 33 | +# ── Identity / contract ───────────────────────────────────────────────────────
|
| 34 | +
|
| 35 | +
|
| 36 | +class TestRailsEnricherIdentity:
|
| 37 | + def test_framework_name(self):
|
| 38 | + store = _mock_store()
|
| 39 | + assert RailsEnricher(store).framework_name == "rails"
|
| 40 | +
|
| 41 | + def test_is_framework_enricher_subclass(self):
|
| 42 | + assert issubclass(RailsEnricher, FrameworkEnricher)
|
| 43 | +
|
| 44 | + def test_detection_patterns_contains_t_detection_files_contains_gemfile(self):
|
| 45 | + store = _mock_store()
|
| 46 | + patterns
|
| 47 | +
|
| 48 | + def test_droutes(self):
|
| 49 | + store = _moconfig/routes.rbpatterns
|
| 50 | +
|
| 51 | + def test_depplicationke_store_for_fragment(
|
| 52 | + _mock_store()
|
| 53 | + assert "ApplicationController" in RailsEnricher(store).detection_patterns
|
| 54 | +
|
| 55 | + def test_detection_r)
|
| 56 | +
|
| 57 | + def test_dactive_record(self):
|
| 58 | + store = _mock_store()
|
| 59 | + assert "ActiveRecordpatterns
|
| 60 | +
|
| 61 | + dhas_four_entries(self):
|
| 62 | + store = _m"""Tests for navegado""""Tests for navegador.enrichment.rails — RailsEnricher."""
|
| 63 | +
|
| 64 | +from unittest.mock import MagicMock, call
|
| 65 | +
|
| 66 | +import pytest
|
| 67 | +
|
| 68 | +from navegador.enrichment.base import EnrichmentResult, FrameworkEnricher
|
| 69 | +from navegador.enrichment.rails import RailsEnricher
|
| 70 | +from navegador.graph.store import GraphStore
|
| 71 | +
|
| 72 | +
|
| 73 | +# ── Helpers ───────────────────────────────────────────────────────────────────
|
| 74 | +
|
| 75 | +
|
| 76 | +def _mock_store(result_set=None):
|
| 77 | + """Return a GraphStore backed by a mock FalkorDB graph."""
|
| 78 | + client = MagicMock()
|
| 79 | + graph = MagicMock()
|
| 80 | + graph.query.return_value = MagicMock(result_set=result_set)
|
| 81 | + client.select_graph.return_value = graph
|
| 82 | + return GraphStore(client)
|
| 83 | +
|
| 84 | +
|
| 85 | +def _store_with_side_effect(side_effect):
|
| 86 | + """Return a GraphStore whose graph.query uses a side_effect callable."""
|
| 87 | + client = MagicMock()
|
| 88 | + graph = MagicMock()
|
| 89 | + graph.query.side_effect = side_effect
|
| 90 | + client.select_graph.return_value = graph
|
| 91 | + return GraphStore(client)
|
| 92 | +
|
| 93 | +
|
| 94 | +# ── Identity / contract ───────────────────────────────────────────────────────
|
| 95 | +
|
| 96 | +
|
| 97 | +class TestRailsEnricherIdentity:
|
| 98 | + def test_framework_name(self):
|
| 99 | + store = _mock_store()
|
| 100 | + assert RailsEnricher(store).framework_name == "rails"
|
| 101 | +
|
| 102 | + def test_is_framework_enricher_subclass(self):
|
| 103 | + assert issubclass(RailsEnricher, FrameworkEnricher)
|
| 104 | +
|
| 105 | + def test_detection_patterns_contains_rails(self):
|
| 106 | + store = _mock_store()
|
| 107 | + assert "rails" in RailsEnricher(store).detection_patterns
|
| 108 | +
|
| 109 | + def test_detection_patterns_contains_active_record(self):
|
| 110 | + store = _mock_store()
|
| 111 | + assert "active_record" in RailsEnricher(store).detection_patterns
|
| 112 | +
|
| 113 | + def test_detection_patterns_contains_action_controller(self):
|
| 114 | + store = _mock_store()
|
| 115 | + assert "action_controller" in RailsEnricher(store).detection_patterns
|
| 116 | +
|
| 117 | + def test_detection_files_contains_gemfile(self):
|
| 118 | + store = _mock_store()
|
| 119 | + assert "Gemfile" in RailsEnricher(store).detection_files
|
| 120 | +
|
| 121 | + def test_detection_patterns_has_three_entries(self):
|
| 122 | + store = _mock_store()
|
| 123 | + assert len(RailsEnricher(store).detection_patterns) == 3
|
| 124 | +
|
| 125 | +
|
| 126 | +# ── enrich() return type ──────────────────────────────────────────────────────
|
| 127 | +
|
| 128 | +
|
| 129 | +class TestRailsEnricherEnrichReturnType:
|
| 130 | + def test_returns_enrichment_result(self):
|
| 131 | + store = _mock_store(result_set=[])
|
| 132 | + result = RailsEnricher(store).enrich()
|
| 133 | + assert isinstance(result, EnrichmentResult)
|
| 134 | +
|
| 135 | + def test_result_has_promoted_attribute(self):
|
| 136 | + store = _mock_store(result_set=[])
|
| 137 | + result = RailsEnricher(store).enrich()
|
| 138 | + assert hasattr(result, "promoted")
|
| 139 | +
|
| 140 | + def test_result_has_edges_added_attribute(self):
|
| 141 | + store = _mock_store(result_set=[])
|
| 142 | + result = RailsEnricher(store).enrich()
|
| 143 | + assert hasattr(result, "edges_added")
|
| 144 | +
|
| 145 | + def test_result_has_patterns_found_attribute(self):
|
| 146 | + store = _mock_store(result_set=[])
|
| 147 | + result = RailsEnricher(store).enrich()
|
| 148 | + assert hasattr(result, "patterns_found")
|
| 149 | +
|
| 150 | +
|
| 151 | +# ── enrich() with no matching nodes ──────────────────────────────────────────
|
| 152 | +
|
| 153 | +
|
| 154 | +class TestRailsEnricherNoMatches:
|
| 155 | + def test_promoted_is_zero_when_no_nodes(self):
|
| 156 | + store = _mock_store(result_set=[])
|
| 157 | + result = RailsEnricher(store).enrich()
|
| 158 | + assert result.promoted == 0
|
| 159 | +
|
| 160 | + def test_all_pattern_counts_zero_when_no_nodes(self):
|
| 161 | + store = _mock_store(result_set=[])
|
| 162 | + result = RailsEnricher(store).enrich()
|
| 163 | + for key in ("controllers", "models", "routes", "jobs", "concerns"):
|
| 164 | + assert result.patterns_found[key] == 0
|
| 165 | +
|
| 166 | + def test_patterns_found_has_five_keys(self):
|
| 167 | + store = _mock_store(result_set=[])
|
| 168 | + result = RailsEnricher(store).enrich()
|
| 169 | + assert set(result.patterns_found.keys()) == {
|
| 170 | + "controllers", "models", "routes", "jobs", "concerns"
|
| 171 | + }
|
| 172 | +
|
| 173 | +
|
| 174 | +# ── enrich() with matching nodes ─────────────────────────────────────────────
|
| 175 | +
|
| 176 | +
|
| 177 | +class TestRailsEnricherWithMatches:
|
| 178 | + def _make_store_for_fragment(self, target_fragment, rows):
|
| 179 | + """Return a store that returns `rows` only when the query fragment matches."""
|
| 180 | +
|
| 181 | + def side_effect(cypher, params):
|
| 182 | + fragment = params.get("fragment", "")
|
| 183 | + if fragment == target_fragment:
|
| 184 | + return MagicMock(result_set=rows)
|
| 185 | + return MagicMock(result_set=[])
|
| 186 | +
|
| 187 | + return _store_with_side_effect(side_effect)
|
| 188 | +
|
| 189 | + def test_controller_promoted(self):
|
| 190 | + store = self._make_store_for_fragment(
|
| 191 | + "controllers/", [["UsersController", "app/controllers/users_controller.rb"]]
|
| 192 | + )
|
| 193 | + result = RailsEnricher(store).enrich()
|
| 194 | + assert result.patterns_found["controllers"] == 1
|
| 195 | + assert result.promoted >= 1
|
| 196 | +
|
| 197 | + def test_model_promoted(self):
|
| 198 | + store = self._make_store_for_fragment(
|
| 199 | + "models/", [["User", "app/models/user.rb"]]
|
| 200 | + )
|
| 201 | + result = RailsEnricher(store).enrich()
|
| 202 | + assert result.patterns_found["models"] == 1
|
| 203 | + assert result.promoted >= 1
|
| 204 | +
|
| 205 | + def test_route_promoted(self):
|
| 206 | + store = self._make_store_for_fragment(
|
| 207 | + "routes.rb", [["routes", "config/routes.rb"]]
|
| 208 | + )
|
| 209 | + result = RailsEnricher(store).enrich()
|
| 210 | + assert result.patterns_found["routes"] == 1
|
| 211 | + assert result.promoted >= 1
|
| 212 | +
|
| 213 | + def test_job_promoted(self):
|
| 214 | + store = self._make_store_for_fragment(
|
| 215 | + "jobs/", [["SendEmailJob", "app/jobs/send_email_job.rb"]]
|
| 216 | + )
|
| 217 | + result = RailsEnricher(store).enrich()
|
| 218 | + assert result.patterns_found["jobs"] == 1
|
| 219 | + assert result.promoted >= 1
|
| 220 | +
|
| 221 | + def test_concern_promoted(self):
|
| 222 | + store = self._make_store_for_fragment(
|
| 223 | + "concerns/", [["Auditable", "app/models/concerns/auditable.rb"]]
|
| 224 | + )
|
| 225 | + result = RailsEnricher(store).enrich()
|
| 226 | + assert result.patterns_found["concerns"] == 1
|
| 227 | + assert result.promoted >= 1
|
| 228 | +
|
| 229 | + def test_promoted_count_accumulates_across_types(self):
|
| 230 | + rows_map = {
|
| 231 | + "controllers/": [
|
| 232 | + ["UsersController", "app/controllers/users_controller.rb"],
|
| 233 | + ["PostsController", "app/controllers/posts_controller.rb"],
|
| 234 | + ],
|
| 235 | + "models/": [["User", "app/models/user.rb"]],
|
| 236 | + "routes.rb": [],
|
| 237 | + "jobs/": [],
|
| 238 | + "concerns/": [],
|
| 239 | + }
|
| 240 | +
|
| 241 | + def side_effect(cypher, params):
|
| 242 | + fragment = params.get("fragment", "")
|
| 243 | + return MagicMock(result_set=rows_map.get(fragment, []))
|
| 244 | +
|
| 245 | + store = _store_with_side_effect(side_effect)
|
| 246 | + result = RailsEnricher(store).enrich()
|
| 247 | + assert result.promoted == 3
|
| 248 | + assert result.patterns_found["controllers"] == 2
|
| 249 | + assert result.patterns_found["models"] == 1
|
| 250 | +
|
| 251 | + def test_promote_node_called_with_correct_semantic_type_for_controller(self):
|
| 252 | + store = self._make_store_for_fragment(
|
| 253 | + "controllers/", [["UsersController", "app/controllers/users_controller.rb"]]
|
| 254 | + )
|
| 255 | + RailsEnricher(store).enrich()
|
| 256 | +
|
| 257 | + # The _promote_node path ultimately calls store.query with SET n.semantic_type
|
| 258 | + calls = [str(c) for c in store._graph.query.call_args_list]
|
| 259 | + promote_calls = [c for c in calls if "semantic_type" in c and "RailsController" in c]
|
| 260 | + assert len(promote_calls) >= 1
|
| 261 | +
|
| 262 | + def test_promote_node_called_with_correct_semantic_type_for_model(self):
|
| 263 | + store = self._make_store_for_fragment(
|
| 264 | + "models/", [["User", "app/models/user.rb"]]
|
| 265 | + )
|
| 266 | + RailsEnricher(store).enrich()
|
| 267 | +
|
| 268 | + calls = [str(c) for c in store._graph.query.call_args_list]
|
| 269 | + promote_calls = [c for c in cal |