using System.Net.Http.Json; namespace HSchool.AppHost.Tests; [Collection(AppHostCollection.Name)] public class ClassTeacherApiTests(AppHostFixture fixture) { private static readonly DateTime Start = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc); [Fact] public async Task Assign_WritesId_AndSurvivesReload() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Классный сейв", Start); var staffing = await GetStaffingAsync(client, school.Id); var teacher = staffing.Applicants[0]; await HireAsync(client, school.Id, teacher.Id, "Teacher"); var schoolClass = Assert.Single((await GetStaffingAsync(client, school.Id)).Classes.Take(1)); var assigned = await AssignClassTeacherAsync(client, school.Id, schoolClass.Id, teacher.Id); var slot = Assert.Single(assigned.Classes, row => row.Id == schoolClass.Id); Assert.Equal(teacher.Id, slot.ClassTeacherId); Assert.False(string.IsNullOrWhiteSpace(slot.ClassTeacherName)); using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); reload.EnsureSuccessStatusCode(); var after = await GetStaffingAsync(client, school.Id); var reloaded = Assert.Single(after.Classes, row => row.Id == schoolClass.Id); Assert.Equal(teacher.Id, reloaded.ClassTeacherId); var pupil = await FirstPupilOfClassAsync(client, school.Id, schoolClass.Id); var card = await client.GetFromJsonAsync( $"/api/schools/{school.Id}/people/{Uri.EscapeDataString(pupil)}?lang=ru", TestContext.Current.CancellationToken); Assert.NotNull(card); Assert.Equal(teacher.Id, card.ClassTeacherId); Assert.Equal(reloaded.ClassTeacherName, card.ClassTeacherName); } [Fact] public async Task Assign_RejectsNonTeacherAndUnknownPerson() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Классный отказ", Start); var staffing = await GetStaffingAsync(client, school.Id); var schoolClass = staffing.Classes[0]; var pupil = await FirstPupilOfClassAsync(client, school.Id, schoolClass.Id); using var notTeacher = await client.PutAsJsonAsync( $"/api/schools/{school.Id}/classes/{Uri.EscapeDataString(schoolClass.Id)}/class-teacher", new { personId = pupil }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, notTeacher.StatusCode); Assert.Equal("not-teacher", await ProblemCodeAsync(notTeacher)); using var unknown = await client.PutAsJsonAsync( $"/api/schools/{school.Id}/classes/{Uri.EscapeDataString(schoolClass.Id)}/class-teacher", new { personId = "nobody" }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, unknown.StatusCode); Assert.Equal("unknown-person", await ProblemCodeAsync(unknown)); } [Fact] public async Task Guest_CannotAssign() { using var wipe = fixture.App.CreateHttpClient("server"); await SchoolApiTests.WipeAllSavesAsync(wipe); var owner = await CreateUserAsync("ClassTeacherOwner"); var guest = await CreateUserAsync("ClassTeacherGuest"); await ResetUserAsync(owner); var school = await SchoolApiTests.CreateAsync(owner, "Чужой классный", Start); var staffing = await GetStaffingAsync(owner, school.Id); var schoolClass = staffing.Classes[0]; using var response = await guest.PutAsJsonAsync( $"/api/schools/{school.Id}/classes/{Uri.EscapeDataString(schoolClass.Id)}/class-teacher", new { personId = "a0.p0" }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); Assert.Equal("not-owner", await ProblemCodeAsync(response)); owner.Dispose(); guest.Dispose(); using var cleanup = fixture.App.CreateHttpClient("server"); await SchoolApiTests.WipeAllSavesAsync(cleanup); } [Fact] public async Task Clear_EmptiesSlot_AndClassWithoutTeacherIsValid() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Классный снять", Start); var staffing = await GetStaffingAsync(client, school.Id); var teacher = staffing.Applicants[0]; await HireAsync(client, school.Id, teacher.Id, "Teacher"); var schoolClass = (await GetStaffingAsync(client, school.Id)).Classes[0]; await AssignClassTeacherAsync(client, school.Id, schoolClass.Id, teacher.Id); using var clear = await client.DeleteAsync( $"/api/schools/{school.Id}/classes/{Uri.EscapeDataString(schoolClass.Id)}/class-teacher?lang=ru", TestContext.Current.CancellationToken); clear.EnsureSuccessStatusCode(); var after = await clear.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); Assert.NotNull(after); var slot = Assert.Single(after.Classes, row => row.Id == schoolClass.Id); Assert.Null(slot.ClassTeacherId); Assert.Null(slot.ClassTeacherName); Assert.NotEmpty(after.Classes); } [Fact] public async Task TwoClasses_MayShareTheSameTeacher() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Классный двое", Start); var staffing = await GetStaffingAsync(client, school.Id); Assert.True(staffing.Classes.Count >= 2); var teacher = staffing.Applicants[0]; await HireAsync(client, school.Id, teacher.Id, "Teacher"); var first = staffing.Classes[0]; var second = staffing.Classes[1]; await AssignClassTeacherAsync(client, school.Id, first.Id, teacher.Id); var after = await AssignClassTeacherAsync(client, school.Id, second.Id, teacher.Id); Assert.Equal(teacher.Id, Assert.Single(after.Classes, row => row.Id == first.Id).ClassTeacherId); Assert.Equal(teacher.Id, Assert.Single(after.Classes, row => row.Id == second.Id).ClassTeacherId); } 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 AssignClassTeacherAsync( HttpClient client, int schoolId, string classId, string personId) { using var response = await client.PutAsJsonAsync( $"/api/schools/{schoolId}/classes/{Uri.EscapeDataString(classId)}/class-teacher?lang=ru", new { personId }, TestContext.Current.CancellationToken); response.EnsureSuccessStatusCode(); var staffing = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); Assert.NotNull(staffing); return staffing; } private static async Task FirstPupilOfClassAsync(HttpClient client, int schoolId, string classId) { var people = await client.GetFromJsonAsync( $"/api/schools/{schoolId}/people?role=student&pageSize=50", TestContext.Current.CancellationToken); Assert.NotNull(people); foreach (var person in people.People) { var card = await client.GetFromJsonAsync( $"/api/schools/{schoolId}/people/{Uri.EscapeDataString(person.Id)}", TestContext.Current.CancellationToken); if (card?.ClassId == classId) { return person.Id; } } Assert.Fail($"No pupil found for class {classId}."); return string.Empty; } private async Task CreateUserAsync(string userName) => await SchoolApiTests.CreateIsolatedClientAsync(fixture.App, userName); private static async Task ResetUserAsync(HttpClient client) { await SchoolApiTests.LoginAsync(client); var state = await SchoolApiTests.GetSchoolsAsync(client); foreach (var school in state.Schools) { using var response = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken); response.EnsureSuccessStatusCode(); } foreach (var other in state.Others) { using var response = await client.DeleteAsync($"/api/schools/{other.Id}", TestContext.Current.CancellationToken); if (response.StatusCode == HttpStatusCode.Forbidden) { continue; } 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 StaffingResponse( float Allocated, float Payroll, float Remaining, IReadOnlyList Applicants, IReadOnlyList Staff, IReadOnlyList Classes); private sealed record ApplicantResponse(string Id, string FullName); private sealed record StaffMemberResponse(string Id, string FullName, string Position); private sealed record ClassTeacherSlotResponse( string Id, int Year, string Letter, string? ClassTeacherId, string? ClassTeacherName); private sealed record PeopleListResponse(IReadOnlyList People); private sealed record PersonListItemResponse(string Id); private sealed record PersonCardResponse(string Id, string? ClassId, string? ClassTeacherId, string? ClassTeacherName); }