教室预约
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user