using System.Net; using System.Text.Json; using Tiku.Domain.Catalog; using Tiku.Domain.Commerce; using Tiku.Domain.Common; using Tiku.Domain.Learning; using Tiku.Domain.Operations; using Tiku.Domain.Tenancy; namespace Tiku.IntegrationTests.Api; public sealed class CatalogEndpointTests { [Fact] public async Task Regions_are_filtered_by_active_tenant() { var tenantId = Guid.NewGuid(); var otherTenantId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), Tenant(otherTenantId, "other"), new Region { Id = Guid.NewGuid(), TenantId = tenantId, Name = "浙江", SortOrder = 2, IsActive = true }, new Region { Id = Guid.NewGuid(), TenantId = tenantId, Name = "北京", SortOrder = 1, IsActive = true }, new Region { Id = Guid.NewGuid(), TenantId = tenantId, Name = "停用地区", SortOrder = 0, IsActive = false }, new Region { Id = Guid.NewGuid(), TenantId = otherTenantId, Name = "其他租户地区", IsActive = true }); using var client = factory.CreateClient(); using var response = await client.GetAsync("/api/catalog/regions?tenantCode=master"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(["北京", "浙江"], items.Select(item => item.GetProperty("name").GetString()!).ToArray()); } [Fact] public async Task Subjects_support_common_filters() { var tenantId = Guid.NewGuid(); var regionId = Guid.NewGuid(); var schoolId = Guid.NewGuid(); var majorId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, new School { Id = schoolId, TenantId = tenantId, RegionId = regionId, Name = "测试院校" }, new Major { Id = majorId, TenantId = tenantId, RegionId = regionId, SchoolId = schoolId, Name = "视觉传达" }, new Subject { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, SchoolId = schoolId, MajorId = majorId, Name = "专业理论", Type = SubjectType.Professional, SortOrder = 1, IsActive = true }, new Subject { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, SchoolId = schoolId, MajorId = majorId, Name = "公共英语", Type = SubjectType.Cultural, SortOrder = 2, IsActive = true }, new Subject { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Name = "停用科目", Type = SubjectType.Professional, IsActive = false }); using var client = factory.CreateClient(); using var response = await client.GetAsync( $"/api/catalog/subjects?tenantCode=master®ionId={regionId}&schoolId={schoolId}&majorId={majorId}&type=professional&keyword=理论"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var item = Assert.Single(items); Assert.Equal("专业理论", item.GetProperty("name").GetString()); } [Fact] public async Task Module_nodes_support_root_filter() { var tenantId = Guid.NewGuid(); var parentId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new ModuleNode { Id = parentId, TenantId = tenantId, Name = "根节点", Type = ModuleNodeType.Category, IsActive = true }, new ModuleNode { Id = Guid.NewGuid(), TenantId = tenantId, ParentId = parentId, Name = "子节点", Type = ModuleNodeType.Chapter, IsActive = true }); using var client = factory.CreateClient(); using var response = await client.GetAsync("/api/catalog/module-nodes?tenantCode=master&parentId=root"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var item = Assert.Single(items); Assert.Equal("根节点", item.GetProperty("name").GetString()); } [Fact] public async Task Catalog_requires_known_tenant_for_anonymous_requests() { await using var factory = new ApiTestFactory(); using var client = factory.CreateClient(); using var response = await client.GetAsync("/api/catalog/regions"); var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Equal("tenant_not_found", body.RootElement.GetProperty("code").GetString()); } [Fact] public async Task Banners_are_filtered_by_tenant_region_and_active_status() { var tenantId = Guid.NewGuid(); var otherTenantId = Guid.NewGuid(); var regionId = Guid.NewGuid(); var otherRegionId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), Tenant(otherTenantId, "other"), new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, new Region { Id = otherRegionId, TenantId = tenantId, Name = "江苏" }, new Banner { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Title = "第二张", SortOrder = 2, IsActive = true }, new Banner { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Title = "第一张", SortOrder = 1, IsActive = true }, new Banner { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Title = "停用横幅", IsActive = false }, new Banner { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = otherRegionId, Title = "其他地区横幅", IsActive = true }, new Banner { Id = Guid.NewGuid(), TenantId = otherTenantId, Title = "其他租户横幅", IsActive = true }); using var client = factory.CreateClient(); using var response = await client.GetAsync($"/api/catalog/banners?tenantCode=master®ionId={regionId}"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(["第一张", "第二张"], items.Select(item => item.GetProperty("title").GetString()!).ToArray()); } [Fact] public async Task Faqs_and_announcements_only_return_active_current_tenant_items() { var tenantId = Guid.NewGuid(); var otherTenantId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), Tenant(otherTenantId, "other"), new Faq { Id = Guid.NewGuid(), TenantId = tenantId, Question = "如何购买?", Answer = "在线购买", IsActive = true }, new Faq { Id = Guid.NewGuid(), TenantId = tenantId, Question = "停用 FAQ", IsActive = false }, new Faq { Id = Guid.NewGuid(), TenantId = otherTenantId, Question = "其他租户 FAQ", IsActive = true }, new Announcement { Id = Guid.NewGuid(), TenantId = tenantId, Content = "暑期活动", IsActive = true }, new Announcement { Id = Guid.NewGuid(), TenantId = tenantId, Content = "停用公告", IsActive = false }, new Announcement { Id = Guid.NewGuid(), TenantId = otherTenantId, Content = "其他租户公告", IsActive = true }); using var client = factory.CreateClient(); using var faqResponse = await client.GetAsync("/api/catalog/faqs?tenantCode=master"); using var announcementResponse = await client.GetAsync("/api/catalog/announcements?tenantCode=master"); var faqs = await ReadItemsAsync(faqResponse); var announcements = await ReadItemsAsync(announcementResponse); Assert.Equal(HttpStatusCode.OK, faqResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, announcementResponse.StatusCode); Assert.Equal("如何购买?", Assert.Single(faqs).GetProperty("question").GetString()); Assert.Equal("暑期活动", Assert.Single(announcements).GetProperty("content").GetString()); } [Fact] public async Task Exam_dates_support_region_school_filters_and_days_left() { var tenantId = Guid.NewGuid(); var regionId = Guid.NewGuid(); var schoolId = Guid.NewGuid(); var otherRegionId = Guid.NewGuid(); var otherSchoolId = Guid.NewGuid(); var futureExamAt = new DateTimeOffset(DateTimeOffset.UtcNow.Date.AddDays(10), TimeSpan.Zero); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, new Region { Id = otherRegionId, TenantId = tenantId, Name = "其他地区" }, new School { Id = schoolId, TenantId = tenantId, RegionId = regionId, Name = "测试院校" }, new School { Id = otherSchoolId, TenantId = tenantId, RegionId = otherRegionId, Name = "其他院校" }, new ExamDate { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, SchoolId = schoolId, ExamName = "校考", ExamAt = futureExamAt, IsActive = true }, new ExamDate { Id = Guid.NewGuid(), TenantId = tenantId, ExamName = "通用考试", ExamAt = futureExamAt.AddDays(1), IsActive = true }, new ExamDate { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = otherRegionId, SchoolId = otherSchoolId, ExamName = "其他地区学校考试", ExamAt = futureExamAt.AddDays(2), IsActive = true }, new ExamDate { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, SchoolId = schoolId, ExamName = "停用考试", IsActive = false }); using var client = factory.CreateClient(); using var response = await client.GetAsync( $"/api/catalog/exam-dates?tenantCode=master®ionId={regionId}&schoolId={schoolId}"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(["校考", "通用考试"], items.Select(item => item.GetProperty("examName").GetString()!).ToArray()); Assert.Equal(10, items[0].GetProperty("daysLeft").GetInt32()); } [Fact] public async Task Products_filter_active_region_type_keyword_and_limit() { var tenantId = Guid.NewGuid(); var regionId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, new Product { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Title = "联考资料 B", Type = ProductType.Material, SortOrder = 2, LegacyPriceText = "¥20", IsActive = true }, new Product { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Title = "联考资料 A", Type = ProductType.Material, SortOrder = 1, LegacyPriceText = "¥10", IsActive = true }, new Product { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Title = "联考课程", Type = ProductType.Course, IsActive = true }, new Product { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Title = "停用资料", Type = ProductType.Material, IsActive = false }); using var client = factory.CreateClient(); using var response = await client.GetAsync( $"/api/catalog/products?tenantCode=master®ionId={regionId}&type=material&keyword=联考&limit=1"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var item = Assert.Single(items); Assert.Equal("联考资料 A", item.GetProperty("title").GetString()); Assert.Equal("¥10", item.GetProperty("priceLabel").GetString()); Assert.True(item.GetProperty("isActive").GetBoolean()); } [Fact] public async Task Svip_plans_filter_active_region_and_include_global_plans() { var tenantId = Guid.NewGuid(); var regionId = Guid.NewGuid(); var otherRegionId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new Region { Id = regionId, TenantId = tenantId, Name = "浙江" }, new Region { Id = otherRegionId, TenantId = tenantId, Name = "其他地区" }, new SvipPlan { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Name = "地区月卡", PriceCents = 3000, Days = 30, SortOrder = 2, IsActive = true }, new SvipPlan { Id = Guid.NewGuid(), TenantId = tenantId, Name = "通用周卡", PriceCents = 900, Days = 7, SortOrder = 1, IsActive = true }, new SvipPlan { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = otherRegionId, Name = "其他地区卡", PriceCents = 1000, Days = 7, IsActive = true }, new SvipPlan { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, Name = "停用卡", PriceCents = 1, Days = 1, IsActive = false }); using var client = factory.CreateClient(); using var response = await client.GetAsync($"/api/catalog/svip-plans?tenantCode=master®ionId={regionId}"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(["通用周卡", "地区月卡"], items.Select(item => item.GetProperty("name").GetString()!).ToArray()); Assert.Equal(9m, items[0].GetProperty("price").GetDecimal()); } private static Tenant Tenant(Guid id, string slug) { return new Tenant { Id = id, Slug = slug, Name = slug, Status = TenantStatus.Active }; } private static async Task ReadItemsAsync(HttpResponseMessage response) { var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync()); return body.RootElement .GetProperty("items") .EnumerateArray() .Select(item => item.Clone()) .ToArray(); } }