using System; using System.IO; using Microsoft.VisualStudio.TestTools.UnitTesting; using MySqlConnector; using Serilog; using SyncEngine.Configuration; using SyncEngine.MySql; namespace SyncEngine.Tests { [TestClass] public class ShopSyncLogClientFallbackTests { private static ShopSyncLogEntry SampleEntry() { return new ShopSyncLogEntry { RunStartedAt = DateTime.UtcNow, RunFinishedAt = DateTime.UtcNow, Status = "failed", SourceReachable = false, ProductsUpserted = 0, ProductsWithdrawn = 0, Message = "MySQL down" }; } [TestMethod] public void WriteLogWithFallback_MysqlSuccess_DoesNotCallApi() { var apiCalled = false; var options = new SyncOptions { ShopSyncApiUrl = "https://shop.example.com/api/shop-sync/log", ShopSyncApiKey = "secret" }; var client = new ShopSyncLogClient( new LoggerConfiguration().CreateLogger(), options, new FakeApiClient(() => apiCalled = true)); client.MySqlWriteForTests = (entry, factory) => { }; client.WriteLogWithFallback(SampleEntry(), new MySqlConnectionFactory(options), options); Assert.IsFalse(apiCalled); } [TestMethod] public void WriteLogWithFallback_MysqlFails_UsesApiWhenConfigured() { var apiCalled = false; var options = new SyncOptions { ShopSyncApiUrl = "https://shop.example.com/api/shop-sync/log", ShopSyncApiKey = "secret" }; var client = new ShopSyncLogClient( new LoggerConfiguration().CreateLogger(), options, new FakeApiClient(() => apiCalled = true)); client.MySqlWriteForTests = (entry, factory) => { throw new InvalidOperationException("connection failed"); }; client.WriteLogWithFallback(SampleEntry(), new MySqlConnectionFactory(options), options); Assert.IsTrue(apiCalled); } [TestMethod] public void WriteLogWithFallback_MysqlFailsWithoutApi_Rethrows() { var options = new SyncOptions(); var client = new ShopSyncLogClient(new LoggerConfiguration().CreateLogger(), options, null); client.MySqlWriteForTests = (entry, factory) => { throw new InvalidOperationException("connection failed"); }; try { client.WriteLogWithFallback(SampleEntry(), new MySqlConnectionFactory(options), options); Assert.Fail("Expected InvalidOperationException"); } catch (InvalidOperationException) { } } [TestMethod] public void WriteLogWithFallback_BothFail_ThrowsAggregateException() { var options = new SyncOptions { ShopSyncApiUrl = "https://shop.example.com/api/shop-sync/log", ShopSyncApiKey = "secret" }; var client = new ShopSyncLogClient( new LoggerConfiguration().CreateLogger(), options, new FakeApiClient(() => { throw new InvalidOperationException("API down"); })); client.MySqlWriteForTests = (entry, factory) => { throw new InvalidOperationException("connection failed"); }; try { client.WriteLogWithFallback(SampleEntry(), new MySqlConnectionFactory(options), options); Assert.Fail("Expected AggregateException"); } catch (AggregateException ex) { Assert.AreEqual(2, ex.InnerExceptions.Count); } var files = Directory.GetFiles(_tempDir, "shop-sync-log-failed-*.jsonl"); Assert.AreEqual(1, files.Length); } private string _tempDir; [TestInitialize] public void FallbackSetUp() { _tempDir = Path.Combine(Path.GetTempPath(), "sync-coordinator-fallback-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(_tempDir); ShopSyncLogLocalWriter.SetLogDirectoryProviderForTests(() => _tempDir); } [TestCleanup] public void FallbackTearDown() { ShopSyncLogLocalWriter.ResetLogDirectoryProviderForTests(); if (Directory.Exists(_tempDir)) { Directory.Delete(_tempDir, recursive: true); } } private sealed class FakeApiClient : IShopSyncLogApiClient { private readonly Action _onPost; public FakeApiClient(Action onPost) { _onPost = onPost; } public void PostLog(ShopSyncLogEntry entry) { _onPost(); } } } }