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(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu with{State="Unknown",NextState="Unknown"},nav,pid,hash,started,now)); Check.Throws(()=>NativeSessionStartupReadiness.JourneyEntryRequired(player,menu with{ObservedAt=now.AddSeconds(-1)},nav,pid,hash,started,now)); Check.Throws(()=>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(()=>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(()=>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 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 ReadEntryStatusAsync(CancellationToken token){OnStatus?.Invoke();return ReadAsync("",token);} public Task 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); } } }