feat(lmxproxy): phase 5 — client core (ILmxProxyClient, connection, read/write/subscribe)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
using FluentAssertions;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.LmxProxy.Client.Domain;
|
||||
using ZB.MOM.WW.LmxProxy.Client.Tests.Fakes;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Client.Tests;
|
||||
|
||||
public class LmxProxyClientReadWriteTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReadAsync_ReturnsVtqFromResponse()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
fake.ReadResponseToReturn = new ReadResponse
|
||||
{
|
||||
Success = true,
|
||||
Vtq = new VtqMessage
|
||||
{
|
||||
Tag = "TestTag",
|
||||
Value = new TypedValue { DoubleValue = 42.5 },
|
||||
TimestampUtcTicks = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks,
|
||||
Quality = new QualityCode { StatusCode = 0x00000000 }
|
||||
}
|
||||
};
|
||||
|
||||
var result = await client.ReadAsync("TestTag");
|
||||
|
||||
result.Value.Should().Be(42.5);
|
||||
result.Quality.Should().Be(Quality.Good);
|
||||
fake.ReadCalls.Should().HaveCount(1);
|
||||
fake.ReadCalls[0].Tag.Should().Be("TestTag");
|
||||
fake.ReadCalls[0].SessionId.Should().Be("test-session-123");
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_ThrowsOnFailureResponse()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
fake.ReadResponseToReturn = new ReadResponse { Success = false, Message = "Tag not found" };
|
||||
|
||||
var act = () => client.ReadAsync("BadTag");
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Tag not found*");
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadAsync_ThrowsWhenNotConnected()
|
||||
{
|
||||
var client = new LmxProxyClient("localhost", 50051, null, null);
|
||||
|
||||
var act = () => client.ReadAsync("AnyTag");
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*not connected*");
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReadBatchAsync_ReturnsDictionaryOfVtqs()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
fake.ReadBatchResponseToReturn = new ReadBatchResponse
|
||||
{
|
||||
Success = true,
|
||||
Vtqs =
|
||||
[
|
||||
new VtqMessage
|
||||
{
|
||||
Tag = "Tag1",
|
||||
Value = new TypedValue { Int32Value = 100 },
|
||||
TimestampUtcTicks = DateTime.UtcNow.Ticks,
|
||||
Quality = new QualityCode { StatusCode = 0x00000000 }
|
||||
},
|
||||
new VtqMessage
|
||||
{
|
||||
Tag = "Tag2",
|
||||
Value = new TypedValue { BoolValue = true },
|
||||
TimestampUtcTicks = DateTime.UtcNow.Ticks,
|
||||
Quality = new QualityCode { StatusCode = 0x00000000 }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var result = await client.ReadBatchAsync(["Tag1", "Tag2"]);
|
||||
|
||||
result.Should().HaveCount(2);
|
||||
result["Tag1"].Value.Should().Be(100);
|
||||
result["Tag2"].Value.Should().Be(true);
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_SendsTypedValueDirectly()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
var typedValue = new TypedValue { DoubleValue = 99.9 };
|
||||
|
||||
await client.WriteAsync("TestTag", typedValue);
|
||||
|
||||
fake.WriteCalls.Should().HaveCount(1);
|
||||
fake.WriteCalls[0].Tag.Should().Be("TestTag");
|
||||
fake.WriteCalls[0].Value.Should().NotBeNull();
|
||||
fake.WriteCalls[0].Value!.DoubleValue.Should().Be(99.9);
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteAsync_ThrowsOnFailureResponse()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
fake.WriteResponseToReturn = new WriteResponse { Success = false, Message = "Write error" };
|
||||
|
||||
var act = () => client.WriteAsync("Tag", new TypedValue { Int32Value = 1 });
|
||||
|
||||
await act.Should().ThrowAsync<InvalidOperationException>()
|
||||
.WithMessage("*Write error*");
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteBatchAsync_SendsAllItems()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
var values = new Dictionary<string, TypedValue>
|
||||
{
|
||||
["Tag1"] = new TypedValue { DoubleValue = 1.0 },
|
||||
["Tag2"] = new TypedValue { Int32Value = 2 },
|
||||
["Tag3"] = new TypedValue { BoolValue = true }
|
||||
};
|
||||
|
||||
await client.WriteBatchAsync(values);
|
||||
|
||||
fake.WriteBatchCalls.Should().HaveCount(1);
|
||||
fake.WriteBatchCalls[0].Items.Should().HaveCount(3);
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WriteBatchAndWaitAsync_ReturnsResponse()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
fake.WriteBatchAndWaitResponseToReturn = new WriteBatchAndWaitResponse
|
||||
{
|
||||
Success = true,
|
||||
FlagReached = true,
|
||||
ElapsedMs = 150,
|
||||
WriteResults = [new WriteResult { Tag = "Tag1", Success = true }]
|
||||
};
|
||||
var values = new Dictionary<string, TypedValue>
|
||||
{
|
||||
["Tag1"] = new TypedValue { Int32Value = 1 }
|
||||
};
|
||||
|
||||
var result = await client.WriteBatchAndWaitAsync(
|
||||
values, "FlagTag", new TypedValue { BoolValue = true });
|
||||
|
||||
result.FlagReached.Should().BeTrue();
|
||||
result.ElapsedMs.Should().Be(150);
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckApiKeyAsync_ReturnsApiKeyInfo()
|
||||
{
|
||||
var (client, fake) = TestableClient.CreateConnected();
|
||||
fake.CheckApiKeyResponseToReturn = new CheckApiKeyResponse { IsValid = true, Message = "Admin key" };
|
||||
|
||||
var result = await client.CheckApiKeyAsync("my-api-key");
|
||||
|
||||
result.IsValid.Should().BeTrue();
|
||||
result.Description.Should().Be("Admin key");
|
||||
client.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user