junction/tests/Junction.Tests/Unit/MachineRepositoryContractTests.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

236 lines
9.6 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.Persistence;
using Moq;
using Xunit;
namespace Junction.Tests.Unit
{
/// <summary>
/// Shape/contract tests for <see cref="IMachineRepository"/> using a Moq mock.
/// No real database: these assert the async signatures and Result shapes only.
/// </summary>
public class MachineRepositoryContractTests
{
private static readonly OperationError SampleError =
new OperationError("repo.error", "repository", "boom");
private static Machine SampleMachine(Guid id) =>
new Machine(id, "Mill", "mtconnect", null, TimeSpan.FromSeconds(2));
private static MachineSnapshot SampleSnapshot(Guid machineId) =>
new MachineSnapshot(machineId, DateTimeOffset.UtcNow, ConnectionState.Connected, null);
[Fact]
public async Task GetAllAsync_Ok_WrapsReadOnlyListOfMachines()
{
var machines = new List<Machine> { SampleMachine(Guid.NewGuid()) };
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<IReadOnlyList<Machine>>.Ok(machines));
Result<IReadOnlyList<Machine>> result = await mock.Object.GetAllAsync(CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.False(result.WasCancelled);
Assert.Single(result.Value);
}
[Fact]
public async Task GetAllAsync_Fail_HasErrorsNoValue()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetAllAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<IReadOnlyList<Machine>>.Fail(SampleError));
var result = await mock.Object.GetAllAsync(CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Single(result.Errors);
Assert.Equal("repo.error", result.Errors[0].Code);
}
[Fact]
public async Task GetByIdAsync_Ok_WrapsMachine()
{
var id = Guid.NewGuid();
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetByIdAsync(id, It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<Machine>.Ok(SampleMachine(id)));
var result = await mock.Object.GetByIdAsync(id, CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(id, result.Value.Id);
}
[Fact]
public async Task GetByIdAsync_Fail_NotFoundShape()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<Machine>.Fail(SampleError));
var result = await mock.Object.GetByIdAsync(Guid.NewGuid(), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.Errors);
}
[Fact]
public async Task GetByIdAsync_Cancelled_SetsWasCancelled()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<Machine>.Cancelled());
var result = await mock.Object.GetByIdAsync(Guid.NewGuid(), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.True(result.WasCancelled);
}
[Fact]
public async Task UpsertAsync_ReturnsNonGenericResult_Ok()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.UpsertAsync(It.IsAny<Machine>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Ok());
Result result = await mock.Object.UpsertAsync(SampleMachine(Guid.NewGuid()), CancellationToken.None);
Assert.IsType<Result>(result);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task UpsertAsync_Fail_CarriesErrors()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.UpsertAsync(It.IsAny<Machine>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Fail(SampleError));
var result = await mock.Object.UpsertAsync(SampleMachine(Guid.NewGuid()), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Single(result.Errors);
}
[Fact]
public async Task DeleteAsync_ReturnsNonGenericResult_Ok()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.DeleteAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Ok());
Result result = await mock.Object.DeleteAsync(Guid.NewGuid(), CancellationToken.None);
Assert.IsType<Result>(result);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task DeleteAsync_Cancelled_SetsWasCancelled()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.DeleteAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Cancelled());
var result = await mock.Object.DeleteAsync(Guid.NewGuid(), CancellationToken.None);
Assert.True(result.WasCancelled);
Assert.False(result.IsSuccess);
}
[Fact]
public async Task SaveSnapshotAsync_ReturnsNonGenericResult_Ok()
{
var machineId = Guid.NewGuid();
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.SaveSnapshotAsync(It.IsAny<MachineSnapshot>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Ok());
Result result = await mock.Object.SaveSnapshotAsync(SampleSnapshot(machineId), CancellationToken.None);
Assert.IsType<Result>(result);
Assert.True(result.IsSuccess);
}
[Fact]
public async Task SaveSnapshotAsync_Fail_CarriesErrors()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.SaveSnapshotAsync(It.IsAny<MachineSnapshot>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result.Fail(SampleError));
var result = await mock.Object.SaveSnapshotAsync(SampleSnapshot(Guid.NewGuid()), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.Errors);
}
[Fact]
public async Task GetLatestSnapshotAsync_Ok_WrapsSnapshot()
{
var machineId = Guid.NewGuid();
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetLatestSnapshotAsync(machineId, It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<MachineSnapshot>.Ok(SampleSnapshot(machineId)));
Result<MachineSnapshot> result = await mock.Object.GetLatestSnapshotAsync(machineId, CancellationToken.None);
Assert.True(result.IsSuccess);
Assert.Equal(machineId, result.Value.MachineId);
Assert.Equal(ConnectionState.Connected, result.Value.ConnectionState);
}
[Fact]
public async Task GetLatestSnapshotAsync_Fail_NoneStored()
{
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetLatestSnapshotAsync(It.IsAny<Guid>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(Result<MachineSnapshot>.Fail(SampleError));
var result = await mock.Object.GetLatestSnapshotAsync(Guid.NewGuid(), CancellationToken.None);
Assert.False(result.IsSuccess);
Assert.Single(result.Errors);
}
[Fact]
public async Task AllMethods_HonorPassedCancellationToken()
{
using var cts = new CancellationTokenSource();
var token = cts.Token;
var mock = new Mock<IMachineRepository>();
mock.Setup(r => r.GetAllAsync(token))
.ReturnsAsync(Result<IReadOnlyList<Machine>>.Ok(new List<Machine>()));
mock.Setup(r => r.GetByIdAsync(It.IsAny<Guid>(), token))
.ReturnsAsync(Result<Machine>.Ok(SampleMachine(Guid.NewGuid())));
mock.Setup(r => r.UpsertAsync(It.IsAny<Machine>(), token)).ReturnsAsync(Result.Ok());
mock.Setup(r => r.DeleteAsync(It.IsAny<Guid>(), token)).ReturnsAsync(Result.Ok());
mock.Setup(r => r.SaveSnapshotAsync(It.IsAny<MachineSnapshot>(), token)).ReturnsAsync(Result.Ok());
mock.Setup(r => r.GetLatestSnapshotAsync(It.IsAny<Guid>(), token))
.ReturnsAsync(Result<MachineSnapshot>.Ok(SampleSnapshot(Guid.NewGuid())));
await mock.Object.GetAllAsync(token);
await mock.Object.GetByIdAsync(Guid.NewGuid(), token);
await mock.Object.UpsertAsync(SampleMachine(Guid.NewGuid()), token);
await mock.Object.DeleteAsync(Guid.NewGuid(), token);
await mock.Object.SaveSnapshotAsync(SampleSnapshot(Guid.NewGuid()), token);
await mock.Object.GetLatestSnapshotAsync(Guid.NewGuid(), token);
mock.Verify(r => r.GetAllAsync(token), Times.Once);
mock.Verify(r => r.GetByIdAsync(It.IsAny<Guid>(), token), Times.Once);
mock.Verify(r => r.UpsertAsync(It.IsAny<Machine>(), token), Times.Once);
mock.Verify(r => r.DeleteAsync(It.IsAny<Guid>(), token), Times.Once);
mock.Verify(r => r.SaveSnapshotAsync(It.IsAny<MachineSnapshot>(), token), Times.Once);
mock.Verify(r => r.GetLatestSnapshotAsync(It.IsAny<Guid>(), token), Times.Once);
}
}
}