From 634ef69a2779347eaae3e6a17f7a344d7d3400a7 Mon Sep 17 00:00:00 2001 From: biss Date: Sun, 26 Jul 2026 08:53:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Jiaowu.Api/Controllers/AuthController.cs | 107 ++++++++-------- tests/Jiaowu.Api.Tests/AuthControllerTests.cs | 120 ++++++++++++++++++ 2 files changed, 176 insertions(+), 51 deletions(-) create mode 100644 tests/Jiaowu.Api.Tests/AuthControllerTests.cs diff --git a/src/Jiaowu.Api/Controllers/AuthController.cs b/src/Jiaowu.Api/Controllers/AuthController.cs index b85975b..d2b4e2c 100644 --- a/src/Jiaowu.Api/Controllers/AuthController.cs +++ b/src/Jiaowu.Api/Controllers/AuthController.cs @@ -57,59 +57,64 @@ public sealed class AuthController( { var name = request.Name.Trim(); var studentNumber = request.StudentNumber.Trim(); - var student = await db.Students - .Include(x => x.AdministrativeClass) - .ThenInclude(x => x!.Major) - .FirstOrDefaultAsync(x => - x.Status == StudentStatus.Active && - x.Name == name && - x.StudentNumber == studentNumber && - x.EnrollmentYear == request.Grade && - x.AdministrativeClassId == request.AdministrativeClassId && - x.AdministrativeClass!.Grade == request.Grade && - x.AdministrativeClass.MajorId == request.MajorId && - x.AdministrativeClass.Major!.CollegeId == request.CollegeId, - cancellationToken); - if (student is null) - return ActivationProblem( - "填写的信息与在籍学生档案不完全一致,请核对后重试。", - StatusCodes.Status400BadRequest); - if (student.UserId.HasValue) - return ActivationProblem( - "该学号已经激活,请直接登录;如忘记密码请联系管理员重置。", - StatusCodes.Status409Conflict); - if (await userManager.FindByNameAsync(studentNumber) is not null) - return ActivationProblem( - "该学号已有登录账号但未正确关联,请联系管理员处理。", - StatusCodes.Status409Conflict); + var executionStrategy = db.Database.CreateExecutionStrategy(); + return await executionStrategy.ExecuteAsync(async () => + { + var student = await db.Students + .Include(x => x.AdministrativeClass) + .ThenInclude(x => x!.Major) + .FirstOrDefaultAsync(x => + x.Status == StudentStatus.Active && + x.Name == name && + x.StudentNumber == studentNumber && + x.EnrollmentYear == request.Grade && + x.AdministrativeClassId == request.AdministrativeClassId && + x.AdministrativeClass!.Grade == request.Grade && + x.AdministrativeClass.MajorId == request.MajorId && + x.AdministrativeClass.Major!.CollegeId == request.CollegeId, + cancellationToken); + if (student is null) + return ActivationProblem( + "填写的信息与在籍学生档案不完全一致,请核对后重试。", + StatusCodes.Status400BadRequest); + if (student.UserId.HasValue) + return ActivationProblem( + "该学号已经激活,请直接登录;如忘记密码请联系管理员重置。", + StatusCodes.Status409Conflict); + if (await userManager.FindByNameAsync(studentNumber) is not null) + return ActivationProblem( + "该学号已有登录账号但未正确关联,请联系管理员处理。", + StatusCodes.Status409Conflict); - await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken); - var user = new ApplicationUser - { - UserName = studentNumber, - DisplayName = student.Name, - StaffNumber = studentNumber, - CollegeId = request.CollegeId, - IsEnabled = true, - LockoutEnabled = true - }; - var result = await userManager.CreateAsync(user, request.Password); - if (!result.Succeeded) - { - await transaction.RollbackAsync(cancellationToken); - return IdentityValidationProblem(result); - } - result = await userManager.AddToRoleAsync(user, SystemRoles.Student); - if (!result.Succeeded) - { - await transaction.RollbackAsync(cancellationToken); - return IdentityValidationProblem(result); - } + await using var transaction = + await db.Database.BeginTransactionAsync(cancellationToken); + var user = new ApplicationUser + { + UserName = studentNumber, + DisplayName = student.Name, + StaffNumber = studentNumber, + CollegeId = request.CollegeId, + IsEnabled = true, + LockoutEnabled = true + }; + var result = await userManager.CreateAsync(user, request.Password); + if (!result.Succeeded) + { + await transaction.RollbackAsync(cancellationToken); + return IdentityValidationProblem(result); + } + result = await userManager.AddToRoleAsync(user, SystemRoles.Student); + if (!result.Succeeded) + { + await transaction.RollbackAsync(cancellationToken); + return IdentityValidationProblem(result); + } - student.UserId = user.Id; - await db.SaveChangesAsync(cancellationToken); - await transaction.CommitAsync(cancellationToken); - return Ok(new { UserName = studentNumber }); + student.UserId = user.Id; + await db.SaveChangesAsync(cancellationToken); + await transaction.CommitAsync(cancellationToken); + return Ok(new { UserName = studentNumber }); + }); } [AllowAnonymous] diff --git a/tests/Jiaowu.Api.Tests/AuthControllerTests.cs b/tests/Jiaowu.Api.Tests/AuthControllerTests.cs new file mode 100644 index 0000000..8360f0f --- /dev/null +++ b/tests/Jiaowu.Api.Tests/AuthControllerTests.cs @@ -0,0 +1,120 @@ +using Jiaowu.Api.Controllers; +using Jiaowu.Api.Domain.Academic; +using Jiaowu.Api.Domain.Identity; +using Jiaowu.Api.Infrastructure.Auth; +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 ActivateStudent_WorksWithRetryingExecutionStrategy() + { + await using var connection = new SqliteConnection("Data Source=:memory:"); + await connection.OpenAsync(); + var services = new ServiceCollection(); + services.AddLogging(); + services.AddDbContext(options => options + .UseSqlite(connection) + .ReplaceService()); + services + .AddIdentityCore(options => + { + options.Password.RequiredLength = 8; + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireUppercase = true; + options.Password.RequireNonAlphanumeric = true; + }) + .AddRoles() + .AddEntityFrameworkStores(); + + await using var provider = services.BuildServiceProvider(); + await using var scope = provider.CreateAsyncScope(); + var db = scope.ServiceProvider.GetRequiredService(); + await db.Database.EnsureCreatedAsync(); + var roleManager = scope.ServiceProvider + .GetRequiredService>(); + 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>(); + Assert.True(db.Database.CreateExecutionStrategy().RetriesOnFailure); + var controller = new AuthController(db, userManager, new StubTokenService()); + 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(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 roles) => string.Empty; + } +}