using System.Net; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; using Tiku.Api.Contracts; using Tiku.Application.Profile; using Tiku.Domain.Catalog; using Tiku.Domain.Commerce; using Tiku.Domain.Common; using Tiku.Domain.Identity; using Tiku.Domain.Learning; using Tiku.Domain.Operations; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.IntegrationTests.Api; public sealed class ProfileEndpointTests { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { Converters = { new JsonStringEnumConverter() } }; [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/student/profile/me"); var getJson = await ReadJsonAsync(getResponse); using var patchResponse = await client.PatchAsJsonAsync( "/api/student/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(); var otherRegionId = Guid.NewGuid(); await factory.SeedAsync( new Region { Id = regionId, TenantId = seed.TenantId, Name = "四川" }, new Region { Id = otherRegionId, TenantId = seed.TenantId, Name = "其他地区" }, new School { Id = schoolId, TenantId = seed.TenantId, RegionId = regionId, Name = "美术学院" }, 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 = otherRegionId, ExamName = "其他地区考试", ExamAt = DateTimeOffset.UtcNow.AddDays(1) }); using var client = factory.CreateClient(); await LoginAsync(client, seed); using var response = await client.GetAsync("/api/student/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/student/profile/me"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task Student_can_manage_notifications_badges_and_feedbacks() { await using var factory = new ApiTestFactory(); var seed = await SeedStudentAsync(factory); var notificationId = Guid.NewGuid(); var badgeId = Guid.NewGuid(); await factory.SeedAsync( new StudentProfile { TenantId = seed.TenantId, UserId = seed.UserId }, new UserNotification { Id = notificationId, TenantId = seed.TenantId, UserId = seed.UserId, NotificationType = "system", Title = "欢迎", Message = "欢迎回来", Status = NotificationStatus.Unread }, new Badge { Id = badgeId, TenantId = seed.TenantId, Name = "首练", Category = "learning", IsActive = true }, new UserBadge { TenantId = seed.TenantId, UserId = seed.UserId, BadgeId = badgeId, GrantedAt = DateTimeOffset.UtcNow }); using var client = factory.CreateClient(); await LoginAsync(client, seed); var notificationsResponse = await client.GetAsync("/api/student/profile/notifications"); var notificationsJson = await ReadJsonAsync(notificationsResponse); var statusResponse = await client.PostAsJsonAsync( "/api/student/profile/notifications/status", new NotificationStatusDto { NotificationIds = [notificationId], Status = "read" }); var badgesResponse = await client.GetAsync("/api/student/profile/badges?includeLocked=true"); var badgesJson = await ReadJsonAsync(badgesResponse); var feedbackResponse = await client.PostAsJsonAsync( "/api/student/profile/feedbacks", new SubmitFeedbackDto { Type = "suggestion", Title = "建议", Description = "希望增加练习提醒", Priority = "normal" }); var feedbackJson = await ReadJsonAsync(feedbackResponse); var feedbacksResponse = await client.GetAsync("/api/student/profile/feedbacks?type=suggestion"); var feedbacksJson = await ReadJsonAsync(feedbacksResponse); Assert.Equal(HttpStatusCode.OK, notificationsResponse.StatusCode); Assert.Equal("unread", notificationsJson.RootElement.GetProperty("items").EnumerateArray().Single().GetProperty("status") .GetString()); Assert.Equal(HttpStatusCode.OK, statusResponse.StatusCode); Assert.Equal(HttpStatusCode.OK, badgesResponse.StatusCode); Assert.True(badgesJson.RootElement.GetProperty("items").EnumerateArray().Single().GetProperty("isUnlocked") .GetBoolean()); Assert.Equal(HttpStatusCode.OK, feedbackResponse.StatusCode); Assert.Equal("suggestion", feedbackJson.RootElement.GetProperty("type").GetString()); Assert.Equal(HttpStatusCode.OK, feedbacksResponse.StatusCode); Assert.Single(feedbacksJson.RootElement.EnumerateArray()); using var scope = factory.CreateSystemScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); Assert.Equal(NotificationStatus.Read, dbContext.UserNotifications.Single(item => item.Id == notificationId).Status); Assert.Single(dbContext.ReportStatusEvents); } [Fact] public async Task Student_can_check_in_once_per_day_and_query_score_events() { await using var factory = new ApiTestFactory(); var seed = await SeedStudentAsync(factory); await factory.SeedAsync( new StudentProfile { TenantId = seed.TenantId, UserId = seed.UserId }, new PointActivityTask { TenantId = seed.TenantId, TaskKey = "daily_check_in", Title = "每日签到", TaskType = PointActivityTaskType.DailyLogin, Points = 5, MaxClaimsPerUser = 3650 }); using var client = factory.CreateClient(); await LoginAsync(client, seed); var firstResponse = await client.PostAsync("/api/student/profile/check-in", null); var first = await firstResponse.Content.ReadFromJsonAsync(JsonOptions); var secondResponse = await client.PostAsync("/api/student/profile/check-in", null); var second = await secondResponse.Content.ReadFromJsonAsync(JsonOptions); var eventsResponse = await client.GetAsync("/api/student/profile/score-events?sourceType=daily_check_in"); var events = await eventsResponse.Content.ReadFromJsonAsync(JsonOptions); Assert.Equal(HttpStatusCode.OK, firstResponse.StatusCode); Assert.False(first!.AlreadyCheckedIn); Assert.Equal(5, first.Points); Assert.Equal(HttpStatusCode.OK, secondResponse.StatusCode); Assert.True(second!.AlreadyCheckedIn); Assert.Equal(first.ScoreEventId, second.ScoreEventId); Assert.Equal(HttpStatusCode.OK, eventsResponse.StatusCode); var item = Assert.Single(events!.Items); Assert.Equal("check_in", item.EventType); Assert.Equal(5, item.Points); using var scope = factory.CreateSystemScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); Assert.Single(dbContext.PointActivityClaims.Where(claim => claim.TenantId == seed.TenantId && claim.UserId == seed.UserId && claim.SourceType == "daily_check_in")); Assert.Single(dbContext.UserScoreEvents.Where(scoreEvent => scoreEvent.TenantId == seed.TenantId && scoreEvent.UserId == seed.UserId && scoreEvent.SourceType == "daily_check_in")); } 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)}"; 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" }.WithTestPassword(), new TenantMembership { TenantId = tenantId, UserId = userId, Role = TenantRole.Student, Status = MembershipStatus.Active }); return (tenantId, userId, phone); } private static async Task LoginAsync( HttpClient client, (Guid TenantId, Guid UserId, string Phone) seed) { client.UseAccessToken(await client.LoginAsTenantAsync(seed.TenantId, seed.Phone)); } private static async Task ReadJsonAsync(HttpResponseMessage response) { var stream = await response.Content.ReadAsStreamAsync(); return await JsonDocument.ParseAsync(stream); } }