using XFEEDToolBox.Core.Data; using System.Collections.Concurrent; using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; using XFEExtension.NetCore.XUnit.Attributes; namespace XFEEDToolBox.Test; [TestFixture] public class ConfirmedTradeReceiptTests { private sealed class Fixture:IConfirmedTradeCommandDriver { public PlayerState Player=new(){InGame=true,Docked=true,MarketId=1,CommanderId="receipt-test",CurrentShipId=7,Ship="Type9", LoadGameAt=DateTimeOffset.UtcNow,Credits=10000,CreditsReliable=true,CargoReliable=true,CargoCapacity=10}; public readonly TradeRunner Runner; public readonly List Serviced=[]; public readonly HashSet JournalNames=[]; public readonly ConcurrentQueue Messages=new(); public Func? ChangeReceipt; public int Trades,Travel;public bool Released;public bool RepeatKey; private readonly string firstKey=Guid.NewGuid().ToString("N"); public Fixture() {Runner=new(()=>Player,(id,key)=>Check.Quote(id,key,id==1?100:200,id==1?90:180));Runner.Progress+=progress=>Messages.Enqueue(progress.Message);} public static RoutePlan Plan=>new(){Request=Check.Request with{MaxLegs=1},Legs=[new(){Number=1,From=Check.Station(1),To=Check.Station(2),Orders=[new("gold","Gold",3,100,180)]}]}; public Task PreflightAsync(CancellationToken token)=>Task.CompletedTask; public async Task ServiceDockedAsync(DockedServiceRequest request,Func beforeInput,CancellationToken token) { await Task.Delay(30,token);await beforeInput(token); Player=Player with{Credits=Player.Credits-11};Serviced.Add(request.MarketId); } public async Task ExecuteTradeAsync(TradeAction action,Func beforeInput,CancellationToken token) { await beforeInput(token);Trades++;var before=Player;var buy=action.Kind==TradeActionKind.Buy; var delta=buy?3:-3;var price=buy?100:180; Player=Player with{Credits=Player.Credits-price*delta,CargoCount=before.CargoCount+delta,Inventory=buy?[new("gold","Gold",3)]:[]}; var receipt=new ConfirmedTradeReceipt(RepeatKey||buy?firstKey:Guid.NewGuid().ToString("N"),Player.MarketId!.Value,"gold",delta,price, (long)before.Credits!.Value,(long)Player.Credits!.Value,before.CargoCount,Player.CargoCount,Player.CommanderId,7,Player.LoadGameAt!.Value,DateTimeOffset.UtcNow,true,true); return ChangeReceipt?.Invoke(receipt)??receipt; } public async Task ExecuteAsync(TradeAction action,Func beforeInput,CancellationToken token) { if(action.Kind!=TradeActionKind.Travel)throw new InvalidOperationException("Receipted orders must not execute twice."); await beforeInput(token);Check.That(Serviced.Contains(1));Travel++;Player=Player with{MarketId=2};JournalNames.Add("Docked"); Runner.Observe(new(1,DateTimeOffset.UtcNow,Check.Event(new{@event="Docked",MarketID=2}))); } public Task ReleaseAllAsync(){Released=true;return Task.CompletedTask;} } [Test] public async Task ServerReceiptsCompleteWithBothMaintenanceTasksAndOnlyAnActualDockingEvent() { var fixture=new Fixture();decimal profit=0;fixture.Runner.Progress+=p=>profit=p.ActualProfit; await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2)); Check.Equal(RunState.Completed,fixture.Runner.State);Check.Equal(2,fixture.Trades);Check.Equal(1,fixture.Travel); Check.Equal(2,fixture.Serviced.Count);Check.Equal(240m,profit);Check.Equal(10218m,fixture.Player.Credits); Check.Equal(1,fixture.JournalNames.Count);Check.That(fixture.JournalNames.Contains("Docked"));Check.That(fixture.Released); } [Test] public async Task WrongServerOrderOrSessionStopsBeforeDeparture() { foreach(var change in new Func[] { r=>r with{MarketId=99},r=>r with{QuantityDelta=1},r=>r with{ServerConfirmed=false},r=>r with{ClientApplied=false}, r=>r with{CreditsAfter=r.CreditsAfter+1},r=>r with{CargoAfter=9},r=>r with{ShipId=8},r=>r with{SessionStartedAt=r.SessionStartedAt.AddSeconds(-1)}, r=>r with{ConfirmedAt=r.ConfirmedAt.AddMinutes(-5)},r=>r with{OperationKey=""} }) { var fixture=new Fixture{ChangeReceipt=change}; await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2)); Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(1,fixture.Trades);Check.Equal(0,fixture.Travel);Check.That(fixture.Released); } } [Test] public async Task ReusedNativeOperationKeyCannotCountASecondTrade() { var fixture=new Fixture{RepeatKey=true}; await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2)); Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(2,fixture.Trades);Check.Equal(1,fixture.Travel);Check.That(fixture.Released); } [Test] public async Task UnknownActualShipStopsBeforeAnyServiceTradeOrDeparture() { var fixture=new Fixture();fixture.Player=fixture.Player with{Ship=""}; await fixture.Runner.RunAsync(Fixture.Plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2)); Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(0,fixture.Trades);Check.Equal(0,fixture.Travel);Check.Equal(0,fixture.Serviced.Count); Check.That(fixture.Released);Check.That(fixture.Messages.Any(message=>message.Contains("实际船型尺寸",StringComparison.Ordinal))); } [Test] public async Task TooSmallDestinationPadStopsBeforeAnyServiceTradeOrDeparture() { var fixture=new Fixture();var plan=Fixture.Plan;var leg=plan.Legs[0]; plan=plan with{Legs=[leg with{To=leg.To with{MaxLandingPad=LandingPad.Medium}}]}; await fixture.Runner.RunAsync(plan,ExecutionMode.NativeBridge,fixture).WaitAsync(TimeSpan.FromSeconds(2)); Check.Equal(RunState.Failed,fixture.Runner.State);Check.Equal(0,fixture.Trades);Check.Equal(0,fixture.Travel);Check.Equal(0,fixture.Serviced.Count); Check.That(fixture.Released);Check.That(fixture.Messages.Any(message=>message.Contains("停机坪",StringComparison.Ordinal))); } }