151 lines
7.8 KiB
C#
151 lines
7.8 KiB
C#
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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(await controller.GetBatches(
|
|
pageSize: 20, ct: CancellationToken.None));
|
|
var page = Assert.IsType<CursorPagedResult<object>>(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<ConflictObjectResult>(action);
|
|
Assert.Contains("没有体测记录", Assert.IsType<ProblemDetails>(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<Fixture> CreateAsync(bool includeSecondCollege = false)
|
|
{
|
|
var connection = new SqliteConnection("Data Source=:memory:");
|
|
await connection.OpenAsync();
|
|
var db = new AppDbContext(new DbContextOptionsBuilder<AppDbContext>().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<string>([role]));
|
|
}
|
|
}
|