using Microsoft.Extensions.Caching.Memory; using NSubstitute; using ScadaLink.CentralUI.ScriptAnalysis; namespace ScadaLink.CentralUI.Tests.ScriptAnalysis; public class ScriptAnalysisServiceTests { private readonly ISharedScriptCatalog _catalog = Substitute.For(); private readonly IMemoryCache _cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 }); private readonly ScriptAnalysisService _svc; public ScriptAnalysisServiceTests() { _catalog.GetNamesAsync().Returns(Array.Empty()); _svc = new ScriptAnalysisService(_catalog, _cache); } [Fact] public void EmptyCode_NoMarkers() { var resp = _svc.Diagnose(new DiagnoseRequest("")); Assert.Empty(resp.Markers); } [Fact] public void CleanScript_NoMarkers() { var resp = _svc.Diagnose(new DiagnoseRequest("var x = 1 + 2; return x;")); Assert.Empty(resp.Markers); } [Fact] public void MissingSemicolon_ReportsRoslynDiagnostic() { var resp = _svc.Diagnose(new DiagnoseRequest("var x = 1\n")); Assert.Contains(resp.Markers, m => m.Code.StartsWith("CS")); } [Fact] public void ForbiddenUsingDirective_RaisesSCADA001() { var resp = _svc.Diagnose(new DiagnoseRequest("using System.IO;")); Assert.Contains(resp.Markers, m => m.Code == "SCADA001" && m.Message.Contains("System.IO")); } [Theory] [InlineData("System.Diagnostics")] [InlineData("System.Reflection")] [InlineData("System.Net")] public void ForbiddenUsing_AllBannedNamespaces(string ns) { var resp = _svc.Diagnose(new DiagnoseRequest($"using {ns};")); Assert.Contains(resp.Markers, m => m.Code == "SCADA001" && m.Message.Contains(ns)); } [Fact] public void ForbiddenTypeUsage_ResolvesViaSemanticModel() { var resp = _svc.Diagnose(new DiagnoseRequest( "using System.IO; var s = File.ReadAllText(\"x\");")); Assert.Contains(resp.Markers, m => m.Code == "SCADA002" && m.Message.Contains("File")); } [Fact] public void UserIdentifierNamedFile_DoesNotFalsePositive() { // No System.IO import; user defines their own 'File' local. var resp = _svc.Diagnose(new DiagnoseRequest( "var File = \"hello\"; return File.Length;")); Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA002"); } [Fact] public void UserIdentifierNamedThread_DoesNotFalsePositive() { var resp = _svc.Diagnose(new DiagnoseRequest( "var Thread = 42; return Thread;")); Assert.DoesNotContain(resp.Markers, m => m.Code == "SCADA002"); } [Fact] public void DiagnosticsAreCached_SecondCallSkipsRecompile() { var req = new DiagnoseRequest("using System.IO;"); var first = _svc.Diagnose(req); var second = _svc.Diagnose(req); // Same instance reference indicates the cache returned the prior result. Assert.Same(first, second); } [Fact] public void DifferentCode_GetsDifferentCacheEntries() { var a = _svc.Diagnose(new DiagnoseRequest("var x = 1;")); var b = _svc.Diagnose(new DiagnoseRequest("var y = 2;")); Assert.NotSame(a, b); } [Fact] public async Task ParametersStringLiteral_ReturnsDeclaredParameterNames() { var req = new CompletionsRequest( CodeText: "var x = Parameters[\"", Line: 1, Column: 21, DeclaredParameters: new[] { "name", "temperature" }); var resp = await _svc.CompleteAsync(req); Assert.Contains(resp.Items, i => i.Label == "name" && i.Detail == "declared parameter"); Assert.Contains(resp.Items, i => i.Label == "temperature"); } [Fact] public async Task CallScriptStringLiteral_ReturnsSiblingNames() { var req = new CompletionsRequest( CodeText: "var x = CallScript(\"", Line: 1, Column: 21, SiblingScripts: new[] { "SiblingA", "SiblingB" }); var resp = await _svc.CompleteAsync(req); Assert.Contains(resp.Items, i => i.Label == "SiblingA" && i.Detail == "sibling script"); Assert.Contains(resp.Items, i => i.Label == "SiblingB"); } [Fact] public async Task CallSharedStringLiteral_ResolvesViaCatalog() { _catalog.GetNamesAsync().Returns(new[] { "GetWeather", "Greet" }); var req = new CompletionsRequest( CodeText: "var x = CallShared(\"", Line: 1, Column: 21); var resp = await _svc.CompleteAsync(req); Assert.Contains(resp.Items, i => i.Label == "GetWeather" && i.Detail == "shared script"); Assert.Contains(resp.Items, i => i.Label == "Greet"); } [Fact] public async Task GeneralCompletion_ReturnsInScopeSymbols() { // At file scope of a script, ScriptHost members + the System namespace are visible. var req = new CompletionsRequest("var x = ", 1, 9); var resp = await _svc.CompleteAsync(req); Assert.Contains(resp.Items, i => i.Label == "Parameters"); Assert.Contains(resp.Items, i => i.Label == "CallShared"); } }