using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Tiku.Api.Controllers; using Tiku.Domain.Identity; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Auth; using Tiku.Infrastructure.Persistence; namespace Tiku.IntegrationTests.Api; public sealed class AuthEndpointTests { [Fact] public async Task Password_login_can_access_current_user_and_tenant() { await using var factory = new ApiTestFactory(); var seed = await SeedLoginUserAsync(factory); using var client = factory.CreateClient(); var loginResponse = await client.PostAsJsonAsync( "/api/auth/login/password", new PasswordLoginHttpRequest(seed.TenantId, seed.Phone, "passw0rd!")); 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"); var tenantResponse = await client.GetAsync("/api/tenants/current"); Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, tenantResponse.StatusCode); Assert.Contains(seed.UserId.ToString(), await meResponse.Content.ReadAsStringAsync(), StringComparison.OrdinalIgnoreCase); Assert.Contains(seed.TenantId.ToString(), await tenantResponse.Content.ReadAsStringAsync(), StringComparison.OrdinalIgnoreCase); } [Fact] public async Task Sms_login_can_access_current_user() { await using var factory = new ApiTestFactory(); var seed = await SeedLoginUserAsync(factory); await SeedSmsCodeAsync(factory, seed.TenantId, seed.Phone, "123456"); using var client = factory.CreateClient(); var loginResponse = await client.PostAsJsonAsync( "/api/auth/login/sms", new SmsLoginHttpRequest(seed.TenantId, seed.Phone, "123456")); 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"); Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, meResponse.StatusCode); } [Fact] public async Task Logout_revokes_access_and_refresh_tokens() { await using var factory = new ApiTestFactory(); var seed = await SeedLoginUserAsync(factory); using var client = factory.CreateClient(); var loginResponse = await client.PostAsJsonAsync( "/api/auth/login/password", new PasswordLoginHttpRequest(seed.TenantId, seed.Phone, "passw0rd!")); var loginJson = await ReadJsonAsync(loginResponse); var tokens = loginJson.RootElement.GetProperty("tokens"); var accessToken = tokens.GetProperty("accessToken").GetString(); var refreshToken = tokens.GetProperty("refreshToken").GetString(); var logoutResponse = await client.PostAsJsonAsync( "/api/auth/logout", new RefreshHttpRequest(refreshToken!)); client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); var meResponse = await client.GetAsync("/api/me"); var refreshResponse = await client.PostAsJsonAsync( "/api/auth/refresh", new RefreshHttpRequest(refreshToken!)); Assert.Equal(HttpStatusCode.NoContent, logoutResponse.StatusCode); Assert.Equal(HttpStatusCode.Unauthorized, meResponse.StatusCode); Assert.Equal(HttpStatusCode.Unauthorized, refreshResponse.StatusCode); } private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedLoginUserAsync( ApiTestFactory factory) { var tenantId = Guid.NewGuid(); var userId = Guid.NewGuid(); var phone = "13800000000"; var passwordHash = new PasswordHasher().Hash("passw0rd!"); await factory.SeedAsync( new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Test Tenant" }, new User { Id = userId, Phone = phone, Name = "Test User" }, new TenantMembership { TenantId = tenantId, UserId = userId, Role = TenantRole.TenantAdmin, Status = MembershipStatus.Active }, new UserIdentity { UserId = userId, Provider = "password", ProviderSubject = phone, Phone = phone, SecretPayload = CreateSecretPayload(passwordHash) }); return (tenantId, userId, phone); } private static async Task SeedSmsCodeAsync( ApiTestFactory factory, Guid tenantId, string phone, string code) { using var scope = factory.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.SmsVerificationCodes.Add(new SmsVerificationCode { TenantId = tenantId, Phone = phone, Purpose = SmsPurpose.Login, CodeHash = SmsCodeHashing.Hash(tenantId, phone, SmsPurpose.Login, code), Status = SmsVerificationStatus.Sent, ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(5) }); await dbContext.SaveChangesAsync(); } private static async Task ReadJsonAsync(HttpResponseMessage response) { var stream = await response.Content.ReadAsStreamAsync(); return await JsonDocument.ParseAsync(stream); } private static JsonElement CreateSecretPayload(string passwordHash) { using var document = JsonDocument.Parse( $$"""{"passwordHash":{{JsonSerializer.Serialize(passwordHash)}}}"""); return document.RootElement.Clone(); } }