feat: add wechat authentication

This commit is contained in:
xiong
2026-07-26 13:12:53 +08:00
parent 241b9f777e
commit 70e2503d18
15 changed files with 722 additions and 7 deletions

View File

@@ -152,9 +152,94 @@ public sealed class AuthServiceTests
await Assert.ThrowsAsync<SessionRevokedException>(() =>
service.RefreshAsync(new RefreshSessionRequest(
login.Tokens.RefreshToken,
null,
null)));
login.Tokens.RefreshToken,
null,
null)));
}
[Fact]
public async Task Wechat_miniapp_login_creates_user_identity_membership_and_session()
{
await using var context = CreateContext();
var tenantId = await SeedTenantWithWechatProviderAsync(context, "wechat-miniapp");
var service = CreateAuthService(
context,
new FakeWechatOAuthClient(
MiniAppIdentity: new WechatIdentity(
"mini-open-id",
"union-id",
null,
null,
"session-key",
"""{"openid":"mini-open-id","unionid":"union-id","session_key":"session-key"}""")));
var result = await service.LoginWithWechatMiniAppAsync(new WechatLoginRequest(
tenantId,
"wx-code",
null,
null));
Assert.Equal(tenantId, result.Tenant.TenantId);
Assert.Single(context.Users);
Assert.Contains(context.UserIdentities, identity =>
identity.Provider == "wechat-miniapp" &&
identity.ProviderSubject == "wx-app-id:mini-open-id" &&
identity.OpenId == "mini-open-id" &&
identity.UnionId == "union-id");
Assert.Contains(context.TenantMemberships, membership =>
membership.TenantId == tenantId &&
membership.UserId == result.UserId &&
membership.Status == MembershipStatus.Active);
Assert.Single(context.AuthSessions);
}
[Fact]
public async Task Wechat_union_id_reuses_existing_user_across_providers()
{
await using var context = CreateContext();
var tenantId = await SeedTenantWithWechatProviderAsync(context, "wechat-miniapp");
context.TenantAuthProviders.Add(new TenantAuthProvider
{
TenantId = tenantId,
Provider = "wechat_web",
Status = TenantAuthProviderStatus.Testing,
ConfigPublic = WechatProviderConfig()
});
var user = new User
{
Id = Guid.NewGuid(),
Name = "Existing"
};
context.Users.Add(user);
context.UserIdentities.Add(new UserIdentity
{
UserId = user.Id,
Provider = "wechat_web",
ProviderSubject = "wx-app-id:web-open-id",
OpenId = "web-open-id",
UnionId = "same-union"
});
await context.SaveChangesAsync();
var service = CreateAuthService(
context,
new FakeWechatOAuthClient(
MiniAppIdentity: new WechatIdentity(
"mini-open-id",
"same-union",
null,
null,
"session-key",
"""{"openid":"mini-open-id","unionid":"same-union","session_key":"session-key"}""")));
var result = await service.LoginWithWechatMiniAppAsync(new WechatLoginRequest(
tenantId,
"wx-code",
null,
null));
Assert.Equal(user.Id, result.UserId);
Assert.Single(context.Users);
Assert.Equal(2, context.UserIdentities.Count());
}
private static TikuDbContext CreateContext()
@@ -166,7 +251,9 @@ public sealed class AuthServiceTests
return new TikuDbContext(options);
}
private static IAuthService CreateAuthService(TikuDbContext context)
private static IAuthService CreateAuthService(
TikuDbContext context,
IWechatOAuthClient? wechatOAuthClient = null)
{
var tokenService = new TokenService(Options.Create(JwtOptions));
var sessionService = new SessionService(context, tokenService, Options.Create(JwtOptions));
@@ -176,7 +263,40 @@ public sealed class AuthServiceTests
context,
new PasswordHasher(),
smsService,
sessionService);
sessionService,
wechatOAuthClient ?? new FakeWechatOAuthClient());
}
private static async Task<Guid> SeedTenantWithWechatProviderAsync(
TikuDbContext context,
string provider)
{
var tenant = new Tenant
{
Id = Guid.NewGuid(),
Slug = Guid.NewGuid().ToString("N"),
Name = "Wechat Tenant"
};
context.Tenants.Add(tenant);
context.TenantAuthProviders.Add(new TenantAuthProvider
{
TenantId = tenant.Id,
Provider = provider,
Status = TenantAuthProviderStatus.Testing,
ConfigPublic = WechatProviderConfig()
});
await context.SaveChangesAsync();
return tenant.Id;
}
private static JsonElement WechatProviderConfig()
{
return JsonSerializer.SerializeToElement(new
{
appId = "wx-app-id",
appSecret = "wx-app-secret"
});
}
private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedUserAsync(
@@ -226,4 +346,37 @@ public sealed class AuthServiceTests
$$"""{"passwordHash":{{JsonSerializer.Serialize(passwordHash)}}}""");
return document.RootElement.Clone();
}
private sealed class FakeWechatOAuthClient(
WechatIdentity? WebIdentity = null,
WechatIdentity? MiniAppIdentity = null) : IWechatOAuthClient
{
public Task<WechatIdentity> ExchangeWebCodeAsync(
WechatProviderOptions options,
string code,
CancellationToken cancellationToken = default)
{
return Task.FromResult(WebIdentity ?? new WechatIdentity(
"web-open-id",
"union-id",
"Wechat User",
"https://example.test/avatar.png",
null,
"""{"openid":"web-open-id","unionid":"union-id"}"""));
}
public Task<WechatIdentity> ExchangeMiniAppCodeAsync(
WechatProviderOptions options,
string code,
CancellationToken cancellationToken = default)
{
return Task.FromResult(MiniAppIdentity ?? new WechatIdentity(
"mini-open-id",
"union-id",
null,
null,
"session-key",
"""{"openid":"mini-open-id","unionid":"union-id","session_key":"session-key"}"""));
}
}
}