XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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);
        }
    }
}