using Jiaowu.Api.Controllers; using Jiaowu.Api.Contracts; using Jiaowu.Api.Domain.Academic; using Jiaowu.Api.Domain.Identity; using Jiaowu.Api.Infrastructure.Auth; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class PhysicalFitnessControllerTests { [Fact] public async Task UpsertRecords_CalculatesAndPersistsItemSnapshot() { await using var fixture = await Fixture.CreateAsync(); var controller = new PhysicalFitnessController(fixture.Db, fixture.AllScope); var action = await controller.UpsertRecords(fixture.Batch.Id, new UpsertPhysicalTestRecordsRequest([ new PhysicalTestRecordRequest( fixture.Student.StudentNumber, PhysicalTestRecordStatus.Normal, 1, 175, 70, 5040, 6.7m, 24.9m, 273, 19, 197, null, "已复核") ]), CancellationToken.None); Assert.IsType(action); fixture.Db.ChangeTracker.Clear(); var saved = await fixture.Db.PhysicalTestRecords.Include(x => x.Items).SingleAsync(); Assert.Equal(100m, saved.TotalScore); Assert.Equal(PhysicalTestGrade.Excellent, saved.Grade); Assert.Equal(9, saved.Items.Count); Assert.Equal("已复核", saved.Notes); Assert.Equal(PhysicalTestBatchStatus.Draft, (await fixture.Db.PhysicalTestBatches.FindAsync(fixture.Batch.Id))!.Status); fixture.Db.ChangeTracker.Clear(); var secondController = new PhysicalFitnessController(fixture.Db, fixture.AllScope); var updated = await secondController.UpsertRecords(fixture.Batch.Id, new UpsertPhysicalTestRecordsRequest([ new PhysicalTestRecordRequest( fixture.Student.StudentNumber, PhysicalTestRecordStatus.Normal, 1, 175, 70, 4920, 6.8m, 23.1m, 268, 18, 202, null, "二次复核") ]), CancellationToken.None); Assert.IsType(updated); fixture.Db.ChangeTracker.Clear(); var replacement = await fixture.Db.PhysicalTestRecords.Include(x => x.Items).SingleAsync(); Assert.Equal(9, replacement.Items.Count); Assert.Equal("二次复核", replacement.Notes); } [Fact] public async Task CollegeViewer_OnlySeesRecordsInsideCollegeScope() { await using var fixture = await Fixture.CreateAsync(includeSecondCollege: true); fixture.Db.PhysicalTestRecords.AddRange( new PhysicalTestRecord { PhysicalTestBatchId = fixture.Batch.Id, StudentId = fixture.Student.Id, GradeLevel = 1, Status = PhysicalTestRecordStatus.Exempt, SpecialReason = "测试" }, new PhysicalTestRecord { PhysicalTestBatchId = fixture.Batch.Id, StudentId = fixture.OtherStudent!.Id, GradeLevel = 1, Status = PhysicalTestRecordStatus.Exempt, SpecialReason = "测试" }); await fixture.Db.SaveChangesAsync(); var controller = new PhysicalFitnessController(fixture.Db, fixture.CollegeScope); var action = Assert.IsType(await controller.GetRecords( fixture.Batch.Id, pageSize: 30, ct: CancellationToken.None)); var json = System.Text.Json.JsonSerializer.Serialize(action.Value); Assert.Contains(fixture.Student.StudentNumber, json); Assert.DoesNotContain(fixture.OtherStudent.StudentNumber, json); var batches = Assert.IsType(await controller.GetBatches( pageSize: 20, ct: CancellationToken.None)); var page = Assert.IsType>(batches.Value); var visibleCount = page.Items.Single().GetType().GetProperty("RecordCount")!.GetValue(page.Items.Single()); Assert.Equal(1, visibleCount); } [Fact] public async Task Publish_RejectsEmptyBatch() { await using var fixture = await Fixture.CreateAsync(); var controller = new PhysicalFitnessController(fixture.Db, fixture.AllScope); var action = await controller.Publish(fixture.Batch.Id, CancellationToken.None); var conflict = Assert.IsType(action); Assert.Contains("没有体测记录", Assert.IsType(conflict.Value).Detail); } private sealed class Fixture : IAsyncDisposable { private readonly SqliteConnection connection; public AppDbContext Db { get; } public College College { get; } public Student Student { get; } public Student? OtherStudent { get; } public PhysicalTestBatch Batch { get; } public ICurrentUserDataScope AllScope { get; } public ICurrentUserDataScope CollegeScope { get; } private Fixture(SqliteConnection connection, AppDbContext db, College college, Student student, Student? otherStudent, PhysicalTestBatch batch) { this.connection = connection; Db = db; College = college; Student = student; OtherStudent = otherStudent; Batch = batch; AllScope = new TestScope(null, DataScope.All, SystemRoles.PhysicalEducationAdmin); CollegeScope = new TestScope(college.Id, DataScope.College, SystemRoles.CollegeAdmin); } public static async Task CreateAsync(bool includeSecondCollege = false) { var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var db = new AppDbContext(new DbContextOptionsBuilder().UseSqlite(connection).Options); await db.Database.EnsureCreatedAsync(); 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 = "202600001", Name = "学生甲", Gender = Gender.Male, AdministrativeClassId = administrativeClass.Id, EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1) }; Student? otherStudent = null; db.AddRange(college, major, administrativeClass, student); if (includeSecondCollege) { var otherCollege = new College { Code = "ART", Name = "艺术学院" }; var otherMajor = new Major { Code = "ART", Name = "艺术学", CollegeId = otherCollege.Id, DegreeType = "艺术学" }; var otherClass = new AdministrativeClass { Code = "ART202601", Name = "艺术 2026 级 1 班", MajorId = otherMajor.Id, Grade = 2026 }; otherStudent = new Student { StudentNumber = "202600002", Name = "学生乙", Gender = Gender.Female, AdministrativeClassId = otherClass.Id, EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1) }; db.AddRange(otherCollege, otherMajor, otherClass, otherStudent); } var batch = new PhysicalTestBatch { Name = "秋季体测", AcademicYear = "2026-2027", TestDate = new DateOnly(2026, 10, 15), StandardVersion = "国家学生体质健康标准(2014年修订)" }; db.Add(batch); await db.SaveChangesAsync(); return new Fixture(connection, db, college, student, otherStudent, batch); } public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await connection.DisposeAsync(); } } private sealed class TestScope(Guid? collegeId, DataScope dataScope, string role) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( Guid.NewGuid(), "测试用户", collegeId, dataScope, new HashSet([role])); } }