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

@@ -2,13 +2,14 @@ using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Npgsql;
using Tiku.Application.Auth;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.IntegrationTests.Api;
public sealed class ApiTestFactory : WebApplicationFactory<Program>
public sealed class ApiTestFactory(IWechatOAuthClient? wechatOAuthClient = null) : WebApplicationFactory<Program>
{
private readonly string databaseName = Guid.NewGuid().ToString();
@@ -28,6 +29,11 @@ public sealed class ApiTestFactory : WebApplicationFactory<Program>
services.AddDbContext<TikuDbContext>(options =>
options.UseInMemoryDatabase(databaseName));
if (wechatOAuthClient is not null)
{
services.AddSingleton(wechatOAuthClient);
}
});
}

View File

@@ -2,6 +2,7 @@ using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Auth;
using Tiku.Api.Controllers;
using Tiku.Domain.Identity;
using Tiku.Domain.Tenancy;
@@ -91,6 +92,53 @@ public sealed class AuthEndpointTests
Assert.Equal(HttpStatusCode.Unauthorized, refreshResponse.StatusCode);
}
[Fact]
public async Task Wechat_miniapp_login_can_access_current_user()
{
await using var factory = new ApiTestFactory(new FakeWechatOAuthClient());
var tenantId = Guid.NewGuid();
await factory.SeedAsync(
new Tenant
{
Id = tenantId,
Slug = tenantId.ToString("N"),
Name = "Wechat Tenant"
},
new TenantAuthProvider
{
TenantId = tenantId,
Provider = "wechat-miniapp",
Status = TenantAuthProviderStatus.Testing,
ConfigPublic = JsonSerializer.SerializeToElement(new
{
appId = "wx-app-id",
appSecret = "wx-app-secret"
})
});
using var client = factory.CreateClient();
var loginResponse = await client.PostAsJsonAsync(
"/api/auth/oauth/wechat-miniapp",
new WechatLoginHttpRequest(tenantId, "wx-code"));
var loginJson = await ReadJsonAsync(loginResponse);
var accessToken = loginJson.RootElement
.GetProperty("tokens")
.GetProperty("accessToken")
.GetString();
client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken);
var meResponse = await client.GetAsync("/api/me");
using var scope = factory.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode);
Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode);
Assert.Contains(dbContext.UserIdentities, identity =>
identity.Provider == "wechat-miniapp" &&
identity.OpenId == "mini-open-id" &&
identity.UnionId == "union-id");
}
private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedLoginUserAsync(
ApiTestFactory factory)
{
@@ -163,4 +211,35 @@ public sealed class AuthEndpointTests
$$"""{"passwordHash":{{JsonSerializer.Serialize(passwordHash)}}}""");
return document.RootElement.Clone();
}
private sealed class FakeWechatOAuthClient : IWechatOAuthClient
{
public Task<WechatIdentity> ExchangeWebCodeAsync(
WechatProviderOptions options,
string code,
CancellationToken cancellationToken = default)
{
return Task.FromResult(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(new WechatIdentity(
"mini-open-id",
"union-id",
null,
null,
"session-key",
"""{"openid":"mini-open-id","unionid":"union-id","session_key":"session-key"}"""));
}
}
}