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 Jiaowu.Api.Infrastructure.Timetables; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; namespace Jiaowu.Api.Tests; public sealed class ExperimentsControllerTests { [Fact] public async Task CentralizedProject_PublishesAndUsesTeachingTaskRoster() { await using var fixture = await ExperimentFixture.CreateAsync(); var controller = fixture.Controller(fixture.ManagerScope); var created = await controller.CreateProject( fixture.ProjectRequest(ExperimentArrangementMode.Centralized), CancellationToken.None); Assert.IsType(created); var project = await fixture.Db.ExperimentProjects.SingleAsync(); var sessionResult = await controller.CreateSession( project.Id, fixture.SessionRequest(1, 2, 10), CancellationToken.None); Assert.IsType(sessionResult); Assert.Equal( 1, (await fixture.Db.ExperimentSessions.SingleAsync()).Capacity); var published = await controller.PublishProject( project.Id, CancellationToken.None); Assert.IsType(published); Assert.Equal( ExperimentProjectStatus.Published, (await fixture.Db.ExperimentProjects.SingleAsync()).Status); var session = await fixture.Db.ExperimentSessions.SingleAsync(); var participants = await controller.GetParticipants( session.Id, CancellationToken.None); var ok = Assert.IsType(participants); var rows = Assert.IsAssignableFrom>(ok.Value); Assert.Single(rows); } [Fact] public async Task SelfScheduledBooking_IsSinglePerProjectAndCanBeChanged() { await using var fixture = await ExperimentFixture.CreateAsync(); var manager = fixture.Controller(fixture.ManagerScope); await manager.CreateProject( fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled), CancellationToken.None); var project = await fixture.Db.ExperimentProjects.SingleAsync(); await manager.CreateSession( project.Id, fixture.SessionRequest(3, 2), CancellationToken.None); await manager.CreateSession( project.Id, fixture.SessionRequest(5, 2), CancellationToken.None); await manager.PublishProject(project.Id, CancellationToken.None); var sessions = await fixture.Db.ExperimentSessions .OrderBy(x => x.StartPeriod) .ToListAsync(); var student = fixture.Controller(fixture.StudentScope); Assert.IsType( await student.Book(sessions[0].Id, CancellationToken.None)); Assert.IsType( await student.Book(sessions[1].Id, CancellationToken.None)); var booking = await fixture.Db.ExperimentBookings.SingleAsync(); Assert.IsType( await student.CancelBooking(booking.Id, CancellationToken.None)); Assert.IsType( await student.Book(sessions[1].Id, CancellationToken.None)); var active = await fixture.Db.ExperimentBookings.SingleAsync(); Assert.Equal(ExperimentBookingStatus.Booked, active.Status); Assert.Equal(sessions[1].Id, active.ExperimentSessionId); Assert.Equal(0, (await fixture.Db.ExperimentSessions.FindAsync( sessions[0].Id))!.ReservedCount); Assert.Equal(1, (await fixture.Db.ExperimentSessions.FindAsync( sessions[1].Id))!.ReservedCount); } [Fact] public async Task StudentBooking_RejectsPublishedTimetableConflict() { await using var fixture = await ExperimentFixture.CreateAsync(); var manager = fixture.Controller(fixture.ManagerScope); await manager.CreateProject( fixture.ProjectRequest(ExperimentArrangementMode.SelfScheduled), CancellationToken.None); var project = await fixture.Db.ExperimentProjects.SingleAsync(); await manager.CreateSession( project.Id, fixture.SessionRequest(1, 2), CancellationToken.None); await manager.PublishProject(project.Id, CancellationToken.None); fixture.Db.SchedulePlans.Add(new SchedulePlan { AcademicTermId = fixture.Term.Id, Name = "正式课表", Version = "V1", Status = SchedulePlanStatus.Published, Entries = [ new ScheduleEntry { TeachingTaskId = fixture.Task.Id, ClassroomId = fixture.SecondClassroom.Id, DayOfWeek = 1, StartPeriod = 1, PeriodCount = 2, StartWeek = 1, EndWeek = 18, WeekPattern = WeekPattern.All } ] }); await fixture.Db.SaveChangesAsync(); var session = await fixture.Db.ExperimentSessions.SingleAsync(); var result = await fixture.Controller(fixture.StudentScope) .Book(session.Id, CancellationToken.None); Assert.IsType(result); Assert.Empty(fixture.Db.ExperimentBookings); } private sealed class ExperimentFixture : IAsyncDisposable { private ExperimentFixture( SqliteConnection connection, AppDbContext db, AcademicTerm term, TeachingTask task, Classroom classroom, Classroom secondClassroom, ICurrentUserDataScope managerScope, ICurrentUserDataScope studentScope) { Connection = connection; Db = db; Term = term; Task = task; Classroom = classroom; SecondClassroom = secondClassroom; ManagerScope = managerScope; StudentScope = studentScope; } private SqliteConnection Connection { get; } public AppDbContext Db { get; } public AcademicTerm Term { get; } public TeachingTask Task { get; } public Classroom Classroom { get; } public Classroom SecondClassroom { get; } public ICurrentUserDataScope ManagerScope { get; } public ICurrentUserDataScope StudentScope { 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 manager = User("manager", "学院实验管理员"); var studentUser = User("student", "实验学生"); var campus = new Campus { Code = "MAIN", Name = "主校区" }; var building = new Building { Code = "LAB", Name = "实验中心", CampusId = campus.Id }; var classroom = new Classroom { Code = "LAB101", Name = "实验室 101", BuildingId = building.Id, Capacity = 40 }; var secondClassroom = new Classroom { Code = "LAB102", Name = "实验室 102", BuildingId = building.Id, Capacity = 40 }; var college = new College { Code = "CS", Name = "计算机学院" }; manager.CollegeId = college.Id; var major = new Major { Code = "CS", Name = "计算机科学与技术", CollegeId = college.Id, DegreeType = "工学" }; var administrativeClass = new AdministrativeClass { Code = "CS2099", Name = "计科 2099", MajorId = major.Id, Grade = 2099 }; var student = new Student { StudentNumber = "20990001", Name = "实验学生", AdministrativeClassId = administrativeClass.Id, EnrollmentYear = 2099, EnrollmentDate = new DateOnly(2099, 9, 1), UserId = studentUser.Id }; var teacher = new Teacher { TeacherNumber = "T2099", Name = "实验教师", CollegeId = college.Id }; var term = new AcademicTerm { Code = "2099-1", Name = "2099—2100 学年第一学期", AcademicYear = "2099-2100", Season = TermSeason.Autumn, StartDate = new DateOnly(2099, 9, 7), EndDate = new DateOnly(2100, 1, 17), IsCurrent = true }; var course = new Course { Code = "CSLAB", Name = "系统实验", CollegeId = college.Id, Credits = 2, TotalHours = 32, LectureHours = 16, PracticeHours = 16, Nature = CourseNature.Practice, AssessmentMethod = AssessmentMethod.Assessment }; var task = new TeachingTask { TaskNumber = "2099-1-CSLAB-01", Name = "系统实验教学班", AcademicTermId = term.Id, CourseId = course.Id, Capacity = 40, Status = TeachingTaskStatus.Published, Teachers = [ new TeachingTaskTeacher { TeacherId = teacher.Id, IsPrimary = true } ], Classes = [ new TeachingTaskClass { AdministrativeClassId = administrativeClass.Id } ] }; db.AddRange( manager, studentUser, campus, building, classroom, secondClassroom, college, major, administrativeClass, student, teacher, term, course, task); for (var period = 1; period <= 12; period++) { db.ScheduleTimeSlots.Add(new ScheduleTimeSlot { AcademicTermId = term.Id, PeriodNumber = period, Name = $"第 {period} 节", StartsAt = new TimeOnly(8, 0).AddMinutes((period - 1) * 50), EndsAt = new TimeOnly(8, 45).AddMinutes((period - 1) * 50) }); } await db.SaveChangesAsync(); return new ExperimentFixture( connection, db, term, task, classroom, secondClassroom, Scope( manager, SystemRoles.CollegeAdmin, DataScope.College), Scope(studentUser, SystemRoles.Student, DataScope.Self)); } public ExperimentsController Controller( ICurrentUserDataScope currentScope) => new( Db, currentScope, new ClassroomReservationAvailabilityService(Db)); public ExperimentProjectRequest ProjectRequest( ExperimentArrangementMode mode) => new( Task.Id, mode == ExperimentArrangementMode.Centralized ? "LAB-C" : "LAB-S", mode == ExperimentArrangementMode.Centralized ? "集中上机实验" : "自主上机实验", mode, "完成规定实验项目。", "携带校园卡。", Term.StartDate, Term.StartDate.AddDays(14)); public ExperimentSessionRequest SessionRequest( int startPeriod, int periodCount, int capacity = 1) => new( Classroom.Id, Term.StartDate, startPeriod, periodCount, capacity, null); public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await Connection.DisposeAsync(); } private static ApplicationUser User( string userName, string displayName) => new() { Id = Guid.NewGuid(), UserName = userName, NormalizedUserName = userName.ToUpperInvariant(), DisplayName = displayName, IsEnabled = true }; private static ICurrentUserDataScope Scope( ApplicationUser user, string role, DataScope dataScope) => new FixedScope(new CurrentUserScope( user.Id, user.DisplayName, user.CollegeId, dataScope, new HashSet([role]))); } private sealed class FixedScope(CurrentUserScope current) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = current; } }