Files
h-school/tests/HSchool.AppHost.Tests/StaffingApiTests.cs
T

261 lines
11 KiB
C#

using System.Net.Http.Json;
namespace HSchool.AppHost.Tests;
[Collection(AppHostCollection.Name)]
public class StaffingApiTests(AppHostFixture fixture)
{
private static readonly DateTime Start = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task GetStaffing_UnknownSchool_IsNotFound()
{
using var client = fixture.App.CreateHttpClient("server");
using var response = await client.GetAsync("/api/schools/999999/staffing", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("unknown-school", await ProblemCodeAsync(response));
}
[Fact]
public async Task GetStaffing_EmptySchool_HasPoolAndNoPayroll()
{
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.Equal(10_000f, staffing.Allocated);
Assert.Equal(0f, staffing.Payroll);
Assert.Equal(10_000f, staffing.Remaining);
Assert.Equal(12, staffing.Applicants.Count);
Assert.Empty(staffing.Staff);
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "Mathematics");
Assert.Contains(staffing.Uncovered, subject => subject.DefName == "PrimarySchool");
}
[Fact]
public async Task Hire_AddsStaff_RemovesApplicant_AndRejectsASecondHireOfTheSamePerson()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Штат наём", Start);
var before = await GetStaffingAsync(client, school.Id);
var applicant = before.Applicants[0];
var after = await HireAsync(client, school.Id, applicant.Id, "Teacher");
Assert.Contains(after.Staff, member => member.Id == applicant.Id && member.Position == "Teacher");
Assert.DoesNotContain(after.Applicants, candidate => candidate.Id == applicant.Id);
Assert.True(after.Payroll > 0f);
Assert.True(after.Payroll <= after.Allocated);
using var again = await client.PostAsJsonAsync(
$"/api/schools/{school.Id}/staff/hire",
new { personId = applicant.Id, position = "Teacher" },
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Conflict, again.StatusCode);
Assert.Equal("already-hired", await ProblemCodeAsync(again));
var people = await client.GetFromJsonAsync<PeopleListResponse>(
$"/api/schools/{school.Id}/people?role=staff&pageSize=10",
TestContext.Current.CancellationToken);
Assert.NotNull(people);
Assert.Contains(people.People, person => person.Id == applicant.Id);
}
[Fact]
public async Task Hire_UnknownApplicant_IsNotFound()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Штат неизвестный", Start);
using var response = await client.PostAsJsonAsync(
$"/api/schools/{school.Id}/staff/hire",
new { personId = "nobody", position = "Teacher" },
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("unknown-applicant", await ProblemCodeAsync(response));
}
[Fact]
public async Task Hire_PastTheCap_IsRejectedWithTheNumbers()
{
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);
ProblemResponse? problem = null;
while (staffing.Applicants.Count > 0)
{
var applicant = staffing.Applicants[0];
using var response = await client.PostAsJsonAsync(
$"/api/schools/{school.Id}/staff/hire",
new { personId = applicant.Id, position = "Teacher" },
TestContext.Current.CancellationToken);
if (response.StatusCode == HttpStatusCode.Conflict)
{
problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
break;
}
response.EnsureSuccessStatusCode();
staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(staffing);
}
Assert.Equal("payroll-exceeded", problem?.Code);
Assert.Equal(10_000f, problem?.Allocated);
Assert.True(problem?.Payroll <= 10_000f);
Assert.True(problem?.Attempted > 10_000f);
Assert.True(staffing.Staff.Count >= 1);
}
[Fact]
public async Task AssignAndUnassign_HonourTheCap_AndReloadKeepsAssignments()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Штат предметы", Start);
var before = await GetStaffingAsync(client, school.Id);
var hired = await HireAsync(client, school.Id, before.Applicants[0].Id, "Teacher");
var teacher = hired.Staff.Single();
var assigned = await AssignAsync(client, school.Id, teacher.Id, "Mathematics");
Assert.Equal(hired.Payroll, assigned.Payroll);
assigned = await AssignAsync(client, school.Id, teacher.Id, "RussianLanguage");
Assert.True(assigned.Payroll > hired.Payroll);
Assert.Equal(2, assigned.Staff.Single().Subjects.Count);
using var over = await client.PostAsJsonAsync(
$"/api/schools/{school.Id}/staff/{Uri.EscapeDataString(teacher.Id)}/subjects",
new { subject = "Literature" },
TestContext.Current.CancellationToken);
// Cheap asks may still fit; only the numbers matter when the cap actually bites.
if (over.StatusCode == HttpStatusCode.Conflict)
{
var problem = await over.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
Assert.Equal("payroll-exceeded", problem?.Code);
Assert.True(problem?.Attempted > problem?.Allocated);
Assert.Equal(assigned.Payroll, problem?.Payroll);
}
else
{
over.EnsureSuccessStatusCode();
assigned = await over.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(assigned);
}
var payrollWithSubjects = assigned.Payroll;
using var unassign = await client.DeleteAsync(
$"/api/schools/{school.Id}/staff/{Uri.EscapeDataString(teacher.Id)}/subjects/RussianLanguage",
TestContext.Current.CancellationToken);
unassign.EnsureSuccessStatusCode();
var afterDrop = await unassign.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(afterDrop);
Assert.True(afterDrop.Payroll < payrollWithSubjects);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
var restored = await GetStaffingAsync(client, school.Id);
Assert.Equal(afterDrop.Payroll, restored.Payroll);
Assert.Equal(teacher.Id, restored.Staff.Single().Id);
Assert.Equal(
afterDrop.Staff.Single().Subjects.Select(subject => subject.DefName),
restored.Staff.Single().Subjects.Select(subject => subject.DefName));
}
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<StaffingResponse> 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();
var staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(staffing);
return staffing;
}
private static async Task<StaffingResponse> 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();
var staffing = await response.Content.ReadFromJsonAsync<StaffingResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(staffing);
return staffing;
}
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, float? Allocated, float? Payroll, float? Remaining, float? Attempted);
private sealed record StaffingResponse(
float Allocated,
float Payroll,
float Remaining,
IReadOnlyList<UncoveredSubjectResponse> Uncovered,
IReadOnlyList<ApplicantResponse> Applicants,
IReadOnlyList<StaffMemberResponse> Staff);
private sealed record UncoveredSubjectResponse(string DefName, string Label, int GradeMin, int GradeMax, int HoursPerWeek);
private sealed record ApplicantResponse(string Id, string FullName, bool Female, int Age, bool IsParent, float HourlyWageAsk, float MonthlyBase);
private sealed record StaffMemberResponse(
string Id,
string FullName,
bool Female,
int Age,
bool IsParent,
string Position,
string PositionLabel,
float HourlyWageAsk,
float MonthlyPay,
IReadOnlyList<DefLabelResponse> Subjects);
private sealed record DefLabelResponse(string DefName, string Label);
private sealed record PeopleListResponse(
int Total,
int Page,
int PageSize,
IReadOnlyList<PersonListItemResponse> People,
object Filters);
private sealed record PersonListItemResponse(
string Id,
string FullName,
string Surname,
string Given,
string Patronymic,
bool Female,
int Age,
IReadOnlyList<string> Roles,
int? ClassYear,
string? ClassLetter,
string? Position,
string? PositionLabel);
}