feat: enforce tenant isolation and shared question bank

This commit is contained in:
2026-07-27 16:59:12 +08:00
parent 28e9a9fa41
commit db4c7b4496
137 changed files with 6402 additions and 112274 deletions

View File

@@ -13,6 +13,60 @@ namespace Tiku.IntegrationTests.Api;
public sealed class AuthEndpointTests
{
[Fact]
public async Task Custom_host_rejects_jwt_from_another_tenant_and_ignores_spoofed_tenant_header()
{
await using var factory = new ApiTestFactory();
var tenantB = await SeedLoginUserAsync(factory);
var tenantAId = Guid.NewGuid();
await factory.SeedAsync(
new Tenant
{
Id = tenantAId,
Slug = "tenant-a",
Name = "Tenant A",
Status = TenantStatus.Active,
Mode = TenantMode.Saas
},
new TenantDomain
{
TenantId = tenantAId,
Host = "a.example.test",
DomainType = TenantDomainType.Custom,
Status = TenantDomainStatus.Active,
IsPrimary = true
});
using var client = factory.CreateClient();
var loginResponse = await client.PostAsJsonAsync(
"/api/auth/login/password",
new PasswordLoginDto
{
TenantCode = tenantB.TenantId.ToString("N"),
Phone = tenantB.Phone,
Password = "passw0rd!"
});
var loginJson = await ReadJsonAsync(loginResponse);
var accessToken = loginJson.RootElement.GetProperty("tokens").GetProperty("accessToken").GetString();
using var jwtRequest = new HttpRequestMessage(HttpMethod.Get, "/api/me");
jwtRequest.Headers.Host = "a.example.test";
jwtRequest.Headers.Authorization = new("Bearer", accessToken);
var jwtResponse = await client.SendAsync(jwtRequest);
using var spoofRequest = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login/password");
spoofRequest.Headers.Host = "a.example.test";
spoofRequest.Headers.Add("x-tenant-code", tenantB.TenantId.ToString("N"));
spoofRequest.Content = JsonContent.Create(new PasswordLoginDto
{
Phone = tenantB.Phone,
Password = "passw0rd!"
});
var spoofResponse = await client.SendAsync(spoofRequest);
Assert.Equal(HttpStatusCode.Forbidden, jwtResponse.StatusCode);
Assert.NotEqual(HttpStatusCode.OK, spoofResponse.StatusCode);
}
[Fact]
public async Task Password_login_can_access_current_user_and_tenant()
{
@@ -24,7 +78,7 @@ public sealed class AuthEndpointTests
"/api/auth/login/password",
new PasswordLoginDto
{
TenantId = seed.TenantId,
TenantCode = seed.TenantId.ToString("N"),
Phone = seed.Phone,
Password = "passw0rd!"
});
@@ -57,7 +111,7 @@ public sealed class AuthEndpointTests
"/api/auth/login/sms",
new SmsLoginDto
{
TenantId = seed.TenantId,
TenantCode = seed.TenantId.ToString("N"),
Phone = seed.Phone,
Code = "123456"
});
@@ -84,7 +138,7 @@ public sealed class AuthEndpointTests
"/api/auth/login/password",
new PasswordLoginDto
{
TenantId = seed.TenantId,
TenantCode = seed.TenantId.ToString("N"),
Phone = seed.Phone,
Password = "passw0rd!"
});
@@ -136,7 +190,7 @@ public sealed class AuthEndpointTests
"/api/auth/oauth/wechat-miniapp",
new OAuthCodeDto
{
TenantId = tenantId,
TenantCode = tenantId.ToString("N"),
Code = "wx-code"
});
var loginJson = await ReadJsonAsync(loginResponse);
@@ -147,7 +201,7 @@ public sealed class AuthEndpointTests
client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken);
var meResponse = await client.GetAsync("/api/me");
using var scope = factory.Services.CreateScope();
using var scope = factory.CreateSystemScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
@@ -204,7 +258,7 @@ public sealed class AuthEndpointTests
string phone,
string code)
{
using var scope = factory.Services.CreateScope();
using var scope = factory.CreateSystemScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
dbContext.SmsVerificationCodes.Add(new SmsVerificationCode
{