"""Tests for duplicate_rate computation and API response.""" import json import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest # Mock cv2 and numpy before any imports that need them sys.modules.setdefault("cv2", MagicMock()) ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "apps" / "api")) sys.path.insert(0, str(ROOT / "packages")) sys.path.insert(0, str(ROOT / "apps" / "worker")) class TestComputeDuplicateRate: """Test VideoDeduplicator.compute_duplicate_rate.""" def _make_fingerprint(self, md5="abc123", phashes=None, duration_ms=10000): from video_processing.dedup import VideoFingerprint return VideoFingerprint( md5=md5, keyframe_phashes=phashes or ["ff00ff00ff00ff00"], color_histograms=[], duration=duration_ms, resolution=(1920, 1080), ) def _make_existing_video(self, vid, fingerprint_dict): from packages.domain import GeneratedVideo return GeneratedVideo( id=vid, project_id="proj1", generation_task_id="task1", name=f"video-{vid}", file_url=f"https://example.com/{vid}.mp4", file_size=1000, duration=10.0, width=1920, height=1080, fps=25.0, video_fingerprint=fingerprint_dict, ) def test_no_existing_videos_returns_zero(self): from video_processing.dedup import VideoDeduplicator deduplicator = VideoDeduplicator() fingerprint = self._make_fingerprint() session = MagicMock() with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo: mock_repo = MockRepo.return_value mock_repo.list_by_project.return_value = [] rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session) assert rate["duplicate_rate"] == 0.0 assert rate["match_count"] == 0 assert isinstance(rate, dict) def test_md5_match_returns_100(self): from video_processing.dedup import VideoDeduplicator deduplicator = VideoDeduplicator() fingerprint = self._make_fingerprint(md5="exact_md5") session = MagicMock() existing = self._make_existing_video("vid2", {"md5": "exact_md5", "keyframe_phashes": ["aa"]}) with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo: mock_repo = MockRepo.return_value mock_repo.list_by_project.return_value = [existing] rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session) assert rate["duplicate_rate"] == 100.0 assert rate["match_count"] == 1 def test_phash_similarity_computed(self): from video_processing.dedup import VideoDeduplicator deduplicator = VideoDeduplicator() # Two very similar phashes fingerprint = self._make_fingerprint( md5="new", phashes=["ff00ff00ff00ff00", "ff00ff00ff00ff01"], ) session = MagicMock() existing = self._make_existing_video( "vid2", {"md5": "other", "keyframe_phashes": ["ff00ff00ff00ff00", "ff00ff00ff00ff02"]}, ) with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo: mock_repo = MockRepo.return_value mock_repo.list_by_project.return_value = [existing] mock_repo._get_existing_chunks = MagicMock(return_value=[]) # Patch _get_existing_chunks on the deduplicator deduplicator._get_existing_chunks = MagicMock(return_value=[]) rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session) # With identical phashes, frame_match_rate should be high assert rate["duplicate_rate"] >= 0.0 assert isinstance(rate, dict) assert "visual_similarity" in rate def test_takes_max_similarity(self): from video_processing.dedup import VideoDeduplicator deduplicator = VideoDeduplicator() fingerprint = self._make_fingerprint( md5="new", phashes=["aa00aa00aa00aa00"], ) session = MagicMock() # Two existing videos with different phashes existing1 = self._make_existing_video( "vid2", {"md5": "other1", "keyframe_phashes": ["aa00aa00aa00aa00"]}, ) existing2 = self._make_existing_video( "vid3", {"md5": "other2", "keyframe_phashes": ["ff00ff00ff00ff00"]}, ) with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo: mock_repo = MockRepo.return_value mock_repo.list_by_project.return_value = [existing1, existing2] deduplicator._get_existing_chunks = MagicMock(return_value=[]) rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session) # Should take the max across all videos assert rate["duplicate_rate"] >= 0.0 assert isinstance(rate["duplicate_rate"], float) def test_user_id_scope_cross_project(self): from video_processing.dedup import VideoDeduplicator deduplicator = VideoDeduplicator() fingerprint = self._make_fingerprint(md5="exact_md5_x") session = MagicMock() existing = self._make_existing_video("vid2", {"md5": "exact_md5_x", "keyframe_phashes": ["aa"]}) with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo: mock_repo = MockRepo.return_value mock_repo.list_by_user.return_value = [existing] rate = deduplicator.compute_duplicate_rate( fingerprint, "proj1", "vid1", session, scope="user", user_id="user1", ) # Should use list_by_user and find the match mock_repo.list_by_user.assert_called_once_with("user1") assert rate["duplicate_rate"] == 100.0 def test_return_dict_structure(self): """compute_duplicate_rate returns dict with three fields.""" from video_processing.dedup import VideoDeduplicator deduplicator = VideoDeduplicator() fingerprint = self._make_fingerprint() session = MagicMock() with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo: mock_repo = MockRepo.return_value mock_repo.list_by_project.return_value = [] rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session) assert isinstance(rate, dict) assert "duplicate_rate" in rate assert "visual_similarity" in rate assert "match_count" in rate assert isinstance(rate["duplicate_rate"], float) assert isinstance(rate["visual_similarity"], float) assert isinstance(rate["match_count"], int) def test_backward_compat_no_scope(self): """Not passing scope defaults to project-level.""" from video_processing.dedup import VideoDeduplicator deduplicator = VideoDeduplicator() fingerprint = self._make_fingerprint() session = MagicMock() with patch("video_processing.dedup.SQLAlchemyGeneratedVideoRepository") as MockRepo: mock_repo = MockRepo.return_value mock_repo.list_by_project.return_value = [] rate = deduplicator.compute_duplicate_rate(fingerprint, "proj1", "vid1", session) mock_repo.list_by_project.assert_called_once_with("proj1") assert rate["duplicate_rate"] == 0.0