using Grpc.Core; using NSubstitute; using LmxFakeProxy.Bridge; using LmxFakeProxy.Grpc; using LmxFakeProxy.Sessions; using LmxFakeProxy.Services; namespace LmxFakeProxy.Tests; public class ScadaServiceTests { private readonly IOpcUaBridge _mockBridge; private readonly SessionManager _sessionMgr; private readonly TagMapper _tagMapper; private readonly ScadaServiceImpl _service; public ScadaServiceTests() { _mockBridge = Substitute.For(); _mockBridge.IsConnected.Returns(true); _sessionMgr = new SessionManager(null); _tagMapper = new TagMapper("ns=3;s="); _service = new ScadaServiceImpl(_sessionMgr, _mockBridge, _tagMapper); } private string ConnectClient(string clientId = "test-client") { var (_, _, sessionId) = _sessionMgr.Connect(clientId, ""); return sessionId; } private static ServerCallContext MockContext() { return new TestServerCallContext(); } [Fact] public async Task Connect_ReturnsSessionId() { var resp = await _service.Connect( new ConnectRequest { ClientId = "c1", ApiKey = "" }, MockContext()); Assert.True(resp.Success); Assert.NotEmpty(resp.SessionId); } [Fact] public async Task Read_ValidSession_ReturnsVtq() { var sid = ConnectClient(); _mockBridge.ReadAsync("ns=3;s=Motor.Speed", Arg.Any()) .Returns(new OpcUaReadResult(42.5, DateTime.UtcNow, 0)); var resp = await _service.Read( new ReadRequest { SessionId = sid, Tag = "Motor.Speed" }, MockContext()); Assert.True(resp.Success); Assert.Equal("42.5", resp.Vtq.Value); Assert.Equal("Good", resp.Vtq.Quality); } [Fact] public async Task Read_InvalidSession_ReturnsFailure() { var resp = await _service.Read( new ReadRequest { SessionId = "bogus", Tag = "Motor.Speed" }, MockContext()); Assert.False(resp.Success); Assert.Contains("Invalid", resp.Message); } [Fact] public async Task ReadBatch_ReturnsAllTags() { var sid = ConnectClient(); _mockBridge.ReadAsync(Arg.Any(), Arg.Any()) .Returns(new OpcUaReadResult(1.0, DateTime.UtcNow, 0)); var req = new ReadBatchRequest { SessionId = sid }; req.Tags.AddRange(new[] { "Motor.Speed", "Pump.FlowRate" }); var resp = await _service.ReadBatch(req, MockContext()); Assert.True(resp.Success); Assert.Equal(2, resp.Vtqs.Count); } [Fact] public async Task Write_ValidSession_Succeeds() { var sid = ConnectClient(); _mockBridge.WriteAsync("ns=3;s=Motor.Speed", Arg.Any(), Arg.Any()) .Returns(0u); var resp = await _service.Write( new WriteRequest { SessionId = sid, Tag = "Motor.Speed", Value = "42.5" }, MockContext()); Assert.True(resp.Success); } [Fact] public async Task Write_InvalidSession_ReturnsFailure() { var resp = await _service.Write( new WriteRequest { SessionId = "bogus", Tag = "Motor.Speed", Value = "42.5" }, MockContext()); Assert.False(resp.Success); } [Fact] public async Task WriteBatch_ReturnsPerItemResults() { var sid = ConnectClient(); _mockBridge.WriteAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(0u); var req = new WriteBatchRequest { SessionId = sid }; req.Items.Add(new WriteItem { Tag = "Motor.Speed", Value = "42.5" }); req.Items.Add(new WriteItem { Tag = "Pump.FlowRate", Value = "10.0" }); var resp = await _service.WriteBatch(req, MockContext()); Assert.True(resp.Success); Assert.Equal(2, resp.Results.Count); Assert.All(resp.Results, r => Assert.True(r.Success)); } [Fact] public async Task CheckApiKey_Valid_ReturnsTrue() { var resp = await _service.CheckApiKey( new CheckApiKeyRequest { ApiKey = "anything" }, MockContext()); Assert.True(resp.IsValid); } [Fact] public async Task CheckApiKey_Invalid_ReturnsFalse() { var mgr = new SessionManager("secret"); var svc = new ScadaServiceImpl(mgr, _mockBridge, _tagMapper); var resp = await svc.CheckApiKey( new CheckApiKeyRequest { ApiKey = "wrong" }, MockContext()); Assert.False(resp.IsValid); } } /// /// Minimal ServerCallContext for unit testing gRPC services. /// internal class TestServerCallContext : ServerCallContext { protected override string MethodCore => "test"; protected override string HostCore => "localhost"; protected override string PeerCore => "test-peer"; protected override DateTime DeadlineCore => DateTime.MaxValue; protected override Metadata RequestHeadersCore => new(); protected override CancellationToken CancellationTokenCore => CancellationToken.None; protected override Metadata ResponseTrailersCore => new(); protected override Status StatusCore { get; set; } protected override WriteOptions? WriteOptionsCore { get; set; } protected override AuthContext AuthContextCore => new("test", new Dictionary>()); protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions? options) => throw new NotImplementedException(); protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) => Task.CompletedTask; }