using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NativeSessionEntryTests
{
[Test]public void AuthorizedJourneyChecksTheFreshNativeMenuInsteadOfAnOldLoadGameOrAnAlivePid()
{
var now=DateTimeOffset.UtcNow;var started=now.AddMinutes(-1);const int pid=71;var hash=NativeRelogChannel.SupportedGameSha256;
var player=new PlayerState{InGame=true,LoadGameAt=started.AddMinutes(-1),LocationAt=started.AddMinutes(-1),CargoReliable=true,
CargoAt=started.AddMinutes(-1),SystemName="Old system",Navigation=new(){CurrentSystemAddress=1}};
var menu=new NativeSessionFrame(1,now,"MainMenu","MainMenu",[]);
var nav=System.Text.Json.JsonSerializer.SerializeToElement(new{schema=1,name="XFEEDToolBox.Navigation",processId=pid,gameSha256=hash,
verified=true,inGame=false,continuousPose=false,continuousHeat=false,ready=true});
Check.That(NativeSessionStartupReadiness.JourneyEntryRequired(player,menu,nav,pid,hash,started,now));
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu with{State="Unknown",NextState="Unknown"},nav,pid,hash,started,now));
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu with{ObservedAt=now.AddSeconds(-1)},nav,pid,hash,started,now));
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu,nav,pid+1,hash,started,now));
var live=System.Text.Json.JsonSerializer.SerializeToElement(new{schema=1,name="XFEEDToolBox.Navigation",processId=pid,gameSha256=hash,
verified=true,inGame=true,continuousPose=true,continuousHeat=true});
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu,live,pid,hash,started,now));
var complete=player with{LoadGameAt=started.AddSeconds(1),LocationAt=now,CargoAt=now};
Check.That(!NativeSessionStartupReadiness.JourneyEntryRequired(complete,menu with{State="InGame",NextState="InGame"},live,pid,hash,started,now));
Check.Throws<FlightThreatPauseException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(complete with{Navigation=complete.Navigation with{ExternalThreat=true}},menu,nav,pid,hash,started,now));
}
private sealed class Channel(string mode):INativeRelogChannel,INativeSessionEntryStatus
{
internal PlayerState Player=new(){CommanderId="test",GameMode=mode,LoadGameAt=DateTimeOffset.UtcNow.AddMinutes(-1)};
internal string State="MainMenu",Request="";internal bool Ended,Applied,LostReply,Stale;
internal int BeginFailures,BeginCalls;
internal bool DenyBegin;
internal Action? OnStatus;
internal string IdentityFailureAt="";
private Exception IdentityFailure()=>IdentityFailureAt.StartsWith("wrapped-",StringComparison.Ordinal)
?new IOException("transport retained a protocol failure",new InvalidDataException("identity changed")):new InvalidDataException("identity changed");
private long sequence;
internal List<string> Calls=[];
public Task BeginAsync(string owner,CancellationToken token)
{BeginCalls++;if(IdentityFailureAt.EndsWith("begin",StringComparison.Ordinal))throw IdentityFailure();if(DenyBegin||BeginFailures-->0)throw new InvalidOperationException("Wait for an idle game and stop automatic flight before relogging");return Task.CompletedTask;}
public Task EndAsync(string owner){Ended=true;return Task.CompletedTask;}
public Task<NativeSessionFrame> ReadEntryStatusAsync(CancellationToken token){OnStatus?.Invoke();return ReadAsync("",token);}
public Task<NativeSessionFrame> ReadAsync(string owner,CancellationToken token)
{
if(IdentityFailureAt.EndsWith("read",StringComparison.Ordinal)&&owner.Length>0)throw IdentityFailure();
return Task.FromResult(new NativeSessionFrame(++sequence,
DateTimeOffset.UtcNow.AddSeconds(Stale?-2:0),State,State,State=="MainMenu"?[new("ContinueMenu",true)]:State=="ContinueMenu"?[new("SetupPlaySolo",true),new("SetupPlayWithEveryone",true)]:[],Request,Applied));
}
public Task SubmitAsync(string owner,string requestId,NativeSessionFrame frame,string action,CancellationToken token)
{
if(IdentityFailureAt.EndsWith("submit",StringComparison.Ordinal))throw IdentityFailure();
Calls.Add(action);Request=requestId;Applied=true;State=action=="ContinueMenu"?"ContinueMenu":"InGame";
if(State=="InGame")
{var at=DateTimeOffset.UtcNow;Player=Player with{InGame=true,LoadGameAt=at,LocationAt=at,CargoAt=at,CargoReliable=true,SystemName="Sol",Navigation=new(){CurrentSystemAddress=42}};}
if(LostReply)throw new IOException("applied original reply was lost");return Task.CompletedTask;
}
}
[Test]public async Task NewSessionEntersOnlyTheRequestedSoloOrOpenMode()
{
foreach(var mode in new[]{"Solo","Open"})
{
var channel=new Channel(mode);await new NativeSessionEntryWorkflow(channel,()=>channel.Player,mode,"test"){PollInterval=TimeSpan.FromMilliseconds(1)}.RunAsync(default);
Check.Equal(mode=="Solo"?"ContinueMenu,SetupPlaySolo":"ContinueMenu,SetupPlayWithEveryone",string.Join(',',channel.Calls));Check.That(channel.Ended);
}
}
[Test]public async Task LostRepliesNeverDuplicateAnEntrySelection()
{
var channel=new Channel("Solo"){LostReply=true};await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1)}.RunAsync(default);
Check.Equal(2,channel.Calls.Count);Check.That(channel.Ended);
}
[Test]public async Task StartupWaitsForTheRealMenuBeforeSelectingTheMode()
{
var channel=new Channel("Solo"){BeginFailures=3};
await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1)}.RunAsync(default);
Check.Equal(4,channel.BeginCalls);Check.Equal("ContinueMenu,SetupPlaySolo",string.Join(',',channel.Calls));Check.That(channel.Ended);
}
[Test]public async Task UnavailableStartupIsBoundedAndCannotSelectAnything()
{
var channel=new Channel("Solo"){BeginFailures=int.MaxValue};var failed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1),Timeout=TimeSpan.FromMilliseconds(30)}.RunAsync(default);}catch(TimeoutException){failed=true;}
Check.That(failed&&channel.Ended&&channel.Calls.Count==0);
}
[Test]public async Task StaleFramesCannotStartLoadingAndAlwaysReleaseTheLease()
{
var channel=new Channel("Solo"){Stale=true};var failed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1),Timeout=TimeSpan.FromMilliseconds(30)}.RunAsync(default);}catch(TimeoutException){failed=true;}
Check.That(failed&&channel.Ended&&channel.Calls.Count==0);
}
[Test]public async Task ARealConnectionFailureIsTypedEvenWhenEntryCannotAcquireTheMenuLease()
{
var channel=new Channel("Solo"){State="FailedToConnect",DenyBegin=true};var typed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{PollInterval=TimeSpan.FromMilliseconds(1),ConnectionFailureConfirmation=TimeSpan.FromMilliseconds(5),Timeout=TimeSpan.FromSeconds(1)}.RunAsync(default);}
catch(NativeSessionConnectionFailedException){typed=true;}
Check.That(typed&&channel.Ended);Check.Equal(0,channel.Calls.Count);
}
[Test]public async Task ALoadGameWithoutLocationAndCargoCannotCompleteOrHideConnectionFailure()
{
var loaded=DateTimeOffset.UtcNow;var channel=new Channel("Solo"){State="FailedToConnect",DenyBegin=true,
Player=new(){CommanderId="test",GameMode="Solo",InGame=true,LoadGameAt=loaded}};
var typed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{ProcessStartedAt=loaded.AddMinutes(-1),PollInterval=TimeSpan.FromMilliseconds(1),ConnectionFailureConfirmation=TimeSpan.FromMilliseconds(5),Timeout=TimeSpan.FromSeconds(1)}.RunAsync(default);}
catch(NativeSessionConnectionFailedException){typed=true;}
Check.That(typed&&channel.Ended);Check.Equal(0,channel.Calls.Count);
}
[Test]public async Task CancellationAndChangedIdentityWinDuringTheReadOnlyFailureObservation()
{
foreach(var cancel in new[]{false,true})
{
using var stop=new CancellationTokenSource();var channel=new Channel("Solo"){State="FailedToConnect",DenyBegin=true};
channel.OnStatus=()=>{if(cancel)stop.Cancel();else channel.Player=channel.Player with{CommanderId="other"};};
var rejected=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{PollInterval=TimeSpan.FromMilliseconds(1),ConnectionFailureConfirmation=TimeSpan.Zero}.RunAsync(stop.Token);}
catch(OperationCanceledException)when(cancel){rejected=true;}
catch(InvalidDataException)when(!cancel){rejected=true;}
Check.That(rejected&&channel.Ended);Check.Equal(0,channel.Calls.Count);
}
}
[Test]public async Task IdentityFailuresAreNeverSwallowedByGenericIoRetriesAtBeginReadOrSubmit()
{
foreach(var stage in new[]{"begin","read","submit","wrapped-begin","wrapped-read","wrapped-submit"})
{
var channel=new Channel("Solo"){IdentityFailureAt=stage};var rejected=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{PollInterval=TimeSpan.FromMilliseconds(1),Timeout=TimeSpan.FromMilliseconds(50)}.RunAsync(default);}
catch(Exception error)when(NavigationRecovery.HasInvalidDataCause(error)){rejected=true;}
Check.That(rejected&&channel.Ended);Check.Equal(0,channel.Calls.Count);Check.Equal(1,channel.BeginCalls);
}
}
}
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Session;
using XFEEDToolBox.Core.Navigation;
using XFEExtension.NetCore.XUnit.Attributes;
namespace XFEEDToolBox.Test;
[TestFixture]
public class NativeSessionEntryTests
{
[Test]public void AuthorizedJourneyChecksTheFreshNativeMenuInsteadOfAnOldLoadGameOrAnAlivePid()
{
var now=DateTimeOffset.UtcNow;var started=now.AddMinutes(-1);const int pid=71;var hash=NativeRelogChannel.SupportedGameSha256;
var player=new PlayerState{InGame=true,LoadGameAt=started.AddMinutes(-1),LocationAt=started.AddMinutes(-1),CargoReliable=true,
CargoAt=started.AddMinutes(-1),SystemName="Old system",Navigation=new(){CurrentSystemAddress=1}};
var menu=new NativeSessionFrame(1,now,"MainMenu","MainMenu",[]);
var nav=System.Text.Json.JsonSerializer.SerializeToElement(new{schema=1,name="XFEEDToolBox.Navigation",processId=pid,gameSha256=hash,
verified=true,inGame=false,continuousPose=false,continuousHeat=false,ready=true});
Check.That(NativeSessionStartupReadiness.JourneyEntryRequired(player,menu,nav,pid,hash,started,now));
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu with{State="Unknown",NextState="Unknown"},nav,pid,hash,started,now));
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu with{ObservedAt=now.AddSeconds(-1)},nav,pid,hash,started,now));
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu,nav,pid+1,hash,started,now));
var live=System.Text.Json.JsonSerializer.SerializeToElement(new{schema=1,name="XFEEDToolBox.Navigation",processId=pid,gameSha256=hash,
verified=true,inGame=true,continuousPose=true,continuousHeat=true});
Check.Throws<InvalidDataException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu,live,pid,hash,started,now));
var complete=player with{LoadGameAt=started.AddSeconds(1),LocationAt=now,CargoAt=now};
Check.That(!NativeSessionStartupReadiness.JourneyEntryRequired(complete,menu with{State="InGame",NextState="InGame"},live,pid,hash,started,now));
Check.Throws<FlightThreatPauseException>(()=>NativeSessionStartupReadiness.JourneyEntryRequired(complete with{Navigation=complete.Navigation with{ExternalThreat=true}},menu,nav,pid,hash,started,now));
}
private sealed class Channel(string mode):INativeRelogChannel,INativeSessionEntryStatus
{
internal PlayerState Player=new(){CommanderId="test",GameMode=mode,LoadGameAt=DateTimeOffset.UtcNow.AddMinutes(-1)};
internal string State="MainMenu",Request="";internal bool Ended,Applied,LostReply,Stale;
internal int BeginFailures,BeginCalls;
internal bool DenyBegin;
internal Action? OnStatus;
internal string IdentityFailureAt="";
private Exception IdentityFailure()=>IdentityFailureAt.StartsWith("wrapped-",StringComparison.Ordinal)
?new IOException("transport retained a protocol failure",new InvalidDataException("identity changed")):new InvalidDataException("identity changed");
private long sequence;
internal List<string> Calls=[];
public Task BeginAsync(string owner,CancellationToken token)
{BeginCalls++;if(IdentityFailureAt.EndsWith("begin",StringComparison.Ordinal))throw IdentityFailure();if(DenyBegin||BeginFailures-->0)throw new InvalidOperationException("Wait for an idle game and stop automatic flight before relogging");return Task.CompletedTask;}
public Task EndAsync(string owner){Ended=true;return Task.CompletedTask;}
public Task<NativeSessionFrame> ReadEntryStatusAsync(CancellationToken token){OnStatus?.Invoke();return ReadAsync("",token);}
public Task<NativeSessionFrame> ReadAsync(string owner,CancellationToken token)
{
if(IdentityFailureAt.EndsWith("read",StringComparison.Ordinal)&&owner.Length>0)throw IdentityFailure();
return Task.FromResult(new NativeSessionFrame(++sequence,
DateTimeOffset.UtcNow.AddSeconds(Stale?-2:0),State,State,State=="MainMenu"?[new("ContinueMenu",true)]:State=="ContinueMenu"?[new("SetupPlaySolo",true),new("SetupPlayWithEveryone",true)]:[],Request,Applied));
}
public Task SubmitAsync(string owner,string requestId,NativeSessionFrame frame,string action,CancellationToken token)
{
if(IdentityFailureAt.EndsWith("submit",StringComparison.Ordinal))throw IdentityFailure();
Calls.Add(action);Request=requestId;Applied=true;State=action=="ContinueMenu"?"ContinueMenu":"InGame";
if(State=="InGame")
{var at=DateTimeOffset.UtcNow;Player=Player with{InGame=true,LoadGameAt=at,LocationAt=at,CargoAt=at,CargoReliable=true,SystemName="Sol",Navigation=new(){CurrentSystemAddress=42}};}
if(LostReply)throw new IOException("applied original reply was lost");return Task.CompletedTask;
}
}
[Test]public async Task NewSessionEntersOnlyTheRequestedSoloOrOpenMode()
{
foreach(var mode in new[]{"Solo","Open"})
{
var channel=new Channel(mode);await new NativeSessionEntryWorkflow(channel,()=>channel.Player,mode,"test"){PollInterval=TimeSpan.FromMilliseconds(1)}.RunAsync(default);
Check.Equal(mode=="Solo"?"ContinueMenu,SetupPlaySolo":"ContinueMenu,SetupPlayWithEveryone",string.Join(',',channel.Calls));Check.That(channel.Ended);
}
}
[Test]public async Task LostRepliesNeverDuplicateAnEntrySelection()
{
var channel=new Channel("Solo"){LostReply=true};await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1)}.RunAsync(default);
Check.Equal(2,channel.Calls.Count);Check.That(channel.Ended);
}
[Test]public async Task StartupWaitsForTheRealMenuBeforeSelectingTheMode()
{
var channel=new Channel("Solo"){BeginFailures=3};
await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1)}.RunAsync(default);
Check.Equal(4,channel.BeginCalls);Check.Equal("ContinueMenu,SetupPlaySolo",string.Join(',',channel.Calls));Check.That(channel.Ended);
}
[Test]public async Task UnavailableStartupIsBoundedAndCannotSelectAnything()
{
var channel=new Channel("Solo"){BeginFailures=int.MaxValue};var failed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1),Timeout=TimeSpan.FromMilliseconds(30)}.RunAsync(default);}catch(TimeoutException){failed=true;}
Check.That(failed&&channel.Ended&&channel.Calls.Count==0);
}
[Test]public async Task StaleFramesCannotStartLoadingAndAlwaysReleaseTheLease()
{
var channel=new Channel("Solo"){Stale=true};var failed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test"){PollInterval=TimeSpan.FromMilliseconds(1),Timeout=TimeSpan.FromMilliseconds(30)}.RunAsync(default);}catch(TimeoutException){failed=true;}
Check.That(failed&&channel.Ended&&channel.Calls.Count==0);
}
[Test]public async Task ARealConnectionFailureIsTypedEvenWhenEntryCannotAcquireTheMenuLease()
{
var channel=new Channel("Solo"){State="FailedToConnect",DenyBegin=true};var typed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{PollInterval=TimeSpan.FromMilliseconds(1),ConnectionFailureConfirmation=TimeSpan.FromMilliseconds(5),Timeout=TimeSpan.FromSeconds(1)}.RunAsync(default);}
catch(NativeSessionConnectionFailedException){typed=true;}
Check.That(typed&&channel.Ended);Check.Equal(0,channel.Calls.Count);
}
[Test]public async Task ALoadGameWithoutLocationAndCargoCannotCompleteOrHideConnectionFailure()
{
var loaded=DateTimeOffset.UtcNow;var channel=new Channel("Solo"){State="FailedToConnect",DenyBegin=true,
Player=new(){CommanderId="test",GameMode="Solo",InGame=true,LoadGameAt=loaded}};
var typed=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{ProcessStartedAt=loaded.AddMinutes(-1),PollInterval=TimeSpan.FromMilliseconds(1),ConnectionFailureConfirmation=TimeSpan.FromMilliseconds(5),Timeout=TimeSpan.FromSeconds(1)}.RunAsync(default);}
catch(NativeSessionConnectionFailedException){typed=true;}
Check.That(typed&&channel.Ended);Check.Equal(0,channel.Calls.Count);
}
[Test]public async Task CancellationAndChangedIdentityWinDuringTheReadOnlyFailureObservation()
{
foreach(var cancel in new[]{false,true})
{
using var stop=new CancellationTokenSource();var channel=new Channel("Solo"){State="FailedToConnect",DenyBegin=true};
channel.OnStatus=()=>{if(cancel)stop.Cancel();else channel.Player=channel.Player with{CommanderId="other"};};
var rejected=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{PollInterval=TimeSpan.FromMilliseconds(1),ConnectionFailureConfirmation=TimeSpan.Zero}.RunAsync(stop.Token);}
catch(OperationCanceledException)when(cancel){rejected=true;}
catch(InvalidDataException)when(!cancel){rejected=true;}
Check.That(rejected&&channel.Ended);Check.Equal(0,channel.Calls.Count);
}
}
[Test]public async Task IdentityFailuresAreNeverSwallowedByGenericIoRetriesAtBeginReadOrSubmit()
{
foreach(var stage in new[]{"begin","read","submit","wrapped-begin","wrapped-read","wrapped-submit"})
{
var channel=new Channel("Solo"){IdentityFailureAt=stage};var rejected=false;
try{await new NativeSessionEntryWorkflow(channel,()=>channel.Player,"Solo","test")
{PollInterval=TimeSpan.FromMilliseconds(1),Timeout=TimeSpan.FromMilliseconds(50)}.RunAsync(default);}
catch(Exception error)when(NavigationRecovery.HasInvalidDataCause(error)){rejected=true;}
Check.That(rejected&&channel.Ended);Check.Equal(0,channel.Calls.Count);Check.Equal(1,channel.BeginCalls);
}
}
}