From d61881ffafa7abd1730ae8128c1fbd4a241b3a37 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 10 Jul 2026 06:59:45 -0400 Subject: [PATCH] =?UTF-8?q?refactor(site-runtime):=20excise=20vestigial=20?= =?UTF-8?q?site-side=20notification-list=20surface=20=E2=80=94=20repo,=20D?= =?UTF-8?q?I=20registration,=20dead=20write=20paths=20(arch-review=2008=20?= =?UTF-8?q?=C2=A71.3/#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj --- docs/components/SiteRuntime.md | 2 +- .../Persistence/SiteStorageService.cs | 80 +---- .../SiteNotificationRepository.cs | 300 ------------------ .../ServiceCollectionExtensions.cs | 1 - .../CompositionRootTests.cs | 13 +- .../Persistence/ArtifactStorageTests.cs | 85 +++-- .../Repositories/SiteRepositoryTests.cs | 25 +- 7 files changed, 73 insertions(+), 433 deletions(-) delete mode 100644 src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteNotificationRepository.cs diff --git a/docs/components/SiteRuntime.md b/docs/components/SiteRuntime.md index e5675159..a114056c 100644 --- a/docs/components/SiteRuntime.md +++ b/docs/components/SiteRuntime.md @@ -12,7 +12,7 @@ The component code lives in `src/ZB.MOM.WW.ScadaBridge.SiteRuntime/`: - `Scripts/` — `ScriptCompilationService`, `ScriptExecutionScheduler`, `SharedScriptLibrary`, `ScriptRuntimeContext`, `ScopeAccessors`, `TriggerExpressionGlobals`. - `Streaming/` — `SiteStreamManager` (the site-wide Akka broadcast stream). - `Persistence/` — `SiteStorageService` (raw SQLite via `Microsoft.Data.Sqlite`), `SiteStorageInitializer`. -- `Repositories/` — `SiteExternalSystemRepository`, `SiteNotificationRepository`. +- `Repositories/` — `SiteExternalSystemRepository`. (The `SiteNotificationRepository` variant was removed 2026-07-10, arch-review 08 §1.3/#23, because notification config is central-only and never lives on a site.) - `Tracking/` — `OperationTrackingStore`, `OperationTrackingOptions`. `ServiceCollectionExtensions.AddSiteRuntime(connectionString)` registers all singletons; the `Host` calls it and wires the `DeploymentManagerActor` cluster singleton separately via `AkkaHostedService`. diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs index 60800328..52a4680e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Persistence/SiteStorageService.cs @@ -799,6 +799,12 @@ public class SiteStorageService /// idempotent and is invoked on every artifact apply / deploy so existing exposure /// is cleared, not just future writes. The tables themselves are retained (the /// schema is harmless once empty); only their contents are removed. + /// + /// The site-side write paths (StoreNotificationListAsync/StoreSmtpConfigurationAsync) + /// and the SiteNotificationRepository were removed 2026-07-10 (arch-review 08 §1.3/#23) + /// because notification config is central-only and must never be written to a site — this + /// purge is the sole remaining touchpoint, kept only to scrub DBs written by older builds; + /// do not reintroduce site-side notification writes. /// /// A task that completes when both tables have been emptied. public async Task PurgeCentralOnlyNotificationConfigAsync() @@ -813,80 +819,6 @@ public class SiteStorageService await command.ExecuteNonQueryAsync(); } - /// - /// Stores or updates a notification list. - /// - /// The name of the notification list. - /// List of recipient email addresses. - /// A task that completes when the notification list has been stored or updated. - public async Task StoreNotificationListAsync(string name, IReadOnlyList recipientEmails) - { - await using var connection = new SqliteConnection(_connectionString); - await connection.OpenAsync(); - - await using var command = connection.CreateCommand(); - command.CommandText = @" - INSERT INTO notification_lists (name, recipient_emails, updated_at) - VALUES (@name, @emails, @updatedAt) - ON CONFLICT(name) DO UPDATE SET - recipient_emails = excluded.recipient_emails, - updated_at = excluded.updated_at"; - - command.Parameters.AddWithValue("@name", name); - command.Parameters.AddWithValue("@emails", System.Text.Json.JsonSerializer.Serialize(recipientEmails)); - command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O")); - - await command.ExecuteNonQueryAsync(); - } - - // ── SMTP Configuration CRUD ── - - /// - /// Stores or updates an SMTP configuration. - /// - /// The name of the SMTP configuration. - /// SMTP server hostname. - /// SMTP server port. - /// Authentication mode ('None', 'BasicAuth', 'OAuth2'). - /// Email address used as the sender. - /// Username for authentication, if applicable. - /// Password for authentication, if applicable. - /// OAuth2 configuration JSON, if applicable. - /// A task that completes when the SMTP configuration has been stored or updated. - public async Task StoreSmtpConfigurationAsync( - string name, string server, int port, string authMode, string fromAddress, - string? username, string? password, string? oauthConfig) - { - await using var connection = new SqliteConnection(_connectionString); - await connection.OpenAsync(); - - await using var command = connection.CreateCommand(); - command.CommandText = @" - INSERT INTO smtp_configurations (name, server, port, auth_mode, from_address, username, password, oauth_config, updated_at) - VALUES (@name, @server, @port, @authMode, @fromAddress, @username, @password, @oauthConfig, @updatedAt) - ON CONFLICT(name) DO UPDATE SET - server = excluded.server, - port = excluded.port, - auth_mode = excluded.auth_mode, - from_address = excluded.from_address, - username = excluded.username, - password = excluded.password, - oauth_config = excluded.oauth_config, - updated_at = excluded.updated_at"; - - command.Parameters.AddWithValue("@name", name); - command.Parameters.AddWithValue("@server", server); - command.Parameters.AddWithValue("@port", port); - command.Parameters.AddWithValue("@authMode", authMode); - command.Parameters.AddWithValue("@fromAddress", fromAddress); - command.Parameters.AddWithValue("@username", (object?)username ?? DBNull.Value); - command.Parameters.AddWithValue("@password", (object?)password ?? DBNull.Value); - command.Parameters.AddWithValue("@oauthConfig", (object?)oauthConfig ?? DBNull.Value); - command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O")); - - await command.ExecuteNonQueryAsync(); - } - // ── Data Connection Definition CRUD ── /// diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteNotificationRepository.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteNotificationRepository.cs deleted file mode 100644 index c69171bc..00000000 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/Repositories/SiteNotificationRepository.cs +++ /dev/null @@ -1,300 +0,0 @@ -using System.Text.Json; -using Microsoft.Data.Sqlite; -using ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications; -using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; -using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; - -namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories; - -/// -/// Site-side read-only implementation of -/// backed by the local SQLite database via . -/// Write operations throw because site-local -/// artifacts are managed exclusively through deployment from Central. -/// -public class SiteNotificationRepository : INotificationRepository -{ - private readonly SiteStorageService _storage; - - /// - /// Initializes a new instance of the class. - /// - /// The site storage service for database access. - public SiteNotificationRepository(SiteStorageService storage) - { - _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - } - - // ── NotificationList (read) ── - - /// - public async Task GetListByNameAsync( - string name, CancellationToken cancellationToken = default) - { - await using var connection = CreateConnection(); - await connection.OpenAsync(cancellationToken); - - await using var command = connection.CreateCommand(); - command.CommandText = @" - SELECT name, recipient_emails - FROM notification_lists - WHERE name = @name"; - command.Parameters.AddWithValue("@name", name); - - await using var reader = await command.ExecuteReaderAsync(cancellationToken); - if (!await reader.ReadAsync(cancellationToken)) - return null; - - return MapNotificationList(reader); - } - - /// - public async Task> GetAllNotificationListsAsync( - CancellationToken cancellationToken = default) - { - await using var connection = CreateConnection(); - await connection.OpenAsync(cancellationToken); - - await using var command = connection.CreateCommand(); - command.CommandText = "SELECT name, recipient_emails FROM notification_lists"; - - var results = new List(); - await using var reader = await command.ExecuteReaderAsync(cancellationToken); - while (await reader.ReadAsync(cancellationToken)) - { - results.Add(MapNotificationList(reader)); - } - - return results; - } - - /// - public async Task GetNotificationListByIdAsync( - int id, CancellationToken cancellationToken = default) - { - // The SQLite table is keyed by name, not by integer ID. - // Scan all rows and match on the synthetic ID derived from the name. - var all = await GetAllNotificationListsAsync(cancellationToken); - return all.FirstOrDefault(l => l.Id == id); - } - - // ── NotificationRecipient (read) ── - - /// - public async Task> GetRecipientsByListIdAsync( - int notificationListId, CancellationToken cancellationToken = default) - { - // Find the parent list to get its name, then parse the recipient_emails JSON. - var list = await GetNotificationListByIdAsync(notificationListId, cancellationToken); - if (list is null) - return Array.Empty(); - - await using var connection = CreateConnection(); - await connection.OpenAsync(cancellationToken); - - await using var command = connection.CreateCommand(); - command.CommandText = @" - SELECT recipient_emails - FROM notification_lists - WHERE name = @name"; - command.Parameters.AddWithValue("@name", list.Name); - - var json = (string?)await command.ExecuteScalarAsync(cancellationToken); - if (string.IsNullOrWhiteSpace(json)) - return Array.Empty(); - - return ParseRecipientEmails(json, notificationListId); - } - - /// - public async Task GetRecipientByIdAsync( - int id, CancellationToken cancellationToken = default) - { - // Scan all lists and their recipients to find the matching synthetic ID. - var lists = await GetAllNotificationListsAsync(cancellationToken); - foreach (var list in lists) - { - var recipients = await GetRecipientsByListIdAsync(list.Id, cancellationToken); - var match = recipients.FirstOrDefault(r => r.Id == id); - if (match is not null) - return match; - } - - return null; - } - - // ── SmtpConfiguration (read) ── - - /// - public async Task> GetAllSmtpConfigurationsAsync( - CancellationToken cancellationToken = default) - { - await using var connection = CreateConnection(); - await connection.OpenAsync(cancellationToken); - - await using var command = connection.CreateCommand(); - command.CommandText = @" - SELECT name, server, port, auth_mode, from_address, username, password, oauth_config - FROM smtp_configurations"; - - var results = new List(); - await using var reader = await command.ExecuteReaderAsync(cancellationToken); - while (await reader.ReadAsync(cancellationToken)) - { - results.Add(MapSmtpConfiguration(reader)); - } - - return results; - } - - /// - public async Task GetSmtpConfigurationByIdAsync( - int id, CancellationToken cancellationToken = default) - { - var all = await GetAllSmtpConfigurationsAsync(cancellationToken); - return all.FirstOrDefault(s => s.Id == id); - } - - // ── Write operations (not supported on site) ── - - /// - public Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - // ── SmsConfiguration (central-only — never deployed to or served from the site) ── - - /// - public Task GetSmsConfigurationAsync(CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task GetSmsConfigurationByIdAsync(int id, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task> GetAllSmsConfigurationsAsync(CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task AddSmsConfigurationAsync(SmsConfiguration smsConfiguration, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task UpdateSmsConfigurationAsync(SmsConfiguration smsConfiguration, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task DeleteSmsConfigurationAsync(int id, CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - /// - public Task SaveChangesAsync(CancellationToken cancellationToken = default) - => throw new NotSupportedException("Managed via artifact deployment from Central"); - - // ── Private helpers ── - - /// - /// Creates a new SQLite connection against the site database via - /// instead of - /// reaching into its private connection-string field via reflection. - /// - private SqliteConnection CreateConnection() => _storage.CreateConnection(); - - private static NotificationList MapNotificationList(SqliteDataReader reader) - { - var name = reader.GetString(0); - var list = new NotificationList(name) - { - Id = GenerateSyntheticId(name) - }; - - // Eagerly populate Recipients from the JSON column. - var emailsJson = reader.GetString(1); - var recipients = ParseRecipientEmails(emailsJson, list.Id); - foreach (var r in recipients) - { - list.Recipients.Add(r); - } - - return list; - } - - private static IReadOnlyList ParseRecipientEmails( - string json, int notificationListId) - { - try - { - var emails = JsonSerializer.Deserialize>(json, - new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - - if (emails is null) - return Array.Empty(); - - return emails.Select(email => new NotificationRecipient( - name: email, - emailAddress: email) - { - Id = GenerateSyntheticId($"{notificationListId}:{email}"), - NotificationListId = notificationListId - }).ToList(); - } - catch (JsonException) - { - return Array.Empty(); - } - } - - private static SmtpConfiguration MapSmtpConfiguration(SqliteDataReader reader) - { - var name = reader.GetString(0); - return new SmtpConfiguration( - host: reader.GetString(1), - authType: reader.GetString(3), - fromAddress: reader.GetString(4)) - { - Id = GenerateSyntheticId(name), - Port = reader.GetInt32(2), - Credentials = reader.IsDBNull(5) ? null : reader.GetString(5), - TlsMode = reader.IsDBNull(7) ? null : reader.GetString(7) - }; - } - - /// - /// Generates a stable positive integer ID from a string name. - /// Uses a deterministic FNV-1a hash rather than , - /// which is randomized per process on .NET Core and would change every restart. - /// - private static int GenerateSyntheticId(string name) => SyntheticId.From(name); -} diff --git a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs index b5db5550..20b612da 100644 --- a/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ServiceCollectionExtensions.cs @@ -62,7 +62,6 @@ public static class ServiceCollectionExtensions // Site-local repository implementations backed by SQLite services.AddScoped(); - services.AddScoped(); // Notify-and-fetch: typed HttpClient for fetching deployment configs from central. services.AddHttpClient() diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs index ba94e831..f054b651 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs @@ -424,7 +424,8 @@ public class SiteCompositionRootTests : IDisposable public static IEnumerable SiteScopedServices => new[] { new object[] { typeof(IExternalSystemRepository) }, - new object[] { typeof(INotificationRepository) }, + // INotificationRepository is intentionally absent — notification config is + // central-only (arch-review 08 §1.3/#23); the site-side variant was removed. new object[] { typeof(ExternalSystemClient) }, new object[] { typeof(IExternalSystemClient) }, new object[] { typeof(DatabaseGateway) }, @@ -442,11 +443,15 @@ public class SiteCompositionRootTests : IDisposable } [Fact] - public void Site_NotificationRepository_IsSiteImplementation() + public void Site_NotificationRepository_IsNotRegistered() { + // Notification delivery is central-only (CLAUDE.md "Notification delivery is + // central-only"); the site-local notification_lists/smtp_configurations tables + // are purged on every artifact apply, so a site-side INotificationRepository + // could only ever serve empty results. It must not be registered at all. using var scope = _host.Services.CreateScope(); - var repo = scope.ServiceProvider.GetRequiredService(); - Assert.IsType(repo); + var repo = scope.ServiceProvider.GetService(); + Assert.Null(repo); } // --- Options --- diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/ArtifactStorageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/ArtifactStorageTests.cs index e753bc1c..7d3e8423 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/ArtifactStorageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Persistence/ArtifactStorageTests.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging.Abstractions; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; -using ZB.MOM.WW.ScadaBridge.SiteRuntime.Repositories; namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Persistence; @@ -123,46 +122,67 @@ public class ArtifactStorageTests : IAsyncLifetime, IDisposable // Upsert should not throw } - // ── Notification List Storage ── - - [Fact] - public async Task StoreNotificationList_DoesNotThrow() - { - await _storage.StoreNotificationListAsync( - "Ops Team", ["ops@example.com", "admin@example.com"]); - } - - [Fact] - public async Task StoreNotificationList_Upserts() - { - await _storage.StoreNotificationListAsync("Team1", ["a@b.com"]); - await _storage.StoreNotificationListAsync("Team1", ["x@y.com", "z@w.com"]); - - // Upsert should not throw - } - // ── DeploymentManager-025 / SiteRuntime-031: central-only notif/SMTP purge ── + // + // Notification config is central-only. The site-side write paths and + // SiteNotificationRepository were removed 2026-07-10 (arch-review 08 §1.3/#23); + // PurgeCentralOnlyNotificationConfigAsync is retained as the security cleanup for + // DBs written by older builds. These tests seed the (still-present) tables via raw + // SQL — the only way rows can now exist — and assert the purge empties them. + + private async Task SeedNotificationRowAsync(string name, string emailsJson) + { + await using var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={_dbFile}"); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = + "INSERT INTO notification_lists (name, recipient_emails, updated_at) VALUES (@n, @e, @u)"; + command.Parameters.AddWithValue("@n", name); + command.Parameters.AddWithValue("@e", emailsJson); + command.Parameters.AddWithValue("@u", DateTimeOffset.UtcNow.ToString("O")); + await command.ExecuteNonQueryAsync(); + } + + private async Task SeedSmtpRowAsync(string name, string password) + { + await using var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={_dbFile}"); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = + @"INSERT INTO smtp_configurations (name, server, port, auth_mode, from_address, username, password, oauth_config, updated_at) + VALUES (@n, 'smtp.example.com', 587, 'BasicAuth', 'noreply@example.com', 'smtpuser', @p, NULL, @u)"; + command.Parameters.AddWithValue("@n", name); + command.Parameters.AddWithValue("@p", password); + command.Parameters.AddWithValue("@u", DateTimeOffset.UtcNow.ToString("O")); + await command.ExecuteNonQueryAsync(); + } + + private async Task RowCountAsync(string table) + { + await using var connection = new Microsoft.Data.Sqlite.SqliteConnection($"Data Source={_dbFile}"); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = $"SELECT COUNT(*) FROM {table}"; + return (long)(await command.ExecuteScalarAsync())!; + } [Fact] public async Task PurgeCentralOnlyNotificationConfig_RemovesPersistedNotificationListsAndSmtpRows() { // Simulate a pre-fix build that already shipped a notification list and an // SMTP config (with a plaintext password) to the site. - await _storage.StoreNotificationListAsync("Ops Team", ["ops@example.com"]); - await _storage.StoreSmtpConfigurationAsync( - "smtp.example.com:587", "smtp.example.com", 587, "BasicAuth", - "noreply@example.com", "smtpuser", "PLAINTEXT-SECRET", null); + await SeedNotificationRowAsync("Ops Team", "[\"ops@example.com\"]"); + await SeedSmtpRowAsync("smtp.example.com:587", "PLAINTEXT-SECRET"); - var repo = new SiteNotificationRepository(_storage); - Assert.NotEmpty(await repo.GetAllNotificationListsAsync()); - Assert.NotEmpty(await repo.GetAllSmtpConfigurationsAsync()); + Assert.Equal(1, await RowCountAsync("notification_lists")); + Assert.Equal(1, await RowCountAsync("smtp_configurations")); // The fix: every artifact apply/deploy purges these central-only rows. await _storage.PurgeCentralOnlyNotificationConfigAsync(); // Both tables are now empty — the plaintext SMTP credential is gone. - Assert.Empty(await repo.GetAllNotificationListsAsync()); - Assert.Empty(await repo.GetAllSmtpConfigurationsAsync()); + Assert.Equal(0, await RowCountAsync("notification_lists")); + Assert.Equal(0, await RowCountAsync("smtp_configurations")); } [Fact] @@ -172,9 +192,8 @@ public class ArtifactStorageTests : IAsyncLifetime, IDisposable await _storage.PurgeCentralOnlyNotificationConfigAsync(); await _storage.PurgeCentralOnlyNotificationConfigAsync(); - var repo = new SiteNotificationRepository(_storage); - Assert.Empty(await repo.GetAllNotificationListsAsync()); - Assert.Empty(await repo.GetAllSmtpConfigurationsAsync()); + Assert.Equal(0, await RowCountAsync("notification_lists")); + Assert.Equal(0, await RowCountAsync("smtp_configurations")); } // ── Schema includes all WP-33 tables ── @@ -186,7 +205,9 @@ public class ArtifactStorageTests : IAsyncLifetime, IDisposable await _storage.StoreSharedScriptAsync("s", "code", null, null); await _storage.StoreExternalSystemAsync("e", "url", "None", null, null); await _storage.StoreDatabaseConnectionAsync("d", "connstr", 1, TimeSpan.Zero); - await _storage.StoreNotificationListAsync("n", ["email@test.com"]); + + // notification_lists / smtp_configurations remain in the schema (kept for the + // security purge) — their presence is exercised by the purge tests above. // All succeeded without exceptions = tables exist } diff --git a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs index 14b1f08b..a5b68ffb 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests/Repositories/SiteRepositoryTests.cs @@ -199,25 +199,8 @@ public class SiteRepositoryTests : IDisposable Assert.Null(await repo.GetDatabaseConnectionByNameAsync("DoesNotExist")); } - /// - /// SiteRuntime-007: the same stability guarantee for notification lists. - /// - [Fact] - public async Task NotificationRepository_SyntheticId_IsStableAcrossRestart() - { - var storage1 = NewStorage(); - await storage1.InitializeAsync(); - await storage1.StoreNotificationListAsync( - "OnCall", new[] { "a@example.com", "b@example.com" }); - - var repo1 = new SiteNotificationRepository(storage1); - var idBeforeRestart = (await repo1.GetAllNotificationListsAsync())[0].Id; - - var storage2 = NewStorage(); - var repo2 = new SiteNotificationRepository(storage2); - var found = await repo2.GetNotificationListByIdAsync(idBeforeRestart); - - Assert.NotNull(found); - Assert.Equal("OnCall", found.Name); - } + // NotificationRepository_SyntheticId_IsStableAcrossRestart was removed 2026-07-10 + // (arch-review 08 §1.3/#23) along with the vestigial SiteNotificationRepository — + // notification config is central-only and never lives on a site. The synthetic-ID + // stability guarantee is still exercised by ExternalSystemRepository_SyntheticId_IsStableAcrossRestart. }