forked from gongxuegit/tiku-backend.net
feat: modularize external providers
This commit is contained in:
@@ -2,11 +2,13 @@ using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Domain.Identity;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Auth;
|
||||
using Tiku.Infrastructure.Commerce;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.IntegrationTests.Api;
|
||||
@@ -166,6 +168,11 @@ public sealed class AuthEndpointTests
|
||||
{
|
||||
await using var factory = new ApiTestFactory(new FakeWechatOAuthClient());
|
||||
var tenantId = Guid.NewGuid();
|
||||
var secretRef = "tenant_secrets:identity:wechat_miniapp:default";
|
||||
var protectedSecret = ProtectTenantSecret(
|
||||
tenantId,
|
||||
secretRef,
|
||||
JsonSerializer.SerializeToElement(new { appSecret = "wx-app-secret" }));
|
||||
await factory.SeedAsync(
|
||||
new Tenant
|
||||
{
|
||||
@@ -173,16 +180,30 @@ public sealed class AuthEndpointTests
|
||||
Slug = tenantId.ToString("N"),
|
||||
Name = "Wechat Tenant"
|
||||
},
|
||||
new TenantAuthProvider
|
||||
new TenantExternalProvider
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Provider = "wechat-miniapp",
|
||||
Status = TenantAuthProviderStatus.Testing,
|
||||
Provider = "wechat_miniapp",
|
||||
Capability = TenantExternalProviderCapability.Identity,
|
||||
Status = TenantExternalProviderStatus.Active,
|
||||
SecretRef = secretRef,
|
||||
ConfigPublic = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
appId = "wx-app-id",
|
||||
appSecret = "wx-app-secret"
|
||||
appId = "wx-app-id"
|
||||
})
|
||||
},
|
||||
new TenantSecret
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Purpose = "identity",
|
||||
Provider = "wechat_miniapp",
|
||||
SecretKey = "default",
|
||||
SecretRef = secretRef,
|
||||
Status = TenantSecretStatus.Active,
|
||||
EncryptionKeyId = protectedSecret.KeyId,
|
||||
EncryptedPayload = protectedSecret.Ciphertext,
|
||||
EncryptionNonce = protectedSecret.Nonce,
|
||||
EncryptionTag = protectedSecret.Tag
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
@@ -207,7 +228,7 @@ public sealed class AuthEndpointTests
|
||||
Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
|
||||
Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode);
|
||||
Assert.Contains(dbContext.UserIdentities, identity =>
|
||||
identity.Provider == "wechat-miniapp" &&
|
||||
identity.Provider == "wechat_miniapp" &&
|
||||
identity.OpenId == "mini-open-id" &&
|
||||
identity.UnionId == "union-id");
|
||||
}
|
||||
@@ -285,6 +306,16 @@ public sealed class AuthEndpointTests
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
|
||||
private static ProtectedTenantSecret ProtectTenantSecret(Guid tenantId, string secretRef, JsonElement payload)
|
||||
{
|
||||
var protector = new TenantSecretProtector(Options.Create(new TenantSecretEncryptionOptions
|
||||
{
|
||||
KeyId = "development-v1",
|
||||
MasterKey = TenantSecretEncryptionOptions.DevelopmentMasterKey
|
||||
}));
|
||||
return protector.Protect(tenantId, secretRef, payload);
|
||||
}
|
||||
|
||||
private sealed class FakeWechatOAuthClient : IWechatOAuthClient
|
||||
{
|
||||
public Task<WechatIdentity> ExchangeWebCodeAsync(
|
||||
|
||||
@@ -375,11 +375,13 @@ public sealed class CommerceEndpointTests
|
||||
Days = 39,
|
||||
IsActive = true
|
||||
},
|
||||
new TenantPaymentAccount
|
||||
new TenantExternalProvider
|
||||
{
|
||||
TenantId = tenantId,
|
||||
Capability = TenantExternalProviderCapability.Payment,
|
||||
Provider = "manual",
|
||||
Status = TenantPaymentAccountStatus.Active
|
||||
Status = TenantExternalProviderStatus.Active,
|
||||
ConfigPublic = JsonSerializer.SerializeToElement(new { mode = "TenantCollect" })
|
||||
}
|
||||
};
|
||||
if (includeMembership)
|
||||
|
||||
@@ -217,7 +217,7 @@ public sealed class TenantAdminDirectEndpointTests
|
||||
});
|
||||
var authProviderResponse = await client.PutAsJsonAsync(
|
||||
"/api/tenant-admin/auth-providers",
|
||||
new UpsertTenantAuthProviderDto
|
||||
new UpsertTenantIdentityProviderDto
|
||||
{
|
||||
Provider = "wechat_mp",
|
||||
Status = "active",
|
||||
@@ -244,7 +244,10 @@ public sealed class TenantAdminDirectEndpointTests
|
||||
Assert.True(await dbContext.TenantBrandings.AnyAsync(item => item.TenantId == seed.TenantId && item.BrandName == "机构题库"));
|
||||
Assert.True(await dbContext.TenantThemeConfigs.AnyAsync(item => item.TenantId == seed.TenantId && item.ActiveTemplateCode == "classic"));
|
||||
Assert.True(await dbContext.TenantDomains.AnyAsync(item => item.TenantId == seed.TenantId && item.Host == "learn.example.com" && item.IsPrimary));
|
||||
Assert.True(await dbContext.TenantAuthProviders.AnyAsync(item => item.TenantId == seed.TenantId && item.Provider == "wechat_mp"));
|
||||
Assert.True(await dbContext.TenantExternalProviders.AnyAsync(item =>
|
||||
item.TenantId == seed.TenantId &&
|
||||
item.Capability == TenantExternalProviderCapability.Identity &&
|
||||
item.Provider == "wechat_mp"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -42,7 +42,7 @@ public sealed class TenantCommerceEndpointTests
|
||||
new UpsertPaymentAccountDto
|
||||
{
|
||||
Provider = "wechat_pay",
|
||||
Status = TenantPaymentAccountStatus.Active,
|
||||
Status = TenantExternalProviderStatus.Active,
|
||||
ConfigPublic = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
merchantId = "mch",
|
||||
@@ -80,11 +80,11 @@ public sealed class TenantCommerceEndpointTests
|
||||
new UpsertPaymentAccountDto
|
||||
{
|
||||
Provider = "wechat_pay",
|
||||
Status = TenantPaymentAccountStatus.Active,
|
||||
Status = TenantExternalProviderStatus.Active,
|
||||
SecretRef = "tenant_secrets:payment:wechat_pay:default",
|
||||
ConfigPublic = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
merchantId = "mch",
|
||||
secretRef = "tenant_secrets:payment:wechat_pay:default"
|
||||
merchantId = "mch"
|
||||
})
|
||||
});
|
||||
var accountsResponse = await client.GetAsync("/api/tenant-commerce/payment-accounts?provider=wechat_pay");
|
||||
|
||||
@@ -39,6 +39,46 @@ public sealed class ArchitectureBoundaryTests
|
||||
$"Forbidden tenant-boundary bypasses were found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Business_code_does_not_depend_on_supabase_or_removed_provider_models()
|
||||
{
|
||||
var root = FindRepositoryRoot();
|
||||
var sourceRoots = new[] { "Tiku.Api", "Tiku.Application", "Tiku.Domain", "Tiku.Infrastructure" };
|
||||
var forbidden = new[]
|
||||
{
|
||||
"Supabase",
|
||||
"supabase_storage",
|
||||
"SUPABASE_",
|
||||
"TenantAuthProvider",
|
||||
"TenantPaymentAccount",
|
||||
"TenantPaymentMode",
|
||||
"TenantPaymentAccountStatus",
|
||||
"TenantAuthProviderStatus"
|
||||
};
|
||||
|
||||
var allowedPathFragments = new[]
|
||||
{
|
||||
$"{Path.DirectorySeparatorChar}Persistence{Path.DirectorySeparatorChar}Migrations{Path.DirectorySeparatorChar}"
|
||||
};
|
||||
|
||||
var violations = sourceRoots
|
||||
.SelectMany(directory => Directory.EnumerateFiles(
|
||||
Path.Combine(root, directory),
|
||||
"*.cs",
|
||||
SearchOption.AllDirectories))
|
||||
.Where(path => !allowedPathFragments.Any(fragment => path.Contains(fragment, StringComparison.Ordinal)))
|
||||
.SelectMany(path => File.ReadLines(path)
|
||||
.Select((line, index) => new { path, line, lineNumber = index + 1 }))
|
||||
.Where(candidate => forbidden.Any(symbol =>
|
||||
candidate.line.Contains(symbol, StringComparison.Ordinal)))
|
||||
.Select(candidate => $"{Path.GetRelativePath(root, candidate.path)}:{candidate.lineNumber}")
|
||||
.ToArray();
|
||||
|
||||
Assert.True(
|
||||
violations.Length == 0,
|
||||
$"Forbidden external-provider coupling was found:{Environment.NewLine}{string.Join(Environment.NewLine, violations)}");
|
||||
}
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
||||
@@ -68,7 +68,7 @@ public sealed class PersistenceModelTests
|
||||
Assert.Contains("practice_session_report_sections", tableNames);
|
||||
Assert.Contains("dashboard_daily_stats", tableNames);
|
||||
Assert.Contains("revenue_daily_stats", tableNames);
|
||||
Assert.Contains("tenant_auth_providers", tableNames);
|
||||
Assert.Contains("tenant_external_providers", tableNames);
|
||||
Assert.Contains("tenant_secrets", tableNames);
|
||||
Assert.Contains("sms_verification_codes", tableNames);
|
||||
Assert.Contains("auth_login_events", tableNames);
|
||||
@@ -90,7 +90,6 @@ public sealed class PersistenceModelTests
|
||||
Assert.Contains("activation_codes", tableNames);
|
||||
Assert.Contains("coupons", tableNames);
|
||||
Assert.Contains("coupon_redemptions", tableNames);
|
||||
Assert.Contains("tenant_payment_accounts", tableNames);
|
||||
Assert.Contains("tenant_subscriptions", tableNames);
|
||||
Assert.Contains("tenant_usage_records", tableNames);
|
||||
Assert.Contains("commerce_refund_requests", tableNames);
|
||||
@@ -506,7 +505,8 @@ public sealed class PersistenceModelTests
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(TenantAuthProvider), nameof(TenantAuthProvider.ConfigPublic), "'{}'::jsonb")]
|
||||
[InlineData(typeof(TenantExternalProvider), nameof(TenantExternalProvider.ConfigPublic), "'{}'::jsonb")]
|
||||
[InlineData(typeof(TenantExternalProvider), nameof(TenantExternalProvider.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(SmsVerificationCode), nameof(SmsVerificationCode.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(AuthLoginEvent), nameof(AuthLoginEvent.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(AuthSession), nameof(AuthSession.Metadata), "'{}'::jsonb")]
|
||||
@@ -536,9 +536,10 @@ public sealed class PersistenceModelTests
|
||||
{
|
||||
using var context = new TikuDbContext(Options);
|
||||
|
||||
AssertHasUniqueIndex<TenantAuthProvider>(
|
||||
nameof(TenantAuthProvider.TenantId),
|
||||
nameof(TenantAuthProvider.Provider));
|
||||
AssertHasUniqueIndex<TenantExternalProvider>(
|
||||
nameof(TenantExternalProvider.TenantId),
|
||||
nameof(TenantExternalProvider.Capability),
|
||||
nameof(TenantExternalProvider.Provider));
|
||||
AssertHasUniqueIndex<TenantSecret>(
|
||||
nameof(TenantSecret.TenantId),
|
||||
nameof(TenantSecret.SecretRef));
|
||||
@@ -574,7 +575,6 @@ public sealed class PersistenceModelTests
|
||||
[InlineData(typeof(Payment), nameof(Payment.RawPayload), "'{}'::jsonb")]
|
||||
[InlineData(typeof(PaymentEvent), nameof(PaymentEvent.Payload), "'{}'::jsonb")]
|
||||
[InlineData(typeof(Entitlement), nameof(Entitlement.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(TenantPaymentAccount), nameof(TenantPaymentAccount.ConfigPublic), "'{}'::jsonb")]
|
||||
[InlineData(typeof(TenantSubscription), nameof(TenantSubscription.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(TenantUsageRecord), nameof(TenantUsageRecord.Metadata), "'{}'::jsonb")]
|
||||
[InlineData(typeof(CommerceRefundRequest), nameof(CommerceRefundRequest.Metadata), "'{}'::jsonb")]
|
||||
@@ -622,9 +622,6 @@ public sealed class PersistenceModelTests
|
||||
|
||||
AssertHasUniqueIndex<Order>(nameof(Order.TenantId), nameof(Order.OrderNo));
|
||||
AssertHasUniqueIndex<ActivationCode>(nameof(ActivationCode.TenantId), nameof(ActivationCode.Code));
|
||||
AssertHasUniqueIndex<TenantPaymentAccount>(
|
||||
nameof(TenantPaymentAccount.TenantId),
|
||||
nameof(TenantPaymentAccount.Provider));
|
||||
AssertHasUniqueIndex<CommerceRefundRequest>(
|
||||
nameof(CommerceRefundRequest.TenantId),
|
||||
nameof(CommerceRefundRequest.RefundNo));
|
||||
|
||||
Reference in New Issue
Block a user