forked from gongxuegit/tiku-backend.net
feat: add content navigation readonly endpoints
This commit is contained in:
269
Tiku.IntegrationTests/Api/ContentNavigationEndpointTests.cs
Normal file
269
Tiku.IntegrationTests/Api/ContentNavigationEndpointTests.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
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 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/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/catalog/content-nodes?tenantCode=master");
|
||||
using var response = await client.GetAsync($"/api/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/catalog/question-collections?tenantCode=master&entryId={entryId}&nodeId={nodeId}&collectionType=chapter");
|
||||
using var blueprintsResponse = await client.GetAsync(
|
||||
$"/api/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 = publishedQuestionId,
|
||||
TenantId = tenantId,
|
||||
PrimaryCollectionId = collectionId,
|
||||
Type = "choice",
|
||||
TypeLabel = "单选题",
|
||||
Status = QuestionStatus.Published,
|
||||
CurrentVersionId = versionId
|
||||
},
|
||||
new QuestionVersion
|
||||
{
|
||||
Id = versionId,
|
||||
TenantId = tenantId,
|
||||
QuestionId = publishedQuestionId,
|
||||
Content = "题干"
|
||||
},
|
||||
new Question
|
||||
{
|
||||
Id = archivedQuestionId,
|
||||
TenantId = tenantId,
|
||||
PrimaryCollectionId = collectionId,
|
||||
Type = "choice",
|
||||
Status = QuestionStatus.Archived
|
||||
},
|
||||
new QuestionCollectionItem
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
CollectionId = collectionId,
|
||||
QuestionId = publishedQuestionId,
|
||||
SortOrder = 1,
|
||||
Score = 2
|
||||
},
|
||||
new QuestionCollectionItem
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
CollectionId = collectionId,
|
||||
QuestionId = archivedQuestionId,
|
||||
SortOrder = 2
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
using var response = await client.GetAsync($"/api/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<JsonElement[]> ReadItemsAsync(HttpResponseMessage response)
|
||||
{
|
||||
var body = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
||||
return body.RootElement
|
||||
.GetProperty("items")
|
||||
.EnumerateArray()
|
||||
.Select(item => item.Clone())
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user