forked from xiongyuxing/tiku-backend.net
265 lines
10 KiB
C#
265 lines
10 KiB
C#
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.Operations;
|
|
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();
|
|
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.Services.CreateScope();
|
|
var dbContext = scope.ServiceProvider.GetRequiredService<TikuDbContext>();
|
|
Assert.Equal(NotificationStatus.Read, dbContext.UserNotifications.Single(item => item.Id == notificationId).Status);
|
|
Assert.Single(dbContext.ReportStatusEvents);
|
|
}
|
|
|
|
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<JsonDocument> 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();
|
|
}
|
|
}
|