using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Tests.Harness; /// /// An in-memory ConfigDb that behaves like SQL Server where the InMemory provider does not (lmxopcua#868 third /// review): a FailureReason longer than the column refuses the whole SaveChanges with a /// over SQL error 2628. It can also reject (, /// error 547) or fail transiently (, deadlock 1205) every update of /// an existing NodeDeploymentState row — each a real SqlException inside EF's /// , the shape SQL Server produces (fourth review M1). /// public sealed class ConstraintEnforcingDbFactory(string dbName) : IDbContextFactory { private readonly Interceptor _interceptor = new(); /// Every update of an existing node row is rejected as a constraint violation. public bool PoisonNodeStateUpdates { get => _interceptor.Poison; set => _interceptor.Poison = value; } /// Every update of an existing node row fails as if the connection dropped. public bool FailNodeStateUpdatesTransiently { get => _interceptor.Transient; set => _interceptor.Transient = value; } private int _failNextContexts; /// The next context creations throw (an unreachable server). public void FailNextContexts(int count) => Volatile.Write(ref _failNextContexts, count); /// public OtOpcUaConfigDbContext CreateDbContext() => Volatile.Read(ref _failNextContexts) > 0 && Interlocked.Decrement(ref _failNextContexts) >= 0 ? throw new InvalidOperationException("ConfigDb unreachable (simulated)") : Create(); private OtOpcUaConfigDbContext Create() => new(new DbContextOptionsBuilder() .UseInMemoryDatabase(dbName) .AddInterceptors(_interceptor) .Options); private sealed class Interceptor : SaveChangesInterceptor { public volatile bool Poison; public volatile bool Transient; public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) { Check(eventData.Context!); return result; } public override ValueTask> SavingChangesAsync( DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { Check(eventData.Context!); return ValueTask.FromResult(result); } private void Check(DbContext ctx) { foreach (var e in ctx.ChangeTracker.Entries()) { var reason = e.Entity switch { NodeDeploymentState n => n.FailureReason, Deployment d => d.FailureReason, _ => null, }; if (reason is { Length: > FailureReasons.MaxLength }) throw new DbUpdateException("An error occurred while saving the entity changes.", SqlExceptionFactory.Create(2628, "String or binary data would be truncated in column 'FailureReason'.")); if (e.Entity is NodeDeploymentState && e.State == EntityState.Modified) { if (Poison) throw new DbUpdateException("An error occurred while saving the entity changes.", SqlExceptionFactory.Create(547, "The UPDATE statement conflicted with a CHECK constraint.")); if (Transient) throw new DbUpdateException("An error occurred while saving the entity changes.", SqlExceptionFactory.Create(1205, "Transaction was deadlocked on lock resources and has been chosen as the deadlock victim.")); } } } } }