using System.Net;
using System.Net.Http.Json;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Text.Json;
using MAX.Framework.Commons;
using MAX.Framework.Security;
using MAX.Plugins;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Store.Modules.CloudProduct.Controllers;
using Store.Modules.CloudProduct.Entities;
using Store.Modules.CloudProduct.Services;
/// 通过真实 HTTP 路由验证图片识别的 DTO 绑定、ApiResult 包装及缓存。
internal static class HttpChecks
{
public static async Task RunAsync(IFreeSql database)
{
var userName = "http-check-" + Guid.NewGuid().ToString("N");
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.WebHost.UseUrls("http://127.0.0.1:0");
using var context = new PluginContext("CloudProduct", "Plugins/CloudProduct", builder.Environment);
context.Configuration["ProductInspection:Model"] = "test-vision";
context.Configuration["ProductInspection:ApiKey"] = "test-key";
builder.Services.AddSingleton(new PluginContext(context));
builder.Services.AddKeyedSingleton("ProductInspectionAnalysisGate", (_, _) => new SemaphoreSlim(5, 5));
builder.Services.AddKeyedSingleton("ProductInspectionConnection", database);
builder.Services.AddSingleton();
builder.Services.AddScoped();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped();
builder.Services.AddAuthentication("test").AddScheme("test", _ => { });
builder.Services.AddAuthorization();
builder.Services.AddControllers(options =>
{
options.Conventions.Add(new MAX.Framework.Conventions.ApiRoutePrefixConvention());
options.Filters.Add();
options.Filters.Add();
}).AddApplicationPart(typeof(ProductInspectionController).Assembly);
await using var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapPost("/test/chat", RecognitionEndpoint.RespondAsync);
await app.StartAsync();
var address = app.Services.GetRequiredService().Features.Get()!.Addresses.Single();
context.Configuration["ProductInspection:Endpoint"] = address + "/test/chat";
using var client = new HttpClient { BaseAddress = new Uri(address + "/api/ProductInspection/") };
var checks = 0;
void Check(bool condition, string message)
{
if (!condition) throw new Exception(message);
checks++;
}
// 随机化图片内容及登录身份,清理时只删除本次检查创建的行。
var data = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jZQAAAABJRU5ErkJggg==")
.Concat(Guid.NewGuid().ToByteArray()).ToArray();
var image = Convert.ToBase64String(data);
var key = Guid.NewGuid().ToString("N");
async Task Inspect(string image, string? md5)
{
using var response = await client.PostAsJsonAsync("InspectImage", new { userName, md5, image });
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
return doc.RootElement.Clone();
}
try
{
client.DefaultRequestHeaders.Add("X-Test-User", userName);
var first = await Inspect(image, key.ToUpperInvariant());
Check(first.TryGetProperty("code", out var firstCode) && firstCode.GetInt32() == 200, first.ToString());
string[] expected = ["急速发货", "螺旋伸缩", "金属挂钩", "可调节", "不挡扇叶", "倒挂钩卡口"];
Check(first.GetProperty("data").Deserialize()!.SequenceEqual(expected), "Ordered recognition array");
Check((await Inspect(image, key)).GetProperty("data").Deserialize()!.SequenceEqual(expected), "Cache hit preserves item order");
var stored = await database.Select().Where(x => x.CacheKey == key).FirstAsync();
Check(stored.RecognitionResult == """["急速发货","螺旋伸缩","金属挂钩","可调节","不挡扇叶","倒挂钩卡口"]""", "Cache stores distinct items with readable Chinese");
await database.Update().Set(x => x.RecognitionResult, "[]").Where(x => x.CacheKey == key).ExecuteAffrowsAsync();
var otherImage = Convert.ToBase64String(data.Append((byte)42).ToArray());
var hit = (await Inspect(otherImage, key)).GetProperty("data");
Check(hit.GetArrayLength() == 0, "Same client key reuses cached result for different image bytes");
await database.Update().Set(x => x.RecognitionResult, """{"text":"legacy","logoBrands":[]}""")
.Where(x => x.CacheKey == key).ExecuteAffrowsAsync();
Check((await Inspect(image, key)).GetProperty("data").Deserialize()!.SequenceEqual(expected), "Refresh legacy cache");
Check((await Inspect(Convert.ToBase64String(new byte[30]), key)).GetProperty("code").GetInt32() != 200, "Invalid image denied");
Check((await Inspect("invalid!", key)).GetProperty("code").GetInt32() != 200, "Malformed Base64 denied before cache hit");
foreach (var invalid in new[] { "invalid", new string('z', 32), null })
{
var rejected = await Inspect(otherImage, invalid);
Check(!rejected.TryGetProperty("code", out var code) || code.GetInt32() != 200, "Reject missing or malformed MD5 before cache hit");
}
Console.WriteLine($"PASS {checks} HTTP and MySQL checks");
}
finally
{
await app.StopAsync();
await database.Delete().Where(x => x.CacheKey == key).ExecuteAffrowsAsync();
}
}
}
/// 仅测试宿主使用的身份处理器,生产插件始终沿用宿主 Bearer 认证。
internal sealed class TestAuthentication(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder)
: AuthenticationHandler(options, logger, encoder)
{
protected override Task HandleAuthenticateAsync()
{
var name = Request.Headers["X-Test-User"].ToString();
if (string.IsNullOrEmpty(name)) return Task.FromResult(AuthenticateResult.NoResult());
var principal = new ClaimsPrincipal(new ClaimsIdentity([new Claim(JwtClaimTypes.UserName, name)], Scheme.Name));
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, Scheme.Name)));
}
}
/// 返回模型协议示例,使 HTTP 检查无需外部密钥。
internal static class RecognitionEndpoint
{
private const string Content = """["\u6025\u901F\u53D1\u8D27","急速发货","螺旋伸缩","急速发货","金属挂钩","可调节","不挡扇叶","倒挂钩卡口"]""";
internal static async Task RespondAsync(HttpRequest request, CancellationToken cancellationToken)
{
using var body = await JsonDocument.ParseAsync(request.Body, cancellationToken: cancellationToken);
var root = body.RootElement;
var parts = root.GetProperty("messages")[0].GetProperty("content");
if (root.GetProperty("model").GetString() != "test-vision"
|| request.Headers.Authorization.ToString() != "Bearer test-key"
|| root.GetProperty("stream").GetBoolean() || root.GetProperty("enable_thinking").GetBoolean()
|| !parts[0].GetProperty("image_url").GetProperty("url").GetString()!.StartsWith("data:image/png;base64,iVBOR")
|| !parts[1].GetProperty("text").GetString()!.Contains("JSON 字符串数组"))
throw new Exception("Invalid model request");
return Results.Json(new { choices = new[] { new { message = new { content = Content } } } });
}
}