Files
tiku-backend.net/Tiku.IntegrationTests/Api/ProfileEndpointTests.cs

298 lines
12 KiB
C#

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.Common;
using Tiku.Domain.Commerce;
using Tiku.Domain.Identity;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Auth;
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/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();
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/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);
}
[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/profile/notifications");
var notificationsJson = await ReadJsonAsync(notificationsResponse);
var statusResponse = await client.PostAsJsonAsync(
"/api/profile/notifications/status",
new NotificationStatusDto { NotificationIds = [notificationId], Status = "read" });
var badgesResponse = await client.GetAsync("/api/profile/badges?includeLocked=true");
var badgesJson = await ReadJsonAsync(badgesResponse);
var feedbackResponse = await client.PostAsJsonAsync(
"/api/profile/feedbacks",
new SubmitFeedbackDto
{
Type = "suggestion",
Title = "建议",
Description = "希望增加练习提醒",
Priority = "normal"
});
var feedbackJson = await ReadJsonAsync(feedbackResponse);
var feedbacksResponse = await client.GetAsync("/api/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<TikuDbContext>();
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/profile/check-in", null);
var first = await firstResponse.Content.ReadFromJsonAsync<CheckInResult>(JsonOptions);
var secondResponse = await client.PostAsync("/api/profile/check-in", null);
var second = await secondResponse.Content.ReadFromJsonAsync<CheckInResult>(JsonOptions);
var eventsResponse = await client.GetAsync("/api/profile/score-events?sourceType=daily_check_in");
var events = await eventsResponse.Content.ReadFromJsonAsync<ProfileScoreEventList>(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<TikuDbContext>();
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<JsonDocument> ReadJsonAsync(HttpResponseMessage response)
{
var stream = await response.Content.ReadAsStreamAsync();
return await JsonDocument.ParseAsync(stream);
}
}