using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ScadaLink.SiteRuntime.Persistence; namespace ScadaLink.SiteRuntime.Tests.Persistence; /// /// Tests for SiteStorageService using file-based SQLite (temp files). /// Validates the schema, CRUD operations, and constraint behavior. /// public class SiteStorageServiceTests : IAsyncLifetime, IDisposable { private readonly string _dbFile; private SiteStorageService _storage = null!; public SiteStorageServiceTests() { _dbFile = Path.Combine(Path.GetTempPath(), $"site-storage-test-{Guid.NewGuid():N}.db"); } public async Task InitializeAsync() { _storage = new SiteStorageService( $"Data Source={_dbFile}", NullLogger.Instance); await _storage.InitializeAsync(); } public Task DisposeAsync() => Task.CompletedTask; public void Dispose() { try { File.Delete(_dbFile); } catch { /* cleanup */ } } [Fact] public async Task InitializeAsync_CreatesTablesWithoutError() { // Already called in InitializeAsync — just verify no exception // Call again to verify idempotency (CREATE IF NOT EXISTS) await _storage.InitializeAsync(); } [Fact] public async Task StoreAndRetrieve_DeployedConfig_RoundTrips() { await _storage.StoreDeployedConfigAsync( "Pump1", "{\"test\":true}", "dep-001", "sha256:abc", isEnabled: true); var configs = await _storage.GetAllDeployedConfigsAsync(); Assert.Single(configs); Assert.Equal("Pump1", configs[0].InstanceUniqueName); Assert.Equal("{\"test\":true}", configs[0].ConfigJson); Assert.Equal("dep-001", configs[0].DeploymentId); Assert.Equal("sha256:abc", configs[0].RevisionHash); Assert.True(configs[0].IsEnabled); } [Fact] public async Task StoreDeployedConfig_Upserts_OnConflict() { await _storage.StoreDeployedConfigAsync( "Pump1", "{\"v\":1}", "dep-001", "sha256:aaa", isEnabled: true); await _storage.StoreDeployedConfigAsync( "Pump1", "{\"v\":2}", "dep-002", "sha256:bbb", isEnabled: false); var configs = await _storage.GetAllDeployedConfigsAsync(); Assert.Single(configs); Assert.Equal("{\"v\":2}", configs[0].ConfigJson); Assert.Equal("dep-002", configs[0].DeploymentId); Assert.False(configs[0].IsEnabled); } [Fact] public async Task RemoveDeployedConfig_RemovesConfigAndOverrides() { await _storage.StoreDeployedConfigAsync( "Pump1", "{}", "dep-001", "sha256:aaa", isEnabled: true); await _storage.SetStaticOverrideAsync("Pump1", "Temperature", "100"); await _storage.RemoveDeployedConfigAsync("Pump1"); var configs = await _storage.GetAllDeployedConfigsAsync(); var overrides = await _storage.GetStaticOverridesAsync("Pump1"); Assert.Empty(configs); Assert.Empty(overrides); } [Fact] public async Task SetInstanceEnabled_UpdatesFlag() { await _storage.StoreDeployedConfigAsync( "Pump1", "{}", "dep-001", "sha256:aaa", isEnabled: true); await _storage.SetInstanceEnabledAsync("Pump1", false); var configs = await _storage.GetAllDeployedConfigsAsync(); Assert.False(configs[0].IsEnabled); await _storage.SetInstanceEnabledAsync("Pump1", true); configs = await _storage.GetAllDeployedConfigsAsync(); Assert.True(configs[0].IsEnabled); } [Fact] public async Task SetInstanceEnabled_NonExistent_DoesNotThrow() { // Should not throw for a missing instance await _storage.SetInstanceEnabledAsync("DoesNotExist", true); } // ── Static Override Tests ── [Fact] public async Task SetAndGetStaticOverride_RoundTrips() { await _storage.SetStaticOverrideAsync("Pump1", "Temperature", "98.6"); var overrides = await _storage.GetStaticOverridesAsync("Pump1"); Assert.Single(overrides); Assert.Equal("98.6", overrides["Temperature"]); } [Fact] public async Task SetStaticOverride_Upserts_OnConflict() { await _storage.SetStaticOverrideAsync("Pump1", "Temperature", "98.6"); await _storage.SetStaticOverrideAsync("Pump1", "Temperature", "100.0"); var overrides = await _storage.GetStaticOverridesAsync("Pump1"); Assert.Single(overrides); Assert.Equal("100.0", overrides["Temperature"]); } [Fact] public async Task ClearStaticOverrides_RemovesAll() { await _storage.SetStaticOverrideAsync("Pump1", "Temperature", "98.6"); await _storage.SetStaticOverrideAsync("Pump1", "Pressure", "50.0"); await _storage.ClearStaticOverridesAsync("Pump1"); var overrides = await _storage.GetStaticOverridesAsync("Pump1"); Assert.Empty(overrides); } [Fact] public async Task GetStaticOverrides_IsolatedPerInstance() { await _storage.SetStaticOverrideAsync("Pump1", "Temperature", "98.6"); await _storage.SetStaticOverrideAsync("Pump2", "Pressure", "50.0"); var pump1 = await _storage.GetStaticOverridesAsync("Pump1"); var pump2 = await _storage.GetStaticOverridesAsync("Pump2"); Assert.Single(pump1); Assert.Single(pump2); Assert.True(pump1.ContainsKey("Temperature")); Assert.True(pump2.ContainsKey("Pressure")); } [Fact] public async Task MultipleInstances_IndependentLifecycle() { await _storage.StoreDeployedConfigAsync("Pump1", "{}", "d1", "h1", true); await _storage.StoreDeployedConfigAsync("Pump2", "{}", "d2", "h2", true); await _storage.StoreDeployedConfigAsync("Pump3", "{}", "d3", "h3", false); var configs = await _storage.GetAllDeployedConfigsAsync(); Assert.Equal(3, configs.Count); await _storage.RemoveDeployedConfigAsync("Pump2"); configs = await _storage.GetAllDeployedConfigsAsync(); Assert.Equal(2, configs.Count); Assert.DoesNotContain(configs, c => c.InstanceUniqueName == "Pump2"); } // ── Negative Tests ── [Fact] public async Task Schema_DoesNotContain_AlarmStateTable() { // Per design: no alarm state table in site SQLite var configs = await _storage.GetAllDeployedConfigsAsync(); var overrides = await _storage.GetStaticOverridesAsync("nonexistent"); Assert.Empty(configs); Assert.Empty(overrides); } }