Enhance school creation and map management features by updating the API to support mod packs and map layouts. Introduce a new map snapshot protocol for efficient data handling during school sessions. Revise documentation to reflect these changes, including updates to the protocol and architecture documents. Improve UI components for mod selection and map editing, ensuring a better user experience. Update tests to validate new functionalities and ensure robustness.
ci / server (push) Failing after 3m31s
ci / client (push) Successful in 14s

This commit is contained in:
Leonid Pershin
2026-08-18 15:15:49 +03:00
parent 1bc75244e8
commit 30cc937069
36 changed files with 1876 additions and 171 deletions
@@ -132,6 +132,92 @@ public class SchoolApiTests(AppHostFixture fixture)
Assert.Equal(suggestion.Name, created.Name);
}
[Fact]
public async Task Mods_ListCoreAsRequired()
{
using var client = fixture.App.CreateHttpClient("server");
var response = await client.GetFromJsonAsync<ModsResponse>("/api/mods", TestContext.Current.CancellationToken);
Assert.NotNull(response);
var core = Assert.Single(response.Mods, pack => pack.Id == "core");
Assert.True(core.Required);
}
[Fact]
public async Task Catalog_LabelsDefsInTheRequestedLanguage()
{
using var client = fixture.App.CreateHttpClient("server");
var ru = await client.GetFromJsonAsync<CatalogResponse>("/api/catalog?lang=ru", TestContext.Current.CancellationToken);
var en = await client.GetFromJsonAsync<CatalogResponse>("/api/catalog?lang=en", TestContext.Current.CancellationToken);
Assert.NotNull(ru);
Assert.NotNull(en);
Assert.Equal("Кабинет директора", Assert.Single(ru.Rooms, room => room.DefName == "PrincipalsOffice").Label);
Assert.Equal("Principal's office", Assert.Single(en.Rooms, room => room.DefName == "PrincipalsOffice").Label);
Assert.Equal("yard", ru.DefaultMap.Territory?.Id);
}
[Fact]
public async Task CreateSchool_WithABrokenMap_IsRejected()
{
using var client = fixture.App.CreateHttpClient("server");
await ResetAsync(client);
using var response = await client.PostAsJsonAsync(
"/api/schools",
new
{
name = "Дырявая",
startDate = ExpectedDefaultStart,
map = new
{
territory = new { id = "yard", def = "SchoolYard" },
buildings = Array.Empty<object>(),
floors = Array.Empty<object>(),
rooms = Array.Empty<object>(),
links = Array.Empty<object>(),
},
},
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("invalid-map", await ProblemCodeAsync(response));
}
[Fact]
public async Task CreateSchool_WithAnUnknownMod_IsRejected()
{
using var client = fixture.App.CreateHttpClient("server");
await ResetAsync(client);
using var response = await client.PostAsJsonAsync(
"/api/schools",
new
{
name = "Чужой мод",
startDate = ExpectedDefaultStart,
modIds = new[] { "no-such-mod" },
},
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal("unknown-mod", await ProblemCodeAsync(response));
}
[Fact]
public async Task CreateSchool_WithACustomConnectedMap_Succeeds()
{
using var client = fixture.App.CreateHttpClient("server");
await ResetAsync(client);
var created = await CreateWithMapAsync(client, "Своя карта", ExpectedDefaultStart, SimpleCustomMap);
Assert.Equal("Своя карта", created.Name);
Assert.Contains((await GetSchoolsAsync(client)).Schools, school => school.Id == created.Id);
}
[Fact]
public async Task Status_ReportsTheLoopAndTheLimit()
{
@@ -188,6 +274,31 @@ public class SchoolApiTests(AppHostFixture fixture)
return created;
}
internal static async Task<SchoolResponse> CreateWithMapAsync(HttpClient client, string name, DateTime startDate, object map)
{
using var response = await client.PostAsJsonAsync(
"/api/schools",
new { name, startDate, map },
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<SchoolResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
return created;
}
internal static readonly object SimpleCustomMap = new
{
territory = new { id = "yard", def = "SchoolYard" },
buildings = new[] { new { id = "main", def = "MainBuilding" } },
floors = new[] { new { id = "floor-1", def = "StandardFloor", building = "main", label = "1" } },
rooms = new[]
{
new { id = "office", def = "PrincipalsOffice", building = "main", floor = "floor-1", slots = Array.Empty<object>() },
},
links = new[] { new { a = "yard", b = "office" } },
};
private static Task<HttpResponseMessage> PostAsync(HttpClient client, string name, DateTime startDate) =>
client.PostAsJsonAsync(
"/api/schools",
@@ -213,4 +324,26 @@ public class SchoolApiTests(AppHostFixture fixture)
private sealed record ProblemResponse(string? Code);
private sealed record StatusResponse(uint Tick, int TickRate, int Schools, int MaxSchools, int Connections);
private sealed record ModsResponse(IReadOnlyList<ModInfoResponse> Mods);
private sealed record ModInfoResponse(string Id, bool Required);
private sealed record CatalogResponse(
IReadOnlyList<DefInfoResponse> Territories,
IReadOnlyList<DefInfoResponse> Buildings,
IReadOnlyList<DefInfoResponse> Floors,
IReadOnlyList<RoomInfoResponse> Rooms,
IReadOnlyList<DefInfoResponse> Things,
MapLayoutResponse DefaultMap);
private sealed record DefInfoResponse(string DefName, string Label);
private sealed record RoomInfoResponse(string DefName, string Label, IReadOnlyList<RoomSlotResponse> Slots, IReadOnlyList<string> Positions);
private sealed record RoomSlotResponse(string Key, string Thing);
private sealed record MapLayoutResponse(TerritoryResponse? Territory);
private sealed record TerritoryResponse(string Id, string Def);
}