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 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 GetTimetableAsync(HttpClient client, int schoolId) { var table = await client.GetFromJsonAsync( $"/api/schools/{schoolId}/timetable?lang=ru", TestContext.Current.CancellationToken); Assert.NotNull(table); return table; } private static async Task GetStaffingAsync(HttpClient client, int schoolId) { var staffing = await client.GetFromJsonAsync( $"/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 ProblemCodeAsync(HttpResponseMessage response) { var problem = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); return problem?.Code; } private sealed record ProblemResponse(string? Code); private sealed record TimetableResponse( int WeekDays, int LessonCount, IReadOnlyList Lessons, IReadOnlyList 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 Applicants, IReadOnlyList Staff); private sealed record ApplicantResponse(string Id); private sealed record StaffMemberResponse(string Id); }