using ClosedXML.Excel; 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 TeachingWorkflowRosterTests { [Fact] public async Task ClassAssignedStudents_AppearAcrossTeachingAndExamWorkflows() { await using var connection = new SqliteConnection("Data Source=:memory:"); await connection.OpenAsync(); var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; await using var db = new AppDbContext(options); await db.Database.EnsureCreatedAsync(); var teacherUserId = Guid.NewGuid(); var studentUserId = Guid.NewGuid(); var teacherUser = new ApplicationUser { Id = teacherUserId, UserName = "T001", NormalizedUserName = "T001", DisplayName = "张老师" }; var studentUser = new ApplicationUser { Id = studentUserId, UserName = "202601001", NormalizedUserName = "202601001", DisplayName = "周同学" }; var campus = new Campus { Code = "MAIN", Name = "主校区" }; var college = new College { Code = "CS", Name = "计算机学院", CampusId = campus.Id }; var building = new Building { Code = "B01", Name = "第一教学楼", CampusId = campus.Id }; var classroom = new Classroom { Code = "B01-101", Name = "101", BuildingId = building.Id, Capacity = 60 }; 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 student = new Student { StudentNumber = "202601001", Name = "周同学", AdministrativeClassId = administrativeClass.Id, EnrollmentYear = 2026, EnrollmentDate = new DateOnly(2026, 9, 1), UserId = studentUserId }; 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 } ], Classes = [ new TeachingTaskClass { AdministrativeClassId = administrativeClass.Id } ] }; db.AddRange( teacherUser, studentUser, campus, college, building, classroom, major, administrativeClass, student, teacher, course, term, task); await db.SaveChangesAsync(); var examSession = new ExamSession { TeachingTaskId = task.Id, ExamDate = new DateOnly(2027, 1, 8), StartPeriod = 1, PeriodCount = 2, StartsAt = new DateTime(2027, 1, 8, 8, 0, 0, DateTimeKind.Utc), EndsAt = new DateTime(2027, 1, 8, 9, 50, 0, DateTimeKind.Utc), RequiredInvigilatorCount = 1 }; var examPlan = new ExamPlan { AcademicTermId = term.Id, Name = "期末考试", Status = ExamPlanStatus.Draft, Sessions = [examSession], Rooms = [ new ExamRoomAssignment { CourseId = course.Id, ClassroomId = classroom.Id, ExamDate = new DateOnly(2027, 1, 8), StartPeriod = 1, PeriodCount = 2, StartsAt = new DateTime(2027, 1, 8, 8, 0, 0, DateTimeKind.Utc), EndsAt = new DateTime(2027, 1, 8, 9, 50, 0, DateTimeKind.Utc), RequiredInvigilatorCount = 1, SessionLinks = [ new ExamRoomSession { ExamSessionId = examSession.Id } ], Seats = [ new ExamSeatAssignment { ExamSessionId = examSession.Id, StudentId = student.Id, SeatNumber = 1 } ], Invigilators = [ new ExamRoomInvigilator { TeacherId = teacher.Id } ] } ] }; db.ExamPlans.Add(examPlan); await db.SaveChangesAsync(); var scope = new TeacherDataScope(teacherUserId); var attendance = new AttendanceController(db, scope); var attendanceResult = await attendance.CreateSheet( new AttendanceSheetRequest( task.Id, "第1周点名", new DateTime(2026, 9, 3), null, AttendanceCheckInMethod.Manual, null, null, null, null), CancellationToken.None); Assert.IsType(attendanceResult); Assert.Equal(1, await db.AttendanceRecords.CountAsync()); var grades = new GradesController(db, scope); var gradeResult = await grades.CreateSheet( new GradeSheetRequest(task.Id, 40, 60, []), CancellationToken.None); Assert.IsType(gradeResult); Assert.Equal(1, await db.GradeRecords.CountAsync()); var selections = new CourseSelectionsController(db, scope); Assert.Single(ReadItems(await selections.GetMyTeachingTasks( term.Id, CancellationToken.None))); var roster = Assert.IsType( await selections.GetTeachingTaskRoster(task.Id, CancellationToken.None)); Assert.Equal(1, ReadIntProperty(roster.Value!, "StudentCount")); var adjustments = new CourseAdjustmentsController(db, scope); Assert.Single(ReadItems(await adjustments.GetTaskOptions( term.Id, CancellationToken.None))); var managerScope = new ManagerDataScope(); var exams = new ExamsController( db, managerScope, NoOpAppCache.Instance); var publishResult = await exams.Publish(examPlan.Id, CancellationToken.None); Assert.IsType(publishResult); var planResult = Assert.IsType( await exams.GetPlan( examPlan.Id, cancellationToken: CancellationToken.None)); var planSessions = ReadEnumerableProperty(planResult.Value!, "Sessions"); Assert.Equal(1, ReadIntProperty(planSessions.Single(), "StudentCount")); var returnedRoom = Assert.Single( ReadEnumerableProperty(planSessions.Single(), "ExamRooms")); Assert.Equal( classroom.Id, returnedRoom.GetType().GetProperty("ClassroomId")!.GetValue(returnedRoom)); Assert.Equal(1, ReadIntProperty(returnedRoom, "SeatCount")); Assert.Equal(1, ReadIntProperty(returnedRoom, "TotalSeatCount")); Assert.False( (bool)returnedRoom.GetType().GetProperty("IsMixed")! .GetValue(returnedRoom)!); Assert.Equal( [teacher.Id], ReadEnumerableProperty(returnedRoom, "InvigilatorIds").Cast()); Assert.Equal( [teacher.Name], ReadEnumerableProperty(returnedRoom, "InvigilatorNames").Cast()); var examSessionId = examPlan.Sessions.Single().Id; var rosterResult = Assert.IsType( await exams.GetRoster(examSessionId, CancellationToken.None)); Assert.Single(ReadEnumerableProperty(rosterResult.Value!, "Students")); var studentExams = new ExamsController( db, new StudentDataScope(studentUserId), NoOpAppCache.Instance); Assert.Single(ReadItems( await studentExams.GetMySchedule(CancellationToken.None))); var teacherExams = new ExamsController( db, scope, NoOpAppCache.Instance); Assert.Single(ReadItems( await teacherExams.GetMySchedule(CancellationToken.None))); var exportResult = Assert.IsType( await exams.ExportSignInSheets(examPlan.Id, CancellationToken.None)); using var workbookStream = new MemoryStream(exportResult.FileContents); using var workbook = new XLWorkbook(workbookStream); Assert.Equal(2, workbook.Worksheets.Count); var signInSheet = workbook.Worksheets.Single(x => x.Name != "考场汇总"); Assert.Equal("202601001", signInSheet.Cell("C8").GetString()); Assert.Equal("周同学", signInSheet.Cell("D8").GetString()); } private static List ReadItems(ActionResult result) { var ok = Assert.IsType(result); return Assert.IsAssignableFrom(ok.Value) .Cast() .ToList(); } private static int ReadIntProperty(object value, string name) => (int)value.GetType().GetProperty(name)!.GetValue(value)!; private static List ReadEnumerableProperty(object value, string name) => Assert.IsAssignableFrom( value.GetType().GetProperty(name)!.GetValue(value)) .Cast() .ToList(); private sealed class TeacherDataScope(Guid userId) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( userId, "张老师", null, DataScope.Self, new HashSet([SystemRoles.Teacher])); } private sealed class StudentDataScope(Guid userId) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( userId, "周同学", null, DataScope.Self, new HashSet([SystemRoles.Student])); } private sealed class ManagerDataScope : ICurrentUserDataScope { public CurrentUserScope Current { get; } = new( Guid.NewGuid(), "考试管理员", null, DataScope.All, new HashSet([SystemRoles.AcademicAdmin])); } }