|
10 | 10 | resolve_fuzzy_match, |
11 | 11 | resolve_license, |
12 | 12 | find_exact_match_license_url, |
| 13 | + assign_license_by_url, |
13 | 14 | MatchingLicense, |
14 | 15 | ) |
15 | 16 | from shared.database_gen.sqlacodegen_models import License |
@@ -246,5 +247,154 @@ def test_matching_license_dataclass(self): |
246 | 247 | self.assertEqual(ml.confidence, 1.0) |
247 | 248 |
|
248 | 249 |
|
249 | | -if __name__ == "__main__": |
250 | | - unittest.main() |
| 250 | +class TestAssignLicenseByUrl(unittest.TestCase): |
| 251 | + """Unit tests for assign_license_by_url.""" |
| 252 | + |
| 253 | + def _make_match(self, license_id="MIT", match_type="exact", confidence=1.0): |
| 254 | + return MatchingLicense( |
| 255 | + license_id=license_id, |
| 256 | + license_url="http://example.com/license", |
| 257 | + normalized_url="example.com/license", |
| 258 | + match_type=match_type, |
| 259 | + confidence=confidence, |
| 260 | + matched_name="MIT License", |
| 261 | + matched_catalog_url="http://example.com/license", |
| 262 | + matched_source="db.license", |
| 263 | + ) |
| 264 | + |
| 265 | + def _make_feed(self, license_url="http://example.com/license", license_id=None): |
| 266 | + feed = MagicMock() |
| 267 | + feed.stable_id = "test-feed-1" |
| 268 | + feed.id = "feed-id-1" |
| 269 | + feed.license_url = license_url |
| 270 | + feed.license_id = license_id |
| 271 | + feed.license_notes = None |
| 272 | + feed.feed_license_changes = [] |
| 273 | + return feed |
| 274 | + |
| 275 | + # --- No license_url --- |
| 276 | + |
| 277 | + def test_no_license_url_returns_none(self): |
| 278 | + feed = self._make_feed(license_url=None) |
| 279 | + result = assign_license_by_url(feed, MagicMock()) |
| 280 | + self.assertIsNone(result) |
| 281 | + self.assertIsNone(feed.license_id) |
| 282 | + |
| 283 | + def test_empty_license_url_returns_none(self): |
| 284 | + feed = self._make_feed(license_url="") |
| 285 | + result = assign_license_by_url(feed, MagicMock()) |
| 286 | + self.assertIsNone(result) |
| 287 | + |
| 288 | + # --- No match --- |
| 289 | + |
| 290 | + @patch("shared.common.license_utils.resolve_license") |
| 291 | + def test_no_match_returns_none(self, mock_resolve): |
| 292 | + mock_resolve.return_value = [] |
| 293 | + feed = self._make_feed() |
| 294 | + result = assign_license_by_url(feed, MagicMock()) |
| 295 | + self.assertIsNone(result) |
| 296 | + self.assertIsNone(feed.license_id) |
| 297 | + self.assertEqual(feed.feed_license_changes, []) |
| 298 | + |
| 299 | + # --- Multiple matches --- |
| 300 | + |
| 301 | + @patch("shared.common.license_utils.resolve_license") |
| 302 | + def test_multiple_matches_skips_assignment(self, mock_resolve): |
| 303 | + mock_resolve.return_value = [ |
| 304 | + self._make_match("MIT", "fuzzy", 0.96), |
| 305 | + self._make_match("Apache-2.0", "fuzzy", 0.94), |
| 306 | + ] |
| 307 | + feed = self._make_feed() |
| 308 | + result = assign_license_by_url(feed, MagicMock()) |
| 309 | + self.assertIsNone(result) |
| 310 | + self.assertIsNone(feed.license_id) |
| 311 | + self.assertEqual(feed.feed_license_changes, []) |
| 312 | + |
| 313 | + # --- Single exact match — auto-verified --- |
| 314 | + |
| 315 | + @patch("shared.common.license_utils.resolve_license") |
| 316 | + def test_exact_match_assigns_and_marks_verified(self, mock_resolve): |
| 317 | + match = self._make_match("MIT", "exact", 1.0) |
| 318 | + mock_resolve.return_value = [match] |
| 319 | + feed = self._make_feed() |
| 320 | + |
| 321 | + result = assign_license_by_url(feed, MagicMock()) |
| 322 | + |
| 323 | + self.assertEqual(result, match) |
| 324 | + self.assertEqual(feed.license_id, "MIT") |
| 325 | + self.assertEqual(len(feed.feed_license_changes), 1) |
| 326 | + self.assertTrue(feed.feed_license_changes[0].verified) |
| 327 | + |
| 328 | + @patch("shared.common.license_utils.resolve_license") |
| 329 | + def test_heuristic_high_confidence_assigns_and_marks_verified(self, mock_resolve): |
| 330 | + match = self._make_match("CC-BY-4.0", "heuristic", 0.99) |
| 331 | + mock_resolve.return_value = [match] |
| 332 | + feed = self._make_feed() |
| 333 | + |
| 334 | + result = assign_license_by_url(feed, MagicMock()) |
| 335 | + |
| 336 | + self.assertEqual(result, match) |
| 337 | + self.assertEqual(feed.license_id, "CC-BY-4.0") |
| 338 | + self.assertTrue(feed.feed_license_changes[0].verified) |
| 339 | + |
| 340 | + @patch("shared.common.license_utils.resolve_license") |
| 341 | + def test_threshold_boundary_095_marks_verified(self, mock_resolve): |
| 342 | + match = self._make_match("ODbL-1.0", "heuristic", 0.95) |
| 343 | + mock_resolve.return_value = [match] |
| 344 | + feed = self._make_feed() |
| 345 | + |
| 346 | + assign_license_by_url(feed, MagicMock()) |
| 347 | + |
| 348 | + self.assertTrue(feed.feed_license_changes[0].verified) |
| 349 | + |
| 350 | + # --- Fuzzy / low-confidence match — unverified --- |
| 351 | + |
| 352 | + @patch("shared.common.license_utils.resolve_license") |
| 353 | + def test_fuzzy_match_assigns_but_unverified(self, mock_resolve): |
| 354 | + match = self._make_match("MIT", "fuzzy", 0.94) |
| 355 | + mock_resolve.return_value = [match] |
| 356 | + feed = self._make_feed() |
| 357 | + |
| 358 | + result = assign_license_by_url(feed, MagicMock()) |
| 359 | + |
| 360 | + self.assertEqual(result, match) |
| 361 | + self.assertEqual(feed.license_id, "MIT") |
| 362 | + self.assertFalse(feed.feed_license_changes[0].verified) |
| 363 | + |
| 364 | + @patch("shared.common.license_utils.resolve_license") |
| 365 | + def test_below_threshold_unverified(self, mock_resolve): |
| 366 | + match = self._make_match("MIT", "heuristic", 0.80) |
| 367 | + mock_resolve.return_value = [match] |
| 368 | + feed = self._make_feed() |
| 369 | + |
| 370 | + assign_license_by_url(feed, MagicMock()) |
| 371 | + |
| 372 | + self.assertFalse(feed.feed_license_changes[0].verified) |
| 373 | + |
| 374 | + # --- Duplicate assignment guard --- |
| 375 | + |
| 376 | + @patch("shared.common.license_utils.resolve_license") |
| 377 | + def test_same_license_id_no_new_audit_row(self, mock_resolve): |
| 378 | + match = self._make_match("MIT", "exact", 1.0) |
| 379 | + mock_resolve.return_value = [match] |
| 380 | + feed = self._make_feed(license_id="MIT") # already assigned |
| 381 | + |
| 382 | + result = assign_license_by_url(feed, MagicMock()) |
| 383 | + |
| 384 | + self.assertEqual(result, match) |
| 385 | + self.assertEqual(feed.license_id, "MIT") |
| 386 | + self.assertEqual(feed.feed_license_changes, []) # no new audit row |
| 387 | + |
| 388 | + # --- only_if_single=False allows multiple matches --- |
| 389 | + |
| 390 | + @patch("shared.common.license_utils.resolve_license") |
| 391 | + def test_only_if_single_false_assigns_best_match(self, mock_resolve): |
| 392 | + best = self._make_match("MIT", "fuzzy", 0.97) |
| 393 | + second = self._make_match("Apache-2.0", "fuzzy", 0.94) |
| 394 | + mock_resolve.return_value = [best, second] |
| 395 | + feed = self._make_feed() |
| 396 | + |
| 397 | + result = assign_license_by_url(feed, MagicMock(), only_if_single=False) |
| 398 | + |
| 399 | + self.assertEqual(result, best) |
| 400 | + self.assertEqual(feed.license_id, "MIT") |
0 commit comments