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.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class CourseAdjustmentsControllerTests { [Fact] public async Task Reschedule_MovesOnlyTheSelectedOccurrence() { await using var fixture = await Fixture.CreateAsync(); var teacherController = new CourseAdjustmentsController( fixture.Db, new TestScope(fixture.TeacherUserId, DataScope.Self, SystemRoles.Teacher)); var targetDate = new DateOnly(2026, 9, 24); var created = await teacherController.Create( new CourseAdjustmentRequest( fixture.Task.Id, CourseAdjustmentType.Reschedule, "参加学院教研活动", true, targetDate, null, 3, null, fixture.Classroom.Id, null, null, null, fixture.Entry.Id, 3), CancellationToken.None); Assert.IsType(created); var adjustment = await fixture.Db.CourseAdjustments.SingleAsync(); Assert.Equal(new DateOnly(2026, 9, 16), adjustment.SourceDate); Assert.Equal(1, adjustment.SourceStartPeriod); Assert.Equal(2, adjustment.SourcePeriodCount); var adminController = new CourseAdjustmentsController( fixture.Db, new TestScope(Guid.NewGuid(), DataScope.All, SystemRoles.SuperAdmin)); var approved = await adminController.Approve( adjustment.Id, CancellationToken.None); Assert.IsType(approved); var entries = await fixture.Db.ScheduleEntries .OrderBy(x => x.StartWeek) .ThenBy(x => x.DayOfWeek) .ToListAsync(); Assert.Contains(entries, x => x.DayOfWeek == 3 && x.StartWeek == 1 && x.EndWeek == 2); Assert.Contains(entries, x => x.DayOfWeek == 3 && x.StartWeek == 4 && x.EndWeek == 5); Assert.Contains(entries, x => x.DayOfWeek == 4 && x.StartWeek == 4 && x.EndWeek == 4 && x.StartPeriod == 3 && x.PeriodCount == 2); Assert.DoesNotContain(entries, x => x.DayOfWeek == 3 && x.StartWeek <= 3 && x.EndWeek >= 3); } [Fact] public async Task Cancel_RemovesOnlyTheSelectedMiddleWeek() { await using var fixture = await Fixture.CreateAsync(); var teacherController = new CourseAdjustmentsController( fixture.Db, new TestScope(fixture.TeacherUserId, DataScope.Self, SystemRoles.Teacher)); await teacherController.Create( new CourseAdjustmentRequest( fixture.Task.Id, CourseAdjustmentType.Cancel, "参加培训", true, null, null, null, null, null, null, null, null, fixture.Entry.Id, 3), CancellationToken.None); var adjustment = await fixture.Db.CourseAdjustments.SingleAsync(); var adminController = new CourseAdjustmentsController( fixture.Db, new TestScope(Guid.NewGuid(), DataScope.All, SystemRoles.SuperAdmin)); var approved = await adminController.Approve( adjustment.Id, CancellationToken.None); Assert.IsType(approved); var entries = await fixture.Db.ScheduleEntries .OrderBy(x => x.StartWeek) .ToListAsync(); Assert.Equal(2, entries.Count); Assert.Contains(entries, x => x.StartWeek == 1 && x.EndWeek == 2); Assert.Contains(entries, x => x.StartWeek == 4 && x.EndWeek == 5); } private sealed class Fixture : IAsyncDisposable { private readonly SqliteConnection connection; private Fixture( SqliteConnection connection, AppDbContext db, Guid teacherUserId, TeachingTask task, ScheduleEntry entry, Classroom classroom) { this.connection = connection; Db = db; TeacherUserId = teacherUserId; Task = task; Entry = entry; Classroom = classroom; } public AppDbContext Db { get; } public Guid TeacherUserId { get; } public TeachingTask Task { get; } public ScheduleEntry Entry { get; } public Classroom Classroom { get; } public static async Task CreateAsync() { var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var db = new AppDbContext(new DbContextOptionsBuilder() .UseSqlite(connection) .Options); await db.Database.EnsureCreatedAsync(); var teacherUserId = Guid.NewGuid(); var user = new ApplicationUser { Id = teacherUserId, UserName = "T001", NormalizedUserName = "T001", DisplayName = "张老师" }; var campus = new Campus { Code = "MAIN", Name = "主校区" }; var college = new College { Code = "CS", Name = "计算机学院", CampusId = campus.Id }; var building = new Building { Code = "A", Name = "教学楼 A", CampusId = campus.Id }; var classroom = new Classroom { Code = "A101", Name = "A101", BuildingId = building.Id, Capacity = 60 }; var teacher = new Teacher { TeacherNumber = "T001", Name = "张老师", CollegeId = college.Id, UserId = teacherUserId }; 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 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 task = new TeachingTask { TaskNumber = "2026-1-CS101-01", Name = "程序设计基础教学班", AcademicTermId = term.Id, CourseId = course.Id, Capacity = 60, Status = TeachingTaskStatus.Published, Teachers = [ new TeachingTaskTeacher { TeacherId = teacher.Id, IsPrimary = true } ] }; var plan = new SchedulePlan { AcademicTermId = term.Id, Name = "正式课表", Version = "1.0", Status = SchedulePlanStatus.Published }; var entry = new ScheduleEntry { SchedulePlanId = plan.Id, TeachingTaskId = task.Id, ClassroomId = classroom.Id, DayOfWeek = 3, StartPeriod = 1, PeriodCount = 2, StartWeek = 1, EndWeek = 5, WeekPattern = WeekPattern.All }; db.AddRange( user, campus, college, building, classroom, teacher, course, term, task, plan, entry); await db.SaveChangesAsync(); return new Fixture(connection, db, teacherUserId, task, entry, classroom); } public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await connection.DisposeAsync(); } } private sealed class TestScope( Guid userId, DataScope scope, string role) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( userId, "测试用户", null, scope, new HashSet([role])); } }