EmailStorageProcessorTests.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. using System.IO.Abstractions.TestingHelpers;
  2. using System.Text.Json;
  3. using Quadarax.Foundation.Core.Logging;
  4. using qdr.app.studiou.orders2printpack.EmailStorage;
  5. using Moq;
  6. namespace qdr.app.studiou.orders2printpack.Test
  7. {
  8. [TestFixture]
  9. public class EmailStorageProcessorTests
  10. {
  11. private Mock<ILogger> _mockLogger;
  12. private Mock<ILog> _mockLog;
  13. private MockFileSystem _mockFileSystem;
  14. private EmailTemplate _emailTemplate;
  15. private EmailProcessorConfiguration _configuration;
  16. private string _testBasePath;
  17. private EmailStorageProcessor _processor;
  18. [SetUp]
  19. public void SetUp()
  20. {
  21. // Setup mocks
  22. _mockLogger = new Mock<ILogger>();
  23. _mockLog = new Mock<ILog>();
  24. _mockLogger.Setup(x => x.GetLogger(It.IsAny<Type>())).Returns(_mockLog.Object);
  25. // Setup test paths
  26. _testBasePath = @"C:\TestInput";
  27. // Setup mock file system with initial directory structure
  28. _mockFileSystem = new MockFileSystem(new Dictionary<string, MockFileData>
  29. {
  30. // Create base directory
  31. { _testBasePath, new MockDirectoryData() },
  32. // Create a test recipient directory with JPG files
  33. { Path.Combine(_testBasePath, "test@example.com"), new MockDirectoryData() },
  34. { Path.Combine(_testBasePath, "test@example.com", "1001_photo1.jpg"), new MockFileData("fake jpg content") },
  35. { Path.Combine(_testBasePath, "test@example.com", "1001_photo2.jpg"), new MockFileData("fake jpg content") },
  36. { Path.Combine(_testBasePath, "test@example.com", "1002_photo1.jpg"), new MockFileData("fake jpg content") },
  37. });
  38. // Setup email template
  39. _emailTemplate = new EmailTemplate
  40. {
  41. Code = "TEST_TEMPLATE",
  42. Subject = "Your photos are ready - Orders {{ORDERS}}",
  43. Body = "Hello {{RECIPIENT}},\n\nYour photos are ready for download:\n{{DOWNLOAD_LINK}}\nPassword: {{PASSWORD}}\n\nOrders: {{ORDERS}}"
  44. };
  45. // Setup configuration
  46. _configuration = new EmailProcessorConfiguration
  47. {
  48. TssAPIKey = "test-api-key",
  49. TssUrl = "https://test-tss-url.com",
  50. SmtpServer = "smtp.test.com",
  51. SmtpServerPort = 587,
  52. SmtpServerUser = "test@smtp.com",
  53. SmtpServerPassword = "test-password",
  54. SmtpEnableSsl = true
  55. };
  56. }
  57. [TearDown]
  58. public void TearDown()
  59. {
  60. _processor?.Dispose();
  61. }
  62. private EmailStorageProcessor CreateProcessor()
  63. {
  64. return new EmailStorageProcessor(_mockFileSystem, _testBasePath, _emailTemplate, _configuration, _mockLogger.Object);
  65. }
  66. #region Constructor Tests
  67. [Test]
  68. public void Constructor_WithValidParameters_InitializesSuccessfully()
  69. {
  70. // Act
  71. _processor = CreateProcessor();
  72. // Assert
  73. Assert.That(_processor, Is.Not.Null);
  74. Assert.That(_processor.Metadata, Is.Not.Null);
  75. Assert.That(_processor.Metadata.Items, Is.Not.Null);
  76. }
  77. [Test]
  78. public void Constructor_WithNullFileSystem_ThrowsArgumentNullException()
  79. {
  80. // Act & Assert
  81. Assert.Throws<ArgumentNullException>(() =>
  82. new EmailStorageProcessor(null, _testBasePath, _emailTemplate, _configuration, _mockLogger.Object));
  83. }
  84. [Test]
  85. public void Constructor_WithNullPath_ThrowsArgumentNullException()
  86. {
  87. // Act & Assert
  88. Assert.Throws<ArgumentNullException>(() =>
  89. new EmailStorageProcessor(_mockFileSystem, null, _emailTemplate, _configuration, _mockLogger.Object));
  90. }
  91. [Test]
  92. public void Constructor_WithNullEmailTemplate_ThrowsArgumentNullException()
  93. {
  94. // Act & Assert
  95. Assert.Throws<ArgumentNullException>(() =>
  96. new EmailStorageProcessor(_mockFileSystem, _testBasePath, null, _configuration, _mockLogger.Object));
  97. }
  98. [Test]
  99. public void Constructor_WithNullConfiguration_ThrowsArgumentNullException()
  100. {
  101. // Act & Assert
  102. Assert.Throws<ArgumentNullException>(() =>
  103. new EmailStorageProcessor(_mockFileSystem, _testBasePath, _emailTemplate, null, _mockLogger.Object));
  104. }
  105. [Test]
  106. public void Constructor_WithNullLogger_ThrowsArgumentNullException()
  107. {
  108. // Act & Assert
  109. Assert.Throws<ArgumentNullException>(() =>
  110. new EmailStorageProcessor(_mockFileSystem, _testBasePath, _emailTemplate, _configuration, null));
  111. }
  112. [Test]
  113. public void Constructor_CreatesRequiredDirectories()
  114. {
  115. // Act
  116. _processor = CreateProcessor();
  117. // Assert
  118. Assert.That(_mockFileSystem.Directory.Exists(Path.Combine(_testBasePath, ".processing")), Is.True);
  119. Assert.That(_mockFileSystem.Directory.Exists(Path.Combine(_testBasePath, ".sending")), Is.True);
  120. Assert.That(_mockFileSystem.Directory.Exists(Path.Combine(_testBasePath, ".sent")), Is.True);
  121. }
  122. [Test]
  123. public void Constructor_ScansExistingDirectories_CreatesMetadataItems()
  124. {
  125. // Act
  126. _processor = CreateProcessor();
  127. // Assert
  128. Assert.That(_processor.Metadata.Items.Count, Is.EqualTo(1));
  129. var item = _processor.Metadata.Items.First();
  130. Assert.That(item.DirectoryName, Is.EqualTo("test@example.com"));
  131. Assert.That(item.EmailAddress, Is.EqualTo("test@example.com"));
  132. Assert.That(item.State, Is.EqualTo(EmailStateEnum.Ready));
  133. Assert.That(item.InternalFilesCount, Is.EqualTo(3));
  134. Assert.That(item.Orders, Is.EqualTo("1001,1002"));
  135. }
  136. #endregion
  137. #region Property Tests
  138. [Test]
  139. public void CountTotal_ReturnsCorrectCount()
  140. {
  141. // Arrange
  142. _processor = CreateProcessor();
  143. // Act
  144. var count = _processor.CountTotal;
  145. // Assert
  146. Assert.That(count, Is.EqualTo(1));
  147. }
  148. [Test]
  149. public void CountProcessing_WithProcessingItems_ReturnsCorrectCount()
  150. {
  151. // Arrange
  152. _processor = CreateProcessor();
  153. _processor.Metadata.Items.First().State = EmailStateEnum.Processing;
  154. // Act
  155. var count = _processor.CountProcessing;
  156. // Assert
  157. Assert.That(count, Is.EqualTo(1));
  158. }
  159. [Test]
  160. public void CountSending_WithSendingItems_ReturnsCorrectCount()
  161. {
  162. // Arrange
  163. _processor = CreateProcessor();
  164. _processor.Metadata.Items.First().State = EmailStateEnum.Sending;
  165. // Act
  166. var count = _processor.CountSending;
  167. // Assert
  168. Assert.That(count, Is.EqualTo(1));
  169. }
  170. [Test]
  171. public void CountSent_WithSentItems_ReturnsCorrectCount()
  172. {
  173. // Arrange
  174. _processor = CreateProcessor();
  175. _processor.Metadata.Items.First().State = EmailStateEnum.Sent;
  176. // Act
  177. var count = _processor.CountSent;
  178. // Assert
  179. Assert.That(count, Is.EqualTo(1));
  180. }
  181. #endregion
  182. #region ResetItem Tests
  183. [Test]
  184. public void ResetItem_WithValidRecipient_ResetsItemToReadyState()
  185. {
  186. // Arrange
  187. _processor = CreateProcessor();
  188. var item = _processor.Metadata.Items.First();
  189. item.State = EmailStateEnum.Error;
  190. item.FileNameAttachment = "test.zip";
  191. item.SizeAttachment = 1000;
  192. item.UploadPermalink = "http://test.com";
  193. item.DownloadPassword = "password";
  194. item.ProcessedDate = DateTime.Now;
  195. item.SentDate = DateTime.Now;
  196. item.ErrorMessage = "Test error";
  197. // Act
  198. _processor.ResetItem("test@example.com");
  199. // Assert
  200. Assert.That(item.State, Is.EqualTo(EmailStateEnum.Ready));
  201. Assert.That(item.FileNameAttachment, Is.EqualTo(string.Empty));
  202. Assert.That(item.SizeAttachment, Is.EqualTo(0));
  203. Assert.That(item.UploadPermalink, Is.EqualTo(string.Empty));
  204. Assert.That(item.DownloadPassword, Is.EqualTo(string.Empty));
  205. Assert.That(item.ProcessedDate, Is.Null);
  206. Assert.That(item.SentDate, Is.Null);
  207. Assert.That(item.ErrorMessage, Is.Null);
  208. }
  209. [Test]
  210. public void ResetItem_WithNonExistentRecipient_LogsWarning()
  211. {
  212. // Arrange
  213. _processor = CreateProcessor();
  214. // Act
  215. _processor.ResetItem("nonexistent@example.com");
  216. // Assert
  217. _mockLog.Verify(x => x.Log(LogSeverityEnum.Warn, It.IsAny<string>()), Times.Once);
  218. }
  219. [Test]
  220. public void ResetItem_CleansUpAssociatedFiles()
  221. {
  222. // Arrange
  223. _processor = CreateProcessor();
  224. var item = _processor.Metadata.Items.First();
  225. item.FileNameAttachment = "test.zip";
  226. // Create test files in processing, sending, and sent directories
  227. var processingFile = Path.Combine(_testBasePath, ".processing", "test.zip");
  228. var sendingFile = Path.Combine(_testBasePath, ".sending", "test.zip");
  229. var sentFile = Path.Combine(_testBasePath, ".sent", "test.zip");
  230. _mockFileSystem.AddFile(processingFile, new MockFileData("test"));
  231. _mockFileSystem.AddFile(sendingFile, new MockFileData("test"));
  232. _mockFileSystem.AddFile(sentFile, new MockFileData("test"));
  233. // Act
  234. _processor.ResetItem("test@example.com");
  235. // Assert
  236. Assert.That(_mockFileSystem.File.Exists(processingFile), Is.False);
  237. Assert.That(_mockFileSystem.File.Exists(sendingFile), Is.False);
  238. Assert.That(_mockFileSystem.File.Exists(sentFile), Is.False);
  239. }
  240. #endregion
  241. #region ProcessItemAsync Tests
  242. [Test]
  243. public async Task ProcessItemAsync_WithNonExistentRecipient_LogsErrorAndReturns()
  244. {
  245. // Arrange
  246. _processor = CreateProcessor();
  247. // Act
  248. await _processor.ProcessItemAsync("nonexistent@example.com");
  249. // Assert
  250. _mockLog.Verify(x => x.Log(LogSeverityEnum.Error, It.IsAny<string>()), Times.Once);
  251. }
  252. [Test]
  253. public async Task ProcessItemAsync_WithValidRecipient_UpdatesStateToProcessing()
  254. {
  255. // Arrange
  256. _processor = CreateProcessor();
  257. bool stateChangeEventFired = false;
  258. EmailStateEnum capturedState = EmailStateEnum.Ready;
  259. _processor.EmailItemStateChanged += (sender, recipient, state, progress) =>
  260. {
  261. if (state == EmailStateEnum.Processing)
  262. {
  263. stateChangeEventFired = true;
  264. capturedState = state;
  265. }
  266. };
  267. // Act
  268. try
  269. {
  270. await _processor.ProcessItemAsync("test@example.com");
  271. }
  272. catch
  273. {
  274. // Expected to fail due to missing external dependencies, but we want to check state change
  275. }
  276. // Assert
  277. Assert.That(stateChangeEventFired, Is.True);
  278. Assert.That(capturedState, Is.EqualTo(EmailStateEnum.Processing));
  279. }
  280. #endregion
  281. #region ProcessAllAsync Tests
  282. [Test]
  283. public async Task ProcessAllAsync_ProcessesAllReadyItems()
  284. {
  285. // Arrange
  286. _processor = CreateProcessor();
  287. // Add another test item
  288. _mockFileSystem.AddDirectory(Path.Combine(_testBasePath, "test2@example.com"));
  289. _mockFileSystem.AddFile(Path.Combine(_testBasePath, "test2@example.com", "2001_photo1.jpg"), new MockFileData("content"));
  290. // Recreate processor to pick up new directory
  291. _processor.Dispose();
  292. _processor = CreateProcessor();
  293. // Act
  294. try
  295. {
  296. await _processor.ProcessAllAsync();
  297. }
  298. catch
  299. {
  300. // Expected to fail due to missing external dependencies
  301. }
  302. // Assert
  303. // Verify that processing was attempted for all items
  304. _mockLog.Verify(x => x.Log(LogSeverityEnum.Info, It.Is<string>(s => s.Contains("Starting processing for recipient:"))), Times.AtLeast(1));
  305. }
  306. #endregion
  307. #region Stop Method Tests
  308. [Test]
  309. public void Stop_SetsCancellationToken()
  310. {
  311. // Arrange
  312. _processor = CreateProcessor();
  313. // Act
  314. _processor.Stop();
  315. // Assert
  316. _mockLog.Verify(x => x.Log(LogSeverityEnum.Info, "Processing stop requested."), Times.Once);
  317. }
  318. #endregion
  319. #region Event Tests
  320. [Test]
  321. public void EmailItemStateChanged_WhenStateUpdated_FiresEvent()
  322. {
  323. // Arrange
  324. _processor = CreateProcessor();
  325. bool eventFired = false;
  326. string capturedRecipient = "";
  327. EmailStateEnum capturedState = EmailStateEnum.Ready;
  328. int capturedProgress = 0;
  329. _processor.EmailItemStateChanged += (sender, recipient, state, progress) =>
  330. {
  331. eventFired = true;
  332. capturedRecipient = recipient;
  333. capturedState = state;
  334. capturedProgress = progress;
  335. };
  336. var item = _processor.Metadata.Items.First();
  337. // Act
  338. _processor.ResetItem("test@example.com"); // This should trigger state change
  339. // Assert
  340. Assert.That(eventFired, Is.True);
  341. Assert.That(capturedRecipient, Is.EqualTo("test@example.com"));
  342. Assert.That(capturedState, Is.EqualTo(EmailStateEnum.Ready));
  343. }
  344. #endregion
  345. #region Metadata Management Tests
  346. [Test]
  347. public void Constructor_WithExistingMetadataFile_LoadsMetadata()
  348. {
  349. // Arrange
  350. var existingMetadata = new Metadata
  351. {
  352. Created = DateTime.Now.AddDays(-1),
  353. Modified = DateTime.Now.AddHours(-1),
  354. Items = new List<MetadataItem>
  355. {
  356. new MetadataItem
  357. {
  358. DirectoryName = "existing@example.com",
  359. EmailAddress = "existing@example.com",
  360. State = EmailStateEnum.Sent,
  361. Subject = "Test Subject",
  362. Body = "Test Body",
  363. InternalFilesCount = 5,
  364. Orders = "3001,3002"
  365. }
  366. }
  367. };
  368. var metadataJson = JsonSerializer.Serialize(existingMetadata, new JsonSerializerOptions { WriteIndented = true });
  369. var metadataPath = Path.Combine(_testBasePath, "metadata.json");
  370. _mockFileSystem.AddFile(metadataPath, new MockFileData(metadataJson));
  371. // Act
  372. _processor = CreateProcessor();
  373. // Assert
  374. Assert.That(_processor.Metadata.Items.Count, Is.GreaterThan(0));
  375. var existingItem = _processor.Metadata.Items.FirstOrDefault(x => x.EmailAddress == "existing@example.com");
  376. Assert.That(existingItem, Is.Null);
  377. existingItem = _processor.Metadata.Items.FirstOrDefault(x => x.EmailAddress == "test@example.com");
  378. Assert.That(existingItem.State, Is.EqualTo(EmailStateEnum.Ready));
  379. }
  380. [Test]
  381. public void Constructor_ScansDirectoriesAndUpdatesMetadata()
  382. {
  383. // Arrange - metadata file exists but doesn't contain current directory
  384. var existingMetadata = new Metadata
  385. {
  386. Created = DateTime.Now.AddDays(-1),
  387. Items = new List<MetadataItem>()
  388. };
  389. var metadataJson = JsonSerializer.Serialize(existingMetadata);
  390. var metadataPath = Path.Combine(_testBasePath, "metadata.json");
  391. _mockFileSystem.AddFile(metadataPath, new MockFileData(metadataJson));
  392. // Act
  393. _processor = CreateProcessor();
  394. // Assert
  395. Assert.That(_processor.Metadata.Items.Count, Is.EqualTo(1));
  396. var newItem = _processor.Metadata.Items.First();
  397. Assert.That(newItem.DirectoryName, Is.EqualTo("test@example.com"));
  398. Assert.That(newItem.EmailAddress, Is.EqualTo("test@example.com"));
  399. }
  400. #endregion
  401. #region File Operations Tests
  402. [Test]
  403. public void Constructor_WithOrderNumbersInFilenames_ExtractsOrdersCorrectly()
  404. {
  405. // Arrange - setup files with different order patterns
  406. _mockFileSystem.RemoveFile(Path.Combine(_testBasePath, "test@example.com", "1001_photo1.jpg"));
  407. _mockFileSystem.RemoveFile(Path.Combine(_testBasePath, "test@example.com", "1001_photo2.jpg"));
  408. _mockFileSystem.RemoveFile(Path.Combine(_testBasePath, "test@example.com", "1002_photo1.jpg"));
  409. _mockFileSystem.AddFile(Path.Combine(_testBasePath, "test@example.com", "5001_portrait.jpg"), new MockFileData("content"));
  410. _mockFileSystem.AddFile(Path.Combine(_testBasePath, "test@example.com", "5002_landscape.jpg"), new MockFileData("content"));
  411. _mockFileSystem.AddFile(Path.Combine(_testBasePath, "test@example.com", "5001_closeup.jpg"), new MockFileData("content"));
  412. _mockFileSystem.AddFile(Path.Combine(_testBasePath, "test@example.com", "invalid_name.jpg"), new MockFileData("content"));
  413. // Act
  414. _processor = CreateProcessor();
  415. // Assert
  416. var item = _processor.Metadata.Items.First();
  417. Assert.That(item.Orders, Is.EqualTo("5001,5002")); // Should extract unique order numbers
  418. Assert.That(item.InternalFilesCount, Is.EqualTo(4)); // Should count all JPG files
  419. }
  420. #endregion
  421. #region Integration Tests
  422. [Test]
  423. public void Constructor_WithMultipleRecipientDirectories_CreatesMultipleItems()
  424. {
  425. // Arrange
  426. _mockFileSystem.AddDirectory(Path.Combine(_testBasePath, "user1@test.com"));
  427. _mockFileSystem.AddFile(Path.Combine(_testBasePath, "user1@test.com", "1001_photo.jpg"), new MockFileData("content"));
  428. _mockFileSystem.AddDirectory(Path.Combine(_testBasePath, "user2@test.com"));
  429. _mockFileSystem.AddFile(Path.Combine(_testBasePath, "user2@test.com", "2001_photo.jpg"), new MockFileData("content"));
  430. // Act
  431. _processor = CreateProcessor();
  432. // Assert
  433. Assert.That(_processor.Metadata.Items.Count, Is.EqualTo(3)); // Original + 2 new
  434. var emails = _processor.Metadata.Items.Select(x => x.EmailAddress).ToList();
  435. Assert.That(emails, Contains.Item("test@example.com"));
  436. Assert.That(emails, Contains.Item("user1@test.com"));
  437. Assert.That(emails, Contains.Item("user2@test.com"));
  438. }
  439. [Test]
  440. public void Constructor_IgnoresHiddenDirectories()
  441. {
  442. // Arrange
  443. _mockFileSystem.AddDirectory(Path.Combine(_testBasePath, ".hidden"));
  444. _mockFileSystem.AddFile(Path.Combine(_testBasePath, ".hidden", "1001_photo.jpg"), new MockFileData("content"));
  445. _mockFileSystem.AddDirectory(Path.Combine(_testBasePath, ".processing"));
  446. _mockFileSystem.AddFile(Path.Combine(_testBasePath, ".processing", "test.zip"), new MockFileData("content"));
  447. // Act
  448. _processor = CreateProcessor();
  449. // Assert
  450. // Should only have the original test@example.com directory, not the hidden ones
  451. Assert.That(_processor.Metadata.Items.Count, Is.EqualTo(1));
  452. Assert.That(_processor.Metadata.Items.First().DirectoryName, Is.EqualTo("test@example.com"));
  453. }
  454. #endregion
  455. #region Dispose Tests
  456. [Test]
  457. public void Dispose_DisposesResourcesProperly()
  458. {
  459. // Arrange
  460. _processor = CreateProcessor();
  461. // Act
  462. _processor.Dispose();
  463. // Assert - Should not throw
  464. Assert.Pass("Dispose completed without exceptions");
  465. }
  466. [Test]
  467. public void Dispose_CalledMultipleTimes_DoesNotThrow()
  468. {
  469. // Arrange
  470. _processor = CreateProcessor();
  471. // Act & Assert
  472. Assert.DoesNotThrow(() =>
  473. {
  474. _processor.Dispose();
  475. _processor.Dispose();
  476. _processor.Dispose();
  477. });
  478. }
  479. #endregion
  480. }
  481. }