Complete phase 39 school owners with owner-scoped API, menu, and tests.

Drop golden save load tests in favor of incompatible-save behavior; dress legacy people files on reload; fix AppHost isolation for multi-user socket and save cleanup.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 09:44:08 +03:00
co-authored by Cursor
parent 4b236a7916
commit cc18429108
15 changed files with 371 additions and 341 deletions
+72 -36
View File
@@ -28,11 +28,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task ChangingSpeed_DoesNotResumeAPausedSchool()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Пауза и скорость", StartDate);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
await ReceiveClockAsync(socket);
await SendAsync(socket, buffer =>
@@ -52,7 +52,7 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task NoSchoolOpen_MeansNoClockFramesButTheCalendarKeepsRunning()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Живёт сама", StartDate);
@@ -72,11 +72,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task OpeningASchool_StreamsItsClock()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Ход времени", StartDate);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
var first = await ReceiveClockAsync(socket);
Assert.Equal(school.Id, first.SchoolId);
@@ -93,11 +93,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task OpeningASchool_SendsAMapSnapshot()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Снимок карты", StartDate);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
var snapshot = ProtocolCodec.ReadMapSnapshot(await ReceiveUntilAsync(socket, MessageType.ServerMapSnapshot));
Assert.Equal(school.Id, snapshot.SchoolId);
@@ -116,11 +116,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task OpeningASchool_LabelsTheSnapshotInTheHelloLocale()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "English snapshot", StartDate);
using var socket = await OpenSchoolAsync(school.Id, ProtocolConstants.LocaleEnglish);
using var socket = await OpenSchoolAsync(school.Id, client, ProtocolConstants.LocaleEnglish);
var snapshot = ProtocolCodec.ReadMapSnapshot(await ReceiveUntilAsync(socket, MessageType.ServerMapSnapshot));
Assert.Equal("Principal's office", Assert.Single(snapshot.Nodes, node => node.Id == "principals-office").Name);
@@ -129,11 +129,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task OpeningASchool_WithACustomMap_ReturnsThatLayout()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateWithMapAsync(client, "Упрощённая", StartDate, SchoolApiTests.SimpleCustomMap);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
var snapshot = ProtocolCodec.ReadMapSnapshot(await ReceiveUntilAsync(socket, MessageType.ServerMapSnapshot));
Assert.DoesNotContain(snapshot.Nodes, node => node.Id == "corridor-1");
@@ -144,14 +144,14 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task ReloadFromDisk_RestoresACustomMap()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateWithMapAsync(client, "Карта с диска", StartDate, SchoolApiTests.SimpleCustomMap);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
var snapshot = ProtocolCodec.ReadMapSnapshot(await ReceiveUntilAsync(socket, MessageType.ServerMapSnapshot));
Assert.Contains(snapshot.Nodes, node => node.Id == "office");
@@ -161,12 +161,12 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task OpeningASchoolDuringAMathLesson_PutsOccupancyOnPresenceNotTheSnapshot()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
var school = await SchoolApiTests.CreateAsync(client, "Кто где сейчас", start);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
var snapshot = ProtocolCodec.ReadMapSnapshot(await ReceiveUntilAsync(socket, MessageType.ServerMapSnapshot));
Assert.Contains(snapshot.Nodes, node => node.Id == "classroom-101");
@@ -224,12 +224,12 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task SkipEmpty_DuringWorkHours_IsIgnored()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var start = new DateTime(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
var school = await SchoolApiTests.CreateAsync(client, "Пропуск в учебное время", start);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
await SendAsync(socket, buffer =>
ProtocolCodec.WriteSetRunning(buffer, new ClientSetRunningMessage(Running: false)));
var paused = await ReceiveClockWhereAsync(socket, clock => !clock.Running);
@@ -247,11 +247,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task Pausing_FreezesTheClock()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Пауза", StartDate);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
await ReceiveClockAsync(socket);
await SendAsync(socket, buffer =>
@@ -267,11 +267,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task SpeedIndex_ChangesHowFastTheCalendarMoves()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Быстрая", StartDate);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
await ReceiveClockAsync(socket);
await SendAsync(socket, buffer =>
@@ -288,11 +288,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task LeavingASchool_KeepsItsClockRunning()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Выход", StartDate);
using (var socket = await OpenSchoolAsync(school.Id))
using (var socket = await OpenSchoolAsync(school.Id, client))
{
await ReceiveClockAsync(socket);
await SendAsync(socket, buffer => ProtocolCodec.WriteCloseSchool(buffer));
@@ -309,11 +309,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task APausedSchool_StaysPausedAfterLeaving()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Оставлена на паузе", StartDate);
using (var socket = await OpenSchoolAsync(school.Id))
using (var socket = await OpenSchoolAsync(school.Id, client))
{
await ReceiveClockAsync(socket);
await SendAsync(socket, buffer =>
@@ -333,11 +333,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task DeletingTheOpenSchool_TellsTheClient()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Исчезнет", StartDate);
using var socket = await OpenSchoolAsync(school.Id);
using var socket = await OpenSchoolAsync(school.Id, client);
await ReceiveClockAsync(socket);
using var deleted = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
@@ -365,12 +365,12 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task PausingOneSchool_DoesNotStopAnother()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var paused = await SchoolApiTests.CreateAsync(client, "На паузе", StartDate);
var running = await SchoolApiTests.CreateAsync(client, "Идёт дальше", StartDate);
using var socket = await OpenSchoolAsync(paused.Id);
using var socket = await OpenSchoolAsync(paused.Id, client);
await ReceiveClockAsync(socket);
await SendAsync(socket, buffer =>
ProtocolCodec.WriteSetRunning(buffer, new ClientSetRunningMessage(Running: false)));
@@ -423,11 +423,11 @@ public class GameSocketTests(AppHostFixture fixture)
[Fact]
public async Task ReloadFromDisk_RestoresAPausedClock()
{
using var client = fixture.App.CreateHttpClient("server");
using var client = await CreateOwnerHttpClientAsync();
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Снимок паузы", StartDate);
using (var socket = await OpenSchoolAsync(school.Id))
using (var socket = await OpenSchoolAsync(school.Id, client))
{
await ReceiveClockAsync(socket);
await SendAsync(socket, buffer =>
@@ -512,21 +512,57 @@ public class GameSocketTests(AppHostFixture fixture)
{
var state = await SchoolApiTests.GetSchoolsAsync(client);
var school = state.Schools.SingleOrDefault(candidate => candidate.Id == schoolId);
if (school is not null)
{
return school;
}
Assert.NotNull(school);
return school;
var other = state.Others.SingleOrDefault(candidate => candidate.Id == schoolId);
Assert.NotNull(other);
return new SchoolApiTests.SchoolResponse(
other.Id,
other.Name,
other.GameTime,
other.Running,
other.SpeedIndex,
other.Seed,
Mine: false,
other.ModIds);
}
private async Task<ClientWebSocket> OpenSchoolAsync(int schoolId, byte locale = ProtocolConstants.LocaleRussian)
private async Task<HttpClient> CreateOwnerHttpClientAsync()
{
using var httpClient = fixture.App.CreateHttpClient("server");
var socket = await ConnectAsync(httpClient, locale);
using var template = fixture.App.CreateHttpClient("server");
return await SchoolApiTests.CreateIsolatedClientAsync(template, SchoolApiTests.TestUserName);
}
private async Task<ClientWebSocket> OpenSchoolAsync(
int schoolId,
HttpClient ownerClient,
byte locale = ProtocolConstants.LocaleRussian)
{
var cookie = ownerClient.DefaultRequestHeaders.TryGetValues("Cookie", out var values)
? values.First()
: await SchoolApiTests.WebSocketCookieAsync(ownerClient);
var socket = await ConnectWithCookieAsync(cookie, locale);
await ReceiveUntilAsync(socket, MessageType.ServerWelcome);
await SendAsync(socket, buffer =>
ProtocolCodec.WriteOpenSchool(buffer, new ClientOpenSchoolMessage(schoolId)));
return socket;
}
private async Task<ClientWebSocket> ConnectWithCookieAsync(string cookie, byte locale = ProtocolConstants.LocaleRussian)
{
var socket = new ClientWebSocket();
socket.Options.SetRequestHeader("Cookie", cookie);
var http = fixture.App.GetEndpoint("server", "http");
var uri = new UriBuilder(http) { Scheme = "ws", Path = "/ws/game" }.Uri;
await socket.ConnectAsync(uri, TestContext.Current.CancellationToken).WaitAsync(DefaultTimeout);
await SendAsync(socket, buffer =>
ProtocolCodec.WriteHello(buffer, new ClientHelloMessage(ProtocolConstants.Version, locale)));
return socket;
}
private async Task<ClientWebSocket> ConnectAsync(byte locale = ProtocolConstants.LocaleRussian)
{
using var httpClient = fixture.App.CreateHttpClient("server");