using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using ScadaLink.Commons.Entities.Audit; using ScadaLink.Commons.Entities.Deployment; using ScadaLink.Commons.Entities.ExternalSystems; using ScadaLink.Commons.Entities.InboundApi; using ScadaLink.Commons.Entities.Instances; using ScadaLink.Commons.Entities.Notifications; using ScadaLink.Commons.Entities.Scripts; using ScadaLink.Commons.Entities.Security; using ScadaLink.Commons.Entities.Sites; using ScadaLink.Commons.Entities.Templates; using ScadaLink.Commons.Types.Enums; using ScadaLink.ConfigurationDatabase; namespace ScadaLink.ConfigurationDatabase.Tests; public class DbContextTests : IDisposable { private readonly ScadaLinkDbContext _context; public DbContextTests() { _context = SqliteTestHelper.CreateInMemoryContext(); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public void Schema_CreatesAllTables() { // Verify all DbSet tables exist by checking we can query them without error Assert.NotNull(_context.Templates); Assert.NotNull(_context.TemplateAttributes); Assert.NotNull(_context.TemplateAlarms); Assert.NotNull(_context.TemplateScripts); Assert.NotNull(_context.TemplateCompositions); Assert.NotNull(_context.Instances); Assert.NotNull(_context.InstanceAttributeOverrides); Assert.NotNull(_context.InstanceConnectionBindings); Assert.NotNull(_context.Areas); Assert.NotNull(_context.Sites); Assert.NotNull(_context.DataConnections); Assert.NotNull(_context.SiteDataConnectionAssignments); Assert.NotNull(_context.DeploymentRecords); Assert.NotNull(_context.SystemArtifactDeploymentRecords); Assert.NotNull(_context.ExternalSystemDefinitions); Assert.NotNull(_context.ExternalSystemMethods); Assert.NotNull(_context.DatabaseConnectionDefinitions); Assert.NotNull(_context.NotificationLists); Assert.NotNull(_context.NotificationRecipients); Assert.NotNull(_context.SmtpConfigurations); Assert.NotNull(_context.SharedScripts); Assert.NotNull(_context.LdapGroupMappings); Assert.NotNull(_context.SiteScopeRules); Assert.NotNull(_context.ApiKeys); Assert.NotNull(_context.ApiMethods); Assert.NotNull(_context.AuditLogEntries); // Verify we can enumerate all tables (schema is valid) Assert.Empty(_context.Templates.ToList()); Assert.Empty(_context.Sites.ToList()); Assert.Empty(_context.Instances.ToList()); Assert.Empty(_context.AuditLogEntries.ToList()); } [Fact] public void Template_WithChildren_CascadeCreated() { var template = new Template("TestTemplate") { Description = "A test template" }; template.Attributes.Add(new TemplateAttribute("Attr1") { DataType = DataType.Int32 }); template.Alarms.Add(new TemplateAlarm("Alarm1") { TriggerType = AlarmTriggerType.ValueMatch, PriorityLevel = 1 }); template.Scripts.Add(new TemplateScript("Script1", "return 42;")); _context.Templates.Add(template); _context.SaveChanges(); var loaded = _context.Templates .Include(t => t.Attributes) .Include(t => t.Alarms) .Include(t => t.Scripts) .Single(t => t.Name == "TestTemplate"); Assert.Single(loaded.Attributes); Assert.Single(loaded.Alarms); Assert.Single(loaded.Scripts); Assert.Equal("Attr1", loaded.Attributes.First().Name); } [Fact] public void Template_Inheritance_SelfReference() { var parent = new Template("ParentTemplate"); _context.Templates.Add(parent); _context.SaveChanges(); var child = new Template("ChildTemplate") { ParentTemplateId = parent.Id }; _context.Templates.Add(child); _context.SaveChanges(); var loaded = _context.Templates.Single(t => t.Name == "ChildTemplate"); Assert.Equal(parent.Id, loaded.ParentTemplateId); } [Fact] public void Template_Composition_CreatesRelationship() { var composedTemplate = new Template("ComposedTemplate"); var parentTemplate = new Template("ParentTemplate"); _context.Templates.AddRange(composedTemplate, parentTemplate); _context.SaveChanges(); parentTemplate.Compositions.Add(new TemplateComposition("Module1") { ComposedTemplateId = composedTemplate.Id }); _context.SaveChanges(); var loaded = _context.Templates .Include(t => t.Compositions) .Single(t => t.Name == "ParentTemplate"); Assert.Single(loaded.Compositions); Assert.Equal(composedTemplate.Id, loaded.Compositions.First().ComposedTemplateId); } [Fact] public void Instance_WithOverridesAndBindings() { var site = new Site("Site1", "SITE-001"); var template = new Template("Template1"); var dataConn = new DataConnection("OpcConn", "OpcUa"); _context.Sites.Add(site); _context.Templates.Add(template); _context.DataConnections.Add(dataConn); _context.SaveChanges(); var instance = new Instance("Instance1") { TemplateId = template.Id, SiteId = site.Id, State = InstanceState.Enabled }; instance.AttributeOverrides.Add(new InstanceAttributeOverride("Attr1") { OverrideValue = "42" }); instance.ConnectionBindings.Add(new InstanceConnectionBinding("TagPath") { DataConnectionId = dataConn.Id }); _context.Instances.Add(instance); _context.SaveChanges(); var loaded = _context.Instances .Include(i => i.AttributeOverrides) .Include(i => i.ConnectionBindings) .Single(i => i.UniqueName == "Instance1"); Assert.Single(loaded.AttributeOverrides); Assert.Single(loaded.ConnectionBindings); Assert.Equal(InstanceState.Enabled, loaded.State); } [Fact] public void DeploymentRecord_CreatesWithAllFields() { var site = new Site("Site1", "SITE-001"); var template = new Template("Template1"); _context.Sites.Add(site); _context.Templates.Add(template); _context.SaveChanges(); var instance = new Instance("Instance1") { TemplateId = template.Id, SiteId = site.Id, State = InstanceState.Enabled }; _context.Instances.Add(instance); _context.SaveChanges(); var record = new DeploymentRecord("deploy-001", "admin") { InstanceId = instance.Id, Status = DeploymentStatus.Success, RevisionHash = "abc123", DeployedAt = DateTimeOffset.UtcNow, CompletedAt = DateTimeOffset.UtcNow }; _context.DeploymentRecords.Add(record); _context.SaveChanges(); var loaded = _context.DeploymentRecords.Single(d => d.DeploymentId == "deploy-001"); Assert.Equal(DeploymentStatus.Success, loaded.Status); Assert.Equal("abc123", loaded.RevisionHash); } [Fact] public void AuditLogEntry_WritesAndQueries() { var entry = new AuditLogEntry("admin", "Create", "Template", "1", "TestTemplate") { Timestamp = DateTimeOffset.UtcNow, AfterStateJson = "{\"name\":\"TestTemplate\"}" }; _context.AuditLogEntries.Add(entry); _context.SaveChanges(); var loaded = _context.AuditLogEntries.Single(a => a.User == "admin"); Assert.Equal("Create", loaded.Action); Assert.Equal("Template", loaded.EntityType); Assert.NotNull(loaded.AfterStateJson); } [Fact] public void ExternalSystem_WithMethods() { var system = new ExternalSystemDefinition("ERP", "https://erp.example.com/api", "ApiKey") { MaxRetries = 3, RetryDelay = TimeSpan.FromSeconds(5) }; _context.ExternalSystemDefinitions.Add(system); _context.SaveChanges(); var method = new ExternalSystemMethod("GetOrder", "GET", "/orders/{id}") { ExternalSystemDefinitionId = system.Id }; _context.ExternalSystemMethods.Add(method); _context.SaveChanges(); Assert.Single(_context.ExternalSystemMethods.Where(m => m.ExternalSystemDefinitionId == system.Id)); } [Fact] public void NotificationList_WithRecipients() { var list = new NotificationList("Operators"); list.Recipients.Add(new NotificationRecipient("John", "john@example.com")); list.Recipients.Add(new NotificationRecipient("Jane", "jane@example.com")); _context.NotificationLists.Add(list); _context.SaveChanges(); var loaded = _context.NotificationLists .Include(n => n.Recipients) .Single(n => n.Name == "Operators"); Assert.Equal(2, loaded.Recipients.Count); } [Fact] public void Security_LdapGroupMapping_WithSiteScopeRules() { var site = new Site("Site1", "SITE-001"); _context.Sites.Add(site); _context.SaveChanges(); var mapping = new LdapGroupMapping("CN=Admins,DC=example,DC=com", "Admin"); _context.LdapGroupMappings.Add(mapping); _context.SaveChanges(); var rule = new SiteScopeRule { LdapGroupMappingId = mapping.Id, SiteId = site.Id }; _context.SiteScopeRules.Add(rule); _context.SaveChanges(); Assert.Single(_context.SiteScopeRules.Where(r => r.LdapGroupMappingId == mapping.Id)); } [Fact] public void InboundApi_ApiKeyAndMethod() { var key = new ApiKey("TestKey", "sk-test-123") { IsEnabled = true }; var method = new ApiMethod("GetStatus", "return \"ok\";") { TimeoutSeconds = 30 }; _context.ApiKeys.Add(key); _context.ApiMethods.Add(method); _context.SaveChanges(); Assert.Single(_context.ApiKeys.Where(k => k.Name == "TestKey")); Assert.Single(_context.ApiMethods.Where(m => m.Name == "GetStatus")); } [Fact] public void Area_HierarchyWorks() { var site = new Site("Site1", "SITE-001"); _context.Sites.Add(site); _context.SaveChanges(); var parentArea = new Area("Building A") { SiteId = site.Id }; _context.Areas.Add(parentArea); _context.SaveChanges(); var childArea = new Area("Floor 1") { SiteId = site.Id, ParentAreaId = parentArea.Id }; _context.Areas.Add(childArea); _context.SaveChanges(); var loaded = _context.Areas .Include(a => a.Children) .Single(a => a.Name == "Building A"); Assert.Single(loaded.Children); Assert.Equal("Floor 1", loaded.Children.First().Name); } [Fact] public void SiteDataConnectionAssignment_CreatesBothForeignKeys() { var site = new Site("Site1", "SITE-001"); var conn = new DataConnection("OpcConn", "OpcUa"); _context.Sites.Add(site); _context.DataConnections.Add(conn); _context.SaveChanges(); var assignment = new SiteDataConnectionAssignment { SiteId = site.Id, DataConnectionId = conn.Id }; _context.SiteDataConnectionAssignments.Add(assignment); _context.SaveChanges(); Assert.Single(_context.SiteDataConnectionAssignments.ToList()); } [Fact] public void EnumProperties_StoredAsStrings() { var template = new Template("EnumTest"); template.Attributes.Add(new TemplateAttribute("Attr1") { DataType = DataType.Double }); template.Alarms.Add(new TemplateAlarm("Alarm1") { TriggerType = AlarmTriggerType.RangeViolation, PriorityLevel = 1 }); _context.Templates.Add(template); _context.SaveChanges(); // Query using raw SQL to verify string storage var attr = _context.TemplateAttributes.Single(a => a.Name == "Attr1"); Assert.Equal(DataType.Double, attr.DataType); var alarm = _context.TemplateAlarms.Single(a => a.Name == "Alarm1"); Assert.Equal(AlarmTriggerType.RangeViolation, alarm.TriggerType); } [Fact] public void UniqueConstraint_Template_Name_Enforced() { _context.Templates.Add(new Template("Unique")); _context.SaveChanges(); _context.Templates.Add(new Template("Unique")); Assert.ThrowsAny(() => _context.SaveChanges()); } [Fact] public void DateTimeOffset_MappedCorrectly() { var now = DateTimeOffset.UtcNow; var entry = new AuditLogEntry("user", "Test", "Entity", "1", "Name") { Timestamp = now }; _context.AuditLogEntries.Add(entry); _context.SaveChanges(); _context.ChangeTracker.Clear(); var loaded = _context.AuditLogEntries.Single(); // SQLite has limited DateTimeOffset precision, but the round-trip should preserve the value within a second Assert.True(Math.Abs((loaded.Timestamp - now).TotalSeconds) < 1); } } public class ServiceRegistrationTests { [Fact] public void AddConfigurationDatabase_WithConnectionString_RegistersDbContext() { var services = new ServiceCollection(); services.AddConfigurationDatabase("DataSource=:memory:"); var provider = services.BuildServiceProvider(); var context = provider.GetService(); Assert.NotNull(context); } [Fact] public void AddConfigurationDatabase_NoArgs_DoesNotThrow() { var services = new ServiceCollection(); services.AddConfigurationDatabase(); // Should not register DbContext (no-op for backward compatibility) var provider = services.BuildServiceProvider(); var context = provider.GetService(); Assert.Null(context); } } public class MigrationHelperTests : IDisposable { private readonly ScadaLinkDbContext _context; public MigrationHelperTests() { // Use SQLite with PendingModelChangesWarning suppressed because the migration // was generated for SQL Server and SQLite's model representation differs slightly. var options = new DbContextOptionsBuilder() .UseSqlite("DataSource=:memory:") .ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning)) .Options; _context = new ScadaLinkDbContext(options); _context.Database.OpenConnection(); } public void Dispose() { _context.Database.CloseConnection(); _context.Dispose(); } [Fact] public async Task ApplyOrValidate_ProductionMode_WithPendingMigrations_Throws() { // Database has no schema yet, so pending migrations exist. // The production path uses GetPendingMigrationsAsync which works cross-provider. var ex = await Assert.ThrowsAsync( () => MigrationHelper.ApplyOrValidateMigrationsAsync(_context, isDevelopment: false)); Assert.Contains("pending migration", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public void MigrationExists_InitialCreate() { // Verify the InitialCreate migration is detected as pending var pending = _context.Database.GetPendingMigrations().ToList(); Assert.Contains(pending, m => m.Contains("InitialSchema")); } }