Implement people management features by adding API endpoints for retrieving school rosters and individual person cards. Enhance the UI to support a people browser with filtering and pagination capabilities. Update localization strings for improved user experience and ensure robust handling of person data. Revise documentation to reflect new API functionalities and update tests to validate the new features.
ci / server (push) Failing after 11s
ci / client (push) Successful in 17s

This commit is contained in:
Leonid Pershin
2026-08-18 19:44:42 +03:00
parent 52c5082418
commit 533bd80f5e
23 changed files with 2134 additions and 47 deletions
@@ -0,0 +1,190 @@
using System.Net.Http.Json;
namespace HSchool.AppHost.Tests;
[Collection(AppHostCollection.Name)]
public class PeopleApiTests(AppHostFixture fixture)
{
private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task List_FiltersByRoleAndYear()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Люди фильтр", Start);
var page = await GetPeopleAsync(client, school.Id, "role=student&year=5&pageSize=100");
Assert.NotEmpty(page.People);
Assert.Equal(page.People.Count, page.Total);
Assert.All(page.People, person =>
{
Assert.Contains("student", person.Roles);
Assert.Equal(5, person.ClassYear);
});
Assert.Contains(5, page.Filters.Years);
}
[Fact]
public async Task List_SortsBySurname()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Люди сорт", Start);
var page = await GetPeopleAsync(client, school.Id, "sort=surname&pageSize=20");
var surnames = page.People.Select(person => person.Surname).ToArray();
Assert.Equal(surnames.OrderBy(name => name, StringComparer.Ordinal), surnames);
}
[Fact]
public async Task List_PageBounds()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Люди страницы", Start);
var first = await GetPeopleAsync(client, school.Id, "page=1&pageSize=10");
Assert.True(first.Total > 10);
Assert.Equal(10, first.People.Count);
Assert.Equal(1, first.Page);
Assert.Equal(10, first.PageSize);
var past = await GetPeopleAsync(client, school.Id, "page=999&pageSize=10");
Assert.Equal(first.Total, past.Total);
Assert.Empty(past.People);
using var zero = await client.GetAsync($"/api/schools/{school.Id}/people?page=0", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, zero.StatusCode);
Assert.Equal("invalid-query", await ProblemCodeAsync(zero));
using var huge = await client.GetAsync($"/api/schools/{school.Id}/people?pageSize=101", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, huge.StatusCode);
Assert.Equal("invalid-query", await ProblemCodeAsync(huge));
}
[Fact]
public async Task List_UnknownSchool_IsNotFound()
{
using var client = fixture.App.CreateHttpClient("server");
using var response = await client.GetAsync("/api/schools/999999/people", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("unknown-school", await ProblemCodeAsync(response));
}
[Fact]
public async Task Card_UnknownPerson_IsNotFound()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Люди карточка", Start);
using var response = await client.GetAsync(
$"/api/schools/{school.Id}/people/nobody",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("unknown-person", await ProblemCodeAsync(response));
}
[Fact]
public async Task Card_IncludesFamilyAndLiveNeeds()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Люди семья", Start);
var page = await GetPeopleAsync(client, school.Id, "role=student&year=5&pageSize=10");
var pupil = page.People[0];
var card = await client.GetFromJsonAsync<PersonCardResponse>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(pupil.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.Equal(pupil.Id, card.Id);
Assert.Equal(2, card.Family.Parents.Count);
Assert.Contains(card.Needs, need => need.Id == "Sleep" && need.Value == 1f);
Assert.NotEmpty(card.Body);
Assert.NotEmpty(card.Skills);
var mother = card.Family.Parents.Single(parent => parent.Female);
var motherCard = await client.GetFromJsonAsync<PersonCardResponse>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(mother.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(motherCard);
Assert.Contains(motherCard.Family.Children, child => child.Id == pupil.Id);
}
private static async Task<PeopleListResponse> GetPeopleAsync(HttpClient client, int schoolId, string query)
{
var page = await client.GetFromJsonAsync<PeopleListResponse>(
$"/api/schools/{schoolId}/people?{query}",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
return page;
}
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 PeopleListResponse(
int Total,
int Page,
int PageSize,
IReadOnlyList<PersonListItemResponse> People,
PeopleFilterOptionsResponse Filters);
private sealed record PeopleFilterOptionsResponse(
IReadOnlyList<int> Years,
IReadOnlyList<string> Letters,
IReadOnlyList<DefLabelResponse> Positions);
private sealed record DefLabelResponse(string DefName, string Label);
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);
private sealed record PersonCardResponse(
string Id,
string FullName,
bool Female,
int Age,
DateTime BirthDate,
IReadOnlyList<string> Roles,
int? ClassYear,
string? ClassLetter,
IReadOnlyList<LabeledStatResponse> Body,
IReadOnlyList<LabeledStatResponse> Skills,
IReadOnlyList<DefLabelResponse> Traits,
IReadOnlyList<NeedStatResponse> Needs,
PersonFamilyResponse Family);
private sealed record LabeledStatResponse(string Id, string Label, string Value);
private sealed record NeedStatResponse(string Id, string Label, float Value);
private sealed record PersonFamilyResponse(
IReadOnlyList<PersonRelResponse> Parents,
IReadOnlyList<PersonRelResponse> Children,
IReadOnlyList<PersonRelResponse> Siblings,
IReadOnlyList<PersonRelResponse> Partners);
private sealed record PersonRelResponse(string Id, string FullName, bool Female);
}