feat(lmxproxy): phase 7 — integration test project and test scenarios
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,5 +6,6 @@
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/ZB.MOM.WW.LmxProxy.Host.Tests/ZB.MOM.WW.LmxProxy.Host.Tests.csproj" />
|
||||
<Project Path="tests/ZB.MOM.WW.LmxProxy.Client.Tests/ZB.MOM.WW.LmxProxy.Client.Tests.csproj" />
|
||||
<Project Path="tests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests/ZB.MOM.WW.LmxProxy.Client.IntegrationTests.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Grpc.Core.RpcException>(
|
||||
() => 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
global using Xunit;
|
||||
global using ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<bool>(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<double>(vtq.Value);
|
||||
Assert.True(vtq.Quality.IsGood());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_StringTag_ReturnsStringValue()
|
||||
{
|
||||
var vtq = await Client!.ReadAsync("TestChildObject.TestString");
|
||||
Assert.IsType<string>(vtq.Value);
|
||||
Assert.True(vtq.Quality.IsGood());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Read_DateTimeTag_ReturnsDateTimeValue()
|
||||
{
|
||||
var vtq = await Client!.ReadAsync("TestChildObject.TestDateTime");
|
||||
Assert.IsType<DateTime>(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);
|
||||
}
|
||||
}
|
||||
@@ -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<bool>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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<string, TypedValue>
|
||||
{
|
||||
["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);
|
||||
}
|
||||
}
|
||||
@@ -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<Grpc.Core.RpcException>(
|
||||
() => readOnlyClient.WriteAsync("TestChildObject.TestString",
|
||||
new TypedValue { StringValue = "should-fail" }));
|
||||
Assert.Equal(Grpc.Core.StatusCode.PermissionDenied, ex.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.LmxProxy.Client\ZB.MOM.WW.LmxProxy.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.test.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user