exchanger/tests/Exchanger.Tests/PeripheryControllerSimulatorTests.cs

261 lines
11 KiB
C#

using Exchanger.Core.Session;
using Exchanger.Hardware.Abstractions;
using Exchanger.Hardware.MockHardware;
using Exchanger.Hardware.PeripheryController;
using NUnit.Framework;
namespace Exchanger.Tests;
[TestFixture]
public sealed class PeripheryControllerSimulatorTests
{
private static readonly byte[] HostChallenge =
Convert.FromHexString("00112233445566778899AABBCCDDEEFF");
[TestCase("309100", "ERROR:30:invalid count (0-99)")]
[TestCase("409100", "ERROR:40:invalid count (0-99)")]
public void Simulator_RejectsSingleHopperBatchAboveNinetyNineLikeFirmware(string command, string error)
{
var simulator = new PeripheryControllerSimulator(new IdentityBlockCipher());
var received = new List<string>();
simulator.OnDataReceived += received.Add;
simulator.Connect("SIM", 115200);
simulator.ProcessPendingEvents();
received.Clear();
simulator.SendCommand(command);
simulator.ProcessPendingEvents();
Assert.That(received, Is.EqualTo(new[] { error }));
}
[Test]
public void Simulator_RunsAuthenticatedCardAndDispenseFlowWithoutComPort()
{
var cipher = new IdentityBlockCipher();
var simulator = new PeripheryControllerSimulator(cipher)
{
DigitalInputMask = 67_108_863,
};
var authenticator = new ControllerAuthenticator(cipher, () => HostChallenge);
using var controller = new PeripheryMachineController(
simulator,
authenticator,
PeripheryDispenseMode.Hopper1Auto,
stockSensorInputIndex: 1,
stockLowWhenInputHigh: false);
using var flow = new SessionFlow(TestSettings.Create(), controller, new ManualClock());
simulator.Connect("SIM", 115200);
CompleteStartup(simulator, controller);
Assert.That(controller.IsReady, Is.True);
Assert.That(flow.StockLevel, Is.EqualTo(MachineStockLevel.Enough));
Assert.That(flow.BeginCard(), Is.True);
Assert.That(flow.SelectCardAmount(100), Is.True);
simulator.ReportCashless(100);
simulator.ProcessPendingEvents();
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Completed));
Assert.That(simulator.SentCommands, Does.Contain("ASK:CASHLESS:100"));
Assert.That(simulator.SentCommands.Count(command => command == "30912"), Is.EqualTo(1));
Assert.That(simulator.SentCommands, Does.Not.Contain("40912"));
}
}
[Test]
public void Simulator_RunsAuthenticatedCashAndDispenseFlowWithoutComPort()
{
var cipher = new IdentityBlockCipher();
var simulator = new PeripheryControllerSimulator(cipher)
{
DigitalInputMask = 67_108_863,
};
var authenticator = new ControllerAuthenticator(cipher, () => HostChallenge);
using var controller = new PeripheryMachineController(
simulator,
authenticator,
PeripheryDispenseMode.Hopper1Auto,
stockSensorInputIndex: 1,
stockLowWhenInputHigh: false);
using var flow = new SessionFlow(TestSettings.Create(), controller, new ManualClock());
simulator.Connect("SIM", 115200);
CompleteStartup(simulator, controller);
int commandCount = simulator.SentCommands.Count;
Assert.That(flow.BeginCash(), Is.True);
simulator.ReportBill(50);
simulator.ProcessPendingEvents();
Assert.That(flow.ConfirmCashDispense(), Is.True);
simulator.ProcessPendingEvents();
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Completed));
Assert.That(simulator.SentCommands.Skip(commandCount).Take(3),
Is.EqualTo(new[] { "162", "141", "151" }));
Assert.That(simulator.SentCommands, Does.Contain("ASK:BILL:50"));
Assert.That(simulator.SentCommands, Does.Contain("142"));
Assert.That(simulator.SentCommands, Does.Contain("152"));
Assert.That(simulator.SentCommands.Count(command => command == "3095"), Is.EqualTo(1));
}
}
[Test]
public void Simulator_LargeCardPackageDispensesInSequentialProtocolSizedChunks()
{
var cipher = new IdentityBlockCipher();
var simulator = new PeripheryControllerSimulator(cipher)
{
DigitalInputMask = 67_108_863,
};
var authenticator = new ControllerAuthenticator(cipher, () => HostChallenge);
using var controller = new PeripheryMachineController(
simulator,
authenticator,
PeripheryDispenseMode.Hopper1Auto,
stockSensorInputIndex: 1,
stockLowWhenInputHigh: false);
using var flow = new SessionFlow(TestSettings.Create(), controller, new ManualClock());
simulator.Connect("SIM", 115200);
CompleteStartup(simulator, controller);
Assert.That(flow.BeginCard(), Is.True);
Assert.That(flow.SelectCardAmount(5000), Is.True);
simulator.ReportCashless(5000);
simulator.ProcessPendingEvents();
using (Assert.EnterMultipleScope())
{
Assert.That(flow.TotalTokens, Is.EqualTo(515));
Assert.That(flow.State, Is.EqualTo(SessionState.Completed));
Assert.That(simulator.SentCommands.Count(command => command == "30999"), Is.EqualTo(5));
Assert.That(simulator.SentCommands.Count(command => command == "30920"), Is.EqualTo(1));
Assert.That(simulator.SentCommands.Any(command => command.StartsWith("409", StringComparison.Ordinal)), Is.False);
}
}
[Test]
public void Simulator_ReproducesPartialHopperFailureDeterministically()
{
var cipher = new IdentityBlockCipher();
var simulator = new PeripheryControllerSimulator(cipher);
var authenticator = new ControllerAuthenticator(cipher, () => HostChallenge);
using var controller = new PeripheryMachineController(
simulator,
authenticator,
PeripheryDispenseMode.Hopper1Auto,
stockSensorInputIndex: 1,
stockLowWhenInputHigh: false);
using var flow = new SessionFlow(TestSettings.Create(), controller, new ManualClock());
simulator.Connect("SIM", 115200);
CompleteStartup(simulator, controller);
simulator.FailNextDispenseAfter(3);
flow.BeginCard();
flow.SelectCardAmount(100);
simulator.ReportCashless(100);
simulator.ProcessPendingEvents();
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Error));
Assert.That(flow.ErrorCode, Is.EqualTo(SessionErrorCode.PartialDispense));
Assert.That(simulator.SentCommands.Count(command => command == "30912"), Is.EqualTo(1));
}
}
[Test]
public void MalformedFrame_DoesNotBreakFollowingValidPaymentFrame()
{
var cipher = new IdentityBlockCipher();
var simulator = new PeripheryControllerSimulator(cipher);
var authenticator = new ControllerAuthenticator(cipher, () => HostChallenge);
using var controller = new PeripheryMachineController(
simulator,
authenticator,
PeripheryDispenseMode.Hopper1Auto,
stockSensorInputIndex: 1,
stockLowWhenInputHigh: false);
simulator.Connect("SIM", 115200);
CompleteStartup(simulator, controller);
MachineCardResult? result = null;
controller.CardPaymentFinished += value => result = value;
controller.BeginCardPayment(50);
simulator.InjectFrame("MONEY:CASHLESS:not-a-number");
simulator.ReportCashless(50);
simulator.ProcessPendingEvents();
Assert.That(result, Is.EqualTo(MachineCardResult.Approved));
Assert.That(simulator.SentCommands.Count(command => command == "ASK:CASHLESS:50"), Is.EqualTo(1));
}
[Test]
public void ReconnectStress_DoesNotReplayInterruptedDispenseOrAccumulateStateTransitions()
{
const int cycleCount = 200;
var cipher = new IdentityBlockCipher();
var simulator = new PeripheryControllerSimulator(cipher);
var authenticator = new ControllerAuthenticator(cipher, () => HostChallenge);
using var controller = new PeripheryMachineController(
simulator,
authenticator,
PeripheryDispenseMode.Hopper1Auto,
stockSensorInputIndex: 1,
stockLowWhenInputHigh: false);
var states = new List<MachineConnectionState>();
int dispenseCompletions = 0;
controller.ConnectionStateChanged += states.Add;
controller.DispenseFinished += (_, _, _) => dispenseCompletions++;
for (int cycle = 0; cycle < cycleCount; cycle++)
{
simulator.Connect("SIM", 115200);
Assert.That(controller.ConnectionState, Is.EqualTo(MachineConnectionState.Authenticating));
CompleteStartup(simulator, controller, (ulong)cycle * 3_000UL);
Assert.That(controller.ConnectionState, Is.EqualTo(MachineConnectionState.Ready));
controller.DispenseTokens(1);
simulator.Disconnect();
simulator.ProcessPendingEvents();
using (Assert.EnterMultipleScope())
{
Assert.That(controller.ConnectionState, Is.EqualTo(MachineConnectionState.Disconnected));
Assert.That(controller.StockLevel, Is.EqualTo(MachineStockLevel.Unknown));
Assert.That(dispenseCompletions, Is.Zero);
}
}
using (Assert.EnterMultipleScope())
{
Assert.That(states.Count(state => state == MachineConnectionState.Authenticating), Is.EqualTo(cycleCount));
Assert.That(states.Count(state => state == MachineConnectionState.Ready), Is.EqualTo(cycleCount));
Assert.That(states.Count(state => state == MachineConnectionState.Disconnected), Is.EqualTo(cycleCount));
Assert.That(simulator.SentCommands.Count(command => command == "3091"), Is.EqualTo(cycleCount));
Assert.That(dispenseCompletions, Is.Zero);
}
}
private static void CompleteStartup(
PeripheryControllerSimulator simulator,
PeripheryMachineController controller,
ulong startedAt = 0)
{
simulator.ProcessPendingEvents();
Assert.That(
controller.AuthenticationState,
Is.EqualTo(ControllerAuthenticationState.StartupDelay));
controller.Update(startedAt);
controller.Update(startedAt + 2_000UL);
simulator.ProcessPendingEvents();
Assert.That(controller.IsReady, Is.True);
}
}