""" Unit tests for uploader module. """ import pytest from datetime import datetime from src.uploader import ( CUCalculator, ContentChecker, MockUploadClient, UploadManager, create_mock_upload_manager, ChapterUploadData, UploadResult, UploadStatus, CheckStatus, QuotaInfo ) class TestCUCalculator: """Test suite for CUCalculator.""" def test_calculate_cu_basic(self): """Test basic CU calculation.""" calc = CUCalculator() # Minimum CU (1-100 chars = 1 CU) assert calc.calculate_cu("A" * 50) == 1 assert calc.calculate_cu("A" * 100) == 1 # Second CU (101-200 chars = 2 CU) assert calc.calculate_cu("A" * 101) == 2 assert calc.calculate_cu("A" * 200) == 2 # Third CU (201-300 chars = 3 CU) assert calc.calculate_cu("A" * 201) == 3 assert calc.calculate_cu("A" * 300) == 3 def test_calculate_cu_examples(self): """Test examples from the spec.""" calc = CUCalculator() # "123字" → 2 CU (123/100=1.23→向上取整2) assert calc.calculate_cu("A" * 123) == 2 # "300字" → 3 CU assert calc.calculate_cu("A" * 300) == 3 # "99字" → 1 CU assert calc.calculate_cu("A" * 99) == 1 def test_calculate_cu_empty(self): """Test CU calculation for empty content.""" calc = CUCalculator() assert calc.calculate_cu("") == 0 def test_calculate_cu_from_count(self): """Test CU calculation from character count.""" calc = CUCalculator() assert calc.calculate_cu_from_count(0) == 0 assert calc.calculate_cu_from_count(150) == 2 assert calc.calculate_cu_from_count(350) == 4 def test_calculate_batch(self): """Test batch CU calculation.""" calc = CUCalculator() # (50/100→1) + (150/100→2) = 3 result = calc.calculate_batch(["A" * 50, "A" * 150]) assert result == 3 def test_estimate_cu(self): """Test CU estimation.""" calc = CUCalculator() # 10 chapters × 150 chars each = 10 × 2 CU = 20 CU result = calc.estimate_cu(10, 150) assert result == 20 def test_get_breakdown(self): """Test CU calculation breakdown.""" calc = CUCalculator() breakdown = calc.get_breakdown("A" * 123) assert breakdown["character_count"] == 123 assert breakdown["cu_cost"] == 2 assert breakdown["efficiency_percentage"] == pytest.approx(61.5, 0.1) assert breakdown["remaining_characters_in_cu"] == 77 def test_reverse_calculate_max_chars(self): """Test reverse calculation of max chars.""" calc = CUCalculator() assert calc.reverse_calculate_max_chars(1) == 100 assert calc.reverse_calculate_max_chars(5) == 500 def test_calculate_cost_summary(self): """Test cost summary calculation.""" calc = CUCalculator() contents = ["A" * 50, "A" * 150, "A" * 250] summary = calc.calculate_cost_summary(contents) assert summary["total_chapters"] == 3 assert summary["total_cu"] == 6 # 1 + 2 + 3 = 6 assert summary["min_cu"] == 1 assert summary["max_cu"] == 3 def test_custom_divisor(self): """Test calculator with custom divisor.""" calc = CUCalculator(divisor=50) # With divisor=50: 1-50=1 CU, 51-100=2 CU assert calc.calculate_cu("A" * 25) == 1 assert calc.calculate_cu("A" * 50) == 1 assert calc.calculate_cu("A" * 51) == 2 assert calc.calculate_cu("A" * 100) == 2 class TestContentChecker: """Test suite for ContentChecker.""" def test_check_normal_content(self): """Test checking normal, compliant content.""" checker = ContentChecker() result = checker.check_chapter("Test Title", "This is normal content.") assert result.passed is True assert result.status == CheckStatus.PASSED assert len(result.issues) == 0 def test_check_empty_title(self): """Test checking chapter with empty title.""" checker = ContentChecker() result = checker.check_chapter("", "Content here") assert result.passed is False assert len(result.issues) > 0 assert result.issues[0].type == "empty_title" def test_check_sensitive_words(self): """Test checking for sensitive words.""" checker = ContentChecker() result = checker.check_chapter("Normal Title", "This contains 暴力 content.") assert result.passed is False assert any(i.type == "sensitive_word" for i in result.issues) def test_check_content_too_short(self): """Test checking content that's too short.""" checker = ContentChecker(min_chapter_length=50) result = checker.check_chapter("Title", "Short") assert result.passed is False assert any(i.type == "content_too_short" for i in result.issues) def test_check_content_too_long(self): """Test checking content that's too long.""" checker = ContentChecker(max_chapter_length=100) result = checker.check_chapter("Title", "A" * 101) assert result.passed is False assert any(i.type == "content_too_long" for i in result.issues) def test_check_placeholders(self): """Test checking for unreplaced placeholders.""" checker = ContentChecker() result = checker.check_chapter("Title", "This has [TERM_test] placeholder.") # Bracket-style placeholders are checked assert len(result.warnings) > 0 or len(result.issues) > 0 def test_check_strict_mode(self): """Test strict mode where warnings become errors.""" checker = ContentChecker() result = checker.check_chapter("Title", "This has [TERM_test] placeholder.", strict_mode=True) # In strict mode, warnings become errors assert result.passed is False or len(result.issues) > 0 def test_check_all_chapters(self): """Test checking multiple chapters.""" checker = ContentChecker() chapters = [ {"title": "Chapter 1", "content": "Normal content."}, {"title": "Chapter 2", "content": "Content with 暴力."} ] results = checker.check_all_chapters(chapters) assert len(results) == 2 assert results["ch_0"].passed is True assert results["ch_1"].passed is False def test_add_sensitive_word(self): """Test adding custom sensitive word.""" checker = ContentChecker() checker.add_sensitive_word("testword") assert "testword" in checker.sensitive_words def test_remove_sensitive_word(self): """Test removing sensitive word.""" checker = ContentChecker() checker.remove_sensitive_word("暴力") assert "暴力" not in checker.sensitive_words def test_set_sensitive_words(self): """Test replacing sensitive words list.""" checker = ContentChecker() checker.set_sensitive_words({"custom1", "custom2"}) assert "custom1" in checker.sensitive_words assert "暴力" not in checker.sensitive_words class TestMockUploadClient: """Test suite for MockUploadClient.""" def test_upload_single_chapter(self): """Test uploading a single chapter.""" client = MockUploadClient() result = client.upload_chapter( "work123", { "title": "Chapter 1", "content": "A" * 150, "index": 0 } ) assert result.success is True assert result.cu_consumed == 2 # 150/100 → 2 CU def test_batch_upload(self): """Test batch upload.""" client = MockUploadClient() chapters = [ {"title": "Ch1", "content": "A" * 50, "index": 0}, {"title": "Ch2", "content": "A" * 150, "index": 1} ] result = client.batch_upload("work123", chapters) assert result.total_chapters == 2 assert result.successful == 2 assert result.failed == 0 assert result.status == UploadStatus.COMPLETED def test_check_quota(self): """Test quota checking.""" client = MockUploadClient() quota = client.check_quota() assert quota.total_cu > 0 assert quota.remaining_cu > 0 def test_health_check(self): """Test health check.""" client = MockUploadClient() assert client.health_check() is True class TestUploadManager: """Test suite for UploadManager.""" def test_upload_workflow_dry_run(self): """Test upload workflow in dry run mode.""" mock_client = MockUploadClient() manager = UploadManager(mock_client, dry_run=True) chapters = [ {"title": "Chapter 1", "content": "This is chapter one.", "index": 0}, {"title": "Chapter 2", "content": "This is chapter two.", "index": 1} ] result = manager.upload_workflow("work123", chapters) assert result.status == UploadStatus.COMPLETED assert len(result.upload_results) == 2 def test_upload_workflow_with_content_check_fail(self): """Test workflow with content check failure.""" mock_client = MockUploadClient() manager = UploadManager(mock_client, strict_mode=True) # Content with sensitive word will fail chapters = [ {"title": "", "content": "Content with empty title causes failure", "index": 0} ] result = manager.upload_workflow("work123", chapters) # Empty title causes content check failure in strict mode assert result.status == UploadStatus.FAILED or len(result.upload_results) == 0 def test_estimate_cu(self): """Test CU estimation.""" mock_client = MockUploadClient() manager = UploadManager(mock_client) chapters = [ {"content": "A" * 50}, {"content": "A" * 150} ] estimate = manager.estimate_cu(chapters) assert estimate["total_cu"] == 3 # 1 + 2 def test_preview_upload(self): """Test upload preview.""" mock_client = MockUploadClient() manager = UploadManager(mock_client) chapters = [ {"title": "Ch1", "content": "Content 1"}, {"title": "Ch2", "content": "Content 2"} ] preview = manager.preview_upload("work123", chapters) assert preview["total_chapters"] == 2 assert "content_check" in preview assert "cu_estimate" in preview def test_validate_before_upload(self): """Test pre-upload validation.""" mock_client = MockUploadClient() manager = UploadManager(mock_client) # Invalid chapters (missing title, empty content) chapters = [ {"content": "Content without title"}, {"title": "Title without content", "content": ""} ] validation = manager.validate_before_upload(chapters) # Should have errors or warnings assert validation["has_errors"] is True or validation["has_warnings"] is True assert len(validation["errors"]) + len(validation["warnings"]) >= 2 def test_check_quota(self): """Test checking quota through manager.""" mock_client = MockUploadClient() manager = UploadManager(mock_client) quota_info = manager.check_quota() assert "total_cu" in quota_info assert "remaining_cu" in quota_info class TestChapterUploadData: """Test suite for ChapterUploadData model.""" def test_content_length(self): """Test content_length property.""" data = ChapterUploadData( "ch1", "work1", "Title", "Content here", 0 ) assert data.content_length == 12 def test_word_count(self): """Test word_count property.""" data = ChapterUploadData( "ch1", "work1", "Title", "Content here", 0 ) # word_count removes newlines and spaces assert data.word_count > 0 # Should count non-whitespace characters class TestUploadResult: """Test suite for UploadResult model.""" def test_success_result(self): """Test successful upload result.""" result = UploadResult( success=True, chapter_id="ch1", server_chapter_id="srv_ch1", cu_consumed=2 ) assert result.success is True assert result.cu_consumed == 2 def test_failed_result(self): """Test failed upload result.""" result = UploadResult( success=False, chapter_id="ch1", error_message="Upload failed" ) assert result.success is False assert result.error_message == "Upload failed" class TestQuotaInfo: """Test suite for QuotaInfo model.""" def test_quota_calculations(self): """Test quota calculation properties.""" quota = QuotaInfo( total_cu=1000, used_cu=300, remaining_cu=700 ) assert quota.usage_percentage == 0.3 assert quota.is_exceeded is False def test_quota_exceeded(self): """Test exceeded quota.""" quota = QuotaInfo( total_cu=1000, used_cu=1000, remaining_cu=0 ) assert quota.is_exceeded is True def test_create_mock_upload_manager(): """Test factory function for mock upload manager.""" manager = create_mock_upload_manager() assert isinstance(manager, UploadManager) # Check that it uses a mock client (has upload_count attribute) assert hasattr(manager.client, "upload_count")