Update wire protocol to version 6 and enhance timetable functionality
- Bumped the wire protocol version to 6, reflecting changes in the communication structure.
- Expanded the timetable API with new endpoints for fetching and managing lesson schedules, including `GET /api/schools/{id}/timetable` and `POST /api/schools/{id}/timetable/pin`.
- Updated the protocol documentation to include detailed descriptions of the new timetable features and message structures.
- Enhanced the client-side implementation to support the new timetable functionalities, including lesson pinning and unpinning.
- Revised server-side logic to handle timetable operations and ensure proper integration with existing school management features.
- Added tests to validate the new timetable functionalities and ensure robustness in handling lesson data.
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
[Collection(AppHostCollection.Name)]
|
||||
public class TimetableApiTests(AppHostFixture fixture)
|
||||
{
|
||||
private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
|
||||
private static readonly DateTime SaturdayMorning = new(2012, 4, 7, 10, 20, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public async Task GetTimetable_UnknownSchool_IsNotFound()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
|
||||
using var response = await client.GetAsync("/api/schools/999999/timetable", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
Assert.Equal("unknown-school", await ProblemCodeAsync(response));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HireMathematics_PlacesLessons_UnassignUncoversThem()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Расписание наём", TuesdayMorning);
|
||||
var teacher = await HireMathAsync(client, school.Id);
|
||||
|
||||
var table = await GetTimetableAsync(client, school.Id);
|
||||
Assert.Equal(5, table.WeekDays);
|
||||
Assert.Equal(7, table.LessonCount);
|
||||
Assert.Contains(table.Lessons, lesson => lesson.Subject == "Mathematics" && lesson.TeacherId == teacher);
|
||||
Assert.DoesNotContain(table.Uncovered, row => row.Subject == "Mathematics");
|
||||
|
||||
using var unassign = await client.DeleteAsync(
|
||||
$"/api/schools/{school.Id}/staff/{Uri.EscapeDataString(teacher)}/subjects/Mathematics",
|
||||
TestContext.Current.CancellationToken);
|
||||
unassign.EnsureSuccessStatusCode();
|
||||
|
||||
var after = await GetTimetableAsync(client, school.Id);
|
||||
Assert.DoesNotContain(after.Lessons, lesson => lesson.Subject == "Mathematics");
|
||||
Assert.Contains(after.Uncovered, row => row.Subject == "Mathematics");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PinThenHireAnother_KeepsTheLockedSlot()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Расписание закрепление", TuesdayMorning);
|
||||
var first = await HireMathAsync(client, school.Id);
|
||||
|
||||
var table = await GetTimetableAsync(client, school.Id);
|
||||
var pinned = table.Lessons.First(lesson => lesson.Subject == "Mathematics");
|
||||
|
||||
using var pin = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/timetable/pin",
|
||||
new { pinned.ClassId, pinned.Subject, pinned.RoomId, pinned.Day, pinned.Period },
|
||||
TestContext.Current.CancellationToken);
|
||||
pin.EnsureSuccessStatusCode();
|
||||
|
||||
var staffing = await GetStaffingAsync(client, school.Id);
|
||||
var secondApplicant = staffing.Applicants[0];
|
||||
await HireAsync(client, school.Id, secondApplicant.Id, "Teacher");
|
||||
await AssignAsync(client, school.Id, secondApplicant.Id, "Mathematics");
|
||||
|
||||
var after = await GetTimetableAsync(client, school.Id);
|
||||
Assert.Contains(
|
||||
after.Lessons,
|
||||
lesson =>
|
||||
lesson.ClassId == pinned.ClassId
|
||||
&& lesson.Subject == pinned.Subject
|
||||
&& lesson.TeacherId == first
|
||||
&& lesson.RoomId == pinned.RoomId
|
||||
&& lesson.Day == pinned.Day
|
||||
&& lesson.Period == pinned.Period
|
||||
&& lesson.Locked);
|
||||
|
||||
using var gym = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/timetable/pin",
|
||||
new { pinned.ClassId, subject = "Mathematics", roomId = "gym-hall", day = 0, period = 1 },
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Conflict, gym.StatusCode);
|
||||
Assert.Equal("pin-rejected", await ProblemCodeAsync(gym));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reload_RestoresLockedLessons()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Расписание диск", TuesdayMorning);
|
||||
await HireMathAsync(client, school.Id);
|
||||
|
||||
var table = await GetTimetableAsync(client, school.Id);
|
||||
var pinned = table.Lessons.First(lesson => lesson.Subject == "Mathematics");
|
||||
using var pin = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/timetable/pin",
|
||||
new { pinned.ClassId, pinned.Subject, pinned.RoomId, pinned.Day, pinned.Period },
|
||||
TestContext.Current.CancellationToken);
|
||||
pin.EnsureSuccessStatusCode();
|
||||
|
||||
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
||||
reload.EnsureSuccessStatusCode();
|
||||
|
||||
var restored = await GetTimetableAsync(client, school.Id);
|
||||
Assert.Contains(
|
||||
restored.Lessons,
|
||||
lesson =>
|
||||
lesson.ClassId == pinned.ClassId
|
||||
&& lesson.Subject == pinned.Subject
|
||||
&& lesson.RoomId == pinned.RoomId
|
||||
&& lesson.Day == pinned.Day
|
||||
&& lesson.Period == pinned.Period
|
||||
&& lesson.Locked);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Saturday_HasNoOccupancyOnTheMap()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.ResetAsync(client);
|
||||
var school = await SchoolApiTests.CreateAsync(client, "Расписание суббота", SaturdayMorning);
|
||||
await HireMathAsync(client, school.Id);
|
||||
|
||||
var table = await GetTimetableAsync(client, school.Id);
|
||||
Assert.Contains(table.Lessons, lesson => lesson.Subject == "Mathematics");
|
||||
}
|
||||
|
||||
private static async Task<string> HireMathAsync(HttpClient client, int schoolId)
|
||||
{
|
||||
var staffing = await GetStaffingAsync(client, schoolId);
|
||||
var applicant = staffing.Applicants[0];
|
||||
await HireAsync(client, schoolId, applicant.Id, "Teacher");
|
||||
await AssignAsync(client, schoolId, applicant.Id, "Mathematics");
|
||||
return applicant.Id;
|
||||
}
|
||||
|
||||
private static async Task<TimetableResponse> GetTimetableAsync(HttpClient client, int schoolId)
|
||||
{
|
||||
var table = await client.GetFromJsonAsync<TimetableResponse>(
|
||||
$"/api/schools/{schoolId}/timetable?lang=ru",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(table);
|
||||
return table;
|
||||
}
|
||||
|
||||
private static async Task<StaffingResponse> GetStaffingAsync(HttpClient client, int schoolId)
|
||||
{
|
||||
var staffing = await client.GetFromJsonAsync<StaffingResponse>(
|
||||
$"/api/schools/{schoolId}/staffing?lang=ru",
|
||||
TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
return staffing;
|
||||
}
|
||||
|
||||
private static async Task HireAsync(HttpClient client, int schoolId, string personId, string position)
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{schoolId}/staff/hire",
|
||||
new { personId, position },
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private static async Task AssignAsync(HttpClient client, int schoolId, string personId, string subject)
|
||||
{
|
||||
using var response = await client.PostAsJsonAsync(
|
||||
$"/api/schools/{schoolId}/staff/{Uri.EscapeDataString(personId)}/subjects",
|
||||
new { subject },
|
||||
TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
private static async Task<string?> ProblemCodeAsync(HttpResponseMessage response)
|
||||
{
|
||||
var problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
|
||||
return problem?.Code;
|
||||
}
|
||||
|
||||
private sealed record ProblemResponse(string? Code);
|
||||
|
||||
private sealed record TimetableResponse(
|
||||
int WeekDays,
|
||||
int LessonCount,
|
||||
IReadOnlyList<LessonResponse> Lessons,
|
||||
IReadOnlyList<UncoveredResponse> Uncovered);
|
||||
|
||||
private sealed record LessonResponse(
|
||||
string ClassId,
|
||||
int ClassYear,
|
||||
string ClassLetter,
|
||||
string Subject,
|
||||
string SubjectLabel,
|
||||
string TeacherId,
|
||||
string TeacherName,
|
||||
string RoomId,
|
||||
int Day,
|
||||
int Period,
|
||||
bool Locked);
|
||||
|
||||
private sealed record UncoveredResponse(
|
||||
string ClassId,
|
||||
int ClassYear,
|
||||
string ClassLetter,
|
||||
string Subject,
|
||||
string SubjectLabel,
|
||||
int Hours);
|
||||
|
||||
private sealed record StaffingResponse(
|
||||
IReadOnlyList<ApplicantResponse> Applicants,
|
||||
IReadOnlyList<StaffMemberResponse> Staff);
|
||||
|
||||
private sealed record ApplicantResponse(string Id);
|
||||
|
||||
private sealed record StaffMemberResponse(string Id);
|
||||
}
|
||||
Reference in New Issue
Block a user