""" Tests for Token Clone Scanner (clone_scanner.py) Tests the core logic: string similarity, name similarity scoring, risk labeling, data models, and fast_check surrogate. Network tests are skipped in offline mode. """ import os import sys import unittest # Add parent to path sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from app.clone_scanner import ( CloneReport, CloneScanner, SimilarToken, name_similarity_score, string_similarity, ) class TestStringHelpers(unittest.TestCase): """Core string similarity utilities.""" def test_identical_strings(self): self.assertEqual(string_similarity("Pepe", "Pepe"), 1.0) def test_case_insensitive(self): self.assertAlmostEqual(string_similarity("Pepe", "pepe"), 1.0) def test_completely_different(self): self.assertAlmostEqual(string_similarity("Pepe", "Bitcoin"), 0.0, places=1) def test_partial_match(self): sim = string_similarity("Pepe", "Pepe 2.0") self.assertGreater(sim, 0.3) self.assertLess(sim, 1.0) def test_empty_strings(self): self.assertEqual(string_similarity("", "Pepe"), 0.0) self.assertEqual(string_similarity("Pepe", ""), 0.0) self.assertEqual(string_similarity("", ""), 0.0) class TestNameSimilarity(unittest.TestCase): """Name similarity scoring with clone patterns.""" def test_identical_names(self): self.assertEqual(name_similarity_score("Pepe", "Pepe"), 1.0) def test_prefix_clone(self): """'Baby Pepe' should score high vs 'Pepe'.""" score = name_similarity_score("Baby Pepe", "Pepe") self.assertGreaterEqual(score, 0.65) def test_suffix_clone(self): """'Pepe v2' should score very high vs 'Pepe'.""" score = name_similarity_score("Pepe v2", "Pepe") self.assertGreaterEqual(score, 0.85) def test_known_v2_pattern(self): score = name_similarity_score("SafeMoon", "SafeMoon 2.0") self.assertGreaterEqual(score, 0.85) def test_different_tokens(self): score = name_similarity_score("Bitcoin", "Pepe") self.assertLess(score, 0.3) class TestCloneReport(unittest.TestCase): """CloneReport data model and helpers.""" def test_to_dict_structure(self): report = CloneReport( token_address="0xabc123", chain="ethereum", name="TestToken", symbol="TEST", clone_score=45.5, risk_label="medium", similar_tokens=[ SimilarToken( address="0xdef456", chain="ethereum", name="TestToken Clone", symbol="TSTC", name_similarity=0.85, symbol_similarity=0.6, ) ], matched_keywords=["pepe"], unverified_contract=True, same_deployer_other_tokens=12, ) d = report.to_dict() self.assertEqual(d["token_address"], "0xabc123") self.assertEqual(d["clone_score"], 45.5) self.assertEqual(d["risk_label"], "medium") self.assertEqual(len(d["similar_tokens"]), 1) self.assertEqual(d["matched_keywords"], ["pepe"]) self.assertTrue(d["unverified_contract"]) self.assertEqual(d["same_deployer_other_tokens"], 12) # Check signals sub-dict self.assertIn("bytecode_similarity", d["signals"]) self.assertIn("name_similarity", d["signals"]) self.assertIn("deployer_risk", d["signals"]) self.assertIn("metadata_risk", d["signals"]) def test_summary_format(self): report = CloneReport( token_address="0xabc123def4567890123456789012345678901234", chain="ethereum", name="ShadyToken", symbol="SHADY", clone_score=72.3, risk_label="high", similar_tokens=[ SimilarToken( address="0xdead", chain="ethereum", name="RealToken", symbol="REAL", name_similarity=0.9, ) ], matched_keywords=["pepe", "moon"], unverified_contract=True, ) summary = report.summary() self.assertIn("HIGH", summary) self.assertIn("72", summary) self.assertIn("ShadyToken", summary) self.assertIn("1 similar tokens", summary) def test_risk_label_none(self): report = CloneReport(token_address="0xaaa", chain="ethereum", clone_score=0.0) scanner = CloneScanner() report.risk_label = scanner._label_risk(0.0) self.assertEqual(report.risk_label, "none") def test_risk_label_critical(self): report = CloneReport(token_address="0xaaa", chain="ethereum", clone_score=85.0) scanner = CloneScanner() report.risk_label = scanner._label_risk(85.0) self.assertEqual(report.risk_label, "critical") def test_risk_label_brackets(self): report = CloneReport(token_address="0xaaa", chain="ethereum", clone_score=25.0) scanner = CloneScanner() report.risk_label = scanner._label_risk(25.0) self.assertEqual(report.risk_label, "medium") class TestSimilarToken(unittest.TestCase): """SimilarToken helper properties.""" def test_overall_similarity(self): t = SimilarToken( address="0xabc", chain="ethereum", name="TokenA", symbol="TKA", name_similarity=0.9, symbol_similarity=0.5, ) self.assertEqual(t.overall_similarity, 0.9) def test_overall_uses_symbol_when_higher(self): t = SimilarToken( address="0xabc", chain="ethereum", name="TokenA", symbol="TKA", name_similarity=0.4, symbol_similarity=0.95, ) self.assertEqual(t.overall_similarity, 0.95) class TestScannerScoring(unittest.TestCase): """Unit tests for scoring methods (no network).""" def setUp(self): self.scanner = CloneScanner() def test_score_bytecode_unverified(self): score = self.scanner._score_bytecode_risk(True, {}) self.assertEqual(score, 40) # 40 for unverified def test_score_bytecode_verified(self): score = self.scanner._score_bytecode_risk(False, {"other_unverified": 0}) self.assertEqual(score, 0.0) def test_score_bytecode_mass_deployer(self): score = self.scanner._score_bytecode_risk(True, {"other_unverified": 10, "other_tokens": 30}) # 40 (unverified) + 20 (many unverified) + 15 (mass deployer) = 75 self.assertEqual(score, 75) def test_score_name_no_keywords(self): score = self.scanner._score_name_similarity([], []) self.assertEqual(score, 0.0) def test_score_name_with_keywords(self): score = self.scanner._score_name_similarity([], ["pepe", "moon"]) self.assertEqual(score, 25) def test_score_name_with_high_similarity(self): t = SimilarToken("0x1", "eth", "Clone", "CLO", name_similarity=0.95) score = self.scanner._score_name_similarity([t], []) # 1 high-sim match = 20 self.assertEqual(score, 20) def test_score_deployer_low(self): score = self.scanner._score_deployer_risk({"other_tokens": 2}) self.assertEqual(score, 0.0) def test_score_deployer_medium(self): score = self.scanner._score_deployer_risk({"other_tokens": 10}) self.assertEqual(score, 15) def test_score_deployer_high(self): score = self.scanner._score_deployer_risk({"other_tokens": 30}) self.assertEqual(score, 30) def test_score_deployer_critical(self): score = self.scanner._score_deployer_risk({"other_tokens": 100}) self.assertEqual(score, 40) def test_score_deployer_clone_flag(self): score = self.scanner._score_deployer_risk({"other_tokens": 5, "clone_deployer": True}) # 5 other tokens = no bonus (needs >20), +30 clone flag = 30 self.assertEqual(score, 30) def test_score_metadata_missing(self): score = self.scanner._score_metadata_risk({}, [], []) # Empty dict = no name or symbol = 20+20 = 40 self.assertEqual(score, 40) def test_score_metadata_with_social(self): score = self.scanner._score_metadata_risk({"name": "T", "symbol": "S", "social": {"twitter": "@x"}}, [], []) self.assertEqual(score, 10) # missing website def test_score_metadata_full(self): score = self.scanner._score_metadata_risk( { "name": "Test", "symbol": "TST", "social": {"twitter": "@x", "telegram": "@g"}, "website": "https://t.com", }, [], [], ) self.assertEqual(score, 0.0) def test_compute_composite_score(self): report = CloneReport( token_address="0x1", chain="eth", bytecode_similarity_score=100, name_similarity_score=50, deployer_risk_score=0, metadata_risk_score=0, ) # 0.25*100 + 0.35*50 + 0.25*0 + 0.15*0 = 25 + 17.5 = 42.5 self.assertEqual(self.scanner._compute_clone_score(report), 42.5) def test_well_known_by_name(self): self.assertTrue(self.scanner._is_well_known("Bitcoin", "BTC")) self.assertTrue(self.scanner._is_well_known("Ethereum", "ETH")) def test_well_known_by_symbol(self): self.assertTrue(self.scanner._is_well_known("Random", "ETH")) def test_not_well_known(self): self.assertFalse(self.scanner._is_well_known("ShadyCoin", "SHADY")) def test_valid_evm_address(self): self.assertTrue(self.scanner._validate_address("0x1234567890abcdef1234567890abcdef12345678", "ethereum")) def test_valid_solana_address(self): self.assertTrue(self.scanner._validate_address("So11111111111111111111111111111111111111112", "solana")) def test_invalid_address(self): self.assertFalse(self.scanner._validate_address("not-an-address", "ethereum")) if __name__ == "__main__": unittest.main(verbosity=2)