| """Unit tests for cores.osint — URL normalization, source classification, merging.""" |
|
|
| from __future__ import annotations |
|
|
| from cores.osint import ( |
| normalize_url, extract_domain, extract_root_domain, urls_equal, |
| classify_source, SourceType, |
| merge_reverse_results, deduplicate_matches, assign_confidence, |
| ReverseMatch, |
| ) |
|
|
|
|
| class TestUrlNormalization: |
| def test_normalize_lowercases_scheme_host(self): |
| |
| result = normalize_url("HTTP://Example.COM/Path") |
| assert result.startswith("http://example.com") |
|
|
| def test_normalize_strips_default_port(self): |
| assert ":80" not in normalize_url("http://example.com:80/path") |
| assert ":443" not in normalize_url("https://example.com:443/path") |
|
|
| def test_normalize_strips_trailing_slash(self): |
| assert normalize_url("https://example.com/path/") == "https://example.com/path" |
|
|
| def test_normalize_drops_fragment(self): |
| assert "#" not in normalize_url("https://example.com/path#section") |
|
|
| def test_normalize_drops_tracking_params(self): |
| url = "https://example.com/page?utm_source=foo&id=123" |
| norm = normalize_url(url) |
| assert "utm_source" not in norm |
| assert "id=123" in norm |
|
|
| def test_normalize_sorts_query_params(self): |
| url1 = "https://example.com/p?b=2&a=1" |
| url2 = "https://example.com/p?a=1&b=2" |
| assert normalize_url(url1) == normalize_url(url2) |
|
|
| def test_normalize_empty(self): |
| assert normalize_url("") == "" |
|
|
| def test_extract_domain(self): |
| assert extract_domain("https://www.example.com/path") == "www.example.com" |
|
|
| def test_extract_root_domain_simple(self): |
| assert extract_root_domain("https://www.example.com/path") == "example.com" |
|
|
| def test_extract_root_domain_subdomain(self): |
| assert extract_root_domain("https://blog.shop.example.com/p") == "example.com" |
|
|
| def test_extract_root_domain_two_part_tld(self): |
| assert extract_root_domain("https://www.example.co.uk/p") == "example.co.uk" |
|
|
| def test_urls_equal(self): |
| assert urls_equal("https://example.com/p/", "https://example.com/p") |
| assert not urls_equal("https://example.com/p", "https://example.com/q") |
|
|
|
|
| class TestSourceClassification: |
| def test_classify_social_instagram(self): |
| result = classify_source("https://instagram.com/p/abc123") |
| assert result["source_type"] == SourceType.SOCIAL_MEDIA |
| assert result["platform"] == "instagram" |
|
|
| def test_classify_social_twitter(self): |
| result = classify_source("https://twitter.com/user/status/123") |
| assert result["source_type"] == SourceType.SOCIAL_MEDIA |
| assert result["platform"] == "twitter" |
|
|
| def test_classify_news_cnn(self): |
| result = classify_source("https://cnn.com/2024/article") |
| assert result["source_type"] == SourceType.NEWS |
|
|
| def test_classify_marketplace_ebay(self): |
| result = classify_source("https://ebay.com/itm/123") |
| assert result["source_type"] == SourceType.MARKETPLACE |
|
|
| def test_classify_forum_reddit(self): |
| |
| |
| |
| result = classify_source("https://reddit.com/r/topic") |
| assert result["source_type"] in (SourceType.FORUM, SourceType.SOCIAL_MEDIA) |
|
|
| def test_classify_image_host_imgur(self): |
| |
| result = classify_source("https://imgur.com/gallery/abc") |
| assert result["source_type"] in (SourceType.IMAGE_HOST, SourceType.FORUM) |
|
|
| def test_classify_unknown(self): |
| result = classify_source("https://random-site.org/page") |
| assert result["source_type"] == SourceType.UNKNOWN |
|
|
| def test_classify_blog_heuristic(self): |
| |
| result = classify_source("https://blog.example.net/post") |
| assert result["source_type"] == SourceType.BLOG |
|
|
|
|
| class TestResultMerging: |
| def test_merge_deduplicates_by_url(self): |
| provider_results = { |
| "serpapi": [ |
| {"image_url": "https://a.com/img.jpg", |
| "source_page": "https://example.com/page1", |
| "title": "Page 1", "snippet": "Snippet", "thumbnail": ""}, |
| ], |
| "social_lookup": [ |
| {"image_url": "https://a.com/img.jpg", |
| "source_page": "https://example.com/page1/", |
| "title": "Page 1 (longer title)", "snippet": "", "thumbnail": ""}, |
| ], |
| } |
| merged = merge_reverse_results(provider_results) |
| assert len(merged) == 1 |
| assert len(merged[0]["found_by"]) == 2 |
| |
| assert "longer title" in merged[0]["title"] |
|
|
| def test_merge_classifies_sources(self): |
| provider_results = { |
| "p1": [ |
| {"image_url": "", "source_page": "https://instagram.com/p/abc", |
| "title": "IG Post", "snippet": "", "thumbnail": ""}, |
| ], |
| "p2": [ |
| {"image_url": "", "source_page": "https://cnn.com/article", |
| "title": "News Article", "snippet": "", "thumbnail": ""}, |
| ], |
| } |
| merged = merge_reverse_results(provider_results) |
| types = {m["source_type"] for m in merged} |
| assert "social_media" in types |
| assert "news" in types |
|
|
| def test_merge_assigns_confidence(self): |
| provider_results = { |
| "p1": [{"source_page": "https://cnn.com/article", "title": "News", "snippet": "Text"}], |
| } |
| merged = merge_reverse_results(provider_results) |
| assert 0.0 < merged[0]["confidence"] <= 1.0 |
|
|
| def test_merge_sorts_by_confidence(self): |
| provider_results = { |
| "p1": [ |
| {"source_page": "https://cnn.com/a", "title": "News", "snippet": "x"}, |
| {"source_page": "https://random.org/b", "title": "Unknown", "snippet": ""}, |
| ], |
| } |
| merged = merge_reverse_results(provider_results) |
| |
| assert merged[0]["source_type"] == "news" |
|
|
| def test_deduplicate_matches(self): |
| matches = [ |
| {"source_page": "https://example.com/a", "image_url": ""}, |
| {"source_page": "https://example.com/a/", "image_url": ""}, |
| {"source_page": "https://example.com/b", "image_url": ""}, |
| ] |
| deduped = deduplicate_matches(matches) |
| assert len(deduped) == 2 |
|
|
| def test_assign_confidence_multi_provider(self): |
| m = ReverseMatch( |
| image_url="", source_page="https://cnn.com/a", |
| title="t", snippet="s", thumbnail="", |
| source_type="news", found_by=["p1", "p2", "p3"], |
| ) |
| conf = assign_confidence(m) |
| assert conf > 0.5 |
|
|