forked from xiongyuxing/tiku-backend.net
123 lines
4.9 KiB
C#
123 lines
4.9 KiB
C#
using System.Net.Http.Json;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Tiku.Api.Contracts;
|
|
using Tiku.Api.Controllers;
|
|
using Tiku.Domain.Identity;
|
|
using Tiku.Domain.Tenancy;
|
|
|
|
namespace Tiku.IntegrationTests.Api;
|
|
|
|
public sealed class AuthPasswordLifecycleTests
|
|
{
|
|
[Fact]
|
|
public async Task Phone_and_password_login_authenticates_without_totp()
|
|
{
|
|
await using var factory = new ApiTestFactory();
|
|
var seed = await SeedUserAsync(factory);
|
|
using var client = factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("x-tenant-code", seed.TenantId.ToString("N"));
|
|
|
|
using var login = await PostPasswordLoginAsync(client, seed);
|
|
|
|
Assert.Equal("authenticated", login.RootElement.GetProperty("status").GetString());
|
|
Assert.Equal(seed.Phone, login.RootElement.GetProperty("user").GetProperty("phone").GetString());
|
|
Assert.False(login.RootElement.TryGetProperty("challengeToken", out var challenge) &&
|
|
challenge.ValueKind == JsonValueKind.String);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Forced_password_change_finishes_with_an_authenticated_session()
|
|
{
|
|
await using var factory = new ApiTestFactory();
|
|
var seed = await SeedUserAsync(factory, forcePasswordChange: true);
|
|
using var client = factory.CreateClient();
|
|
client.DefaultRequestHeaders.Add("x-tenant-code", seed.TenantId.ToString("N"));
|
|
|
|
using var login = await PostPasswordLoginAsync(client, seed);
|
|
Assert.Equal("password_change_required", login.RootElement.GetProperty("status").GetString());
|
|
|
|
var response = await client.PostAsJsonAsync(
|
|
"/api/auth/password/change-required",
|
|
new RequiredPasswordChangeDto
|
|
{
|
|
ChallengeToken = login.RootElement.GetProperty("challengeToken").GetString()!,
|
|
NewPassword = "ChangedPassword2026"
|
|
});
|
|
response.EnsureSuccessStatusCode();
|
|
using var changed = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
|
|
|
Assert.Equal("authenticated", changed.RootElement.GetProperty("status").GetString());
|
|
Assert.False(changed.RootElement.TryGetProperty("challengeToken", out var challenge) &&
|
|
challenge.ValueKind == JsonValueKind.String);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(nameof(AuthController.LoginWithPassword), "login/password")]
|
|
[InlineData(nameof(AuthController.SendSmsCode), "sms/send")]
|
|
[InlineData(nameof(AuthController.LoginWithSms), "login/sms")]
|
|
[InlineData(nameof(AuthController.LoginWithWechatWeb), "oauth/wechat")]
|
|
[InlineData(nameof(AuthController.LoginWithWechatMiniApp), "oauth/wechat-miniapp")]
|
|
[InlineData(nameof(AuthController.ChangeRequiredPassword), "password/change-required")]
|
|
[InlineData(nameof(AuthController.Refresh), "refresh")]
|
|
[InlineData(nameof(AuthController.Logout), "logout")]
|
|
[InlineData(nameof(AuthController.LogoutAll), "logout-all")]
|
|
public void Authentication_routes_match_the_v2_contract(string actionName, string route)
|
|
{
|
|
var action = typeof(AuthController).GetMethod(actionName, BindingFlags.Public | BindingFlags.Instance);
|
|
var attribute = action?.GetCustomAttribute<HttpPostAttribute>();
|
|
|
|
Assert.NotNull(attribute);
|
|
Assert.Equal(route, attribute.Template);
|
|
}
|
|
|
|
private static async Task<JsonDocument> PostPasswordLoginAsync(
|
|
HttpClient client,
|
|
(Guid TenantId, string Phone) seed)
|
|
{
|
|
var response = await client.PostAsJsonAsync(
|
|
"/api/auth/login/password",
|
|
new PasswordLoginDto
|
|
{
|
|
Realm = AuthRealm.Tenant,
|
|
TenantCode = seed.TenantId.ToString("N"),
|
|
Identifier = seed.Phone,
|
|
Password = PasswordTestUserExtensions.TestPassword
|
|
});
|
|
response.EnsureSuccessStatusCode();
|
|
return JsonDocument.Parse(await response.Content.ReadAsStringAsync());
|
|
}
|
|
|
|
private static async Task<(Guid TenantId, string Phone)> SeedUserAsync(
|
|
ApiTestFactory factory,
|
|
bool forcePasswordChange = false)
|
|
{
|
|
var tenantId = Guid.NewGuid();
|
|
var userId = Guid.NewGuid();
|
|
const string phone = "13800000000";
|
|
await factory.SeedAsync(
|
|
new Tenant
|
|
{
|
|
Id = tenantId,
|
|
Slug = tenantId.ToString("N"),
|
|
Name = "Password Lifecycle Tenant"
|
|
},
|
|
new User
|
|
{
|
|
Id = userId,
|
|
Phone = phone,
|
|
Name = "Password Lifecycle User",
|
|
ForcePasswordChange = forcePasswordChange
|
|
}.WithTestPassword(),
|
|
new TenantMembership
|
|
{
|
|
TenantId = tenantId,
|
|
UserId = userId,
|
|
Role = TenantRole.TenantAdmin,
|
|
Status = MembershipStatus.Active
|
|
});
|
|
return (tenantId, phone);
|
|
}
|
|
}
|