fix(notification-outbox+test): provider-aware InsertIfNotExists for SQLite + supply ApiKeyPepper in IntegrationTests host config (#286)

This commit is contained in:
Joseph Doherty
2026-06-19 01:03:48 -04:00
parent 649e45b5c0
commit 6a4c9a85b8
2 changed files with 37 additions and 0 deletions
@@ -61,6 +61,39 @@ public class NotificationOutboxRepository : INotificationOutboxRepository
var type = n.Type.ToString();
var status = n.Status.ToString();
// Provider-aware idempotent insert. The production path is SQL Server; the
// NotificationOutbox integration tests run over an in-memory SQLite database,
// and SQLite does not understand the T-SQL `IF NOT EXISTS … INSERT` form
// (it raises 'near "IF": syntax error'). Detect SQLite by provider name so
// ConfigurationDatabase needs no Sqlite package reference; the SQL Server
// path below stays byte-identical.
if (_context.Database.ProviderName == "Microsoft.EntityFrameworkCore.Sqlite")
{
// SQLite's idempotent insert: INSERT OR IGNORE skips the row when the
// NotificationId primary key already exists (no schema prefix in SQLite).
// Same parameterised columns/values as the SQL Server branch — the
// FormattableString interpolation still parameterises every value
// (no concatenation), so this is injection-safe.
var sqliteRowsAffected = await _context.Database.ExecuteSqlInterpolatedAsync(
$@"INSERT OR IGNORE INTO Notifications
(NotificationId, Type, ListName, Subject, Body, TypeData, Status, RetryCount, LastError,
ResolvedTargets, SourceSiteId, SourceNode, SourceInstanceId, SourceScript,
OriginExecutionId, OriginParentExecutionId,
SiteEnqueuedAt, CreatedAt, LastAttemptAt, NextAttemptAt, DeliveredAt)
VALUES
({n.NotificationId}, {type}, {n.ListName}, {n.Subject}, {n.Body}, {n.TypeData}, {status}, {n.RetryCount}, {n.LastError},
{n.ResolvedTargets}, {n.SourceSiteId}, {n.SourceNode}, {n.SourceInstanceId}, {n.SourceScript},
{n.OriginExecutionId}, {n.OriginParentExecutionId},
{n.SiteEnqueuedAt}, {n.CreatedAt}, {n.LastAttemptAt}, {n.NextAttemptAt}, {n.DeliveredAt});",
cancellationToken);
// rowsAffected == 1 -> we inserted; 0 -> the row was IGNOREd because the
// NotificationId already existed. Preserves the true=inserted / false=already-existed
// contract. No SqlException catch needed — INSERT OR IGNORE never raises a
// unique-constraint violation.
return sqliteRowsAffected == 1;
}
// FormattableString interpolation parameterises every value (no concatenation),
// so this is safe against injection even for the string columns.
try
@@ -38,6 +38,10 @@ public class ScadaBridgeWebApplicationFactory : WebApplicationFactory<Program>
["ScadaBridge__Database__MachineDataDb"] = "Server=localhost;Database=ScadaBridge_MachineData_Test;TrustServerCertificate=True",
["ScadaBridge__Database__SkipMigrations"] = "true",
["ScadaBridge__Security__JwtSigningKey"] = "integration-test-signing-key-must-be-at-least-32-chars-long",
// The inbound API-key pepper is a REQUIRED Central config value (StartupValidator
// enforces a >=16-char floor; it backs the peppered-HMAC verifier). Supply a fixed
// test pepper so host boot passes validation in the test environment.
["ScadaBridge__InboundApi__ApiKeyPepper"] = "integration-test-api-key-pepper-0123456789",
// Task 1.4: LDAP settings nest under Security:Ldap (shared LdapOptions) and use
// the renamed keys (Transport replaces LdapUseTls; None == plaintext for the
// GLAuth dev directory, paired with AllowInsecure=true).