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> send):HttpMessageHandler { public int Calls; protected override Task 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 ReadAsync(Memory 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