junction/tests/Junction.Tests/Unit/ProtocolContractTests.cs
dtrentin 507753f82e init: scaffold Junction multi-protocol machine monitor (M1 walking skeleton)
Greenfield C# app reading industrial machine data over pluggable protocols.
M1 walking skeleton: MTConnect plugin polls a machine, Avalonia dashboard
shows live last datum. Build 0 warn/0 err on net48 + net8.0 (Linux),
115 unit tests + 2 docker-gated integration tests.

Projects:
- Junction.Domain (netstandard2.0): Result<T>, models, IProtocolDriver,
  IMachineRepository, plugin manifest. Zero package deps.
- Junction.Core (netstandard2.0): PollingEngine, PluginLoader
  (Assembly.LoadFrom), MachineMonitor, DI extensions.
- Junction.Persistence (netstandard2.0): SqliteMachineRepository (Dapper),
  schema, connection factory. Provider-swap seam to SQL Server.
- Junction.Protocols.MTConnect (netstandard2.0): HTTP driver + namespace-
  version-agnostic parser (MTConnect 1.7 + 2.0). Runtime plugin.
- Junction.App (net48;net8.0): Avalonia MVVM, live dashboard, NLog, DI root.
- Junction.Tests (net8.0): xUnit + Moq, 117 tests.
- mock/: docker-compose MTConnect agent (ladder99/agent).

Target net48 for Windows 7/8 fleet compatibility. Avalonia pinned 11.3.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 23:32:56 +02:00

139 lines
5 KiB
C#

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Junction.Domain;
using Junction.Domain.Models;
using Junction.Domain.Protocols;
using Moq;
using Xunit;
namespace Junction.Tests.Unit
{
/// <summary>
/// Contract/shape tests for the plugin protocol contract. Pure compile + behavior
/// against Moq doubles; no real IO.
/// </summary>
public class ProtocolContractTests
{
private static Machine SampleMachine(IReadOnlyDictionary<string, string>? config = null) =>
new Machine(
Guid.NewGuid(),
"M1",
"mtconnect",
config,
TimeSpan.FromSeconds(1));
private static MachineSnapshot SampleSnapshot(Guid machineId) =>
new MachineSnapshot(
machineId,
DateTimeOffset.UtcNow,
ConnectionState.Connected,
Array.Empty<DataItem>());
[Fact]
public void Driver_ProtocolId_ReturnsConfiguredValue()
{
var mock = new Mock<IProtocolDriver>();
mock.SetupGet(d => d.ProtocolId).Returns("mtconnect");
Assert.Equal("mtconnect", mock.Object.ProtocolId);
}
[Fact]
public async Task Driver_ReadCurrentAsync_Ok_ReturnsSnapshotResult()
{
var machineId = Guid.NewGuid();
var snapshot = SampleSnapshot(machineId);
var mock = new Mock<IProtocolDriver>();
mock.Setup(d => d.ReadCurrentAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<MachineSnapshot>.Ok(snapshot));
Result<MachineSnapshot> result = await mock.Object.ReadCurrentAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Same(snapshot, result.Value);
}
[Fact]
public async Task Driver_ReadCurrentAsync_Fail_ReturnsFailedResult()
{
var error = OperationError.Of("driver", "read failed");
var mock = new Mock<IProtocolDriver>();
mock.Setup(d => d.ReadCurrentAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<MachineSnapshot>.Fail(error));
Result<MachineSnapshot> result = await mock.Object.ReadCurrentAsync(CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.Errors);
}
[Fact]
public async Task Driver_ReadCurrentAsync_ForwardsCancellationToken()
{
using var cts = new CancellationTokenSource();
var mock = new Mock<IProtocolDriver>();
mock.Setup(d => d.ReadCurrentAsync(cts.Token))
.ReturnsAsync(Result<MachineSnapshot>.Cancelled());
Result<MachineSnapshot> result = await mock.Object.ReadCurrentAsync(cts.Token);
Assert.True(result.WasCancelled);
mock.Verify(d => d.ReadCurrentAsync(cts.Token), Times.Once);
}
[Fact]
public void Factory_ProtocolId_ReturnsConfiguredValue()
{
var mock = new Mock<IProtocolDriverFactory>();
mock.SetupGet(f => f.ProtocolId).Returns("mtconnect");
Assert.Equal("mtconnect", mock.Object.ProtocolId);
}
[Fact]
public void Factory_Create_Ok_WrapsDriver()
{
var driver = new Mock<IProtocolDriver>().Object;
var factory = new Mock<IProtocolDriverFactory>();
factory.Setup(f => f.Create(It.IsAny<Machine>()))
.Returns(Result<IProtocolDriver>.Ok(driver));
Result<IProtocolDriver> result = factory.Object.Create(SampleMachine());
Assert.True(result.IsSuccess);
Assert.Same(driver, result.Value);
}
[Fact]
public void Factory_Create_InvalidConfig_ReturnsFail()
{
// Simulate protocol-specific validation: missing required key => Fail.
var factory = new Mock<IProtocolDriverFactory>();
factory.Setup(f => f.Create(It.Is<Machine>(m => !m.ConnectionConfig.ContainsKey("endpoint"))))
.Returns(Result<IProtocolDriver>.Fail(
OperationError.Of("factory", "missing 'endpoint'")));
Result<IProtocolDriver> result = factory.Object.Create(SampleMachine());
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.Errors);
}
[Fact]
public void Factory_Create_ValidConfig_ReturnsOk()
{
var driver = new Mock<IProtocolDriver>().Object;
var factory = new Mock<IProtocolDriverFactory>();
factory.Setup(f => f.Create(It.Is<Machine>(m => m.ConnectionConfig.ContainsKey("endpoint"))))
.Returns(Result<IProtocolDriver>.Ok(driver));
var machine = SampleMachine(new Dictionary<string, string> { ["endpoint"] = "http://x" });
Result<IProtocolDriver> result = factory.Object.Create(machine);
Assert.True(result.IsSuccess);
Assert.Same(driver, result.Value);
}
}
}