新增统一缓存封装:[AppCache.cs (line 12)](/E:/jiaowu/src/Jiaowu.Api/Infrastructure/Caching/AppCache.cs:12) 接入 HybridCache 和可选 Redis:[Program.cs (line 153)](/E:/jiaowu/src/Jiaowu.Api/Program.cs:153) 缓存学生激活选项、基础数据、公开课表和课表选项。 个人课表、选课容量、成绩、权限、通知和任务状态保持实时查询。 基础数据、课程、教师、教学任务、作息、考试和课表发布后自动失效相关缓存。 新增 /health/cache,Redis 故障不影响 /health/ready。 Compose 增加 256MB、allkeys-lfu、无持久化的 redis:8.8-alpine 服务;该镜像标签已由 Docker 官方镜像仓库核对。 更新 [.env.example (line 1)](/E:/jiaowu/.env.example:1) 和 [README.md (line 190)](/E:/jiaowu/README.md:190) 部署说明。
151 lines
5.6 KiB
C#
151 lines
5.6 KiB
C#
using Jiaowu.Api.Controllers;
|
|
using Jiaowu.Api.Domain.Academic;
|
|
using Jiaowu.Api.Domain.Identity;
|
|
using Jiaowu.Api.Infrastructure.Auth;
|
|
using Jiaowu.Api.Infrastructure.Caching;
|
|
using Jiaowu.Api.Infrastructure.Persistence;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Jiaowu.Api.Tests;
|
|
|
|
public sealed class AuthControllerTests
|
|
{
|
|
[Fact]
|
|
public async Task RetriableTransaction_WorksWithRetryingExecutionStrategy()
|
|
{
|
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
|
await connection.OpenAsync();
|
|
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
.UseSqlite(connection)
|
|
.ReplaceService<IExecutionStrategyFactory, RetryingExecutionStrategyFactory>()
|
|
.Options;
|
|
await using var db = new AppDbContext(options);
|
|
await db.Database.EnsureCreatedAsync();
|
|
|
|
Assert.True(db.Database.CreateExecutionStrategy().RetriesOnFailure);
|
|
await db.ExecuteInRetriableTransactionAsync(
|
|
async transaction =>
|
|
{
|
|
db.Colleges.Add(new College { Code = "TX", Name = "事务测试学院" });
|
|
await db.SaveChangesAsync();
|
|
await transaction.CommitAsync();
|
|
},
|
|
CancellationToken.None);
|
|
|
|
Assert.Equal(1, await db.Colleges.CountAsync());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task ActivateStudent_WorksWithRetryingExecutionStrategy()
|
|
{
|
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
|
await connection.OpenAsync();
|
|
var services = new ServiceCollection();
|
|
services.AddLogging();
|
|
services.AddDbContext<AppDbContext>(options => options
|
|
.UseSqlite(connection)
|
|
.ReplaceService<IExecutionStrategyFactory, RetryingExecutionStrategyFactory>());
|
|
services
|
|
.AddIdentityCore<ApplicationUser>(options =>
|
|
{
|
|
options.Password.RequiredLength = 8;
|
|
options.Password.RequireDigit = true;
|
|
options.Password.RequireLowercase = true;
|
|
options.Password.RequireUppercase = true;
|
|
options.Password.RequireNonAlphanumeric = true;
|
|
})
|
|
.AddRoles<ApplicationRole>()
|
|
.AddEntityFrameworkStores<AppDbContext>();
|
|
|
|
await using var provider = services.BuildServiceProvider();
|
|
await using var scope = provider.CreateAsyncScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
await db.Database.EnsureCreatedAsync();
|
|
var roleManager = scope.ServiceProvider
|
|
.GetRequiredService<RoleManager<ApplicationRole>>();
|
|
Assert.True((await roleManager.CreateAsync(new ApplicationRole
|
|
{
|
|
Name = SystemRoles.Student,
|
|
Description = "学生"
|
|
})).Succeeded);
|
|
|
|
var college = new College { Code = "CS", Name = "计算机学院" };
|
|
var major = new Major
|
|
{
|
|
Code = "SE",
|
|
Name = "软件工程",
|
|
CollegeId = college.Id,
|
|
DegreeType = "工学"
|
|
};
|
|
var administrativeClass = new AdministrativeClass
|
|
{
|
|
Code = "SE202601",
|
|
Name = "软件工程 2026 级 1 班",
|
|
MajorId = major.Id,
|
|
Grade = 2026
|
|
};
|
|
var student = new Student
|
|
{
|
|
StudentNumber = "202601999",
|
|
Name = "测试学生",
|
|
AdministrativeClassId = administrativeClass.Id,
|
|
EnrollmentYear = 2026,
|
|
EnrollmentDate = new DateOnly(2026, 9, 1),
|
|
Status = StudentStatus.Active
|
|
};
|
|
db.AddRange(college, major, administrativeClass, student);
|
|
await db.SaveChangesAsync();
|
|
|
|
var userManager = scope.ServiceProvider
|
|
.GetRequiredService<UserManager<ApplicationUser>>();
|
|
Assert.True(db.Database.CreateExecutionStrategy().RetriesOnFailure);
|
|
var controller = new AuthController(
|
|
db,
|
|
userManager,
|
|
new StubTokenService(),
|
|
NoOpAppCache.Instance);
|
|
var request = new StudentActivationRequest(
|
|
student.Name,
|
|
student.StudentNumber,
|
|
college.Id,
|
|
major.Id,
|
|
administrativeClass.Grade,
|
|
administrativeClass.Id,
|
|
"Student@123");
|
|
|
|
var result = await controller.ActivateStudent(request, CancellationToken.None);
|
|
|
|
Assert.IsType<OkObjectResult>(result);
|
|
var user = await userManager.FindByNameAsync(student.StudentNumber);
|
|
Assert.NotNull(user);
|
|
Assert.Equal(student.Name, user.DisplayName);
|
|
Assert.Equal(college.Id, user.CollegeId);
|
|
Assert.True(await userManager.IsInRoleAsync(user, SystemRoles.Student));
|
|
await db.Entry(student).ReloadAsync();
|
|
Assert.Equal(user.Id, student.UserId);
|
|
}
|
|
|
|
private sealed class RetryingExecutionStrategyFactory(
|
|
ExecutionStrategyDependencies dependencies) : IExecutionStrategyFactory
|
|
{
|
|
public IExecutionStrategy Create() => new TestRetryingExecutionStrategy(dependencies);
|
|
}
|
|
|
|
private sealed class TestRetryingExecutionStrategy(
|
|
ExecutionStrategyDependencies dependencies)
|
|
: ExecutionStrategy(dependencies, 1, TimeSpan.Zero)
|
|
{
|
|
protected override bool ShouldRetryOn(Exception exception) => false;
|
|
}
|
|
|
|
private sealed class StubTokenService : ITokenService
|
|
{
|
|
public string Create(ApplicationUser user, IEnumerable<string> roles) => string.Empty;
|
|
}
|
|
}
|