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 ClassroomReservationsControllerTests { [Fact] public async Task Create_UsesApplicantsCollegeAndRejectsPublishedClassConflict() { await using var fixture = await ReservationFixture.CreateAsync(); var controller = fixture.Controller(fixture.ApplicantScope); var conflictedRequest = fixture.Request( fixture.ScheduledClassroom.Id, fixture.ReservationDate, 1, 2); var conflicted = await controller.Create( conflictedRequest, CancellationToken.None); Assert.IsType(conflicted); Assert.Empty(fixture.Db.ClassroomReservations); var created = await controller.Create( fixture.Request( fixture.AvailableClassroom.Id, fixture.ReservationDate, 1, 2), CancellationToken.None); Assert.IsType(created); var reservation = Assert.Single(fixture.Db.ClassroomReservations); Assert.Equal(fixture.Applicant.Id, reservation.ApplicantUserId); Assert.Equal(fixture.CollegeA.Id, reservation.ApplicantCollegeId); Assert.Equal(ClassroomReservationStatus.Submitted, reservation.Status); } [Fact] public async Task Review_OnlyAllowsApplicantCollegeAndApprovesAvailableRoom() { await using var fixture = await ReservationFixture.CreateAsync(); var reservation = fixture.AddSubmittedReservation( fixture.AvailableClassroom.Id, fixture.ReservationDate, 3, 2); await fixture.Db.SaveChangesAsync(); var wrongCollege = await fixture.Controller(fixture.CollegeBReviewerScope) .Approve( reservation.Id, new ReviewClassroomReservationRequest(""), CancellationToken.None); Assert.IsType(wrongCollege); Assert.Equal( ClassroomReservationStatus.Submitted, reservation.Status); var approved = await fixture.Controller(fixture.CollegeAReviewerScope) .Approve( reservation.Id, new ReviewClassroomReservationRequest("同意"), CancellationToken.None); Assert.IsType(approved); var stored = await fixture.Db.ClassroomReservations .AsNoTracking() .SingleAsync(item => item.Id == reservation.Id); Assert.Equal(ClassroomReservationStatus.Approved, stored.Status); Assert.Equal(fixture.CollegeAReviewer.Id, stored.ReviewedByUserId); } [Fact] public async Task Approve_RechecksApprovedReservationConflict() { await using var fixture = await ReservationFixture.CreateAsync(); fixture.Db.ClassroomReservations.Add(new ClassroomReservation { ApplicantUserId = fixture.Applicant.Id, ApplicantName = fixture.Applicant.DisplayName, ApplicantCollegeId = fixture.CollegeA.Id, AcademicTermId = fixture.Term.Id, ClassroomId = fixture.AvailableClassroom.Id, ReservationDate = fixture.ReservationDate, StartPeriod = 5, PeriodCount = 2, AttendeeCount = 20, Purpose = "已批准活动", ContactPhone = "13800000000", Status = ClassroomReservationStatus.Approved }); var competing = fixture.AddSubmittedReservation( fixture.AvailableClassroom.Id, fixture.ReservationDate, 6, 2); await fixture.Db.SaveChangesAsync(); var result = await fixture.Controller(fixture.CollegeAReviewerScope) .Approve( competing.Id, new ReviewClassroomReservationRequest(null), CancellationToken.None); Assert.IsType(result); var stored = await fixture.Db.ClassroomReservations .AsNoTracking() .SingleAsync(item => item.Id == competing.Id); Assert.Equal(ClassroomReservationStatus.Submitted, stored.Status); } private sealed class ReservationFixture : IAsyncDisposable { private ReservationFixture( SqliteConnection connection, AppDbContext db, College collegeA, ApplicationUser applicant, ApplicationUser collegeAReviewer, AcademicTerm term, Classroom scheduledClassroom, Classroom availableClassroom, DateOnly reservationDate, ICurrentUserDataScope applicantScope, ICurrentUserDataScope collegeAReviewerScope, ICurrentUserDataScope collegeBReviewerScope) { Connection = connection; Db = db; CollegeA = collegeA; Applicant = applicant; CollegeAReviewer = collegeAReviewer; Term = term; ScheduledClassroom = scheduledClassroom; AvailableClassroom = availableClassroom; ReservationDate = reservationDate; ApplicantScope = applicantScope; CollegeAReviewerScope = collegeAReviewerScope; CollegeBReviewerScope = collegeBReviewerScope; } private SqliteConnection Connection { get; } public AppDbContext Db { get; } public College CollegeA { get; } public ApplicationUser Applicant { get; } public ApplicationUser CollegeAReviewer { get; } public AcademicTerm Term { get; } public Classroom ScheduledClassroom { get; } public Classroom AvailableClassroom { get; } public DateOnly ReservationDate { get; } public ICurrentUserDataScope ApplicantScope { get; } public ICurrentUserDataScope CollegeAReviewerScope { get; } public ICurrentUserDataScope CollegeBReviewerScope { 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 campus = new Campus { Code = "MAIN", Name = "主校区" }; var building = new Building { Code = "A", Name = "明德楼", CampusId = campus.Id }; var scheduledClassroom = new Classroom { Code = "A101", Name = "A101", BuildingId = building.Id, Capacity = 60 }; var availableClassroom = new Classroom { Code = "A102", Name = "A102", BuildingId = building.Id, Capacity = 80 }; var collegeA = new College { Code = "CS", Name = "计算机学院" }; var collegeB = new College { Code = "EE", Name = "电子工程学院" }; var applicant = User("student", "测试申请人", collegeA.Id); var collegeAReviewer = User( "college-a", "计算机学院审核人", collegeA.Id); var collegeBReviewer = User( "college-b", "电子工程学院审核人", collegeB.Id); var term = new AcademicTerm { Code = "2099-1", Name = "2099—2100 学年第一学期", AcademicYear = "2099-2100", Season = TermSeason.Autumn, StartDate = new DateOnly(2099, 9, 1), EndDate = new DateOnly(2100, 1, 31), IsCurrent = true }; var course = new Course { Code = "CS101", Name = "程序设计基础", CollegeId = collegeA.Id, Credits = 3, TotalHours = 48, LectureHours = 32, PracticeHours = 16, Nature = CourseNature.MajorRequired, AssessmentMethod = AssessmentMethod.Examination }; var teachingTask = new TeachingTask { TaskNumber = "2099-1-CS101-01", Name = "程序设计基础教学班", AcademicTermId = term.Id, CourseId = course.Id, Capacity = 60, Status = TeachingTaskStatus.Published }; var reservationDate = term.StartDate; var (_, dayOfWeek) = ClassroomReservationAvailabilityService.ResolveTeachingWeek( term, reservationDate); var plan = new SchedulePlan { AcademicTermId = term.Id, Name = "正式课表", Version = "v1", Status = SchedulePlanStatus.Published, Entries = [ new ScheduleEntry { TeachingTaskId = teachingTask.Id, ClassroomId = scheduledClassroom.Id, DayOfWeek = dayOfWeek, StartPeriod = 1, PeriodCount = 2, StartWeek = 1, EndWeek = 18, WeekPattern = WeekPattern.All } ] }; db.AddRange( campus, building, scheduledClassroom, availableClassroom, collegeA, collegeB, applicant, collegeAReviewer, collegeBReviewer, term, course, teachingTask, plan); await db.SaveChangesAsync(); return new ReservationFixture( connection, db, collegeA, applicant, collegeAReviewer, term, scheduledClassroom, availableClassroom, reservationDate, Scope(applicant, SystemRoles.Student), Scope(collegeAReviewer, SystemRoles.CollegeAdmin), Scope(collegeBReviewer, SystemRoles.CollegeAdmin)); } public ClassroomReservationsController Controller( ICurrentUserDataScope currentScope) => new( Db, currentScope, new ClassroomReservationAvailabilityService(Db)); public CreateClassroomReservationRequest Request( Guid classroomId, DateOnly date, int startPeriod, int periodCount) => new( Term.Id, classroomId, date, startPeriod, periodCount, 30, "学院学术活动", "13800000000", null); public ClassroomReservation AddSubmittedReservation( Guid classroomId, DateOnly date, int startPeriod, int periodCount) { var reservation = new ClassroomReservation { ApplicantUserId = Applicant.Id, ApplicantName = Applicant.DisplayName, ApplicantCollegeId = CollegeA.Id, AcademicTermId = Term.Id, ClassroomId = classroomId, ReservationDate = date, StartPeriod = startPeriod, PeriodCount = periodCount, AttendeeCount = 20, Purpose = "学院交流活动", ContactPhone = "13800000000" }; Db.ClassroomReservations.Add(reservation); return reservation; } public async ValueTask DisposeAsync() { await Db.DisposeAsync(); await Connection.DisposeAsync(); } private static ApplicationUser User( string userName, string displayName, Guid collegeId) => new() { Id = Guid.NewGuid(), UserName = userName, NormalizedUserName = userName.ToUpperInvariant(), DisplayName = displayName, CollegeId = collegeId, IsEnabled = true }; private static ICurrentUserDataScope Scope( ApplicationUser user, string role) => new FixedScope(new CurrentUserScope( user.Id, user.DisplayName, user.CollegeId, role == SystemRoles.CollegeAdmin ? DataScope.College : DataScope.Self, new HashSet([role]))); } private sealed class FixedScope(CurrentUserScope current) : ICurrentUserDataScope { public CurrentUserScope Current { get; } = current; } }