using Moq; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Scripts; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Tests; public class SharedScriptServiceTests { private readonly Mock _repoMock; private readonly Mock _auditMock; private readonly SharedScriptService _service; /// Initializes the test class with mocked repository and audit services. public SharedScriptServiceTests() { _repoMock = new Mock(); _auditMock = new Mock(); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())).ReturnsAsync(1); _service = new SharedScriptService(_repoMock.Object, _auditMock.Object); } /// Verifies that create and update both reject invalid code before saving. /// The invalid script code. /// A task that represents the asynchronous operation. [Theory] [InlineData("return System.IO.File.ReadAllText(\"x\");")] [InlineData("return noSuchVariable;")] public async Task CreateAndUpdate_RejectInvalidCodeBeforeSaving(string code) { var existing = new SharedScript("Existing", "return 1;"); _repoMock.Setup(r => r.GetSharedScriptByIdAsync(1, It.IsAny())).ReturnsAsync(existing); var create = await _service.CreateSharedScriptAsync("New", code, null, null, "admin"); var update = await _service.UpdateSharedScriptAsync(1, code, null, null, "admin"); Assert.True(create.IsFailure); Assert.True(update.IsFailure); Assert.Equal("return 1;", existing.Code); _repoMock.Verify(r => r.SaveChangesAsync(It.IsAny()), Times.Never); } /// Verifies that creating a shared script succeeds. /// A task that represents the asynchronous operation. [Fact] public async Task CreateSharedScript_Success() { _repoMock.Setup(r => r.GetSharedScriptByNameAsync("Helpers", It.IsAny())) .ReturnsAsync((SharedScript?)null); var result = await _service.CreateSharedScriptAsync( "Helpers", "public static int Add(int a, int b) { return a + b; }", null, null, "admin"); Assert.True(result.IsSuccess); Assert.Equal("Helpers", result.Value.Name); _repoMock.Verify(r => r.AddSharedScriptAsync(It.IsAny(), It.IsAny()), Times.Once); } /// Verifies that creating a shared script with an empty name fails. /// A task that represents the asynchronous operation. [Fact] public async Task CreateSharedScript_EmptyName_Fails() { var result = await _service.CreateSharedScriptAsync("", "code", null, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("name is required", result.Error); } /// Verifies that creating a shared script with empty code fails. /// A task that represents the asynchronous operation. [Fact] public async Task CreateSharedScript_EmptyCode_Fails() { var result = await _service.CreateSharedScriptAsync("Test", "", null, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("code is required", result.Error); } /// Verifies that creating a shared script with a duplicate name fails. /// A task that represents the asynchronous operation. [Fact] public async Task CreateSharedScript_DuplicateName_Fails() { _repoMock.Setup(r => r.GetSharedScriptByNameAsync("Helpers", It.IsAny())) .ReturnsAsync(new SharedScript("Helpers", "existing code")); var result = await _service.CreateSharedScriptAsync("Helpers", "new code", null, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("already exists", result.Error); } /// Verifies that creating a shared script with unbalanced braces fails. /// A task that represents the asynchronous operation. [Fact] public async Task CreateSharedScript_UnbalancedBraces_Fails() { _repoMock.Setup(r => r.GetSharedScriptByNameAsync("Bad", It.IsAny())) .ReturnsAsync((SharedScript?)null); var result = await _service.CreateSharedScriptAsync("Bad", "public void Run() {", null, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("Syntax error", result.Error); } /// Verifies that updating a shared script succeeds. /// A task that represents the asynchronous operation. [Fact] public async Task UpdateSharedScript_Success() { var existing = new SharedScript("Helpers", "old code") { Id = 1 }; _repoMock.Setup(r => r.GetSharedScriptByIdAsync(1, It.IsAny())).ReturnsAsync(existing); var result = await _service.UpdateSharedScriptAsync(1, "return 42;", null, null, "admin"); Assert.True(result.IsSuccess); Assert.Equal("return 42;", result.Value.Code); } /// Verifies that updating a shared script that does not exist fails. /// A task that represents the asynchronous operation. [Fact] public async Task UpdateSharedScript_NotFound_Fails() { _repoMock.Setup(r => r.GetSharedScriptByIdAsync(999, It.IsAny())).ReturnsAsync((SharedScript?)null); var result = await _service.UpdateSharedScriptAsync(999, "code", null, null, "admin"); Assert.True(result.IsFailure); Assert.Contains("not found", result.Error); } /// Verifies that deleting a shared script succeeds. /// A task that represents the asynchronous operation. [Fact] public async Task DeleteSharedScript_Success() { var existing = new SharedScript("Helpers", "code") { Id = 1 }; _repoMock.Setup(r => r.GetSharedScriptByIdAsync(1, It.IsAny())).ReturnsAsync(existing); var result = await _service.DeleteSharedScriptAsync(1, "admin"); Assert.True(result.IsSuccess); _repoMock.Verify(r => r.DeleteSharedScriptAsync(1, It.IsAny()), Times.Once); } /// Verifies that deleting a shared script that does not exist fails. /// A task that represents the asynchronous operation. [Fact] public async Task DeleteSharedScript_NotFound_Fails() { _repoMock.Setup(r => r.GetSharedScriptByIdAsync(999, It.IsAny())).ReturnsAsync((SharedScript?)null); var result = await _service.DeleteSharedScriptAsync(999, "admin"); Assert.True(result.IsFailure); Assert.Contains("not found", result.Error); } // Syntax validation unit tests /// Verifies that syntax validation of valid code returns the expected result. /// The script code to validate. /// The expected validation result, or null when the code is valid. [Theory] [InlineData("return 42;", null)] [InlineData("public void Run() { }", null)] [InlineData("var x = new int[] { 1, 2, 3 };", null)] [InlineData("if (a > b) { return a; } else { return b; }", null)] public void ValidateSyntax_ValidCode_ReturnsNull(string code, string? expected) { Assert.Equal(expected, SharedScriptService.ValidateSyntax(code)); } /// Verifies that syntax validation of invalid code returns an error. /// The invalid script code. [Theory] [InlineData("public void Run() {")] [InlineData("return a + b);")] [InlineData("var x = new int[] { 1, 2 ;")] public void ValidateSyntax_InvalidCode_ReturnsError(string code) { var result = SharedScriptService.ValidateSyntax(code); Assert.NotNull(result); Assert.Contains("Syntax error", result); } // --- Regression: string/comment-literal awareness --- /// Verifies that delimiters inside strings or comments do not cause a syntax validation error. /// The script code containing delimiters inside a string or comment. [Theory] [InlineData("var s = \"a } brace\"; { }")] // brace inside a normal string [InlineData("var s = \"a ) paren ] bracket\";")] // paren/bracket inside a string [InlineData("var s = @\"verbatim } brace\"; { }")] // brace inside a verbatim string [InlineData("var x = 1; var s = $\"hole {x} literal}}\"; { }")] // interpolated string with braces [InlineData("var c = '}'; if (true) { }")] // char literal containing a brace [InlineData("// a stray } here\nvar x = 1;")] // brace inside a line comment [InlineData("/* a stray ) here */ var x = 1;")] // paren inside a block comment public void ValidateSyntax_DelimiterInsideStringOrComment_ReturnsNull(string code) { Assert.Null(SharedScriptService.ValidateSyntax(code)); } // --- Regression: audit row carries the real script Id --- /// Verifies that the create audit row carries the real script id rather than a literal zero. /// A task that represents the asynchronous operation. [Fact] public async Task CreateSharedScript_AuditRowCarriesRealScriptIdNotLiteralZero() { // Before the fix: AddSharedScriptAsync → LogAsync("0", ...) → SaveChangesAsync. // The audit row was queued with EntityId = "0" because EF Core had // not yet populated the auto-generated key. After the fix: save first, // then log with the real Id, then save the audit row. _repoMock.Setup(r => r.GetSharedScriptByNameAsync("Helpers", It.IsAny())) .ReturnsAsync((SharedScript?)null); SharedScript? added = null; _repoMock.Setup(r => r.AddSharedScriptAsync(It.IsAny(), It.IsAny())) .Callback((s, _) => added = s) .Returns(Task.CompletedTask); _repoMock.Setup(r => r.SaveChangesAsync(It.IsAny())) .Callback(_ => { if (added != null && added.Id == 0) added.Id = 314; }) .ReturnsAsync(1); string? auditedEntityId = null; _auditMock.Setup(a => a.LogAsync( It.IsAny(), "Create", "SharedScript", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Callback( (_, _, _, entityId, _, _, _) => auditedEntityId = entityId) .Returns(Task.CompletedTask); var result = await _service.CreateSharedScriptAsync( "Helpers", "public static int Add(int a, int b) { return a + b; }", null, null, "admin"); Assert.True(result.IsSuccess); Assert.Equal("314", auditedEntityId); Assert.NotEqual("0", auditedEntityId); } }