using System.Text.Json; using XFEExtension.NetCore.XUnit.Attributes; using XFEEDToolBox.Core.Execution; using XFEEDToolBox.Core.Models; namespace XFEEDToolBox.Test; [TestFixture] public class NativeUnsubmittedTradeIntentsTests { private sealed class Channel:IOriginalCommerceChannel { public string Identity=>"100:200"; public DateTimeOffset? ProcessStartedAt{get;set;} public bool Known; public int Calls; public long Credits=10000; public Task CallAsync(object request,CancellationToken token) { Calls++;var op=JsonSerializer.SerializeToElement(request).GetProperty("op").GetString(); return Task.FromResult(op switch{ "commerce.operation.status"=>JsonSerializer.SerializeToElement(new{known=Known,mutationsSent=Known?1:0}), "commerce.inventory"=>JsonSerializer.SerializeToElement(new{verified=true,count=0,credits=Credits}), _=>throw new InvalidOperationException("Only read-only reconciliation is allowed")}); } } private static PlayerState Player()=>new(){InGame=true,Docked=true,CommanderId="test-commander",CurrentShipId=7, LoadGameAt=DateTimeOffset.UtcNow.AddMinutes(-1),Credits=10000,CreditsReliable=true,CargoReliable=true}; private static string Pending(string directory,Channel channel,PlayerState player) { var file=Path.Combine(directory,new string('a',32)+".pending.json"); File.WriteAllText(file,JsonSerializer.Serialize(new{operationKey=new string('a',32),channel=channel.Identity,player.CommanderId,player.CurrentShipId,player.LoadGameAt, intent=new{before=new{verified=true,count=0,credits=10000}}}));return file; } [Test] public async Task SameLiveSessionUnacceptedKeyIsMarkedUnsubmittedWithoutDeletingTheIntent() { using var directory=new TemporaryDirectory();var channel=new Channel();var player=Player();var pending=Pending(directory.Path,channel,player);var original=File.ReadAllText(pending); await NativeUnsubmittedTradeIntents.ReconcileAsync(directory.Path,channel,player,default); using var proof=JsonDocument.Parse(File.ReadAllText(pending.Replace(".pending.json",".confirmed.json"))); Check.That(!proof.RootElement.GetProperty("submitted").GetBoolean());Check.That(!proof.RootElement.TryGetProperty("receipt",out _)); Check.Equal(original,File.ReadAllText(pending));Check.Equal(2,channel.Calls); } [Test] public async Task NewSessionCannotUseTheOldOperationMapAsProof() { using var directory=new TemporaryDirectory();var channel=new Channel();var player=Player();Pending(directory.Path,channel,player); await NativeUnsubmittedTradeIntents.ReconcileAsync(directory.Path,channel,player with{LoadGameAt=DateTimeOffset.UtcNow},default); Check.Equal(0,channel.Calls);Check.Equal(0,Directory.GetFiles(directory.Path,"*.confirmed.json").Length); } [Test] public async Task AcceptedKeyRemainsUnresolved() { using var directory=new TemporaryDirectory();var channel=new Channel{Known=true};var player=Player();Pending(directory.Path,channel,player); await NativeUnsubmittedTradeIntents.ReconcileAsync(directory.Path,channel,player,default); Check.Equal(1,channel.Calls);Check.Equal(0,Directory.GetFiles(directory.Path,"*.confirmed.json").Length); } [Test] public async Task ChangedWalletCannotBeUsedAsUnsubmittedProof() { using var directory=new TemporaryDirectory();var channel=new Channel{Credits=9999};var player=Player();Pending(directory.Path,channel,player); await NativeUnsubmittedTradeIntents.ReconcileAsync(directory.Path,channel,player,default); Check.Equal(0,Directory.GetFiles(directory.Path,"*.confirmed.json").Length); } [Test] public async Task EmptyLegacyIntentCanOnlyResolveInsideItsOriginalProcessAndSession() { using var directory=new TemporaryDirectory();var channel=new Channel{ProcessStartedAt=DateTimeOffset.UtcNow.AddMinutes(-2)};var player=Player(); var pending=Pending(directory.Path,channel,player);File.WriteAllBytes(pending,[]); await NativeUnsubmittedTradeIntents.ReconcileAsync(directory.Path,channel,player,default); using var proof=JsonDocument.Parse(File.ReadAllText(pending.Replace(".pending.json",".confirmed.json"))); Check.That(!proof.RootElement.GetProperty("submitted").GetBoolean()&&proof.RootElement.GetProperty("malformedIntent").GetBoolean()); Check.Equal(0L,new FileInfo(pending).Length);Check.Equal(3,channel.Calls); } [Test] public async Task EmptyIntentFromAnOlderProcessOrSessionCannotUseAbsenceAsProof() { foreach(var oldSession in new[]{false,true}) { using var directory=new TemporaryDirectory();var channel=new Channel{ProcessStartedAt=DateTimeOffset.UtcNow.AddMinutes(-2)};var player=Player(); var pending=Pending(directory.Path,channel,player);File.WriteAllBytes(pending,[]);var future=DateTimeOffset.UtcNow.AddSeconds(1); if(oldSession)player=player with{LoadGameAt=future};else channel.ProcessStartedAt=future; await NativeUnsubmittedTradeIntents.ReconcileAsync(directory.Path,channel,player,default); Check.Equal(0,channel.Calls);Check.Equal(0,Directory.GetFiles(directory.Path,"*.confirmed.json").Length); } } [Test] public async Task AcceptedEmptyIntentAlwaysRemainsUnresolved() { using var directory=new TemporaryDirectory();var channel=new Channel{ProcessStartedAt=DateTimeOffset.UtcNow.AddMinutes(-2),Known=true};var player=Player(); var pending=Pending(directory.Path,channel,player);File.WriteAllBytes(pending,[]); await NativeUnsubmittedTradeIntents.ReconcileAsync(directory.Path,channel,player,default); Check.Equal(1,channel.Calls);Check.Equal(0,Directory.GetFiles(directory.Path,"*.confirmed.json").Length); } }