using System.Net; using System.Text; using EARSystem.Core; using EARSystem.Infrastructure; using EARSystem.Web.Controllers; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; namespace EARSystem.Tests; public class StubHttp(Func> handler) : HttpMessageHandler { public List Requests { get; } = []; protected override Task SendAsync(HttpRequestMessage request, CancellationToken token) { lock (Requests) Requests.Add(request.RequestUri!); return handler(request, token); } public static HttpResponseMessage Json(string value) => new(HttpStatusCode.OK) { Content = new StringContent(value, Encoding.UTF8, "application/json") }; } public class MapClientTests { private static AMapClient Map(StubHttp stub) => new(new HttpClient(stub), Options.Create(new AMapOptions { WebServiceKey = "test-key" })); [Fact] public async Task GcjCoordinatesNeverConvertTwice() { var stub = new StubHttp((_,_)=>Task.FromResult(StubHttp.Json("{}"))); var coordinate = new Coordinate(120.155,30.274); Assert.Equal(coordinate,await Map(stub).ConvertAsync(coordinate,default)); Assert.Empty(stub.Requests); } [Fact] public async Task GpsCoordinatesUseConversionEndpointAndReturnGcj() { var stub = new StubHttp((_,_)=>Task.FromResult(StubHttp.Json("""{"status":"1","locations":"120.159,30.271"}"""))); var result=await Map(stub).ConvertAsync(new(120.155,30.274,"WGS84"),default); Assert.Equal("GCJ02",result.System);Assert.Equal(120.159,result.Longitude); Assert.Equal("/v3/assistant/coordinate/convert",stub.Requests.Single().AbsolutePath); Assert.Contains("coordsys=gps",stub.Requests.Single().Query); } [Fact] public async Task OptionalFieldsRemainUnknownAndDuplicatePoisCollapse() { var stub=new StubHttp((_,_)=>Task.FromResult(StubHttp.Json(""" {"status":"1","pois":[{"id":"B_TEST","name":"测试地点","location":"120.155,30.274","address":[],"business":{"rating":[],"cost":[]}}, {"id":"BAD","name":"无效坐标","location":[]}]} """))); var rows=await Map(stub).SearchAsync(new RecommendationQuery(),default); Assert.Equal(6,stub.Requests.Count);var item=Assert.Single(rows); Assert.Null(item.Price);Assert.Null(item.Rating);Assert.Null(item.IsOpen); Assert.Null(item.Atmosphere);Assert.Null(item.CatalogId);Assert.Equal(0,item.FeedbackCount); Assert.Equal("类型推测",item.AtmosphereSource); } [Fact] public async Task KnownPriceAndRatingAreSeparateFromAtmosphere() { var stub=new StubHttp((_,_)=>Task.FromResult(StubHttp.Json(""" {"status":"1","pois":[{"id":"B_TEST","name":"测试咖啡","type":"餐饮服务;咖啡厅","location":"120.155,30.274","business":{"rating":"4.8","cost":"28"}}]} """))); var rows=await Map(stub).SearchAsync(new RecommendationQuery { Categories=[PlaceCategory.Dining] },default); var item=Assert.Single(rows);Assert.Equal(28m,item.Price);Assert.Equal(4.8,item.Rating); Assert.Null(item.Atmosphere); Assert.Equal(PlaceCategory.Cafe,item.Category); } [Theory] [InlineData("""{"status":"0","info":"INVALID_USER_KEY"}""")] [InlineData("""not-json""")] public async Task FailuresAreSanitizedWithoutProviderKeys(string response) { var stub=new StubHttp((_,_)=>Task.FromResult(StubHttp.Json(response))); var error=await Assert.ThrowsAsync(()=>Map(stub).SearchAsync(new(),default)); Assert.DoesNotContain("test-key",error.Message);Assert.DoesNotContain("INVALID_USER_KEY",error.Message); } [Fact] public async Task CancellationStopsPendingHttpRequests() { var stub=new StubHttp(async (_,ct)=>{await Task.Delay(Timeout.Infinite,ct);return StubHttp.Json("{}");}); using var cancel=new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); await Assert.ThrowsAnyAsync(()=>Map(stub).SearchAsync(new(),cancel.Token)); } private class Factory(HttpClient client) : IHttpClientFactory { public HttpClient CreateClient(string name)=>client; } [Fact] public async Task JsSecurityProxyIsRestrictedAndInjectsServerCredentials() { var stub=new StubHttp((_,_)=>Task.FromResult(StubHttp.Json("{}"))); var controller=new MapProxyController(new Factory(new HttpClient(stub)),Options.Create(new AMapOptions{ JsSecurityCode="server-only-code",BrowserKey="browser-key" })){ ControllerContext=new ControllerContext{HttpContext=new DefaultHttpContext()} }; controller.Request.QueryString=new("?key=untrusted&jscode=untrusted"); Assert.IsType(await controller.Proxy("https://evil.example",default)); Assert.Empty(stub.Requests); Assert.IsType(await controller.Proxy("v4/map/styles",default)); var uri=Assert.Single(stub.Requests); Assert.Equal("webapi.amap.com",uri.Host); Assert.Contains("jscode=server-only-code",uri.Query);Assert.Contains("key=browser-key",uri.Query); Assert.DoesNotContain("untrusted",uri.Query); Assert.Equal("no-store",controller.Response.Headers.CacheControl.ToString()); } }