using System.Reflection; using System.Text; using MAX.Framework.Commons; using System.Text.Json; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using OpenSearch.Client; using OpenSearch.Net; using Store.Modules.CloudProduct.Controllers; using Store.Modules.CloudProduct.Dtos; using Store.Modules.CloudProduct.Entities; using Store.Modules.CloudProduct.Services; if (args.Contains("--integration")) { await IntegrationChecks.RunAsync(args.Contains("--apply-migration"), args.Contains("--migrate-violation-rows")); return; } var checks = 0; void Check(bool condition, string message) { if (!condition) throw new Exception(message); checks++; } void Reject(Action action) { try { action(); } catch (AppException) { checks++; return; } throw new Exception("Expected validation failure"); } var grouped = GoodsViolationService.ValidateAndGroup([ new() { GoodsId = 12, Reasons = " 图片违规 " }, new() { GoodsId = 12, Reasons = "描述违规" }, new() { GoodsId = 12, Reasons = "图片违规" }, new() { GoodsId = 12, Reasons = "涉嫌侵权" }, new() { GoodsId = 12, Reasons = "描述违规" }, new() { GoodsId = 13, Reasons = "中文原因" } ]); Check(grouped.Count == 2 && grouped[12].ToHashSet().SetEquals(new[] { "图片违规", "描述违规", "涉嫌侵权" }), "Duplicate goods/reasons must merge"); Check(grouped[13][0] == "中文原因", "Chinese reason text must survive"); Reject(() => GoodsViolationService.ValidateAndGroup(null)); Reject(() => GoodsViolationService.ValidateAndGroup([])); Reject(() => GoodsViolationService.ValidateAndGroup([null!])); Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = -1, Reasons = "违规" }])); Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = "" }])); Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = " " }])); Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = null! }])); Reject(() => GoodsViolationService.ValidateAndGroup(new ReportGoodsViolationItem[1001])); Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = new string('中', 101) }])); Reject(() => GoodsViolationService.ValidateAndGroup([new() { GoodsId = 1, Reasons = null! }])); // SQL generation only: no database connection or production data access. using var db = new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, "Server=127.0.0.1;Database=checks;User Id=unused;Password=unused").Build(); var sql = db.Select().As("g") .Where(x => x.Status == 0 && x.IsSegmented == 0) .Where("NOT EXISTS (SELECT 1 FROM goods_violation v WHERE v.goods_id = g.goods_id)") .OrderBy(x => x.GoodsId).Limit(1000).ToSql(); Check(sql.Contains("NOT EXISTS") && sql.IndexOf("NOT EXISTS") < sql.IndexOf("limit", StringComparison.OrdinalIgnoreCase), "Violation filter must precede paging"); Check(sql.Contains("FROM `goods` g", StringComparison.OrdinalIgnoreCase), "Anti-join alias must match: " + sql); var pendingSql = db.Select().Where(x => !x.IsIndexDeleted).ToSql(); Check(pendingSql.Contains("is_index_deleted") && !pendingSql.Contains("off_shelf_status"), "Renamed column mapping"); var insertSql = db.Insert(new GoodsViolationEntity { GoodsId = 1, ReasonId = 2 }).NoneParameter().ToSql(); Check(insertSql.Contains("reason_id") && !insertSql.Contains("reason_ids"), "Reason must be stored as a scalar ID"); var request = JsonSerializer.Deserialize("""{"goodsId":1,"reasons":"图片违规"}""", new JsonSerializerOptions(JsonSerializerDefaults.Web))!; Check(request.Reasons == "图片违规", "JSON request accepts reason names"); var reportJson = JsonSerializer.Serialize(new ReportGoodsViolationsResult { GoodsId = 1, IsOffShelf = true }, new JsonSerializerOptions(JsonSerializerDefaults.Web)); Check(reportJson == """{"goodsId":1,"isOffShelf":true}""", "Report response contains only goods ID and off-shelf status"); try { JsonSerializer.Deserialize("""{"goodsId":1,"reasons":["图片违规"]}""", new JsonSerializerOptions(JsonSerializerDefaults.Web)); throw new Exception("Array reason accepted"); } catch (JsonException) { checks++; } Check(!typeof(GoodsController).GetMethods().Any(x => x.Name.Contains("ByReason")), "Reason query must not be exposed"); var removalService = new GoodsViolationService(null!, null!, null!, NullLogger.Instance); Reject(() => removalService.RemoveAsync(null).GetAwaiter().GetResult()); Reject(() => removalService.RemoveAsync([]).GetAwaiter().GetResult()); Reject(() => removalService.RemoveAsync([0]).GetAwaiter().GetResult()); Reject(() => removalService.RemoveAsync([-1]).GetAwaiter().GetResult()); Reject(() => removalService.RemoveAsync(new long[1001]).GetAwaiter().GetResult()); // Exercise deletion with an in-memory OpenSearch transport, without a real server. var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Search:Url"] = "http://localhost:9200", ["Search:IndexName"] = "goods" }).Build(); var serviceType = typeof(IOpenSearchService).Assembly.GetType("Store.Modules.CloudProduct.Services.OpenSearchService")!; var logger = Activator.CreateInstance(typeof(NullLogger<>).MakeGenericType(serviceType)); async Task TestDelete(int status, string body, bool shouldFail) { var service = (IOpenSearchService)Activator.CreateInstance(serviceType, config, logger, db, new GoodsIndexLock(config))!; using var transport = new InMemoryConnection(Encoding.UTF8.GetBytes(body), status); var client = new OpenSearchClient(new ConnectionSettings(new SingleNodeConnectionPool(new Uri("http://localhost:9200")), transport)); serviceType.GetField("_client", BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(service, client); try { await service.DeleteDocumentsAsync([123]); Check(!shouldFail, $"HTTP {status}: expected delete failure"); } catch (AppException) { Check(shouldFail, $"HTTP {status}: unexpected delete failure"); } } await TestDelete(403, "{}", true); await TestDelete(500, "{}", true); await TestDelete(404, "{}", false); await TestDelete(200, """{"errors":false,"items":[{"delete":{"_index":"goods","_id":"123","status":200}}]}""", false); await TestDelete(200, """{"errors":true,"items":[{"delete":{"_index":"goods","_id":"123","status":404,"result":"not_found"}}]}""", false); await TestDelete(200, """{"errors":true,"items":[{"delete":{"_index":"goods","_id":"123","status":429,"error":{"type":"rejected","reason":"busy"}}}]}""", true); await TestDelete(200, """{"errors":false,"items":[]}""", true); Console.WriteLine($"PASS: {checks} checks (no external services accessed)");