2
0

test_upload.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. """
  2. Unit tests for uploader module.
  3. """
  4. import pytest
  5. from datetime import datetime
  6. from src.uploader import (
  7. CUCalculator,
  8. ContentChecker,
  9. MockUploadClient,
  10. UploadManager,
  11. create_mock_upload_manager,
  12. ChapterUploadData,
  13. UploadResult,
  14. UploadStatus,
  15. CheckStatus,
  16. QuotaInfo
  17. )
  18. class TestCUCalculator:
  19. """Test suite for CUCalculator."""
  20. def test_calculate_cu_basic(self):
  21. """Test basic CU calculation."""
  22. calc = CUCalculator()
  23. # Minimum CU (1-100 chars = 1 CU)
  24. assert calc.calculate_cu("A" * 50) == 1
  25. assert calc.calculate_cu("A" * 100) == 1
  26. # Second CU (101-200 chars = 2 CU)
  27. assert calc.calculate_cu("A" * 101) == 2
  28. assert calc.calculate_cu("A" * 200) == 2
  29. # Third CU (201-300 chars = 3 CU)
  30. assert calc.calculate_cu("A" * 201) == 3
  31. assert calc.calculate_cu("A" * 300) == 3
  32. def test_calculate_cu_examples(self):
  33. """Test examples from the spec."""
  34. calc = CUCalculator()
  35. # "123字" → 2 CU (123/100=1.23→向上取整2)
  36. assert calc.calculate_cu("A" * 123) == 2
  37. # "300字" → 3 CU
  38. assert calc.calculate_cu("A" * 300) == 3
  39. # "99字" → 1 CU
  40. assert calc.calculate_cu("A" * 99) == 1
  41. def test_calculate_cu_empty(self):
  42. """Test CU calculation for empty content."""
  43. calc = CUCalculator()
  44. assert calc.calculate_cu("") == 0
  45. def test_calculate_cu_from_count(self):
  46. """Test CU calculation from character count."""
  47. calc = CUCalculator()
  48. assert calc.calculate_cu_from_count(0) == 0
  49. assert calc.calculate_cu_from_count(150) == 2
  50. assert calc.calculate_cu_from_count(350) == 4
  51. def test_calculate_batch(self):
  52. """Test batch CU calculation."""
  53. calc = CUCalculator()
  54. # (50/100→1) + (150/100→2) = 3
  55. result = calc.calculate_batch(["A" * 50, "A" * 150])
  56. assert result == 3
  57. def test_estimate_cu(self):
  58. """Test CU estimation."""
  59. calc = CUCalculator()
  60. # 10 chapters × 150 chars each = 10 × 2 CU = 20 CU
  61. result = calc.estimate_cu(10, 150)
  62. assert result == 20
  63. def test_get_breakdown(self):
  64. """Test CU calculation breakdown."""
  65. calc = CUCalculator()
  66. breakdown = calc.get_breakdown("A" * 123)
  67. assert breakdown["character_count"] == 123
  68. assert breakdown["cu_cost"] == 2
  69. assert breakdown["efficiency_percentage"] == pytest.approx(61.5, 0.1)
  70. assert breakdown["remaining_characters_in_cu"] == 77
  71. def test_reverse_calculate_max_chars(self):
  72. """Test reverse calculation of max chars."""
  73. calc = CUCalculator()
  74. assert calc.reverse_calculate_max_chars(1) == 100
  75. assert calc.reverse_calculate_max_chars(5) == 500
  76. def test_calculate_cost_summary(self):
  77. """Test cost summary calculation."""
  78. calc = CUCalculator()
  79. contents = ["A" * 50, "A" * 150, "A" * 250]
  80. summary = calc.calculate_cost_summary(contents)
  81. assert summary["total_chapters"] == 3
  82. assert summary["total_cu"] == 6 # 1 + 2 + 3 = 6
  83. assert summary["min_cu"] == 1
  84. assert summary["max_cu"] == 3
  85. def test_custom_divisor(self):
  86. """Test calculator with custom divisor."""
  87. calc = CUCalculator(divisor=50)
  88. # With divisor=50: 1-50=1 CU, 51-100=2 CU
  89. assert calc.calculate_cu("A" * 25) == 1
  90. assert calc.calculate_cu("A" * 50) == 1
  91. assert calc.calculate_cu("A" * 51) == 2
  92. assert calc.calculate_cu("A" * 100) == 2
  93. class TestContentChecker:
  94. """Test suite for ContentChecker."""
  95. def test_check_normal_content(self):
  96. """Test checking normal, compliant content."""
  97. checker = ContentChecker()
  98. result = checker.check_chapter("Test Title", "This is normal content.")
  99. assert result.passed is True
  100. assert result.status == CheckStatus.PASSED
  101. assert len(result.issues) == 0
  102. def test_check_empty_title(self):
  103. """Test checking chapter with empty title."""
  104. checker = ContentChecker()
  105. result = checker.check_chapter("", "Content here")
  106. assert result.passed is False
  107. assert len(result.issues) > 0
  108. assert result.issues[0].type == "empty_title"
  109. def test_check_sensitive_words(self):
  110. """Test checking for sensitive words."""
  111. checker = ContentChecker()
  112. result = checker.check_chapter("Normal Title", "This contains 暴力 content.")
  113. assert result.passed is False
  114. assert any(i.type == "sensitive_word" for i in result.issues)
  115. def test_check_content_too_short(self):
  116. """Test checking content that's too short."""
  117. checker = ContentChecker(min_chapter_length=50)
  118. result = checker.check_chapter("Title", "Short")
  119. assert result.passed is False
  120. assert any(i.type == "content_too_short" for i in result.issues)
  121. def test_check_content_too_long(self):
  122. """Test checking content that's too long."""
  123. checker = ContentChecker(max_chapter_length=100)
  124. result = checker.check_chapter("Title", "A" * 101)
  125. assert result.passed is False
  126. assert any(i.type == "content_too_long" for i in result.issues)
  127. def test_check_placeholders(self):
  128. """Test checking for unreplaced placeholders."""
  129. checker = ContentChecker()
  130. result = checker.check_chapter("Title", "This has [TERM_test] placeholder.")
  131. # Bracket-style placeholders are checked
  132. assert len(result.warnings) > 0 or len(result.issues) > 0
  133. def test_check_strict_mode(self):
  134. """Test strict mode where warnings become errors."""
  135. checker = ContentChecker()
  136. result = checker.check_chapter("Title", "This has [TERM_test] placeholder.", strict_mode=True)
  137. # In strict mode, warnings become errors
  138. assert result.passed is False or len(result.issues) > 0
  139. def test_check_all_chapters(self):
  140. """Test checking multiple chapters."""
  141. checker = ContentChecker()
  142. chapters = [
  143. {"title": "Chapter 1", "content": "Normal content."},
  144. {"title": "Chapter 2", "content": "Content with 暴力."}
  145. ]
  146. results = checker.check_all_chapters(chapters)
  147. assert len(results) == 2
  148. assert results["ch_0"].passed is True
  149. assert results["ch_1"].passed is False
  150. def test_add_sensitive_word(self):
  151. """Test adding custom sensitive word."""
  152. checker = ContentChecker()
  153. checker.add_sensitive_word("testword")
  154. assert "testword" in checker.sensitive_words
  155. def test_remove_sensitive_word(self):
  156. """Test removing sensitive word."""
  157. checker = ContentChecker()
  158. checker.remove_sensitive_word("暴力")
  159. assert "暴力" not in checker.sensitive_words
  160. def test_set_sensitive_words(self):
  161. """Test replacing sensitive words list."""
  162. checker = ContentChecker()
  163. checker.set_sensitive_words({"custom1", "custom2"})
  164. assert "custom1" in checker.sensitive_words
  165. assert "暴力" not in checker.sensitive_words
  166. class TestMockUploadClient:
  167. """Test suite for MockUploadClient."""
  168. def test_upload_single_chapter(self):
  169. """Test uploading a single chapter."""
  170. client = MockUploadClient()
  171. result = client.upload_chapter(
  172. "work123",
  173. {
  174. "title": "Chapter 1",
  175. "content": "A" * 150,
  176. "index": 0
  177. }
  178. )
  179. assert result.success is True
  180. assert result.cu_consumed == 2 # 150/100 → 2 CU
  181. def test_batch_upload(self):
  182. """Test batch upload."""
  183. client = MockUploadClient()
  184. chapters = [
  185. {"title": "Ch1", "content": "A" * 50, "index": 0},
  186. {"title": "Ch2", "content": "A" * 150, "index": 1}
  187. ]
  188. result = client.batch_upload("work123", chapters)
  189. assert result.total_chapters == 2
  190. assert result.successful == 2
  191. assert result.failed == 0
  192. assert result.status == UploadStatus.COMPLETED
  193. def test_check_quota(self):
  194. """Test quota checking."""
  195. client = MockUploadClient()
  196. quota = client.check_quota()
  197. assert quota.total_cu > 0
  198. assert quota.remaining_cu > 0
  199. def test_health_check(self):
  200. """Test health check."""
  201. client = MockUploadClient()
  202. assert client.health_check() is True
  203. class TestUploadManager:
  204. """Test suite for UploadManager."""
  205. def test_upload_workflow_dry_run(self):
  206. """Test upload workflow in dry run mode."""
  207. mock_client = MockUploadClient()
  208. manager = UploadManager(mock_client, dry_run=True)
  209. chapters = [
  210. {"title": "Chapter 1", "content": "This is chapter one.", "index": 0},
  211. {"title": "Chapter 2", "content": "This is chapter two.", "index": 1}
  212. ]
  213. result = manager.upload_workflow("work123", chapters)
  214. assert result.status == UploadStatus.COMPLETED
  215. assert len(result.upload_results) == 2
  216. def test_upload_workflow_with_content_check_fail(self):
  217. """Test workflow with content check failure."""
  218. mock_client = MockUploadClient()
  219. manager = UploadManager(mock_client, strict_mode=True)
  220. # Content with sensitive word will fail
  221. chapters = [
  222. {"title": "", "content": "Content with empty title causes failure", "index": 0}
  223. ]
  224. result = manager.upload_workflow("work123", chapters)
  225. # Empty title causes content check failure in strict mode
  226. assert result.status == UploadStatus.FAILED or len(result.upload_results) == 0
  227. def test_estimate_cu(self):
  228. """Test CU estimation."""
  229. mock_client = MockUploadClient()
  230. manager = UploadManager(mock_client)
  231. chapters = [
  232. {"content": "A" * 50},
  233. {"content": "A" * 150}
  234. ]
  235. estimate = manager.estimate_cu(chapters)
  236. assert estimate["total_cu"] == 3 # 1 + 2
  237. def test_preview_upload(self):
  238. """Test upload preview."""
  239. mock_client = MockUploadClient()
  240. manager = UploadManager(mock_client)
  241. chapters = [
  242. {"title": "Ch1", "content": "Content 1"},
  243. {"title": "Ch2", "content": "Content 2"}
  244. ]
  245. preview = manager.preview_upload("work123", chapters)
  246. assert preview["total_chapters"] == 2
  247. assert "content_check" in preview
  248. assert "cu_estimate" in preview
  249. def test_validate_before_upload(self):
  250. """Test pre-upload validation."""
  251. mock_client = MockUploadClient()
  252. manager = UploadManager(mock_client)
  253. # Invalid chapters (missing title, empty content)
  254. chapters = [
  255. {"content": "Content without title"},
  256. {"title": "Title without content", "content": ""}
  257. ]
  258. validation = manager.validate_before_upload(chapters)
  259. # Should have errors or warnings
  260. assert validation["has_errors"] is True or validation["has_warnings"] is True
  261. assert len(validation["errors"]) + len(validation["warnings"]) >= 2
  262. def test_check_quota(self):
  263. """Test checking quota through manager."""
  264. mock_client = MockUploadClient()
  265. manager = UploadManager(mock_client)
  266. quota_info = manager.check_quota()
  267. assert "total_cu" in quota_info
  268. assert "remaining_cu" in quota_info
  269. class TestChapterUploadData:
  270. """Test suite for ChapterUploadData model."""
  271. def test_content_length(self):
  272. """Test content_length property."""
  273. data = ChapterUploadData(
  274. "ch1",
  275. "work1",
  276. "Title",
  277. "Content here",
  278. 0
  279. )
  280. assert data.content_length == 12
  281. def test_word_count(self):
  282. """Test word_count property."""
  283. data = ChapterUploadData(
  284. "ch1",
  285. "work1",
  286. "Title",
  287. "Content here",
  288. 0
  289. )
  290. # word_count removes newlines and spaces
  291. assert data.word_count > 0 # Should count non-whitespace characters
  292. class TestUploadResult:
  293. """Test suite for UploadResult model."""
  294. def test_success_result(self):
  295. """Test successful upload result."""
  296. result = UploadResult(
  297. success=True,
  298. chapter_id="ch1",
  299. server_chapter_id="srv_ch1",
  300. cu_consumed=2
  301. )
  302. assert result.success is True
  303. assert result.cu_consumed == 2
  304. def test_failed_result(self):
  305. """Test failed upload result."""
  306. result = UploadResult(
  307. success=False,
  308. chapter_id="ch1",
  309. error_message="Upload failed"
  310. )
  311. assert result.success is False
  312. assert result.error_message == "Upload failed"
  313. class TestQuotaInfo:
  314. """Test suite for QuotaInfo model."""
  315. def test_quota_calculations(self):
  316. """Test quota calculation properties."""
  317. quota = QuotaInfo(
  318. total_cu=1000,
  319. used_cu=300,
  320. remaining_cu=700
  321. )
  322. assert quota.usage_percentage == 0.3
  323. assert quota.is_exceeded is False
  324. def test_quota_exceeded(self):
  325. """Test exceeded quota."""
  326. quota = QuotaInfo(
  327. total_cu=1000,
  328. used_cu=1000,
  329. remaining_cu=0
  330. )
  331. assert quota.is_exceeded is True
  332. def test_create_mock_upload_manager():
  333. """Test factory function for mock upload manager."""
  334. manager = create_mock_upload_manager()
  335. assert isinstance(manager, UploadManager)
  336. # Check that it uses a mock client (has upload_count attribute)
  337. assert hasattr(manager.client, "upload_count")