using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using Tiku.Api.Contracts; using Tiku.Domain.Catalog; using Tiku.Domain.Common; using Tiku.Domain.Identity; using Tiku.Domain.Learning; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Auth; using Tiku.Infrastructure.Persistence; namespace Tiku.IntegrationTests.Api; public sealed class ProfileEndpointTests { [Fact] public async Task Logged_in_student_can_get_and_update_profile() { await using var factory = new ApiTestFactory(); var seed = await SeedStudentAsync(factory); var regionId = Guid.NewGuid(); var schoolId = Guid.NewGuid(); var majorId = Guid.NewGuid(); await factory.SeedAsync( new Region { Id = regionId, TenantId = seed.TenantId, Name = "四川" }, new School { Id = schoolId, TenantId = seed.TenantId, RegionId = regionId, Name = "美术学院" }, new Major { Id = majorId, TenantId = seed.TenantId, RegionId = regionId, SchoolId = schoolId, Name = "视觉传达" }); using var client = factory.CreateClient(); await LoginAsync(client, seed); using var getResponse = await client.GetAsync("/api/profile/me"); var getJson = await ReadJsonAsync(getResponse); using var patchResponse = await client.PatchAsJsonAsync( "/api/profile/me", new UpdateProfileDto { Name = "张三", AvatarPreset = "female", RegionId = regionId, SelectedSchoolId = schoolId, SelectedMajorId = majorId, Stats = JsonSerializer.SerializeToElement(new { level = 3 }), RecentActivities = JsonSerializer.SerializeToElement(new[] { new { type = "login" } }) }); var patchJson = await ReadJsonAsync(patchResponse); Assert.Equal(HttpStatusCode.OK, getResponse.StatusCode); Assert.NotEqual(Guid.Empty, getJson.RootElement.GetProperty("profileId").GetGuid()); Assert.Equal(HttpStatusCode.OK, patchResponse.StatusCode); Assert.Equal("张三", patchJson.RootElement.GetProperty("name").GetString()); Assert.Equal("female", patchJson.RootElement.GetProperty("avatarPreset").GetString()); Assert.Equal(regionId, patchJson.RootElement.GetProperty("regionId").GetGuid()); } [Fact] public async Task Exam_countdowns_use_current_profile_target() { await using var factory = new ApiTestFactory(); var seed = await SeedStudentAsync(factory); var regionId = Guid.NewGuid(); var schoolId = Guid.NewGuid(); await factory.SeedAsync( new StudentProfile { TenantId = seed.TenantId, UserId = seed.UserId, RegionId = regionId, SelectedSchoolId = schoolId }, new ExamDate { TenantId = seed.TenantId, RegionId = regionId, SchoolId = schoolId, ExamName = "校考", ExamAt = DateTimeOffset.UtcNow.AddDays(10) }, new ExamDate { TenantId = seed.TenantId, RegionId = Guid.NewGuid(), ExamName = "其他地区考试", ExamAt = DateTimeOffset.UtcNow.AddDays(1) }); using var client = factory.CreateClient(); await LoginAsync(client, seed); using var response = await client.GetAsync("/api/profile/exam-countdowns"); var json = await ReadJsonAsync(response); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var item = json.RootElement.GetProperty("items").EnumerateArray().Single(); Assert.Equal("校考", item.GetProperty("examName").GetString()); Assert.True(item.GetProperty("daysLeft").GetInt32() >= 9); } [Fact] public async Task Profile_requires_authentication() { await using var factory = new ApiTestFactory(); using var client = factory.CreateClient(); using var response = await client.GetAsync("/api/profile/me"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } private static async Task<(Guid TenantId, Guid UserId, string Phone)> SeedStudentAsync(ApiTestFactory factory) { var tenantId = Guid.NewGuid(); var userId = Guid.NewGuid(); var phone = $"136{Random.Shared.Next(10000000, 99999999)}"; var passwordHash = new PasswordHasher().Hash("passw0rd!"); await factory.SeedAsync( new Tenant { Id = tenantId, Slug = tenantId.ToString("N"), Name = "Test Tenant", Status = TenantStatus.Active, Metadata = JsonDefaults.Object() }, new User { Id = userId, Phone = phone, Name = "Student" }, new TenantMembership { TenantId = tenantId, UserId = userId, Role = TenantRole.Student, Status = MembershipStatus.Active }, new UserIdentity { UserId = userId, Provider = "password", ProviderSubject = phone, Phone = phone, SecretPayload = CreateSecretPayload(passwordHash) }); return (tenantId, userId, phone); } private static async Task LoginAsync( HttpClient client, (Guid TenantId, Guid UserId, string Phone) seed) { var loginResponse = await client.PostAsJsonAsync( "/api/auth/login/password", new PasswordLoginDto { TenantId = seed.TenantId, Phone = seed.Phone, Password = "passw0rd!" }); var loginJson = await ReadJsonAsync(loginResponse); var accessToken = loginJson.RootElement .GetProperty("tokens") .GetProperty("accessToken") .GetString(); client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); } 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(); } }