diff --git a/test/ApiSiteTests/ApiSiteTests.csproj b/test/ApiSiteTests/ApiSiteTests.csproj index 3feaea6..3294b6b 100644 --- a/test/ApiSiteTests/ApiSiteTests.csproj +++ b/test/ApiSiteTests/ApiSiteTests.csproj @@ -20,6 +20,9 @@ + + + diff --git a/test/ApiSiteTests/ConfigSyncEventHandlerTests.cs b/test/ApiSiteTests/ConfigSyncEventHandlerTests.cs new file mode 100644 index 0000000..022d168 --- /dev/null +++ b/test/ApiSiteTests/ConfigSyncEventHandlerTests.cs @@ -0,0 +1,286 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AgileConfig.Server.Data.Entity; +using AgileConfig.Server.Event; +using AgileConfig.Server.EventHandler; +using AgileConfig.Server.IService; +using AgileConfig.Server.SyncPlugin; +using AgileConfig.Server.SyncPlugin.Contracts; +using AgileConfig.Server.SyncPlugin.Retry; +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace AgileConfig.Server.ApiSiteTests; + +[TestClass] +public class ConfigSyncEventHandlerTests +{ + private Mock _configServiceMock; + private Mock> _syncEngineLoggerMock; + private Mock> _retryServiceLoggerMock; + private Mock _serviceProviderMock; + private Mock _pluginMock; + private SyncEngine _syncEngine; + private SyncRetryService _retryService; + private Mock> _handlerLoggerMock; + private ConfigSyncEventHandler _handler; + + [TestInitialize] + public void Setup() + { + _configServiceMock = new Mock(); + + _syncEngineLoggerMock = new Mock>(); + _syncEngine = new SyncEngine(_syncEngineLoggerMock.Object); + + _serviceProviderMock = new Mock(); + _retryServiceLoggerMock = new Mock>(); + _retryService = new SyncRetryService( + _syncEngine, + _serviceProviderMock.Object, + _retryServiceLoggerMock.Object); + + _handlerLoggerMock = new Mock>(); + + _handler = new ConfigSyncEventHandler( + _configServiceMock.Object, + _syncEngine, + _retryService, + _handlerLoggerMock.Object); + } + + [TestCleanup] + public void Cleanup() + { + _syncEngine?.Dispose(); + } + + private void RegisterMockPlugin(bool returnSuccess = true) + { + _pluginMock = new Mock(); + _pluginMock.Setup(p => p.Name).Returns("mock_plugin"); + _pluginMock.Setup(p => p.DisplayName).Returns("Mock Plugin"); + _pluginMock.Setup(p => p.Description).Returns("Mock sync plugin for testing"); + + _pluginMock.Setup(p => p.InitializeAsync(It.IsAny())) + .ReturnsAsync(new SyncPluginResult { Success = true }); + + _pluginMock.Setup(p => p.SyncAllAsync(It.IsAny())) + .ReturnsAsync(new SyncPluginResult + { + Success = returnSuccess, + Message = returnSuccess ? "Success" : "Mock failure" + }); + + _pluginMock.Setup(p => p.HealthCheckAsync()) + .ReturnsAsync(new SyncPluginHealthResult { Healthy = true }); + + _pluginMock.Setup(p => p.ShutdownAsync()) + .Returns(Task.CompletedTask); + + var config = new SyncPluginConfig + { + PluginName = "mock_plugin", + Enabled = "true", + Settings = new Dictionary() + }; + + _syncEngine.RegisterPlugin(_pluginMock.Object, config); + } + + [TestMethod] + public async Task Handle_WithValidTimeline_ShouldSyncConfigs() + { + // Arrange + RegisterMockPlugin(returnSuccess: true); + + var timeline = new PublishTimeline + { + Id = "timeline-1", + AppId = "app-1", + Env = "PROD", + PublishTime = DateTime.Now + }; + + var evt = new PublishConfigSuccessful(timeline, "testuser"); + + var publishedConfigs = new List + { + new ConfigPublished + { + Id = "config-1", + AppId = "app-1", + Env = "PROD", + Key = "db.connection", + Value = "server=localhost", + Group = "db" + }, + new ConfigPublished + { + Id = "config-2", + AppId = "app-1", + Env = "PROD", + Key = "cache.enabled", + Value = "true", + Group = "cache" + } + }; + + _configServiceMock + .Setup(x => x.GetPublishedConfigsAsync("app-1", "PROD")) + .ReturnsAsync(publishedConfigs); + + // Act + await _handler.Handle(evt); + + // Assert + _configServiceMock.Verify( + x => x.GetPublishedConfigsAsync("app-1", "PROD"), + Times.Once); + + _pluginMock.Verify( + p => p.SyncAllAsync(It.Is(ctx => + ctx.Length == 2 && + ctx.Any(c => c.Key == "db.connection") && + ctx.Any(c => c.Key == "cache.enabled"))), + Times.Once); + } + + [TestMethod] + public async Task Handle_WithNoPublishedConfigs_ShouldNotSync() + { + // Arrange + RegisterMockPlugin(returnSuccess: true); + + var timeline = new PublishTimeline + { + Id = "timeline-1", + AppId = "app-1", + Env = "PROD", + PublishTime = DateTime.Now + }; + + var evt = new PublishConfigSuccessful(timeline, "testuser"); + + _configServiceMock + .Setup(x => x.GetPublishedConfigsAsync("app-1", "PROD")) + .ReturnsAsync(new List()); + + // Act + await _handler.Handle(evt); + + // Assert + _configServiceMock.Verify( + x => x.GetPublishedConfigsAsync("app-1", "PROD"), + Times.Once); + + // Should NOT call sync when no configs + _pluginMock.Verify( + p => p.SyncAllAsync(It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task Handle_WithNullTimeline_ShouldReturnEarly() + { + // Arrange + RegisterMockPlugin(returnSuccess: true); + + var evt = new PublishConfigSuccessful(null!, "testuser"); + + // Act + await _handler.Handle(evt); + + // Assert + _configServiceMock.Verify( + x => x.GetPublishedConfigsAsync(It.IsAny(), It.IsAny()), + Times.Never); + + _pluginMock.Verify( + p => p.SyncAllAsync(It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task Handle_WhenSyncFails_ShouldRecordFailed() + { + // Arrange + RegisterMockPlugin(returnSuccess: false); + + var timeline = new PublishTimeline + { + Id = "timeline-1", + AppId = "app-1", + Env = "PROD", + PublishTime = DateTime.Now + }; + + var evt = new PublishConfigSuccessful(timeline, "testuser"); + + var publishedConfigs = new List + { + new ConfigPublished + { + Id = "config-1", + AppId = "app-1", + Env = "PROD", + Key = "db.connection", + Value = "server=localhost", + Group = "db" + } + }; + + _configServiceMock + .Setup(x => x.GetPublishedConfigsAsync("app-1", "PROD")) + .ReturnsAsync(publishedConfigs); + + // Clear any initial failed records + _retryService.ClearFailedRecords(); + + // Act + await _handler.Handle(evt); + + // Assert - verify failed record was created + var failedRecords = _retryService.GetFailedRecords(); + Assert.AreEqual(1, failedRecords.Count); + Assert.AreEqual("app-1", failedRecords[0].AppId); + Assert.AreEqual("PROD", failedRecords[0].Env); + } + + [TestMethod] + public async Task Handle_WhenExceptionThrown_ShouldRecordFailed() + { + // Arrange + RegisterMockPlugin(returnSuccess: true); + + var timeline = new PublishTimeline + { + Id = "timeline-1", + AppId = "app-1", + Env = "PROD", + PublishTime = DateTime.Now + }; + + var evt = new PublishConfigSuccessful(timeline, "testuser"); + + _configServiceMock + .Setup(x => x.GetPublishedConfigsAsync("app-1", "PROD")) + .ThrowsAsync(new Exception("Database connection error")); + + // Clear any initial failed records + _retryService.ClearFailedRecords(); + + // Act + await _handler.Handle(evt); + + // Assert - verify failed record was created + var failedRecords = _retryService.GetFailedRecords(); + Assert.AreEqual(1, failedRecords.Count); + Assert.AreEqual("app-1", failedRecords[0].AppId); + Assert.AreEqual("PROD", failedRecords[0].Env); + Assert.AreEqual("Database connection error", failedRecords[0].LastError); + } +}