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

EARSystem

【ASP】耳间,网页端的情绪智能推荐系统

公开
关注 0 Fork 0 Star 0
UTF-8
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<HttpRequestMessage, CancellationToken, Task<HttpResponseMessage>> handler) : HttpMessageHandler
{
    public List<Uri> Requests { get; } = [];
    protected override Task<HttpResponseMessage> 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<MapServiceException>(()=>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<OperationCanceledException>(()=>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<NotFoundResult>(await controller.Proxy("https://evil.example",default));
        Assert.Empty(stub.Requests);
        Assert.IsType<FileContentResult>(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());
    }
}