教室预约

This commit is contained in:
2026-07-26 20:11:56 +08:00 Unverified
parent b0679a253d
commit 9f534c22f4
14 changed files with 7436 additions and 31 deletions
@@ -0,0 +1,389 @@
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<ConflictObjectResult>(conflicted);
Assert.Empty(fixture.Db.ClassroomReservations);
var created = await controller.Create(
fixture.Request(
fixture.AvailableClassroom.Id,
fixture.ReservationDate,
1,
2),
CancellationToken.None);
Assert.IsType<CreatedResult>(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<ForbidResult>(wrongCollege);
Assert.Equal(
ClassroomReservationStatus.Submitted,
reservation.Status);
var approved = await fixture.Controller(fixture.CollegeAReviewerScope)
.Approve(
reservation.Id,
new ReviewClassroomReservationRequest("同意"),
CancellationToken.None);
Assert.IsType<NoContentResult>(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<ConflictObjectResult>(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<ReservationFixture> CreateAsync()
{
var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<AppDbContext>()
.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<string>([role])));
}
private sealed class FixedScope(CurrentUserScope current)
: ICurrentUserDataScope
{
public CurrentUserScope Current { get; } = current;
}
}