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.Exams; using Jiaowu.Api.Infrastructure.Persistence; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class ExamDeletionControllerTests { [Fact] public async Task RemoveSessionsBatch_ValidatesCompleteSelectionBeforeRemoving() { await using var fixture = await Fixture.CreateAsync(); var controller = fixture.CreateController(); var selectedIds = fixture.DraftPlan.Sessions .Select(x => x.Id) .ToArray(); var otherPlanSessionId = fixture.PublishedPlan.Sessions.Single().Id; var invalidResult = await controller.RemoveSessionsBatch( fixture.DraftPlan.Id, new RemoveExamSessionsBatchRequest( [selectedIds[0], otherPlanSessionId]), CancellationToken.None); Assert.IsType(invalidResult); Assert.Equal(2, await fixture.Db.ExamSessions.CountAsync( x => x.ExamPlanId == fixture.DraftPlan.Id)); var result = Assert.IsType( await controller.RemoveSessionsBatch( fixture.DraftPlan.Id, new RemoveExamSessionsBatchRequest(selectedIds), CancellationToken.None)); Assert.Equal(2, ReadIntProperty(result.Value!, "removedCount")); Assert.False(await fixture.Db.ExamSessions.AnyAsync( x => x.ExamPlanId == fixture.DraftPlan.Id)); Assert.False(await fixture.Db.ExamSessionInvigilators.AnyAsync( x => selectedIds.Contains(x.ExamSessionId))); Assert.True(await fixture.Db.ExamPlans.AnyAsync( x => x.Id == fixture.DraftPlan.Id)); } [Fact] public async Task RemoveSessionsBatch_RejectsPublishedPlan() { await using var fixture = await Fixture.CreateAsync(); var controller = fixture.CreateController(); var sessionId = fixture.PublishedPlan.Sessions.Single().Id; var result = await controller.RemoveSessionsBatch( fixture.PublishedPlan.Id, new RemoveExamSessionsBatchRequest([sessionId]), CancellationToken.None); Assert.IsType(result); Assert.True(await fixture.Db.ExamSessions.AnyAsync( x => x.Id == sessionId)); } [Fact] public async Task DeletePlan_DeletesDraftWithSessions_AndRejectsPublishedPlan() { await using var fixture = await Fixture.CreateAsync(); var controller = fixture.CreateController(); var draftPlanId = fixture.DraftPlan.Id; var publishedPlanId = fixture.PublishedPlan.Id; var draftSessionIds = fixture.DraftPlan.Sessions .Select(x => x.Id) .ToArray(); var publishedResult = await controller.DeletePlan( publishedPlanId, CancellationToken.None); Assert.IsType(publishedResult); Assert.True(await fixture.Db.ExamPlans.AnyAsync( x => x.Id == publishedPlanId)); var draftResult = await controller.DeletePlan( draftPlanId, CancellationToken.None); Assert.IsType(draftResult); Assert.False(await fixture.Db.ExamPlans.AnyAsync( x => x.Id == draftPlanId)); Assert.False(await fixture.Db.ExamSessions.AnyAsync( x => x.ExamPlanId == draftPlanId)); Assert.False(await fixture.Db.ExamSessionInvigilators.AnyAsync( x => draftSessionIds.Contains(x.ExamSessionId))); Assert.True(await fixture.Db.ExamPlans.AnyAsync( x => x.Id == publishedPlanId)); } private static int ReadIntProperty(object value, string name) => (int)value.GetType().GetProperty(name)!.GetValue(value)!; private sealed class Fixture : IAsyncDisposable { private readonly SqliteConnection connection; private Fixture( SqliteConnection connection, AppDbContext db, ExamPlan draftPlan, ExamPlan publishedPlan) { this.connection = connection; Db = db; DraftPlan = draftPlan; PublishedPlan = publishedPlan; } public AppDbContext Db { get; } public ExamPlan DraftPlan { get; } public ExamPlan PublishedPlan { get; } public static async Task CreateAsync() { var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var college = new College { Code = "CS", Name = "计算机学院" }; var term = new AcademicTerm { Code = "2026-1", Name = "2026—2027 学年第一学期", AcademicYear = "2026-2027", Season = TermSeason.Autumn, StartDate = new DateOnly(2026, 9, 1), EndDate = new DateOnly(2027, 1, 20) }; var course = new Course { Code = "CS101", Name = "程序设计基础", CollegeId = college.Id, Credits = 4, TotalHours = 64, LectureHours = 48, PracticeHours = 16, Nature = CourseNature.MajorRequired, AssessmentMethod = AssessmentMethod.Examination }; var firstTask = CreateTask( term.Id, course.Id, "2026-1-CS101-01"); var secondTask = CreateTask( term.Id, course.Id, "2026-1-CS101-02"); var invigilator = new Teacher { TeacherNumber = "T001", Name = "张老师", CollegeId = college.Id }; var firstDraftSession = CreateSession( firstTask.Id, new DateOnly(2027, 1, 8)); firstDraftSession.Invigilators = [ new ExamSessionInvigilator { TeacherId = invigilator.Id } ]; var draftPlan = new ExamPlan { AcademicTermId = term.Id, Name = "期末考试草稿", Sessions = [ firstDraftSession, CreateSession(secondTask.Id, new DateOnly(2027, 1, 9)) ] }; var publishedPlan = new ExamPlan { AcademicTermId = term.Id, Name = "已发布期末考试", Status = ExamPlanStatus.Published, PublishedAt = DateTime.UtcNow, Sessions = [ CreateSession(firstTask.Id, new DateOnly(2027, 1, 10)) ] }; db.AddRange( college, term, course, firstTask, secondTask, invigilator, draftPlan, publishedPlan); await db.SaveChangesAsync(); return new Fixture( connection, db, draftPlan, publishedPlan); } public ExamsController CreateController() => new( Db, new ManagerDataScope(), new ExamArrangementService(Db), NoOpAppCache.Instance); public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await connection.DisposeAsync(); } private static TeachingTask CreateTask( Guid termId, Guid courseId, string number) => new() { AcademicTermId = termId, CourseId = courseId, TaskNumber = number, Name = $"教学班 {number}", Capacity = 60, Status = TeachingTaskStatus.Published }; private static ExamSession CreateSession( Guid taskId, DateOnly examDate) => new() { TeachingTaskId = taskId, ExamDate = examDate, StartPeriod = 1, PeriodCount = 2, StartsAt = examDate.ToDateTime(new TimeOnly(8, 0)), EndsAt = examDate.ToDateTime(new TimeOnly(9, 50)), RequiredInvigilatorCount = 2 }; } private sealed class ManagerDataScope : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( Guid.NewGuid(), "考试管理员", null, DataScope.All, new HashSet([SystemRoles.AcademicAdmin])); } }