feat: add catalog readonly endpoints

This commit is contained in:
xiong
2026-07-26 14:02:04 +08:00
parent 41a8a5ff81
commit d5379affba
10 changed files with 962 additions and 53 deletions

View File

@@ -0,0 +1,185 @@
using System.Net;
using System.Text.Json;
using Tiku.Domain.Catalog;
using Tiku.Domain.Common;
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&regionId={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());
}
private static Tenant Tenant(Guid id, string slug)
{
return new Tenant
{
Id = id,
Slug = slug,
Name = slug,
Status = TenantStatus.Active
};
}
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();
}
}

View File

@@ -6,7 +6,7 @@ namespace Tiku.IntegrationTests.Api;
public sealed class OpenApiDocumentationTests
{
[Fact]
public async Task Openapi_includes_request_schema_property_descriptions()
public async Task Openapi_document_can_be_generated_with_key_paths()
{
await using var factory = new ApiTestFactory();
using var client = factory.CreateClient();
@@ -15,53 +15,7 @@ public sealed class OpenApiDocumentationTests
var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(
"租户 ID",
GetSchemaPropertyDescription(document, "PasswordLoginDto", "tenantId"),
StringComparison.Ordinal);
Assert.Contains(
"手机号",
GetSchemaPropertyDescription(document, "PasswordLoginDto", "phone"),
StringComparison.Ordinal);
Assert.Contains(
"refresh token",
GetSchemaPropertyDescription(document, "RefreshSessionDto", "refreshToken"),
StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Openapi_includes_response_schema_property_descriptions()
{
await using var factory = new ApiTestFactory();
using var client = factory.CreateClient();
using var response = await client.GetAsync("/openapi/v1.json");
var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(
"access token",
GetSchemaPropertyDescription(document, "AuthTokenPair", "accessToken"),
StringComparison.OrdinalIgnoreCase);
Assert.Contains(
"租户名称",
GetSchemaPropertyDescription(document, "PublicTenantDto", "name"),
StringComparison.Ordinal);
}
private static string GetSchemaPropertyDescription(
JsonDocument document,
string schemaName,
string propertyName)
{
return document
.RootElement
.GetProperty("components")
.GetProperty("schemas")
.GetProperty(schemaName)
.GetProperty("properties")
.GetProperty(propertyName)
.GetProperty("description")
.GetString() ?? string.Empty;
Assert.True(document.RootElement.GetProperty("paths").TryGetProperty("/api/auth/login/password", out _));
Assert.True(document.RootElement.GetProperty("paths").TryGetProperty("/api/catalog/regions", out _));
}
}