exchanger/tests/Exchanger.Tests/SessionFlowTests.cs

493 lines
17 KiB
C#

using Exchanger.Core.Configuration;
using Exchanger.Core.Session;
using Exchanger.Hardware.Abstractions;
using NUnit.Framework;
namespace Exchanger.Tests;
[TestFixture]
public sealed class SessionFlowTests
{
[Test]
public void LowStock_AllowsPaymentWithoutInventingQuantity()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
machine.RaiseStockLevel(MachineStockLevel.Low);
flow.BeginCard();
bool selected = flow.SelectCardAmount(5000);
using (Assert.EnterMultipleScope())
{
Assert.That(selected, Is.True);
Assert.That(flow.StockLevel, Is.EqualTo(MachineStockLevel.Low));
}
}
[Test]
public void UnknownQualitativeStock_BlocksStartingPayment()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
machine.RaiseStockLevel(MachineStockLevel.Unknown);
bool started = flow.BeginCard();
using (Assert.EnterMultipleScope())
{
Assert.That(started, Is.False);
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
Assert.That(machine.BeginCardCalls, Is.Zero);
}
}
[Test]
public void CardApproved_CompletesOnceAndAutomaticallyReturnsHome()
{
var clock = new ManualClock();
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, clock);
Assert.That(flow.BeginCard(), Is.True);
Assert.That(flow.SelectCardAmount(100), Is.True);
machine.RaiseCardResult(MachineCardResult.Approved);
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Dispensing));
Assert.That(flow.BaseTokens, Is.EqualTo(10));
Assert.That(flow.BonusTokens, Is.EqualTo(2));
Assert.That(machine.DispenseCalls, Is.EqualTo(1));
Assert.That(machine.LastDispenseCount, Is.EqualTo(12));
}
machine.CompleteDispense(success: true);
Assert.That(flow.State, Is.EqualTo(SessionState.Completed));
clock.AdvanceMilliseconds(9999);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.Completed));
clock.AdvanceMilliseconds(1);
flow.Update();
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
Assert.That(flow.PaymentMethod, Is.EqualTo(PaymentMethod.None));
Assert.That(flow.SelectedAmountRubles, Is.Zero);
Assert.That(flow.TotalTokens, Is.Zero);
}
}
[Test]
public void CashBalance_ProducesConfirmedDispense()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
Assert.That(flow.BeginCash(), Is.True);
machine.RaiseCashBalance(50);
Assert.That(flow.ConfirmCashDispense(), Is.True);
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Dispensing));
Assert.That(flow.CashBalanceRubles, Is.EqualTo(50));
Assert.That(machine.EndCashCalls, Is.EqualTo(1));
Assert.That(machine.LastDispenseCount, Is.EqualTo(5));
}
machine.CompleteDispense(success: true);
Assert.That(flow.State, Is.EqualTo(SessionState.Completed));
}
[Test]
public void CardAmount_ExceedingInventory_IsRejectedBeforePaymentStarts()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(
TestSettings.Create(),
machine,
new ManualClock(),
maximumAvailableTokens: 11);
Assert.That(flow.BeginCard(), Is.True);
bool selected = flow.SelectCardAmount(100);
using (Assert.EnterMultipleScope())
{
Assert.That(selected, Is.False);
Assert.That(flow.State, Is.EqualTo(SessionState.Error));
Assert.That(flow.ErrorCode, Is.EqualTo(SessionErrorCode.InsufficientTokenInventory));
Assert.That(flow.TotalTokens, Is.EqualTo(12));
Assert.That(machine.BeginCardCalls, Is.Zero);
}
}
[Test]
public void CardAmount_WithDisabledInventoryLimit_StartsNormally()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(
TestSettings.Create(),
machine,
new ManualClock(),
maximumAvailableTokens: null);
Assert.That(flow.BeginCard(), Is.True);
Assert.That(flow.SelectCardAmount(5000), Is.True);
Assert.That(machine.BeginCardCalls, Is.EqualTo(1));
}
[Test]
public void CashAmount_ExceedingInventory_IsRejectedBeforeDispense()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(
TestSettings.Create(),
machine,
new ManualClock(),
maximumAvailableTokens: 4);
Assert.That(flow.BeginCash(), Is.True);
machine.RaiseCashBalance(50);
bool confirmed = flow.ConfirmCashDispense();
using (Assert.EnterMultipleScope())
{
Assert.That(confirmed, Is.False);
Assert.That(flow.State, Is.EqualTo(SessionState.Error));
Assert.That(flow.ErrorCode, Is.EqualTo(SessionErrorCode.InsufficientTokenInventory));
Assert.That(machine.EndCashCalls, Is.EqualTo(1));
Assert.That(machine.DispenseCalls, Is.Zero);
}
}
[Test]
public void CardDeclined_IgnoresLateApprovalAndReturnsHomeAfterErrorTimeout()
{
var clock = new ManualClock();
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, clock);
flow.BeginCard();
flow.SelectCardAmount(100);
machine.RaiseCardResult(MachineCardResult.Declined);
machine.RaiseCardResult(MachineCardResult.Approved);
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.PaymentDeclined));
Assert.That(machine.DispenseCalls, Is.Zero);
}
clock.AdvanceSeconds(45);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
}
[Test]
public void CardCancelled_ReturnsHomeWithoutDispense()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
flow.BeginCard();
flow.SelectCardAmount(100);
machine.RaiseCardResult(MachineCardResult.Cancelled);
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
Assert.That(machine.DispenseCalls, Is.Zero);
}
}
[Test]
public void CardTerminalBack_CancelsActiveRequestAndReturnsToAmountSelection()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(
TestSettings.Create(),
machine,
new ManualClock(),
new SequenceSessionIdGenerator());
var finishedOperations = new List<SessionOperationSnapshot>();
flow.OperationFinished += finishedOperations.Add;
Assert.That(flow.BeginCard(), Is.True);
string operationId = flow.CurrentOperationId;
Assert.That(flow.SelectCardAmount(100), Is.True);
bool returned = flow.ReturnToCardAmounts();
machine.RaiseCardResult(MachineCardResult.Approved);
using (Assert.EnterMultipleScope())
{
Assert.That(returned, Is.True);
Assert.That(flow.State, Is.EqualTo(SessionState.CardAmountSelection));
Assert.That(flow.CurrentOperationId, Is.EqualTo(operationId));
Assert.That(machine.CancelCalls, Is.EqualTo(1));
Assert.That(machine.DispenseCalls, Is.Zero);
Assert.That(finishedOperations, Is.Empty);
}
Assert.That(flow.SelectCardAmount(50), Is.True);
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.CardTerminalWait));
Assert.That(machine.BeginCardCalls, Is.EqualTo(2));
Assert.That(machine.LastCardAmount, Is.EqualTo(50));
}
}
[Test]
public void HardwarePaymentTimeout_UsesTypedTimeoutErrorWithoutDispense()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
flow.BeginCard();
flow.SelectCardAmount(100);
machine.RaiseCardResult(MachineCardResult.Timeout);
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.PaymentDeclined));
Assert.That(flow.ErrorCode, Is.EqualTo(SessionErrorCode.PaymentTimeout));
Assert.That(machine.DispenseCalls, Is.Zero);
}
}
[Test]
public void PaymentTimeout_IsImmediateWithManualClockAndNeverDispenses()
{
var clock = new ManualClock();
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, clock);
flow.BeginCard();
flow.SelectCardAmount(100);
clock.AdvanceSeconds(120);
flow.Update();
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.PaymentDeclined));
Assert.That(machine.CancelCalls, Is.EqualTo(1));
Assert.That(machine.DispenseCalls, Is.Zero);
Assert.That(flow.ErrorTitle, Does.Contain("ВРЕМЯ"));
}
}
[Test]
public void CashAndCashlessPayment_UseIndependentTimeouts()
{
AppSettings settings = TestSettings.Create();
settings.Timeouts.CashPaymentSeconds = 30;
settings.Timeouts.CashlessPaymentSeconds = 90;
var clock = new ManualClock();
var machine = new FakeMachineController();
using var flow = new SessionFlow(settings, machine, clock);
Assert.That(flow.BeginCash(), Is.True);
clock.AdvanceSeconds(29);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.CashAccepting));
clock.AdvanceSeconds(1);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
Assert.That(flow.BeginCard(), Is.True);
Assert.That(flow.SelectCardAmount(100), Is.True);
clock.AdvanceSeconds(89);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.CardTerminalWait));
clock.AdvanceSeconds(1);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.PaymentDeclined));
}
[Test]
public void UserActivity_ExtendsIdleDeadline()
{
var clock = new ManualClock();
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, clock);
flow.BeginCard();
clock.AdvanceSeconds(44);
flow.NotifyUserActivity();
clock.AdvanceSeconds(44);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.CardAmountSelection));
clock.AdvanceSeconds(1);
flow.Update();
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
}
[Test]
public void LowSignal_DoesNotBlockCardPackage()
{
var machine = new FakeMachineController(MachineStockLevel.Low);
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
flow.BeginCard();
bool accepted = flow.SelectCardAmount(100);
using (Assert.EnterMultipleScope())
{
Assert.That(accepted, Is.True);
Assert.That(flow.State, Is.EqualTo(SessionState.CardTerminalWait));
Assert.That(machine.BeginCardCalls, Is.EqualTo(1));
Assert.That(machine.DispenseCalls, Is.Zero);
}
}
[Test]
public void DuplicateActions_StartOnlyOnePaymentAndOneDispense()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
flow.BeginCard();
Assert.That(flow.SelectCardAmount(100), Is.True);
Assert.That(flow.SelectCardAmount(50), Is.False);
machine.RaiseCardResult(MachineCardResult.Approved);
machine.RaiseCardResult(MachineCardResult.Approved);
using (Assert.EnterMultipleScope())
{
Assert.That(machine.BeginCardCalls, Is.EqualTo(1));
Assert.That(machine.LastCardAmount, Is.EqualTo(100));
Assert.That(machine.DispenseCalls, Is.EqualTo(1));
}
}
[Test]
public void PartialDispense_TransitionsToSafeError()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
flow.BeginCard();
flow.SelectCardAmount(100);
machine.RaiseCardResult(MachineCardResult.Approved);
machine.CompleteDispense(success: true, dispensedTokens: 10);
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Error));
Assert.That(flow.ErrorCode, Is.EqualTo(SessionErrorCode.PartialDispense));
Assert.That(flow.ErrorAction, Is.EqualTo(SessionErrorAction.ContactSupport));
}
}
[Test]
public void CancelDuringDispense_IsIgnored()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
flow.BeginCard();
flow.SelectCardAmount(100);
machine.RaiseCardResult(MachineCardResult.Approved);
int cancelsBefore = machine.CancelCalls;
flow.Cancel();
using (Assert.EnterMultipleScope())
{
Assert.That(flow.State, Is.EqualTo(SessionState.Dispensing));
Assert.That(machine.CancelCalls, Is.EqualTo(cancelsBefore));
}
}
[Test]
public void DisposedFlow_DoesNotReceiveMachineEvents()
{
var machine = new FakeMachineController();
var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
flow.BeginCash();
flow.Dispose();
machine.RaiseCashBalance(100);
Assert.That(flow.CashBalanceRubles, Is.Zero);
}
[Test]
public void RepeatedFlowCreation_DoesNotAccumulateMachineSubscriptions()
{
var machine = new FakeMachineController();
for (int index = 0; index < 50; index++)
{
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
Assert.That(machine.TotalSubscriberCount, Is.EqualTo(5));
}
Assert.That(machine.TotalSubscriberCount, Is.Zero);
}
[Test]
public void RepeatedCompletedSessions_RemainDeterministic()
{
var machine = new FakeMachineController();
using var flow = new SessionFlow(TestSettings.Create(), machine, new ManualClock());
for (int index = 0; index < 20; index++)
{
Assert.That(flow.BeginCash(), Is.True);
machine.RaiseCashBalance(10);
Assert.That(flow.ConfirmCashDispense(), Is.True);
machine.CompleteDispense(success: true);
Assert.That(flow.State, Is.EqualTo(SessionState.Completed));
flow.ReturnHome();
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
}
using (Assert.EnterMultipleScope())
{
Assert.That(machine.DispenseCalls, Is.EqualTo(20));
Assert.That(flow.StockLevel, Is.EqualTo(MachineStockLevel.Enough));
}
}
[Test]
public void BeginCash_WhenCashDisabled_IsRejectedBeforeHardwareCall()
{
AppSettings settings = TestSettings.Create();
settings.Payments.CashEnabled = false;
var machine = new FakeMachineController();
using var flow = new SessionFlow(settings, machine, new ManualClock());
bool started = flow.BeginCash();
using (Assert.EnterMultipleScope())
{
Assert.That(started, Is.False);
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
Assert.That(machine.BeginCashCalls, Is.Zero);
}
}
[Test]
public void BeginCard_WhenCardDisabled_IsRejectedBeforeOperationStarts()
{
AppSettings settings = TestSettings.Create();
settings.Payments.CardEnabled = false;
var machine = new FakeMachineController();
using var flow = new SessionFlow(settings, machine, new ManualClock());
bool started = flow.BeginCard();
using (Assert.EnterMultipleScope())
{
Assert.That(started, Is.False);
Assert.That(flow.State, Is.EqualTo(SessionState.Home));
Assert.That(flow.CurrentOperationId, Is.Empty);
}
}
}