feat: modularize external providers

This commit is contained in:
2026-07-27 17:47:21 +08:00
parent db4c7b4496
commit 70b99e6063
47 changed files with 1219 additions and 667 deletions

View File

@@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Auth;
@@ -182,7 +183,7 @@ public sealed class AuthServiceTests
Assert.Equal(tenantId, result.Tenant.TenantId);
Assert.Single(context.Users);
Assert.Contains(context.UserIdentities, identity =>
identity.Provider == "wechat-miniapp" &&
identity.Provider == "wechat_miniapp" &&
identity.ProviderSubject == "wx-app-id:mini-open-id" &&
identity.OpenId == "mini-open-id" &&
identity.UnionId == "union-id");
@@ -198,11 +199,13 @@ public sealed class AuthServiceTests
{
await using var context = CreateContext();
var tenantId = await SeedTenantWithWechatProviderAsync(context, "wechat-miniapp");
context.TenantAuthProviders.Add(new TenantAuthProvider
context.TenantExternalProviders.Add(new TenantExternalProvider
{
TenantId = tenantId,
Provider = "wechat_web",
Status = TenantAuthProviderStatus.Testing,
Capability = TenantExternalProviderCapability.Identity,
Status = TenantExternalProviderStatus.Active,
SecretRef = "tenant_secrets:identity:wechat_web:default",
ConfigPublic = WechatProviderConfig()
});
var user = new User
@@ -257,14 +260,15 @@ public sealed class AuthServiceTests
{
var tokenService = new TokenService(Options.Create(JwtOptions));
var sessionService = new SessionService(context, tokenService, Options.Create(JwtOptions));
var smsService = new SmsVerificationService(context);
var smsService = new SmsVerificationService(context, new FakeSmsProvider());
return new AuthService(
context,
new PasswordHasher(),
smsService,
sessionService,
wechatOAuthClient ?? new FakeWechatOAuthClient());
wechatOAuthClient ?? new FakeWechatOAuthClient(),
new FakeProviderConfigService(context));
}
private static async Task<Guid> SeedTenantWithWechatProviderAsync(
@@ -278,11 +282,13 @@ public sealed class AuthServiceTests
Name = "Wechat Tenant"
};
context.Tenants.Add(tenant);
context.TenantAuthProviders.Add(new TenantAuthProvider
context.TenantExternalProviders.Add(new TenantExternalProvider
{
TenantId = tenant.Id,
Provider = provider,
Status = TenantAuthProviderStatus.Testing,
Provider = provider.Replace("-", "_", StringComparison.Ordinal),
Capability = TenantExternalProviderCapability.Identity,
Status = TenantExternalProviderStatus.Active,
SecretRef = $"tenant_secrets:identity:{provider}:default",
ConfigPublic = WechatProviderConfig()
});
await context.SaveChangesAsync();
@@ -294,8 +300,7 @@ public sealed class AuthServiceTests
{
return JsonSerializer.SerializeToElement(new
{
appId = "wx-app-id",
appSecret = "wx-app-secret"
appId = "wx-app-id"
});
}
@@ -347,6 +352,64 @@ public sealed class AuthServiceTests
return document.RootElement.Clone();
}
private sealed class FakeProviderConfigService(TikuDbContext context) : ITenantExternalProviderConfigService
{
public Task<TenantExternalProviderAccount> GetActiveProviderAsync(
Guid tenantId,
TenantExternalProviderCapability capability,
string? provider = null,
CancellationToken cancellationToken = default)
{
var normalizedProvider = provider?.Replace("-", "_", StringComparison.Ordinal);
var item = context.TenantExternalProviders
.AsEnumerable()
.Where(entity =>
entity.TenantId == tenantId &&
entity.Capability == capability &&
entity.Status == TenantExternalProviderStatus.Active)
.Where(entity => string.IsNullOrWhiteSpace(normalizedProvider) || entity.Provider == normalizedProvider)
.OrderBy(entity => entity.Priority)
.FirstOrDefault()
?? throw new TenantExternalProviderException(
"Tenant external provider is not configured.",
"tenant_external_provider_not_configured");
return Task.FromResult(new TenantExternalProviderAccount(
item.TenantId,
item.Capability,
item.Provider,
item.Status,
item.DisplayName,
item.ConfigPublic,
JsonSerializer.SerializeToElement(new { appSecret = "wx-app-secret" }),
item.SecretRef,
item.Priority,
item.Metadata));
}
public Task<IReadOnlyCollection<TenantExternalProviderItem>> GetProvidersAsync(
Guid tenantId,
TenantExternalProviderCapability? capability = null,
string? provider = null,
int? limit = null,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<TenantExternalProviderItem> UpsertProviderAsync(
Guid tenantId,
UpsertTenantExternalProviderCommand command,
CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
}
private sealed class FakeSmsProvider : ISmsProvider
{
public Task<SmsProviderSendResult> SendAsync(
SmsProviderSendRequest request,
CancellationToken cancellationToken = default) =>
Task.FromResult(new SmsProviderSendResult("fake", "accepted"));
}
private sealed class FakeWechatOAuthClient(
WechatIdentity? WebIdentity = null,
WechatIdentity? MiniAppIdentity = null) : IWechatOAuthClient