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");
@@ -1,103 +0,0 @@
using System.Net.Http.Json;
using System.Text.Json;
namespace HSchool.AppHost.Tests;
[Collection(AppHostCollection.Name)]
public class GoldenSaveTests(AppHostFixture fixture)
{
[Fact]
public async Task CurrentFormatSave_LoadsTheSamePeopleAndTime()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
await InstallAsync(client, "current");
var state = await SchoolApiTests.GetSchoolsAsync(client);
var school = Assert.Single(state.Schools);
Assert.Equal(1, school.Id);
Assert.Equal(1, school.Seed);
Assert.Equal("Золотая", school.Name);
Assert.Equal(new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc), school.GameTime);
Assert.False(school.Running);
var people = await PeopleAsync(client, school.Id);
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
Assert.Contains(people, person => person.Roles.Contains("student"));
}
[Fact]
public async Task SaveWithoutNativeLanguage_LoadsWithoutReshuffling()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
await InstallAsync(client, "legacy-no-native");
var state = await SchoolApiTests.GetSchoolsAsync(client);
var school = Assert.Single(state.Schools);
Assert.Equal(1, school.Id);
Assert.Equal(1, school.Seed);
Assert.Equal("Золотая", school.Name);
var people = await PeopleAsync(client, school.Id);
Assert.Equal(ExpectedNames(), people.Select(row => row.FullName).OrderBy(name => name, StringComparer.Ordinal).ToArray());
}
private async Task InstallAsync(HttpClient client, string folder)
{
var directory = await SavesDirectoryAsync(client);
var source = Path.Combine(AppContext.BaseDirectory, "golden", folder);
foreach (var file in Directory.EnumerateFiles(source))
{
File.Copy(file, Path.Combine(directory, Path.GetFileName(file)), overwrite: true);
}
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
}
private static async Task<string> SavesDirectoryAsync(HttpClient client)
{
var payload = await client.GetFromJsonAsync<SavesDirectoryResponse>(
"/api/dev/saves-directory",
TestContext.Current.CancellationToken);
Assert.NotNull(payload);
Assert.False(string.IsNullOrWhiteSpace(payload.Path));
return payload.Path;
}
private static async Task<IReadOnlyList<PersonRow>> PeopleAsync(HttpClient client, int schoolId)
{
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{schoolId}/people?pageSize=100",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
return page.People
.OrderBy(row => row.Id, StringComparer.Ordinal)
.ToArray();
}
private static string[] ExpectedNames()
{
var json = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "golden", "current", "1.people.json"));
using var document = JsonDocument.Parse(json);
return document.RootElement.GetProperty("people")
.EnumerateArray()
.Select(person =>
{
var name = person.GetProperty("name");
var surname = name.GetProperty("surname").GetString() ?? "";
var given = name.GetProperty("given").GetString() ?? "";
var patronymic = name.GetProperty("patronymic").GetString() ?? "";
return string.Join(' ', new[] { surname, given, patronymic }.Where(part => part.Length > 0));
})
.OrderBy(full => full, StringComparer.Ordinal)
.ToArray();
}
private sealed record SavesDirectoryResponse(string Path);
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
private sealed record PersonRow(string Id, string FullName, IReadOnlyList<string> Roles);
}
+67 -17
View File
@@ -527,6 +527,25 @@ public class SchoolApiTests(AppHostFixture fixture)
_ = await LoginAndGetCookieAsync(client, userName);
}
internal static async Task<HttpClient> CreateIsolatedClientAsync(HttpClient template, string userName)
{
ArgumentNullException.ThrowIfNull(template.BaseAddress);
var login = new HttpClient { BaseAddress = template.BaseAddress };
try
{
var cookie = await LoginAndGetCookieAsync(login, userName);
var handler = new HttpClientHandler { UseCookies = false };
var client = new HttpClient(handler) { BaseAddress = template.BaseAddress };
client.DefaultRequestHeaders.Add("Cookie", cookie);
return client;
}
finally
{
login.Dispose();
}
}
internal static async Task<HttpClient> CreateIsolatedClientAsync(DistributedApplication app, string userName)
{
var login = app.CreateHttpClient("server");
@@ -542,6 +561,29 @@ public class SchoolApiTests(AppHostFixture fixture)
return client;
}
internal static async Task<string> WebSocketCookieAsync(HttpClient client, string userName = TestUserName)
{
if (client.DefaultRequestHeaders.TryGetValues("Cookie", out var values))
{
var cookie = values.First();
if (!string.IsNullOrWhiteSpace(cookie))
{
return cookie;
}
}
ArgumentNullException.ThrowIfNull(client.BaseAddress);
var login = new HttpClient { BaseAddress = client.BaseAddress };
try
{
return await LoginAndGetCookieAsync(login, userName);
}
finally
{
login.Dispose();
}
}
internal static async Task<string> LoginAndGetCookieAsync(HttpClient client, string userName = TestUserName)
{
using var first = await client.PostAsJsonAsync(
@@ -599,12 +641,12 @@ public class SchoolApiTests(AppHostFixture fixture)
{
await LoginAsync(client);
for (var pass = 0; pass < 4; pass++)
for (var pass = 0; pass < 8; pass++)
{
var state = await GetSchoolsAsync(client);
if (state.Schools.Count == 0 && state.Others.Count == 0)
{
break;
return;
}
foreach (var school in state.Schools)
@@ -615,29 +657,37 @@ public class SchoolApiTests(AppHostFixture fixture)
foreach (var other in state.Others)
{
using var response = await client.DeleteAsync($"/api/schools/{other.Id}", TestContext.Current.CancellationToken);
if (response.StatusCode == HttpStatusCode.Forbidden)
HttpClient? ownedClient = null;
var deleter = client;
if (!string.IsNullOrWhiteSpace(other.Owner))
{
continue;
ownedClient = await CreateIsolatedClientAsync(client, other.Owner);
deleter = ownedClient;
}
response.EnsureSuccessStatusCode();
try
{
using var response = await deleter.DeleteAsync($"/api/schools/{other.Id}", TestContext.Current.CancellationToken);
if (response.StatusCode == HttpStatusCode.Forbidden)
{
continue;
}
response.EnsureSuccessStatusCode();
}
finally
{
ownedClient?.Dispose();
}
}
}
var directory = await SavesDirectoryAsync(client);
foreach (var path in Directory.EnumerateFiles(directory))
var left = await GetSchoolsAsync(client);
if (left.Schools.Count > 0 || left.Others.Count > 0)
{
File.Delete(path);
throw new InvalidOperationException(
$"Could not wipe saves: {left.Schools.Count} own and {left.Others.Count} other schools remain.");
}
foreach (var path in Directory.EnumerateDirectories(directory))
{
Directory.Delete(path, recursive: true);
}
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
}
internal static async Task<SchoolsResponse> GetSchoolsAsync(HttpClient client)
@@ -1,4 +1,6 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace HSchool.AppHost.Tests;
@@ -37,6 +39,14 @@ public class SchoolOwnerApiTests(AppHostFixture fixture)
Assert.Equal(2, aliceList.Others.Count);
Assert.All(aliceList.Others, other => Assert.Equal("OwnerBob", other.Owner));
Assert.DoesNotContain(aliceList.Schools, school => school.Name.StartsWith("Bob", StringComparison.Ordinal));
foreach (var client in new[] { alice, bob })
{
client.Dispose();
}
using var cleanup = fixture.App.CreateHttpClient("server");
await SchoolApiTests.WipeAllSavesAsync(cleanup);
}
[Fact]
@@ -99,6 +109,12 @@ public class SchoolOwnerApiTests(AppHostFixture fixture)
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.Forbidden, hire.StatusCode);
Assert.Equal("not-owner", await SchoolApiTests.ProblemCodeAsync(hire));
owner.Dispose();
guest.Dispose();
using var cleanup = fixture.App.CreateHttpClient("server");
await SchoolApiTests.WipeAllSavesAsync(cleanup);
}
[Fact]
@@ -108,19 +124,41 @@ public class SchoolOwnerApiTests(AppHostFixture fixture)
await SchoolApiTests.WipeAllSavesAsync(wipe);
var first = await CreateUserAsync("SaveAlpha");
var created = await SchoolApiTests.CreateAsync(first, "Бесхозная", Start);
var directory = await SavesDirectoryAsync(first);
var golden = Path.Combine(AppContext.BaseDirectory, "golden", "current");
foreach (var file in Directory.EnumerateFiles(golden))
var saveFiles = new[]
{
File.Copy(file, Path.Combine(directory, Path.GetFileName(file)), overwrite: true);
$"{created.Id}.json",
$"{created.Id}.people.json",
$"{created.Id}.timetable.json",
};
var copies = saveFiles
.Select(name => Path.Combine(directory, name))
.Where(File.Exists)
.ToDictionary(path => path, File.ReadAllBytes);
await SchoolApiTests.WipeAllSavesAsync(first);
foreach (var (copiedPath, bytes) in copies)
{
File.WriteAllBytes(copiedPath, bytes);
}
var schoolSavePath = Path.Combine(directory, $"{created.Id}.json");
var node = JsonNode.Parse(File.ReadAllText(schoolSavePath))
?? throw new InvalidOperationException("School save parsed to nothing.");
node.AsObject().Remove("owner");
File.WriteAllText(
schoolSavePath,
node.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
using var reload = await first.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
var firstList = await SchoolApiTests.GetSchoolsAsync(first);
Assert.Empty(firstList.Schools);
var orphan = Assert.Single(firstList.Others, other => other.Name == "Золотая");
var orphan = Assert.Single(firstList.Others, other => other.Name == "Бесхозная");
Assert.Null(orphan.Owner);
var second = await CreateUserAsync("SaveBeta");
@@ -129,6 +167,12 @@ public class SchoolOwnerApiTests(AppHostFixture fixture)
var after = await SchoolApiTests.GetSchoolsAsync(second);
Assert.DoesNotContain(after.Others, other => other.Id == orphan.Id);
first.Dispose();
second.Dispose();
using var cleanup = fixture.App.CreateHttpClient("server");
await SchoolApiTests.WipeAllSavesAsync(cleanup);
}
private async Task<HttpClient> CreateUserAsync(string userName) =>
+10 -3
View File
@@ -19,14 +19,21 @@ public class SchoolSeedTests(AppHostFixture fixture)
var first = await SchoolApiTests.CreateAsync(client, "Сид одинаковый А", Start, seed: 42);
var second = await SchoolApiTests.CreateAsync(client, "Сид одинаковый Б", Start, seed: 42);
var other = await SchoolApiTests.CreateAsync(client, "Сид другой", Start, seed: 7);
var firstYearFive = await YearFiveAsync(client, first.Id);
var secondYearFive = await YearFiveAsync(client, second.Id);
var otherYearFive = await YearFiveAsync(client, other.Id);
Assert.Equal(firstYearFive, secondYearFive);
Assert.NotEqual(firstYearFive, otherYearFive);
using (var delete = await client.DeleteAsync($"/api/schools/{first.Id}", TestContext.Current.CancellationToken))
{
delete.EnsureSuccessStatusCode();
}
var other = await SchoolApiTests.CreateAsync(client, "Сид другой", Start, seed: 7);
var otherYearFive = await YearFiveAsync(client, other.Id);
Assert.NotEqual(secondYearFive, otherYearFive);
}
[Fact]
@@ -26,7 +26,16 @@ public class ServiceabilityTests(AppHostFixture fixture)
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
Assert.Empty((await SchoolApiTests.GetSchoolsAsync(client)).Schools);
var state = await SchoolApiTests.GetSchoolsAsync(client);
Assert.Empty(state.Schools);
var broken = Assert.Single(state.Others);
Assert.Equal(1, broken.Id);
using var people = await client.GetAsync(
$"/api/schools/{broken.Id}/people?pageSize=1",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, people.StatusCode);
Assert.Equal(before, File.ReadAllBytes(path));
}
finally
@@ -54,7 +63,16 @@ public class ServiceabilityTests(AppHostFixture fixture)
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
Assert.Empty((await SchoolApiTests.GetSchoolsAsync(client)).Schools);
var state = await SchoolApiTests.GetSchoolsAsync(client);
Assert.Empty(state.Schools);
var broken = Assert.Single(state.Others);
Assert.Equal(1, broken.Id);
using var people = await client.GetAsync(
$"/api/schools/{broken.Id}/people?pageSize=1",
TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, people.StatusCode);
Assert.Equal(before, File.ReadAllBytes(path));
}
finally
@@ -65,29 +83,6 @@ public class ServiceabilityTests(AppHostFixture fixture)
}
}
[Fact]
public async Task CurrentFormatSave_LoadsAsBefore()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var directory = await SavesDirectoryAsync(client);
try
{
InstallGolden(directory, "current");
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
var school = Assert.Single((await SchoolApiTests.GetSchoolsAsync(client)).Schools);
Assert.Equal(1, school.Id);
Assert.Equal("Золотая", school.Name);
}
finally
{
await SchoolApiTests.ResetAsync(client);
}
}
[Fact]
public async Task Dump_ReturnsPeopleNodesAndTimetable()
{