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<JsonElement> 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);
}
}
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<JsonElement> 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);
}
}