using Microsoft.EntityFrameworkCore; using ScadaLink.Commons.Entities.Deployment; using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.Commons.Entities.Instances; using ScadaLink.Commons.Entities.Notifications; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Entities.Templates; using ScadaLink.Commons.Types.Enums; using ScadaLink.ConfigurationDatabase; using ScadaLink.ConfigurationDatabase.Repositories; using ScadaLink.ConfigurationDatabase.Services; namespace ScadaLink.ConfigurationDatabase.Tests; // Regression coverage for ConfigurationDatabase-010 (repositories / InstanceLocator lacked // direct tests) and ConfigurationDatabase-011 (inconsistent constructor null-guarding). public class ExternalSystemRepositoryTests : IDisposable { private readonly ScadaLinkDbContext _context; private readonly ExternalSystemRepository _repository; public ExternalSystemRepositoryTests() { _context = SqliteTestHelper.CreateInMemoryContext(); _repository = new ExternalSystemRepository(_context); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public async Task AddExternalSystem_AndGetById_RoundTrips() { var def = new ExternalSystemDefinition("Sys", "https://example.test", "ApiKey"); await _repository.AddExternalSystemAsync(def); await _repository.SaveChangesAsync(); var loaded = await _repository.GetExternalSystemByIdAsync(def.Id); Assert.NotNull(loaded); Assert.Equal("Sys", loaded!.Name); } [Fact] public async Task GetMethodsByExternalSystemId_FiltersByParent() { var def = new ExternalSystemDefinition("Sys", "https://example.test", "ApiKey"); await _repository.AddExternalSystemAsync(def); await _repository.SaveChangesAsync(); await _repository.AddExternalSystemMethodAsync( new ExternalSystemMethod("M1", "GET", "/m1") { ExternalSystemDefinitionId = def.Id }); await _repository.SaveChangesAsync(); var methods = await _repository.GetMethodsByExternalSystemIdAsync(def.Id); Assert.Single(methods); } [Fact] public async Task DeleteDatabaseConnection_RemovesEntity() { var conn = new DatabaseConnectionDefinition("Db", "Server=x;Database=y;"); await _repository.AddDatabaseConnectionAsync(conn); await _repository.SaveChangesAsync(); await _repository.DeleteDatabaseConnectionAsync(conn.Id); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetDatabaseConnectionByIdAsync(conn.Id)); } [Fact] public void Constructor_NullContext_Throws() { Assert.Throws(() => new ExternalSystemRepository(null!)); } // ── ExternalSystemGateway-011: name-keyed repository lookups ── [Fact] public async Task GetExternalSystemByName_ReturnsMatchingRow() { await _repository.AddExternalSystemAsync( new ExternalSystemDefinition("Alpha", "https://alpha.test", "ApiKey")); await _repository.AddExternalSystemAsync( new ExternalSystemDefinition("Beta", "https://beta.test", "Basic")); await _repository.SaveChangesAsync(); var loaded = await _repository.GetExternalSystemByNameAsync("Beta"); Assert.NotNull(loaded); Assert.Equal("Beta", loaded!.Name); Assert.Equal("https://beta.test", loaded.EndpointUrl); } [Fact] public async Task GetExternalSystemByName_MissingName_ReturnsNull() { await _repository.AddExternalSystemAsync( new ExternalSystemDefinition("Alpha", "https://alpha.test", "ApiKey")); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetExternalSystemByNameAsync("DoesNotExist")); } [Fact] public async Task GetMethodByName_ReturnsMethodScopedToParentSystem() { var sysA = new ExternalSystemDefinition("SysA", "https://a.test", "ApiKey"); var sysB = new ExternalSystemDefinition("SysB", "https://b.test", "ApiKey"); await _repository.AddExternalSystemAsync(sysA); await _repository.AddExternalSystemAsync(sysB); await _repository.SaveChangesAsync(); // Same method name on two different systems — the lookup must be scoped. await _repository.AddExternalSystemMethodAsync( new ExternalSystemMethod("getData", "GET", "/a") { ExternalSystemDefinitionId = sysA.Id }); await _repository.AddExternalSystemMethodAsync( new ExternalSystemMethod("getData", "POST", "/b") { ExternalSystemDefinitionId = sysB.Id }); await _repository.SaveChangesAsync(); var method = await _repository.GetMethodByNameAsync(sysB.Id, "getData"); Assert.NotNull(method); Assert.Equal(sysB.Id, method!.ExternalSystemDefinitionId); Assert.Equal("POST", method.HttpMethod); Assert.Equal("/b", method.Path); } [Fact] public async Task GetMethodByName_MissingMethod_ReturnsNull() { var sys = new ExternalSystemDefinition("SysA", "https://a.test", "ApiKey"); await _repository.AddExternalSystemAsync(sys); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetMethodByNameAsync(sys.Id, "noSuchMethod")); } [Fact] public async Task GetDatabaseConnectionByName_ReturnsMatchingRow() { await _repository.AddDatabaseConnectionAsync( new DatabaseConnectionDefinition("Plant", "Server=plant;Database=p;")); await _repository.AddDatabaseConnectionAsync( new DatabaseConnectionDefinition("Historian", "Server=hist;Database=h;")); await _repository.SaveChangesAsync(); var loaded = await _repository.GetDatabaseConnectionByNameAsync("Historian"); Assert.NotNull(loaded); Assert.Equal("Historian", loaded!.Name); Assert.Equal("Server=hist;Database=h;", loaded.ConnectionString); } [Fact] public async Task GetDatabaseConnectionByName_MissingName_ReturnsNull() { await _repository.AddDatabaseConnectionAsync( new DatabaseConnectionDefinition("Plant", "Server=plant;Database=p;")); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetDatabaseConnectionByNameAsync("DoesNotExist")); } } public class NotificationRepositoryTests : IDisposable { private readonly ScadaLinkDbContext _context; private readonly NotificationRepository _repository; public NotificationRepositoryTests() { _context = SqliteTestHelper.CreateInMemoryContext(); _repository = new NotificationRepository(_context); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public async Task AddNotificationList_WithRecipients_RoundTrips() { var list = new NotificationList("Ops"); list.Recipients.Add(new NotificationRecipient("Ops Team", "ops@example.test")); await _repository.AddNotificationListAsync(list); await _repository.SaveChangesAsync(); var loaded = await _repository.GetListByNameAsync("Ops"); Assert.NotNull(loaded); var all = await _repository.GetAllNotificationListsAsync(); Assert.Single(all); Assert.Single(all[0].Recipients); } [Fact] public async Task AddSmtpConfiguration_AndGetById_RoundTrips() { var smtp = new SmtpConfiguration("smtp.example.test", "Basic", "from@example.test"); await _repository.AddSmtpConfigurationAsync(smtp); await _repository.SaveChangesAsync(); var loaded = await _repository.GetSmtpConfigurationByIdAsync(smtp.Id); Assert.NotNull(loaded); Assert.Equal("smtp.example.test", loaded!.Host); } [Fact] public async Task DeleteNotificationList_RemovesEntity() { var list = new NotificationList("ToDelete"); await _repository.AddNotificationListAsync(list); await _repository.SaveChangesAsync(); await _repository.DeleteNotificationListAsync(list.Id); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetNotificationListByIdAsync(list.Id)); } [Fact] public async Task NotificationList_PersistsType() { var list = new NotificationList("ops") { Type = NotificationType.Email }; await _repository.AddNotificationListAsync(list); await _repository.SaveChangesAsync(); _context.ChangeTracker.Clear(); var loaded = await _repository.GetListByNameAsync("ops"); Assert.Equal(NotificationType.Email, loaded!.Type); } [Fact] public void Constructor_NullContext_Throws() { Assert.Throws(() => new NotificationRepository(null!)); } } public class SiteRepositoryTests : IDisposable { private readonly ScadaLinkDbContext _context; private readonly SiteRepository _repository; public SiteRepositoryTests() { _context = SqliteTestHelper.CreateInMemoryContext(); _repository = new SiteRepository(_context); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public async Task AddSite_AndGetByIdentifier_RoundTrips() { var site = new Site("Site1", "S-001"); await _repository.AddSiteAsync(site); await _repository.SaveChangesAsync(); var loaded = await _repository.GetSiteByIdentifierAsync("S-001"); Assert.NotNull(loaded); Assert.Equal("Site1", loaded!.Name); } [Fact] public async Task DeleteSite_ViaStubAttachPath_RemovesEntity() { // Exercises the stub-attach delete fallback: the entity is not tracked because the // ChangeTracker is cleared, forcing the Local-miss branch in DeleteSiteAsync. var site = new Site("Site1", "S-001"); await _repository.AddSiteAsync(site); await _repository.SaveChangesAsync(); var id = site.Id; _context.ChangeTracker.Clear(); await _repository.DeleteSiteAsync(id); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetSiteByIdAsync(id)); } [Fact] public async Task DeleteDataConnection_ViaStubAttachPath_RemovesEntity() { var site = new Site("Site1", "S-001"); await _repository.AddSiteAsync(site); await _repository.SaveChangesAsync(); var conn = new DataConnection("Conn1", "OpcUa", site.Id); await _repository.AddDataConnectionAsync(conn); await _repository.SaveChangesAsync(); var id = conn.Id; _context.ChangeTracker.Clear(); await _repository.DeleteDataConnectionAsync(id); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetDataConnectionByIdAsync(id)); } [Fact] public async Task GetInstancesBySiteId_FiltersBySite() { var site = new Site("Site1", "S-001"); var template = new Template("T1"); _context.Sites.Add(site); _context.Templates.Add(template); await _context.SaveChangesAsync(); _context.Instances.Add(new Instance("I1") { SiteId = site.Id, TemplateId = template.Id }); await _context.SaveChangesAsync(); var instances = await _repository.GetInstancesBySiteIdAsync(site.Id); Assert.Single(instances); } [Fact] public void Constructor_NullContext_Throws() { Assert.Throws(() => new SiteRepository(null!)); } } public class DeploymentManagerRepositoryTests : IDisposable { private readonly ScadaLinkDbContext _context; private readonly DeploymentManagerRepository _repository; public DeploymentManagerRepositoryTests() { _context = SqliteTestHelper.CreateInMemoryContext(); _repository = new DeploymentManagerRepository(_context); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } private async Task SeedInstanceAsync() { var site = new Site("Site1", "S-001"); var template = new Template("T1"); _context.Sites.Add(site); _context.Templates.Add(template); await _context.SaveChangesAsync(); var instance = new Instance("Inst1") { SiteId = site.Id, TemplateId = template.Id }; _context.Instances.Add(instance); await _context.SaveChangesAsync(); return instance; } [Fact] public async Task AddDeploymentRecord_AndGetCurrentStatus_ReturnsMostRecent() { var instance = await SeedInstanceAsync(); await _repository.AddDeploymentRecordAsync( new DeploymentRecord("d-001", "admin") { InstanceId = instance.Id, DeployedAt = DateTimeOffset.UtcNow.AddHours(-1) }); await _repository.AddDeploymentRecordAsync( new DeploymentRecord("d-002", "admin") { InstanceId = instance.Id, DeployedAt = DateTimeOffset.UtcNow }); await _repository.SaveChangesAsync(); var current = await _repository.GetCurrentDeploymentStatusAsync(instance.Id); Assert.NotNull(current); Assert.Equal("d-002", current!.DeploymentId); } [Fact] public async Task DeleteDeploymentRecord_ViaStubAttachPath_RemovesEntity() { var instance = await SeedInstanceAsync(); var record = new DeploymentRecord("d-001", "admin") { InstanceId = instance.Id, DeployedAt = DateTimeOffset.UtcNow }; await _repository.AddDeploymentRecordAsync(record); await _repository.SaveChangesAsync(); var id = record.Id; _context.ChangeTracker.Clear(); await _repository.DeleteDeploymentRecordAsync(id); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetDeploymentRecordByIdAsync(id)); } [Fact] public async Task DeleteInstance_RemovesRestrictFkDeploymentRecordsFirst() { // DeploymentRecord has a Restrict FK to Instance; DeleteInstanceAsync must remove // the dependent deployment records explicitly or the delete would fail. var instance = await SeedInstanceAsync(); await _repository.AddDeploymentRecordAsync( new DeploymentRecord("d-001", "admin") { InstanceId = instance.Id, DeployedAt = DateTimeOffset.UtcNow }); await _repository.SaveChangesAsync(); await _repository.DeleteInstanceAsync(instance.Id); await _repository.SaveChangesAsync(); Assert.Null(await _repository.GetInstanceByIdAsync(instance.Id)); Assert.Empty(await _repository.GetDeploymentsByInstanceIdAsync(instance.Id)); } [Fact] public void Constructor_NullContext_Throws() { Assert.Throws(() => new DeploymentManagerRepository(null!)); } } public class InstanceLocatorTests : IDisposable { private readonly ScadaLinkDbContext _context; private readonly InstanceLocator _locator; public InstanceLocatorTests() { _context = SqliteTestHelper.CreateInMemoryContext(); _locator = new InstanceLocator(_context); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public async Task GetSiteIdForInstance_WhenFound_ReturnsSiteIdentifier() { var site = new Site("Site1", "SITE-001"); var template = new Template("T1"); _context.Sites.Add(site); _context.Templates.Add(template); await _context.SaveChangesAsync(); _context.Instances.Add(new Instance("Pump1") { SiteId = site.Id, TemplateId = template.Id }); await _context.SaveChangesAsync(); var result = await _locator.GetSiteIdForInstanceAsync("Pump1"); Assert.Equal("SITE-001", result); } [Fact] public async Task GetSiteIdForInstance_WhenInstanceNotFound_ReturnsNull() { var result = await _locator.GetSiteIdForInstanceAsync("DoesNotExist"); Assert.Null(result); } [Fact] public void Constructor_NullContext_Throws() { Assert.Throws(() => new InstanceLocator(null!)); } }