using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening; using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation; namespace ZB.MOM.WW.ScadaBridge.DeploymentManager.Tests; /// /// WP-8: Tests for deployed vs template-derived state comparison. /// /// PLAN-05 Task 16: adds behavioral coverage proving the read-only /// staleness/comparison path can flatten (and thus hash) an instance whose /// script does NOT compile — because validateScripts: false skips the /// expensive Roslyn /// step while keeping structural/semantic validation. The default (deploy) path /// still compiles. /// public class DeploymentComparisonTests { [Fact] public void DeploymentComparisonResult_MatchingHashes_NotStale() { var result = new DeploymentComparisonResult( 1, "sha256:abc", "sha256:abc", false, DateTimeOffset.UtcNow); Assert.False(result.IsStale); Assert.Equal("sha256:abc", result.DeployedRevisionHash); Assert.Equal("sha256:abc", result.CurrentRevisionHash); } [Fact] public void DeploymentComparisonResult_DifferentHashes_IsStale() { var result = new DeploymentComparisonResult( 1, "sha256:old", "sha256:new", true, DateTimeOffset.UtcNow); Assert.True(result.IsStale); Assert.NotEqual(result.DeployedRevisionHash, result.CurrentRevisionHash); } [Fact] public void DeploymentComparisonResult_ContainsDeployedTimestamp() { var deployedAt = new DateTimeOffset(2026, 3, 16, 12, 0, 0, TimeSpan.Zero); var result = new DeploymentComparisonResult(1, "h1", "h2", true, deployedAt); Assert.Equal(deployedAt, result.DeployedAt); } // ── PLAN-05 Task 16: read-only paths skip Roslyn script compilation ── private const int InstanceId = 1; private const int TemplateId = 10; private const int SiteId = 100; // A deliberately non-compiling script body. The deploy gate // (validateScripts: true) flags this as a ScriptCompilation error; the // read-only staleness/comparison path (validateScripts: false) must skip // the compile entirely and still produce a flattened config + hash. private const string NonCompilingScript = "int x = ;"; private static FlatteningPipeline BuildPipeline( ITemplateEngineRepository templateRepo, ISiteRepository siteRepo, ISharedSchemaRepository sharedSchemaRepo) { sharedSchemaRepo.ListAsync(Arg.Any()).Returns([]); return new FlatteningPipeline( templateRepo, siteRepo, new FlatteningService(), new ValidationService(), new RevisionHashService(), sharedSchemaRepo); } private static (ITemplateEngineRepository, ISiteRepository, ISharedSchemaRepository) ArrangeNonCompilingScript() { var templateRepo = Substitute.For(); var siteRepo = Substitute.For(); var sharedSchemaRepo = Substitute.For(); var template = new Template("Tank") { Id = TemplateId }; template.Scripts.Add(new TemplateScript("BadScript", NonCompilingScript)); var instance = new Instance("Tank-01") { Id = InstanceId, TemplateId = TemplateId, SiteId = SiteId }; templateRepo.GetInstanceByIdAsync(InstanceId, Arg.Any()).Returns(instance); templateRepo.GetTemplateWithChildrenAsync(TemplateId, Arg.Any()).Returns(template); templateRepo.GetCompositionsByTemplateIdAsync(TemplateId, Arg.Any()).Returns([]); templateRepo.GetAllSharedScriptsAsync(Arg.Any()).Returns([]); siteRepo.GetDataConnectionsBySiteIdAsync(SiteId, Arg.Any()).Returns([]); return (templateRepo, siteRepo, sharedSchemaRepo); } [Fact] public async Task FlattenAndValidate_ValidateScriptsFalse_NonCompilingScript_SucceedsWithHashAndNoCompilationError() { var (templateRepo, siteRepo, sharedSchemaRepo) = ArrangeNonCompilingScript(); var sut = BuildPipeline(templateRepo, siteRepo, sharedSchemaRepo); var result = await sut.FlattenAndValidateAsync(InstanceId, validateScripts: false); Assert.True(result.IsSuccess); Assert.False(string.IsNullOrEmpty(result.Value.RevisionHash)); // The Roslyn compile step was skipped, so no ScriptCompilation error is raised // even though the script body does not compile. Assert.DoesNotContain(result.Value.Validation.Errors, e => e.Category == ValidationCategory.ScriptCompilation); } [Fact] public async Task FlattenAndValidate_DefaultValidateScripts_NonCompilingScript_ReportsCompilationError() { var (templateRepo, siteRepo, sharedSchemaRepo) = ArrangeNonCompilingScript(); var sut = BuildPipeline(templateRepo, siteRepo, sharedSchemaRepo); // Default (deploy) path — compilation is the authoritative gate. var result = await sut.FlattenAndValidateAsync(InstanceId); Assert.True(result.IsSuccess); Assert.Contains(result.Value.Validation.Errors, e => e.Category == ValidationCategory.ScriptCompilation); } }