test(architecture): enforce capability boundaries
This commit is contained in:
@@ -27,6 +27,7 @@ internal static class ApiPresentationExtensions
|
||||
services.AddScoped<CommerceAdminActorResolver>();
|
||||
services.AddScoped<LearningActorResolver>();
|
||||
services.AddScoped<PlatformAdminActorResolver>();
|
||||
services.AddScoped<AuthRequestContextResolver>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.RateLimiting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Auth;
|
||||
@@ -22,12 +21,8 @@ public sealed class AuthController(
|
||||
IAuthService authService,
|
||||
IOwnerActivationService ownerActivationService,
|
||||
ISmsVerificationService smsVerificationService,
|
||||
IAuthSessionStore sessionStore,
|
||||
ITenantContext tenantContext,
|
||||
ITenantContextInitializer tenantContextInitializer,
|
||||
ITenantDirectory tenantDirectory,
|
||||
ICurrentUser currentUser,
|
||||
IOptions<TenantResolutionOptions> tenantResolutionOptions) : ControllerBase
|
||||
AuthRequestContextResolver requestContextResolver,
|
||||
ICurrentUser currentUser) : ControllerBase
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[EnableRateLimiting(AuthRateLimitPolicies.Password)]
|
||||
@@ -58,11 +53,12 @@ public sealed class AuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var realm = request.Realm!.Value;
|
||||
EnsureRouteRealm(realm);
|
||||
requestContextResolver.EnsureRouteRealm(realm, Request);
|
||||
if (realm != AuthRealm.Tenant)
|
||||
throw new RequiredFieldException("SMS authentication is only available in the tenant realm.");
|
||||
|
||||
var tenantId = await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken)
|
||||
var tenantId = await requestContextResolver.ResolveRealmTenantIdAsync(realm, request.TenantCode, Request,
|
||||
cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for SMS authentication.");
|
||||
var result = await smsVerificationService.CreateCodeAsync(
|
||||
new SendSmsCodeRequest(
|
||||
@@ -88,13 +84,14 @@ public sealed class AuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var realm = request.Realm!.Value;
|
||||
EnsureRouteRealm(realm);
|
||||
requestContextResolver.EnsureRouteRealm(realm, Request);
|
||||
var identifier = request.Identifier ?? request.Phone;
|
||||
if (string.IsNullOrWhiteSpace(identifier)) throw new RequiredFieldException("identifier is required.");
|
||||
var result = await authService.LoginWithPasswordAsync(
|
||||
new PasswordLoginRequest(
|
||||
realm,
|
||||
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
|
||||
await requestContextResolver.ResolveRealmTenantIdAsync(realm, request.TenantCode, Request,
|
||||
cancellationToken),
|
||||
identifier,
|
||||
request.Password,
|
||||
GetIpAddress(),
|
||||
@@ -116,11 +113,12 @@ public sealed class AuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var realm = request.Realm!.Value;
|
||||
EnsureRouteRealm(realm);
|
||||
requestContextResolver.EnsureRouteRealm(realm, Request);
|
||||
var result = await authService.LoginWithSmsAsync(
|
||||
new SmsLoginRequest(
|
||||
realm,
|
||||
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
|
||||
await requestContextResolver.ResolveRealmTenantIdAsync(realm, request.TenantCode, Request,
|
||||
cancellationToken),
|
||||
request.Phone,
|
||||
request.Code,
|
||||
GetIpAddress(),
|
||||
@@ -142,11 +140,12 @@ public sealed class AuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var realm = request.Realm!.Value;
|
||||
EnsureRouteRealm(realm);
|
||||
requestContextResolver.EnsureRouteRealm(realm, Request);
|
||||
var result = await authService.LoginWithWechatWebAsync(
|
||||
new WechatLoginRequest(
|
||||
realm,
|
||||
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
|
||||
await requestContextResolver.ResolveRealmTenantIdAsync(realm, request.TenantCode, Request,
|
||||
cancellationToken),
|
||||
request.Code,
|
||||
GetIpAddress(),
|
||||
Request.Headers.UserAgent.ToString()),
|
||||
@@ -167,11 +166,12 @@ public sealed class AuthController(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var realm = request.Realm!.Value;
|
||||
EnsureRouteRealm(realm);
|
||||
requestContextResolver.EnsureRouteRealm(realm, Request);
|
||||
var result = await authService.LoginWithWechatMiniAppAsync(
|
||||
new WechatLoginRequest(
|
||||
realm,
|
||||
await ResolveRealmTenantIdAsync(realm, request.TenantCode, cancellationToken),
|
||||
await requestContextResolver.ResolveRealmTenantIdAsync(realm, request.TenantCode, Request,
|
||||
cancellationToken),
|
||||
request.Code,
|
||||
GetIpAddress(),
|
||||
Request.Headers.UserAgent.ToString()),
|
||||
@@ -188,7 +188,7 @@ public sealed class AuthController(
|
||||
[FromBody] RefreshSessionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ResolveRefreshTokenTenant(request.RefreshToken);
|
||||
requestContextResolver.ResolveRefreshTokenTenant(request.RefreshToken, Request);
|
||||
var result = await authService.RefreshAsync(
|
||||
new RefreshSessionRequest(
|
||||
request.RefreshToken,
|
||||
@@ -208,7 +208,7 @@ public sealed class AuthController(
|
||||
[FromBody] RefreshSessionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ResolveRefreshTokenTenant(request.RefreshToken);
|
||||
requestContextResolver.ResolveRefreshTokenTenant(request.RefreshToken, Request);
|
||||
await authService.LogoutAsync(
|
||||
new LogoutSessionRequest(request.RefreshToken),
|
||||
cancellationToken);
|
||||
@@ -238,7 +238,7 @@ public sealed class AuthController(
|
||||
[FromBody] RequiredPasswordChangeDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ResolveAuthChallengeTenant(request.ChallengeToken);
|
||||
requestContextResolver.ResolveAuthChallengeTenant(request.ChallengeToken, Request);
|
||||
var result = await authService.ChangeRequiredPasswordAsync(
|
||||
new PasswordChangeChallengeRequest(
|
||||
request.ChallengeToken, request.NewPassword, GetIpAddress(), Request.Headers.UserAgent.ToString()),
|
||||
@@ -256,7 +256,8 @@ public sealed class AuthController(
|
||||
PasswordResetSmsSendDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = await ResolveRealmTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
|
||||
var tenantId = await requestContextResolver.ResolveRealmTenantIdAsync(AuthRealm.Tenant, request.TenantCode,
|
||||
Request, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
var result = await authService.RequestPasswordResetAsync(
|
||||
new PasswordResetCodeRequest(
|
||||
@@ -278,7 +279,8 @@ public sealed class AuthController(
|
||||
PasswordResetDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tenantId = await ResolveRealmTenantIdAsync(AuthRealm.Tenant, request.TenantCode, cancellationToken)
|
||||
var tenantId = await requestContextResolver.ResolveRealmTenantIdAsync(AuthRealm.Tenant, request.TenantCode,
|
||||
Request, cancellationToken)
|
||||
?? throw new RequiredFieldException("tenantCode is required for password reset.");
|
||||
await authService.ResetPasswordAsync(
|
||||
new PasswordResetRequest(
|
||||
@@ -315,111 +317,9 @@ public sealed class AuthController(
|
||||
return Ok(AuthenticationResultDto.FromApplication(result));
|
||||
}
|
||||
|
||||
private void ResolveRefreshTokenTenant(string refreshToken)
|
||||
{
|
||||
if (!sessionStore.TryParseRefreshToken(refreshToken, out var locator)) return;
|
||||
|
||||
if (locator.Realm == AuthRealm.Platform)
|
||||
{
|
||||
EnsurePlatformHost();
|
||||
if (tenantContext.IsResolved)
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tenantContext.IsResolved)
|
||||
throw new RequiredFieldException(
|
||||
"tenant refresh/logout requires a tenant host or x-tenant-code matching the refresh token.");
|
||||
|
||||
tenantContextInitializer.Initialize(locator.TenantId!.Value, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
private void ResolveAuthChallengeTenant(string challengeToken)
|
||||
{
|
||||
var parts = challengeToken.Split('.', 4);
|
||||
if (parts.Length != 4 || parts[0] != "c1") return;
|
||||
|
||||
if (parts[1] == "p" && parts[2] == "-")
|
||||
{
|
||||
EnsurePlatformHost();
|
||||
if (tenantContext.IsResolved)
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parts[1] != "t" || !Guid.TryParseExact(parts[2], "N", out var tenantId) || !tenantContext.IsResolved)
|
||||
throw new RequiredFieldException(
|
||||
"tenant authentication challenge requires a tenant host or x-tenant-code.");
|
||||
|
||||
tenantContextInitializer.Initialize(tenantId, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
private string? GetIpAddress()
|
||||
{
|
||||
return HttpContext.Connection.RemoteIpAddress?.ToString();
|
||||
}
|
||||
|
||||
private async Task<Guid?> ResolveRealmTenantIdAsync(
|
||||
AuthRealm realm,
|
||||
string? tenantCode,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (realm == AuthRealm.Platform)
|
||||
{
|
||||
EnsurePlatformHost();
|
||||
if (tenantContext.IsResolved || !string.IsNullOrWhiteSpace(tenantCode))
|
||||
throw new RequiredFieldException(
|
||||
"platform realm does not accept tenantCode and must use a platform host.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tenantContext.TenantId.HasValue)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode) &&
|
||||
!string.Equals(tenantContext.TenantCode, tenantCode.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var supplied = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken);
|
||||
if (supplied?.TenantId != tenantContext.TenantId.Value)
|
||||
throw new TenantContextConflictException(
|
||||
tenantContext.TenantId.Value,
|
||||
supplied?.TenantId ?? Guid.Empty);
|
||||
}
|
||||
|
||||
return tenantContext.TenantId.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
throw new RequiredFieldException("tenantCode is required when the request host does not resolve a tenant.");
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantContextInitializer.Initialize(
|
||||
tenant.TenantId,
|
||||
tenant.TenantCode,
|
||||
TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
|
||||
private void EnsurePlatformHost()
|
||||
{
|
||||
var requestHost = Request.Host.Host.Trim().TrimEnd('.');
|
||||
if (!tenantResolutionOptions.Value.PlatformHosts.Any(host =>
|
||||
string.Equals(
|
||||
host.Trim().TrimEnd('.'),
|
||||
requestHost,
|
||||
StringComparison.OrdinalIgnoreCase)))
|
||||
throw new RequiredFieldException("platform realm is only available on a configured platform host.");
|
||||
}
|
||||
|
||||
private void EnsureRouteRealm(AuthRealm realm)
|
||||
{
|
||||
var path = Request.Path.Value ?? string.Empty;
|
||||
var expectedRealm = path.StartsWith("/api/platform/auth/", StringComparison.OrdinalIgnoreCase)
|
||||
? AuthRealm.Platform
|
||||
: AuthRealm.Tenant;
|
||||
if (realm != expectedRealm)
|
||||
throw new RequiredFieldException(
|
||||
$"{realm.ToString().ToLowerInvariant()} realm must use the {expectedRealm.ToString().ToLowerInvariant()} authentication route.");
|
||||
}
|
||||
}
|
||||
|
||||
121
Tiku.Api/Controllers/AuthRequestContextResolver.cs
Normal file
121
Tiku.Api/Controllers/AuthRequestContextResolver.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Api.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Tenancy;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
public sealed class AuthRequestContextResolver(
|
||||
IAuthSessionStore sessionStore,
|
||||
ITenantContext tenantContext,
|
||||
ITenantContextInitializer tenantContextInitializer,
|
||||
ITenantDirectory tenantDirectory,
|
||||
IOptions<TenantResolutionOptions> tenantResolutionOptions)
|
||||
{
|
||||
internal void ResolveRefreshTokenTenant(string refreshToken, HttpRequest request)
|
||||
{
|
||||
if (!sessionStore.TryParseRefreshToken(refreshToken, out var locator)) return;
|
||||
|
||||
if (locator.Realm == AuthRealm.Platform)
|
||||
{
|
||||
EnsurePlatformHost(request);
|
||||
if (tenantContext.IsResolved)
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tenantContext.IsResolved)
|
||||
throw new RequiredFieldException(
|
||||
"tenant refresh/logout requires a tenant host or x-tenant-code matching the refresh token.");
|
||||
|
||||
tenantContextInitializer.Initialize(locator.TenantId!.Value, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
internal void ResolveAuthChallengeTenant(string challengeToken, HttpRequest request)
|
||||
{
|
||||
var parts = challengeToken.Split('.', 4);
|
||||
if (parts.Length != 4 || parts[0] != "c1") return;
|
||||
|
||||
if (parts[1] == "p" && parts[2] == "-")
|
||||
{
|
||||
EnsurePlatformHost(request);
|
||||
if (tenantContext.IsResolved)
|
||||
throw new TenantContextConflictException(tenantContext.TenantId!.Value, Guid.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parts[1] != "t" || !Guid.TryParseExact(parts[2], "N", out var tenantId) || !tenantContext.IsResolved)
|
||||
throw new RequiredFieldException(
|
||||
"tenant authentication challenge requires a tenant host or x-tenant-code.");
|
||||
|
||||
tenantContextInitializer.Initialize(tenantId, null, TenantResolutionSource.RefreshToken);
|
||||
}
|
||||
|
||||
internal async Task<Guid?> ResolveRealmTenantIdAsync(
|
||||
AuthRealm realm,
|
||||
string? tenantCode,
|
||||
HttpRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (realm == AuthRealm.Platform)
|
||||
{
|
||||
EnsurePlatformHost(request);
|
||||
if (tenantContext.IsResolved || !string.IsNullOrWhiteSpace(tenantCode))
|
||||
throw new RequiredFieldException(
|
||||
"platform realm does not accept tenantCode and must use a platform host.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tenantContext.TenantId.HasValue)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(tenantCode) &&
|
||||
!string.Equals(tenantContext.TenantCode, tenantCode.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var supplied = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken);
|
||||
if (supplied?.TenantId != tenantContext.TenantId.Value)
|
||||
throw new TenantContextConflictException(
|
||||
tenantContext.TenantId.Value,
|
||||
supplied?.TenantId ?? Guid.Empty);
|
||||
}
|
||||
|
||||
return tenantContext.TenantId.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tenantCode))
|
||||
throw new RequiredFieldException("tenantCode is required when the request host does not resolve a tenant.");
|
||||
|
||||
var tenant = await tenantDirectory.FindByCodeAsync(tenantCode.Trim(), cancellationToken)
|
||||
?? throw new TenantNotFoundException();
|
||||
tenantContextInitializer.Initialize(
|
||||
tenant.TenantId,
|
||||
tenant.TenantCode,
|
||||
TenantResolutionSource.TenantCode);
|
||||
return tenant.TenantId;
|
||||
}
|
||||
|
||||
internal void EnsureRouteRealm(AuthRealm realm, HttpRequest request)
|
||||
{
|
||||
var path = request.Path.Value ?? string.Empty;
|
||||
var expectedRealm = path.StartsWith("/api/platform/auth/", StringComparison.OrdinalIgnoreCase)
|
||||
? AuthRealm.Platform
|
||||
: AuthRealm.Tenant;
|
||||
if (realm != expectedRealm)
|
||||
throw new RequiredFieldException(
|
||||
$"{realm.ToString().ToLowerInvariant()} realm must use the {expectedRealm.ToString().ToLowerInvariant()} authentication route.");
|
||||
}
|
||||
|
||||
private void EnsurePlatformHost(HttpRequest request)
|
||||
{
|
||||
var requestHost = request.Host.Host.Trim().TrimEnd('.');
|
||||
if (!tenantResolutionOptions.Value.PlatformHosts.Any(host =>
|
||||
string.Equals(
|
||||
host.Trim().TrimEnd('.'),
|
||||
requestHost,
|
||||
StringComparison.OrdinalIgnoreCase)))
|
||||
throw new RequiredFieldException("platform realm is only available on a configured platform host.");
|
||||
}
|
||||
}
|
||||
@@ -69,11 +69,106 @@ public sealed class ArchitectureBoundaryTests
|
||||
: $"{item.Service} ({item.Lines} lines across {item.Files} files; hard limit 800)")
|
||||
.ToArray();
|
||||
|
||||
foreach (var warning in serviceGroups.Where(item =>
|
||||
!legacyLineBudgets.ContainsKey(item.Service) && item.Lines is > 500 and <= 800))
|
||||
Console.WriteLine(
|
||||
$"Architecture warning: {warning.Service} has {warning.Lines} lines across {warning.Files} files.");
|
||||
|
||||
Assert.True(
|
||||
violations.Length == 0,
|
||||
$"Service classes exceeded the hard limit or grew legacy debt:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Capability_contracts_and_services_remain_focused()
|
||||
{
|
||||
var contracts = CapabilityContracts();
|
||||
var contractViolations = contracts
|
||||
.Where(contract => contract.GetMethods().Length > 10)
|
||||
.Select(contract => $"{contract.FullName} ({contract.GetMethods().Length} public methods)")
|
||||
.ToArray();
|
||||
|
||||
var implementations = typeof(Tiku.Infrastructure.Persistence.TikuDbContext).Assembly
|
||||
.GetTypes()
|
||||
.Where(type => !type.IsAbstract && contracts.Any(contract => contract.IsAssignableFrom(type)))
|
||||
.ToArray();
|
||||
var methodViolations = implementations
|
||||
.Select(type => new
|
||||
{
|
||||
Type = type,
|
||||
Count = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly)
|
||||
.Count(method => !method.IsSpecialName)
|
||||
})
|
||||
.Where(item => item.Count > 10)
|
||||
.Select(item => $"{item.Type.FullName} ({item.Count} public methods)")
|
||||
.ToArray();
|
||||
var dependencyCounts = implementations
|
||||
.Select(type => new
|
||||
{
|
||||
Type = type,
|
||||
Count = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.Select(constructor => constructor.GetParameters().Length)
|
||||
.DefaultIfEmpty(0)
|
||||
.Max()
|
||||
})
|
||||
.ToArray();
|
||||
foreach (var warning in dependencyCounts.Where(item => item.Count is > 6 and <= 8))
|
||||
Console.WriteLine($"Architecture warning: {warning.Type.FullName} has {warning.Count} constructor dependencies.");
|
||||
var dependencyViolations = dependencyCounts
|
||||
.Where(item => item.Count > 8)
|
||||
.Select(item => $"{item.Type.FullName} ({item.Count} constructor dependencies)")
|
||||
.ToArray();
|
||||
|
||||
Assert.True(contractViolations.Length == 0,
|
||||
$"Capability interfaces exceeded 10 public methods:{Environment.NewLine}{string.Join(Environment.NewLine, contractViolations)}");
|
||||
Assert.True(methodViolations.Length == 0,
|
||||
$"Capability services exceeded 10 public methods:{Environment.NewLine}{string.Join(Environment.NewLine, methodViolations)}");
|
||||
Assert.True(dependencyViolations.Length == 0,
|
||||
$"Capability services exceeded 8 constructor dependencies:{Environment.NewLine}{string.Join(Environment.NewLine, dependencyViolations)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Controllers_remain_focused_and_legacy_facades_do_not_return()
|
||||
{
|
||||
var controllerViolations = typeof(AuthController).Assembly.GetTypes()
|
||||
.Where(type => !type.IsAbstract && typeof(ControllerBase).IsAssignableFrom(type))
|
||||
.Select(type => new
|
||||
{
|
||||
Type = type,
|
||||
Count = type.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.Select(constructor => constructor.GetParameters().Length)
|
||||
.DefaultIfEmpty(0)
|
||||
.Max()
|
||||
})
|
||||
.Where(item => item.Count > 8)
|
||||
.Select(item => $"{item.Type.FullName} ({item.Count} constructor dependencies)")
|
||||
.ToArray();
|
||||
var legacyNames = new[]
|
||||
{
|
||||
"ITenantAdminDirectService",
|
||||
"TenantAdminDirectService",
|
||||
"IDirectContentService",
|
||||
"DirectContentService",
|
||||
"ICommerceAdminService",
|
||||
"CommerceAdminService",
|
||||
"ILearningActivityService",
|
||||
"LearningActivityService",
|
||||
"IPlatformAdminService",
|
||||
"PlatformAdminService"
|
||||
};
|
||||
var applicationTypes = typeof(Tiku.Application.TenantAdmin.ITenantAdminDashboardService).Assembly.GetTypes();
|
||||
var infrastructureTypes = typeof(Tiku.Infrastructure.Persistence.TikuDbContext).Assembly.GetTypes();
|
||||
var legacyViolations = applicationTypes.Concat(infrastructureTypes)
|
||||
.Where(type => legacyNames.Contains(type.Name, StringComparer.Ordinal))
|
||||
.Select(type => type.FullName)
|
||||
.ToArray();
|
||||
|
||||
Assert.True(controllerViolations.Length == 0,
|
||||
$"Controllers exceeded 8 constructor dependencies:{Environment.NewLine}{string.Join(Environment.NewLine, controllerViolations)}");
|
||||
Assert.True(legacyViolations.Length == 0,
|
||||
$"Legacy aggregate services returned:{Environment.NewLine}{string.Join(Environment.NewLine, legacyViolations)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Background_jobs_are_dispatched_by_module_registered_handlers()
|
||||
{
|
||||
@@ -403,6 +498,50 @@ public sealed class ArchitectureBoundaryTests
|
||||
$"Business-layer third-party provider SDK references were found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
|
||||
}
|
||||
|
||||
private static Type[] CapabilityContracts()
|
||||
{
|
||||
return
|
||||
[
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantAdminDashboardService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantClassService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantStudentService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantSupervisionService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantStudentEngagementService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantMemberAccessService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantSiteSettingsService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantDomainProviderService),
|
||||
typeof(Tiku.Application.TenantAdmin.ITenantEngagementService),
|
||||
typeof(Tiku.Application.Content.IQuestionManagementService),
|
||||
typeof(Tiku.Application.Content.IVocabularyManagementService),
|
||||
typeof(Tiku.Application.Content.IHandbookManagementService),
|
||||
typeof(Tiku.Application.Content.IEducationCatalogManagementService),
|
||||
typeof(Tiku.Application.Content.IScorelineManagementService),
|
||||
typeof(Tiku.Application.Content.IVideoManagementService),
|
||||
typeof(Tiku.Application.Content.IOperationContentManagementService),
|
||||
typeof(Tiku.Application.Content.IContentImportService),
|
||||
typeof(Tiku.Application.Commerce.IPaymentConfigurationService),
|
||||
typeof(Tiku.Application.Commerce.ICommerceOrderAdministrationService),
|
||||
typeof(Tiku.Application.Commerce.IActivationCodeAdministrationService),
|
||||
typeof(Tiku.Application.Commerce.ICouponAdministrationService),
|
||||
typeof(Tiku.Application.Commerce.IRefundAdministrationService),
|
||||
typeof(Tiku.Application.Commerce.IReconciliationAdministrationService),
|
||||
typeof(Tiku.Application.Commerce.ICommerceAdjustmentService),
|
||||
typeof(Tiku.Application.Points.IPointAdministrationService),
|
||||
typeof(Tiku.Application.Learning.ILearningAnalyticsService),
|
||||
typeof(Tiku.Application.Learning.IAnsweringService),
|
||||
typeof(Tiku.Application.Learning.IQuestionReviewService),
|
||||
typeof(Tiku.Application.Learning.IWordLearningService),
|
||||
typeof(Tiku.Application.Learning.IPracticeSessionService),
|
||||
typeof(Tiku.Application.Learning.IPracticeReportService),
|
||||
typeof(Tiku.Application.PlatformAdmin.IPlatformDashboardService),
|
||||
typeof(Tiku.Application.PlatformAdmin.ITenantProvisioningAdministrationService),
|
||||
typeof(Tiku.Application.PlatformAdmin.IPlatformTenantDomainService),
|
||||
typeof(Tiku.Application.PlatformAdmin.IPlatformStaffAccessService),
|
||||
typeof(Tiku.Application.PlatformAdmin.IPlatformAuditAlertService),
|
||||
typeof(Tiku.Application.PlatformAdmin.IPlatformDunningService)
|
||||
];
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
||||
Reference in New Issue
Block a user