From 6d9bf594ec837644feba9c88e3bf7c4f4fa59c91 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 22 Mar 2026 00:31:26 -0400 Subject: [PATCH] =?UTF-8?q?feat(lmxproxy):=20phase=207=20=E2=80=94=20integ?= =?UTF-8?q?ration=20test=20project=20and=20test=20scenarios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- lmxproxy/ZB.MOM.WW.LmxProxy.slnx | 1 + .../CheckApiKeyTests.cs | 25 ++++++++ .../ConnectionTests.cs | 29 +++++++++ .../GlobalUsings.cs | 2 + .../IntegrationTestBase.cs | 56 +++++++++++++++++ .../ReadTests.cs | 61 +++++++++++++++++++ .../SubscribeTests.cs | 34 +++++++++++ .../WriteBatchAndWaitTests.cs | 26 ++++++++ .../WriteTests.cs | 30 +++++++++ ...WW.LmxProxy.Client.IntegrationTests.csproj | 29 +++++++++ .../appsettings.test.json | 9 +++ 11 files changed, 302 insertions(+) create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/CheckApiKeyTests.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ConnectionTests.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/GlobalUsings.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/IntegrationTestBase.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ReadTests.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/SubscribeTests.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteBatchAndWaitTests.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteTests.cs create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests.csproj create mode 100644 lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/appsettings.test.json diff --git a/lmxproxy/ZB.MOM.WW.LmxProxy.slnx b/lmxproxy/ZB.MOM.WW.LmxProxy.slnx index 4f43016..ca75a8e 100644 --- a/lmxproxy/ZB.MOM.WW.LmxProxy.slnx +++ b/lmxproxy/ZB.MOM.WW.LmxProxy.slnx @@ -6,5 +6,6 @@ + diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/CheckApiKeyTests.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/CheckApiKeyTests.cs new file mode 100644 index 0000000..b5095e3 --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/CheckApiKeyTests.cs @@ -0,0 +1,25 @@ +namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests; + +public class CheckApiKeyTests : IntegrationTestBase +{ + [Fact] + public async Task CheckApiKey_ValidReadWrite_ReturnsValid() + { + var info = await Client!.CheckApiKeyAsync(ReadWriteApiKey); + Assert.True(info.IsValid); + } + + [Fact] + public async Task CheckApiKey_ValidReadOnly_ReturnsValid() + { + var info = await Client!.CheckApiKeyAsync(ReadOnlyApiKey); + Assert.True(info.IsValid); + } + + [Fact] + public async Task CheckApiKey_Invalid_ReturnsInvalid() + { + var info = await Client!.CheckApiKeyAsync("totally-invalid-key-12345"); + Assert.False(info.IsValid); + } +} diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ConnectionTests.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ConnectionTests.cs new file mode 100644 index 0000000..e0622e6 --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ConnectionTests.cs @@ -0,0 +1,29 @@ +namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests; + +public class ConnectionTests : IntegrationTestBase +{ + [Fact] + public async Task ConnectAndDisconnect_Succeeds() + { + // Client is connected in InitializeAsync + Assert.True(await Client!.IsConnectedAsync()); + await Client.DisconnectAsync(); + Assert.False(await Client.IsConnectedAsync()); + } + + [Fact] + public async Task ConnectWithInvalidApiKey_Fails() + { + using var badClient = CreateClient(InvalidApiKey); + var ex = await Assert.ThrowsAsync( + () => badClient.ConnectAsync()); + Assert.Equal(Grpc.Core.StatusCode.Unauthenticated, ex.StatusCode); + } + + [Fact] + public async Task DoubleConnect_IsIdempotent() + { + await Client!.ConnectAsync(); // Already connected — should be no-op + Assert.True(await Client.IsConnectedAsync()); + } +} diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/GlobalUsings.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/GlobalUsings.cs new file mode 100644 index 0000000..d2a23ce --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/GlobalUsings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using ZB.MOM.WW.LmxProxy.Client.Domain; diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/IntegrationTestBase.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/IntegrationTestBase.cs new file mode 100644 index 0000000..d11ee7b --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/IntegrationTestBase.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Configuration; +using ZB.MOM.WW.LmxProxy.Client; + +namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests; + +public abstract class IntegrationTestBase : IAsyncLifetime +{ + protected IConfiguration Configuration { get; } + protected string Host { get; } + protected int Port { get; } + protected string ReadWriteApiKey { get; } + protected string ReadOnlyApiKey { get; } + protected string InvalidApiKey { get; } + protected LmxProxyClient? Client { get; set; } + + protected IntegrationTestBase() + { + Configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.test.json") + .Build(); + + var section = Configuration.GetSection("LmxProxy"); + Host = section["Host"] ?? "10.100.0.48"; + Port = int.Parse(section["Port"] ?? "50052"); + ReadWriteApiKey = section["ReadWriteApiKey"] ?? throw new Exception("ReadWriteApiKey not configured"); + ReadOnlyApiKey = section["ReadOnlyApiKey"] ?? throw new Exception("ReadOnlyApiKey not configured"); + InvalidApiKey = section["InvalidApiKey"] ?? "invalid-key"; + } + + protected LmxProxyClient CreateClient(string? apiKey = null) + { + return new LmxProxyClientBuilder() + .WithHost(Host) + .WithPort(Port) + .WithApiKey(apiKey ?? ReadWriteApiKey) + .WithTimeout(TimeSpan.FromSeconds(10)) + .WithRetryPolicy(2, TimeSpan.FromSeconds(1)) + .WithMetrics() + .Build(); + } + + public virtual async Task InitializeAsync() + { + Client = CreateClient(); + await Client.ConnectAsync(); + } + + public virtual async Task DisposeAsync() + { + if (Client is not null) + { + await Client.DisconnectAsync(); + Client.Dispose(); + } + } +} diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ReadTests.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ReadTests.cs new file mode 100644 index 0000000..9047e71 --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ReadTests.cs @@ -0,0 +1,61 @@ +namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests; + +public class ReadTests : IntegrationTestBase +{ + [Fact] + public async Task Read_BoolTag_ReturnsBoolValue() + { + var vtq = await Client!.ReadAsync("TestChildObject.TestBool"); + Assert.IsType(vtq.Value); + Assert.True(vtq.Quality.IsGood()); + } + + [Fact] + public async Task Read_IntTag_ReturnsIntValue() + { + var vtq = await Client!.ReadAsync("TestChildObject.TestInt"); + Assert.True(vtq.Value is int or long); + Assert.True(vtq.Quality.IsGood()); + } + + [Fact] + public async Task Read_FloatTag_ReturnsFloatValue() + { + var vtq = await Client!.ReadAsync("TestChildObject.TestFloat"); + Assert.True(vtq.Value is float or double); + Assert.True(vtq.Quality.IsGood()); + } + + [Fact] + public async Task Read_DoubleTag_ReturnsDoubleValue() + { + var vtq = await Client!.ReadAsync("TestChildObject.TestDouble"); + Assert.IsType(vtq.Value); + Assert.True(vtq.Quality.IsGood()); + } + + [Fact] + public async Task Read_StringTag_ReturnsStringValue() + { + var vtq = await Client!.ReadAsync("TestChildObject.TestString"); + Assert.IsType(vtq.Value); + Assert.True(vtq.Quality.IsGood()); + } + + [Fact] + public async Task Read_DateTimeTag_ReturnsDateTimeValue() + { + var vtq = await Client!.ReadAsync("TestChildObject.TestDateTime"); + Assert.IsType(vtq.Value); + Assert.True(vtq.Quality.IsGood()); + Assert.True(DateTime.UtcNow - vtq.Timestamp < TimeSpan.FromHours(1)); + } + + [Fact] + public async Task ReadBatch_MultipleTags_ReturnsDictionary() + { + var tags = new[] { "TestChildObject.TestString", "TestChildObject.TestInt" }; + var results = await Client!.ReadBatchAsync(tags); + Assert.Equal(2, results.Count); + } +} diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/SubscribeTests.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/SubscribeTests.cs new file mode 100644 index 0000000..ed2d307 --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/SubscribeTests.cs @@ -0,0 +1,34 @@ +using ZB.MOM.WW.LmxProxy.Client.Domain; + +namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests; + +public class SubscribeTests : IntegrationTestBase +{ + [Fact] + public async Task Subscribe_ReceivesUpdates() + { + var received = new List<(string Tag, Vtq Vtq)>(); + var receivedEvent = new TaskCompletionSource(); + + var subscription = await Client!.SubscribeAsync( + new[] { "TestChildObject.TestInt" }, + (tag, vtq) => + { + received.Add((tag, vtq)); + if (received.Count >= 3) + receivedEvent.TrySetResult(true); + }, + ex => receivedEvent.TrySetException(ex)); + + // Wait up to 30 seconds for at least 3 updates + var completed = await Task.WhenAny(receivedEvent.Task, Task.Delay(TimeSpan.FromSeconds(30))); + subscription.Dispose(); + + Assert.True(received.Count >= 1, $"Expected at least 1 update, got {received.Count}"); + + var first = received[0]; + Assert.Equal("TestChildObject.TestInt", first.Tag); + Assert.NotNull(first.Vtq.Value); + Assert.True(first.Vtq.Timestamp > DateTime.MinValue); + } +} diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteBatchAndWaitTests.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteBatchAndWaitTests.cs new file mode 100644 index 0000000..64f6405 --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteBatchAndWaitTests.cs @@ -0,0 +1,26 @@ +using ZB.MOM.WW.LmxProxy.Client.Domain; + +namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests; + +public class WriteBatchAndWaitTests : IntegrationTestBase +{ + [Fact] + public async Task WriteBatchAndWait_TypeAwareComparison() + { + var values = new Dictionary + { + ["TestChildObject.TestString"] = new TypedValue { StringValue = "BatchTest" } + }; + + var response = await Client!.WriteBatchAndWaitAsync( + values, + flagTag: "TestChildObject.TestString", + flagValue: new TypedValue { StringValue = "BatchTest" }, + timeoutMs: 5000, + pollIntervalMs: 200); + + Assert.True(response.Success); + Assert.True(response.FlagReached); + Assert.True(response.ElapsedMs < 5000); + } +} diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteTests.cs b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteTests.cs new file mode 100644 index 0000000..38d34e9 --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/WriteTests.cs @@ -0,0 +1,30 @@ +using ZB.MOM.WW.LmxProxy.Client.Domain; + +namespace ZB.MOM.WW.LmxProxy.Client.IntegrationTests; + +public class WriteTests : IntegrationTestBase +{ + [Fact] + public async Task WriteAndReadBack_StringValue() + { + string testValue = $"IntTest-{DateTime.UtcNow:HHmmss}"; + await Client!.WriteAsync("TestChildObject.TestString", + new TypedValue { StringValue = testValue }); + + await Task.Delay(500); // Allow time for write to propagate + var vtq = await Client.ReadAsync("TestChildObject.TestString"); + Assert.Equal(testValue, vtq.Value); + } + + [Fact] + public async Task WriteWithReadOnlyKey_ThrowsPermissionDenied() + { + using var readOnlyClient = CreateClient(ReadOnlyApiKey); + await readOnlyClient.ConnectAsync(); + + var ex = await Assert.ThrowsAsync( + () => readOnlyClient.WriteAsync("TestChildObject.TestString", + new TypedValue { StringValue = "should-fail" })); + Assert.Equal(Grpc.Core.StatusCode.PermissionDenied, ex.StatusCode); + } +} diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests.csproj b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests.csproj new file mode 100644 index 0000000..7799e60 --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests.csproj @@ -0,0 +1,29 @@ + + + + net10.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/appsettings.test.json b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/appsettings.test.json new file mode 100644 index 0000000..8782d8f --- /dev/null +++ b/lmxproxy/tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/appsettings.test.json @@ -0,0 +1,9 @@ +{ + "LmxProxy": { + "Host": "10.100.0.48", + "Port": 50052, + "ReadWriteApiKey": "REPLACE_WITH_ACTUAL_KEY", + "ReadOnlyApiKey": "REPLACE_WITH_ACTUAL_KEY", + "InvalidApiKey": "invalid-key-that-does-not-exist" + } +}