390 lines
14 KiB
C#
390 lines
14 KiB
C#
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;
|
|
}
|
|
}
|