using System.Net; using System.Text.Json; using Tiku.Domain.Catalog; using Tiku.Domain.Common; using Tiku.Domain.Content; using Tiku.Domain.QuestionBanks; using Tiku.Domain.Tenancy; namespace Tiku.IntegrationTests.Api; public sealed class ContentNavigationEndpointTests { [Fact] public async Task Content_entries_hide_hidden_items_by_default() { 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 ContentEntry { Id = Guid.NewGuid(), TenantId = tenantId, RegionId = regionId, EntryKey = "practice", Name = "公开练习", EntryType = ContentEntryType.QuestionPractice, Visibility = ContentVisibility.Public, SortOrder = 2 }, new ContentEntry { Id = Guid.NewGuid(), TenantId = tenantId, EntryKey = "global", Name = "全局入口", EntryType = ContentEntryType.QuestionPractice, Visibility = ContentVisibility.Public, SortOrder = 1 }, new ContentEntry { Id = Guid.NewGuid(), TenantId = tenantId, EntryKey = "hidden", Name = "隐藏入口", EntryType = ContentEntryType.QuestionPractice, Visibility = ContentVisibility.Hidden }); using var client = factory.CreateClient(); using var response = await client.GetAsync($"/api/public/catalog/content-entries?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()); } [Fact] public async Task Content_nodes_support_required_entry_and_root_filter() { var tenantId = Guid.NewGuid(); var entryId = Guid.NewGuid(); var rootNodeId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new ContentEntry { Id = entryId, TenantId = tenantId, EntryKey = "practice", Name = "练习入口" }, new ContentNode { Id = rootNodeId, TenantId = tenantId, EntryId = entryId, Name = "根内容节点", NodeKey = "root", SortOrder = 1, IsLeaf = false }, new ContentNode { Id = Guid.NewGuid(), TenantId = tenantId, EntryId = entryId, ParentId = rootNodeId, Name = "子内容节点", NodeKey = "child", SortOrder = 2, IsLeaf = true }); using var client = factory.CreateClient(); using var missingEntryResponse = await client.GetAsync("/api/public/catalog/content-nodes?tenantCode=master"); using var response = await client.GetAsync( $"/api/public/catalog/content-nodes?tenantCode=master&entryId={entryId}&parentId=root"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.BadRequest, missingEntryResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var item = Assert.Single(items); Assert.Equal("根内容节点", item.GetProperty("name").GetString()); } [Fact] public async Task Collections_and_blueprints_return_active_items_only() { var tenantId = Guid.NewGuid(); var entryId = Guid.NewGuid(); var nodeId = Guid.NewGuid(); var collectionId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new ContentEntry { Id = entryId, TenantId = tenantId, EntryKey = "practice", Name = "练习入口" }, new ContentNode { Id = nodeId, TenantId = tenantId, EntryId = entryId, Name = "章节" }, new QuestionCollection { Id = collectionId, TenantId = tenantId, EntryId = entryId, NodeId = nodeId, Name = "章节题集", CollectionType = QuestionCollectionType.Chapter, Status = ContentStatus.Active, SortOrder = 1 }, new QuestionCollection { Id = Guid.NewGuid(), TenantId = tenantId, EntryId = entryId, NodeId = nodeId, Name = "草稿题集", Status = ContentStatus.Draft }, new PracticeBlueprint { Id = Guid.NewGuid(), TenantId = tenantId, EntryId = entryId, NodeId = nodeId, CollectionId = collectionId, Name = "章节练习", Mode = PracticeMode.Sequential, Status = ContentStatus.Active }, new PracticeBlueprint { Id = Guid.NewGuid(), TenantId = tenantId, EntryId = entryId, NodeId = nodeId, CollectionId = collectionId, Name = "归档练习", Mode = PracticeMode.Sequential, Status = ContentStatus.Archived }); using var client = factory.CreateClient(); using var collectionsResponse = await client.GetAsync( $"/api/public/catalog/question-collections?tenantCode=master&entryId={entryId}&nodeId={nodeId}&collectionType=chapter"); using var blueprintsResponse = await client.GetAsync( $"/api/public/catalog/practice-blueprints?tenantCode=master&entryId={entryId}&nodeId={nodeId}&collectionId={collectionId}&mode=sequential"); var collections = await ReadItemsAsync(collectionsResponse); var blueprints = await ReadItemsAsync(blueprintsResponse); Assert.Equal(HttpStatusCode.OK, collectionsResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, blueprintsResponse.StatusCode); Assert.Equal("章节题集", Assert.Single(collections).GetProperty("name").GetString()); Assert.Equal("章节练习", Assert.Single(blueprints).GetProperty("name").GetString()); } [Fact] public async Task Collection_questions_return_published_questions_only() { var tenantId = Guid.NewGuid(); var collectionId = Guid.NewGuid(); var publishedQuestionId = Guid.NewGuid(); var archivedQuestionId = Guid.NewGuid(); var versionId = Guid.NewGuid(); await using var factory = new ApiTestFactory(); await factory.SeedAsync( Tenant(tenantId, "master"), new QuestionCollection { Id = collectionId, TenantId = tenantId, Name = "真题套卷", Status = ContentStatus.Active }, new Question { Id = archivedQuestionId, TenantId = tenantId, PrimaryCollectionId = collectionId, Type = "choice", Status = QuestionStatus.Archived }); var archivedReference = await factory.SeedQuestionReferenceAsync( tenantId, tenantId, archivedQuestionId); await factory.SeedAsync(new QuestionCollectionItem { Id = Guid.NewGuid(), TenantId = tenantId, CollectionId = collectionId, QuestionReferenceId = archivedReference.Id, QuestionOwnerTenantId = tenantId, QuestionId = archivedQuestionId, SortOrder = 2 }); await factory.SeedQuestionWithVersionAsync( new Question { Id = publishedQuestionId, TenantId = tenantId, PrimaryCollectionId = collectionId, Type = "choice", TypeLabel = "单选题", Status = QuestionStatus.Published }, new QuestionVersion { Id = versionId, TenantId = tenantId, QuestionId = publishedQuestionId, Content = "题干" }); var publishedReference = await factory.SeedQuestionReferenceAsync( tenantId, tenantId, publishedQuestionId); await factory.SeedAsync(new QuestionCollectionItem { Id = Guid.NewGuid(), TenantId = tenantId, CollectionId = collectionId, QuestionReferenceId = publishedReference.Id, QuestionOwnerTenantId = tenantId, QuestionId = publishedQuestionId, SortOrder = 1, Score = 2 }); using var client = factory.CreateClient(); using var response = await client.GetAsync( $"/api/public/catalog/question-collections/questions?tenantCode=master&collectionId={collectionId}"); var items = await ReadItemsAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var item = Assert.Single(items); Assert.Equal(publishedQuestionId, item.GetProperty("id").GetGuid()); Assert.Equal("题干", item.GetProperty("content").GetString()); } private static Tenant Tenant(Guid id, string slug) { return new Tenant { Id = id, Slug = slug, Name = slug, Status = TenantStatus.Active, Metadata = JsonDefaults.Object() }; } 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(); } }