Add class-teacher slots on SchoolClass with owner-only assign/clear.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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<PersonCardResponse>(
|
||||
$"/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<StaffingResponse>(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<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<StaffingResponse> 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<StaffingResponse>(TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(staffing);
|
||||
return staffing;
|
||||
}
|
||||
|
||||
private static async Task<string> FirstPupilOfClassAsync(HttpClient client, int schoolId, string classId)
|
||||
{
|
||||
var people = await client.GetFromJsonAsync<PeopleListResponse>(
|
||||
$"/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<PersonCardResponse>(
|
||||
$"/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<HttpClient> 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<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 StaffingResponse(
|
||||
float Allocated,
|
||||
float Payroll,
|
||||
float Remaining,
|
||||
IReadOnlyList<ApplicantResponse> Applicants,
|
||||
IReadOnlyList<StaffMemberResponse> Staff,
|
||||
IReadOnlyList<ClassTeacherSlotResponse> 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<PersonListItemResponse> People);
|
||||
|
||||
private sealed record PersonListItemResponse(string Id);
|
||||
|
||||
private sealed record PersonCardResponse(string Id, string? ClassId, string? ClassTeacherId, string? ClassTeacherName);
|
||||
}
|
||||
Reference in New Issue
Block a user