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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.IO.Compression;
using System.Net;
using System.Text;
using System.Text.Json;
using XFEEDToolBox.Core.Galaxy;
using XFEExtension.NetCore.XUnit.Attributes;

namespace XFEEDToolBox.Test;

[TestFixture]
public class GalacticPlaceSearchTests
{
    private const string Result="""{"results":[{"name":"Station","system_name":"Sol","system_x":0,"system_y":0,"system_z":0}]}""";
    private sealed class Handler(Func<HttpRequestMessage,CancellationToken,Task<HttpResponseMessage>> send):HttpMessageHandler
    {
        public int Calls;
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,CancellationToken token){Calls++;return send(request,token);}
    }
    private static HttpResponseMessage CompressedResult()
    {
        using var bytes=new MemoryStream();using(var zip=new GZipStream(bytes,CompressionLevel.Fastest,true))zip.Write(Encoding.UTF8.GetBytes(Result));
        var response=new HttpResponseMessage(HttpStatusCode.OK){Content=new ByteArrayContent(bytes.ToArray())};response.Content.Headers.ContentEncoding.Add("gzip");return response;
    }
    [Test] public async Task CompressedStationSearchReusesResultsButKeepsLocationAndPadFiltersSeparate()
    {
        var handler=new Handler(async(request,token)=>
        {
            Check.That(request.Headers.AcceptEncoding.Any(e=>e.Value=="gzip"),"Large inventories must use compression.");
            using var body=JsonDocument.Parse(await request.Content!.ReadAsStringAsync(token));
            Check.That(body.RootElement.GetProperty("size").GetInt32()<=5,"Nearest-place lookup must not download whole pages of module inventories.");
            return CompressedResult();
        });
        using var http=new HttpClient(handler);var service=new GalacticPlaces(http);
        var first=await service.SearchAsync(GalacticPlaceKind.MaterialTrader,"Raw",new(0,0,0));
        Check.Equal("Station",first.Single().Name);Check.Equal(1,handler.Calls);
        var cached=await service.SearchAsync(GalacticPlaceKind.MaterialTrader,"Raw",new(0,0,0));Check.Equal(1,handler.Calls);Check.That(ReferenceEquals(first,cached));
        await service.SearchAsync(GalacticPlaceKind.MaterialTrader,"Raw",new(1,0,0));
        await service.SearchAsync(GalacticPlaceKind.MaterialTrader,"Raw",new(1,0,0),largePad:true);Check.Equal(3,handler.Calls);
        using var stop=new CancellationTokenSource();stop.Cancel();
        try{await service.SearchAsync(GalacticPlaceKind.MaterialTrader,"Raw",new(0,0,0),token:stop.Token);throw new InvalidOperationException("Cached queries must respect cancellation.");}
        catch(OperationCanceledException){Check.Equal(3,handler.Calls);}
    }
    [Test] public async Task TemporaryServiceFailureRetriesOnceAndPermanentFailureReturnsImmediately()
    {
        var calls=0;var handler=new Handler((_,_)=>Task.FromResult(++calls==1?new HttpResponseMessage(HttpStatusCode.ServiceUnavailable):CompressedResult()));
        using var http=new HttpClient(handler);var service=new GalacticPlaces(http);
        Check.Equal(1,(await service.SearchAsync(GalacticPlaceKind.HighTech,"",new(0,0,0))).Count);Check.Equal(2,handler.Calls);
        var bad=new Handler((_,_)=>Task.FromResult(new HttpResponseMessage(HttpStatusCode.BadRequest)));using var badHttp=new HttpClient(bad);
        try{await new GalacticPlaces(badHttp).SearchAsync(GalacticPlaceKind.HighTech,"",new(0,0,0));throw new InvalidOperationException("Expected HTTP failure.");}
        catch(HttpRequestException){Check.Equal(1,bad.Calls);}
    }
    private sealed class WaitingBody:Stream
    {
        public override bool CanRead=>true;public override bool CanSeek=>false;public override bool CanWrite=>false;
        public override long Length=>throw new NotSupportedException();public override long Position{get=>throw new NotSupportedException();set=>throw new NotSupportedException();}
        public override async ValueTask<int> ReadAsync(Memory<byte> buffer,CancellationToken token=default){await Task.Delay(Timeout.Infinite,token);return 0;}
        public override int Read(byte[] b,int o,int c)=>throw new NotSupportedException();public override void Flush(){}public override long Seek(long o,SeekOrigin s)=>throw new NotSupportedException();public override void SetLength(long l)=>throw new NotSupportedException();public override void Write(byte[] b,int o,int c)=>throw new NotSupportedException();
    }
    [Test] public async Task UserCancellationInterruptsTheResponseBodyAndDoesNotRetryOrPopulateTheCache()
    {
        var handler=new Handler((_,_)=>Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK){Content=new StreamContent(new WaitingBody())}));
        using var http=new HttpClient(handler);var service=new GalacticPlaces(http);using var stop=new CancellationTokenSource(TimeSpan.FromMilliseconds(40));
        var at=System.Diagnostics.Stopwatch.StartNew();
        try{await service.SearchAsync(GalacticPlaceKind.MaterialTrader,"Encoded",new(0,0,0),token:stop.Token);throw new InvalidOperationException("Expected cancellation.");}
        catch(OperationCanceledException){Check.That(at.Elapsed<TimeSpan.FromSeconds(1));Check.Equal(1,handler.Calls);}
    }
}