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);
+ }
+}