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 ApprovalsControllerTests { [Fact] public async Task MyCourses_IncludesPublishedScheduledTaskAfterCurrentTermChanges() { await using var fixture = await ApprovalFixture.CreateAsync(); fixture.CurrentTerm.IsCurrent = false; fixture.Db.AcademicTerms.Add(new AcademicTerm { Code = "2026-2", Name = "2026—2027 学年第二学期", AcademicYear = "2026-2027", Season = TermSeason.Spring, StartDate = new DateOnly(2027, 2, 20), EndDate = new DateOnly(2027, 7, 1), IsCurrent = true }); fixture.Db.SchedulePlans.Add(new SchedulePlan { AcademicTermId = fixture.CurrentTerm.Id, Name = "正式课表", Version = "v1", Status = SchedulePlanStatus.Published, Entries = [ new ScheduleEntry { TeachingTaskId = fixture.CurrentTask.Id, DayOfWeek = 1, StartPeriod = 1, PeriodCount = 2, StartWeek = 1, EndWeek = 16, WeekPattern = WeekPattern.All } ] }); await fixture.Db.SaveChangesAsync(); var result = await fixture.Controller.GetMyEnrolledCourses( CancellationToken.None); var ok = Assert.IsType(result); var options = Assert.IsAssignableFrom< IEnumerable>(ok.Value).ToList(); var option = Assert.Single(options); Assert.Equal(fixture.CurrentTask.Id, option.TeachingTaskId); Assert.Equal(fixture.CurrentCourse.Id, option.CourseId); Assert.True(option.HasPublishedSchedule); } [Fact] public async Task Exemption_RejectsTeachingTaskOutsideStudentsCurrentCourses() { await using var fixture = await ApprovalFixture.CreateAsync(); var unrelatedTask = new TeachingTask { TaskNumber = "2026-1-OTHER-01", Name = "其他教学班", AcademicTermId = fixture.CurrentTerm.Id, CourseId = fixture.SubstituteCourse.Id, Capacity = 30, Status = TeachingTaskStatus.Published }; fixture.Db.TeachingTasks.Add(unrelatedTask); await fixture.Db.SaveChangesAsync(); var result = await fixture.Controller.ApplyExemption( new ExemptionRequest(unrelatedTask.Id, "申请原因"), CancellationToken.None); Assert.IsType(result); Assert.Empty(fixture.Db.CourseExemptions); } [Fact] public async Task Substitution_AcceptsCurrentCourseAndPublishedPassedCourse() { await using var fixture = await ApprovalFixture.CreateAsync(); var historicalTerm = new AcademicTerm { Code = "2025-2", Name = "2025—2026 学年第二学期", AcademicYear = "2025-2026", Season = TermSeason.Spring, StartDate = new DateOnly(2026, 2, 20), EndDate = new DateOnly(2026, 7, 1) }; var historicalTask = new TeachingTask { TaskNumber = "2025-2-PASS-01", Name = "已通过课程教学班", AcademicTermId = historicalTerm.Id, CourseId = fixture.SubstituteCourse.Id, Capacity = 30, Status = TeachingTaskStatus.Closed }; var sheet = new GradeSheet { TeachingTaskId = historicalTask.Id, Status = GradeSheetStatus.Published, PublishedAt = DateTime.UtcNow.AddDays(-30), Records = [ new GradeRecord { StudentId = fixture.Student.Id, TotalScore = 86, GradePoint = 3.6m } ] }; fixture.Db.AddRange(historicalTerm, historicalTask, sheet); await fixture.Db.SaveChangesAsync(); var result = await fixture.Controller.ApplySubstitution( new SubstitutionRequest( fixture.CurrentCourse.Id, fixture.SubstituteCourse.Id, "课程内容相近"), CancellationToken.None); Assert.IsType(result); var substitution = Assert.Single(fixture.Db.CourseSubstitutions); Assert.Equal(fixture.CurrentCourse.Id, substitution.OriginalCourseId); Assert.Equal( fixture.SubstituteCourse.Id, substitution.SubstituteCourseId); } private sealed class ApprovalFixture : IAsyncDisposable { private ApprovalFixture( SqliteConnection connection, AppDbContext db, Student student, AcademicTerm currentTerm, Course currentCourse, Course substituteCourse, TeachingTask currentTask, ApprovalsController controller) { Connection = connection; Db = db; Student = student; CurrentTerm = currentTerm; CurrentCourse = currentCourse; SubstituteCourse = substituteCourse; CurrentTask = currentTask; Controller = controller; } private SqliteConnection Connection { get; } public AppDbContext Db { get; } public Student Student { get; } public AcademicTerm CurrentTerm { get; } public Course CurrentCourse { get; } public Course SubstituteCourse { get; } public TeachingTask CurrentTask { get; } public ApprovalsController Controller { 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 major = new Major { Code = "080901", Name = "计算机科学与技术", CollegeId = college.Id, DegreeType = "工学学士" }; var administrativeClass = new AdministrativeClass { Code = "CS2026-01", Name = "计科 2026-1 班", MajorId = major.Id, Grade = 2026 }; var user = new ApplicationUser { Id = Guid.NewGuid(), UserName = "202601001", NormalizedUserName = "202601001", DisplayName = "测试学生" }; var student = new Student { StudentNumber = "202601001", Name = "测试学生", UserId = user.Id, AdministrativeClassId = administrativeClass.Id, EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1) }; var currentTerm = 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), IsCurrent = true }; var currentCourse = CreateCourse(college.Id, "CS101", "程序设计基础"); var substituteCourse = CreateCourse(college.Id, "CS102", "程序设计进阶"); var currentTask = new TeachingTask { TaskNumber = "2026-1-CS101-01", Name = "程序设计基础教学班", AcademicTermId = currentTerm.Id, CourseId = currentCourse.Id, Capacity = 60, Status = TeachingTaskStatus.Published, Classes = [ new TeachingTaskClass { AdministrativeClassId = administrativeClass.Id } ] }; db.AddRange( college, major, administrativeClass, user, student, currentTerm, currentCourse, substituteCourse, currentTask); await db.SaveChangesAsync(); var controller = new ApprovalsController( db, new StudentDataScope(user.Id)); return new ApprovalFixture( connection, db, student, currentTerm, currentCourse, substituteCourse, currentTask, controller); } public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await Connection.DisposeAsync(); } private static Course CreateCourse( Guid collegeId, string code, string name) => new() { Code = code, Name = name, CollegeId = collegeId, Credits = 3, TotalHours = 48, LectureHours = 32, PracticeHours = 16, Nature = CourseNature.MajorRequired, AssessmentMethod = AssessmentMethod.Examination }; } private sealed class StudentDataScope(Guid userId) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( userId, "测试学生", null, DataScope.Self, new HashSet([SystemRoles.Student])); } }