Merge branch 'phase/39-school-owners'
This commit is contained in:
@@ -22,17 +22,17 @@ public class GameSocketTests(AppHostFixture fixture)
|
||||
|
||||
Assert.Equal(ProtocolConstants.Version, welcome.ProtocolVersion);
|
||||
Assert.Equal(20, welcome.TickRate);
|
||||
Assert.Equal(6, welcome.MaxSchools);
|
||||
Assert.Equal(2, welcome.MaxSchools);
|
||||
}
|
||||
|
||||
[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)));
|
||||
@@ -387,14 +387,47 @@ public class GameSocketTests(AppHostFixture fixture)
|
||||
Assert.True(runningLater.GameTime > running.GameTime, "Pausing one school stopped the other.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Guest_SetRunning_DoesNotChangeOwnerRunning()
|
||||
{
|
||||
using var ownerClient = await SchoolApiTests.CreateIsolatedClientAsync(fixture.App, "GuestClockOwner");
|
||||
await SchoolApiTests.WipeAllSavesAsync(ownerClient);
|
||||
var school = await SchoolApiTests.CreateAsync(ownerClient, "Гостевые часы", StartDate);
|
||||
|
||||
var guestCookie = await SchoolApiTests.LoginAndGetCookieAsync(
|
||||
fixture.App.CreateHttpClient("server"),
|
||||
"GuestClockViewer");
|
||||
|
||||
using var socket = new ClientWebSocket();
|
||||
socket.Options.SetRequestHeader("Cookie", guestCookie);
|
||||
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, ProtocolConstants.LocaleRussian)));
|
||||
await ReceiveUntilAsync(socket, MessageType.ServerWelcome);
|
||||
await SendAsync(socket, buffer =>
|
||||
ProtocolCodec.WriteOpenSchool(buffer, new ClientOpenSchoolMessage(school.Id)));
|
||||
_ = await ReceiveClockAsync(socket);
|
||||
await SendAsync(socket, buffer =>
|
||||
ProtocolCodec.WriteSetRunning(buffer, new ClientSetRunningMessage(Running: false)));
|
||||
await Task.Delay(TimeSpan.FromMilliseconds(300), TestContext.Current.CancellationToken);
|
||||
|
||||
var ownerView = await FindAsync(ownerClient, school.Id);
|
||||
Assert.True(ownerView.Running);
|
||||
|
||||
using var delete = await ownerClient.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
|
||||
delete.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[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 =>
|
||||
@@ -479,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);
|
||||
}
|
||||
@@ -19,7 +19,8 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
|
||||
var state = await GetSchoolsAsync(client);
|
||||
|
||||
Assert.Equal(6, state.MaxSchools);
|
||||
Assert.Equal(2, state.MaxSchools);
|
||||
Assert.Equal(7, state.MaxSchoolsTotal);
|
||||
Assert.Equal(ExpectedDefaultStart, state.DefaultStartDate);
|
||||
|
||||
// Without the "Z" the browser would read the start date in its own time zone and the
|
||||
@@ -493,7 +494,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
|
||||
Assert.NotNull(status);
|
||||
Assert.Equal(20, status.TickRate);
|
||||
Assert.Equal(6, status.MaxSchools);
|
||||
Assert.Equal(2, status.MaxSchools);
|
||||
Assert.True(status.Tick > 0, "The loop should have ticked by now.");
|
||||
}
|
||||
|
||||
@@ -526,6 +527,63 @@ 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");
|
||||
var cookie = await LoginAndGetCookieAsync(login, userName);
|
||||
login.Dispose();
|
||||
|
||||
var handler = new HttpClientHandler { UseCookies = false };
|
||||
var client = new HttpClient(handler)
|
||||
{
|
||||
BaseAddress = new Uri(app.GetEndpoint("server", "http").ToString()),
|
||||
};
|
||||
client.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||
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(
|
||||
@@ -575,15 +633,60 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
}
|
||||
|
||||
internal static async Task ResetAsync(HttpClient client)
|
||||
{
|
||||
await WipeAllSavesAsync(client);
|
||||
}
|
||||
|
||||
internal static async Task WipeAllSavesAsync(HttpClient client)
|
||||
{
|
||||
await LoginAsync(client);
|
||||
|
||||
var state = await GetSchoolsAsync(client);
|
||||
|
||||
foreach (var school in state.Schools)
|
||||
for (var pass = 0; pass < 8; pass++)
|
||||
{
|
||||
using var response = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var state = await GetSchoolsAsync(client);
|
||||
if (state.Schools.Count == 0 && state.Others.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var school in state.Schools)
|
||||
{
|
||||
using var response = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
foreach (var other in state.Others)
|
||||
{
|
||||
HttpClient? ownedClient = null;
|
||||
var deleter = client;
|
||||
if (!string.IsNullOrWhiteSpace(other.Owner))
|
||||
{
|
||||
ownedClient = await CreateIsolatedClientAsync(client, other.Owner);
|
||||
deleter = ownedClient;
|
||||
}
|
||||
|
||||
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 left = await GetSchoolsAsync(client);
|
||||
if (left.Schools.Count > 0 || left.Others.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Could not wipe saves: {left.Schools.Count} own and {left.Others.Count} other schools remain.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -649,7 +752,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
return save;
|
||||
}
|
||||
|
||||
private static async Task<string> SavesDirectoryAsync(HttpClient client)
|
||||
internal static async Task<string> SavesDirectoryAsync(HttpClient client)
|
||||
{
|
||||
var payload = await client.GetFromJsonAsync<PathResponse>(
|
||||
"/api/dev/saves-directory",
|
||||
@@ -663,6 +766,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
string? CountryId,
|
||||
string? ClimatePresetId,
|
||||
string? NativeLanguage,
|
||||
string? Owner,
|
||||
SwarmUiSaveFile? PortraitSettings);
|
||||
|
||||
private sealed record SwarmUiSaveFile(string? ActivePresetId, IReadOnlyList<SwarmUiPresetSave>? Presets);
|
||||
@@ -681,7 +785,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
links = new[] { new { a = "yard", b = "office" } },
|
||||
};
|
||||
|
||||
private static Task<HttpResponseMessage> PostAsync(
|
||||
internal static Task<HttpResponseMessage> PostAsync(
|
||||
HttpClient client,
|
||||
string name,
|
||||
DateTime startDate,
|
||||
@@ -704,14 +808,27 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
bool Running,
|
||||
byte SpeedIndex,
|
||||
int Seed,
|
||||
bool Mine,
|
||||
IReadOnlyList<string>? ModIds = null);
|
||||
|
||||
internal sealed record OtherSchoolResponse(
|
||||
int Id,
|
||||
string Name,
|
||||
DateTime GameTime,
|
||||
bool Running,
|
||||
byte SpeedIndex,
|
||||
int Seed,
|
||||
string? Owner,
|
||||
IReadOnlyList<string>? ModIds = null);
|
||||
|
||||
internal sealed record SchoolsResponse(
|
||||
int MaxSchools,
|
||||
int MaxSchoolsTotal,
|
||||
DateTime DefaultStartDate,
|
||||
double GameMinutesPerRealSecond,
|
||||
int SchoolWeekDays,
|
||||
IReadOnlyList<SchoolResponse> Schools);
|
||||
IReadOnlyList<SchoolResponse> Schools,
|
||||
IReadOnlyList<OtherSchoolResponse> Others);
|
||||
|
||||
private sealed record RandomNameResponse(string Name);
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace HSchool.AppHost.Tests;
|
||||
|
||||
[Collection(AppHostCollection.Name)]
|
||||
public class SchoolOwnerApiTests(AppHostFixture fixture)
|
||||
{
|
||||
private static readonly DateTime Start = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public async Task TwoUsers_EachGetsTwoSchools_ThirdOwnIsLimitReached_OthersListForeignSchools()
|
||||
{
|
||||
using var wipe = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.WipeAllSavesAsync(wipe);
|
||||
|
||||
var alice = await CreateUserAsync("OwnerAlice");
|
||||
var bob = await CreateUserAsync("OwnerBob");
|
||||
await ResetUserAsync(alice);
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
await SchoolApiTests.CreateAsync(alice, $"Alice {i + 1}", Start);
|
||||
}
|
||||
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
await SchoolApiTests.CreateAsync(bob, $"Bob {i + 1}", Start);
|
||||
}
|
||||
|
||||
using var third = await SchoolApiTests.PostAsync(alice, "Alice extra", Start);
|
||||
Assert.Equal(HttpStatusCode.Conflict, third.StatusCode);
|
||||
Assert.Equal("school-limit-reached", await SchoolApiTests.ProblemCodeAsync(third));
|
||||
|
||||
var aliceList = await SchoolApiTests.GetSchoolsAsync(alice);
|
||||
Assert.Equal(2, aliceList.Schools.Count);
|
||||
Assert.All(aliceList.Schools, school => Assert.True(school.Mine));
|
||||
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]
|
||||
public async Task EighthSchool_OnFullServer_ReturnsServerFull()
|
||||
{
|
||||
using var wipe = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.WipeAllSavesAsync(wipe);
|
||||
|
||||
var users = new[] { "SrvA", "SrvB", "SrvC", "SrvD" };
|
||||
var clients = new List<HttpClient>();
|
||||
try
|
||||
{
|
||||
for (var u = 0; u < users.Length; u++)
|
||||
{
|
||||
var client = await CreateUserAsync(users[u]);
|
||||
clients.Add(client);
|
||||
var count = u < 3 ? 2 : 1;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
await SchoolApiTests.CreateAsync(client, $"{users[u]}-{i}", Start);
|
||||
}
|
||||
}
|
||||
|
||||
var spare = await CreateUserAsync("SrvSpare");
|
||||
using var response = await SchoolApiTests.PostAsync(spare, "One too many", Start);
|
||||
Assert.Equal(HttpStatusCode.Conflict, response.StatusCode);
|
||||
Assert.Equal("server-full", await SchoolApiTests.ProblemCodeAsync(response));
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var client in clients)
|
||||
{
|
||||
client.Dispose();
|
||||
}
|
||||
|
||||
using var cleanup = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.WipeAllSavesAsync(cleanup);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Guest_Hire_IsForbidden()
|
||||
{
|
||||
using var wipe = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.WipeAllSavesAsync(wipe);
|
||||
|
||||
var owner = await CreateUserAsync("HireOwner");
|
||||
var guest = await CreateUserAsync("HireGuest");
|
||||
await ResetUserAsync(owner);
|
||||
|
||||
var school = await SchoolApiTests.CreateAsync(owner, "Чужой найм", Start);
|
||||
|
||||
using var staffing = await guest.GetAsync($"/api/schools/{school.Id}/staffing", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.Forbidden, staffing.StatusCode);
|
||||
Assert.Equal("not-owner", await SchoolApiTests.ProblemCodeAsync(staffing));
|
||||
|
||||
using var hire = await guest.PostAsJsonAsync(
|
||||
$"/api/schools/{school.Id}/staff/hire",
|
||||
new { personId = "a0.p0", position = "Teacher" },
|
||||
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]
|
||||
public async Task OwnerlessSave_AppearsInOthers_AndCanBeDeletedByAnotherUser()
|
||||
{
|
||||
using var wipe = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.WipeAllSavesAsync(wipe);
|
||||
|
||||
var first = await CreateUserAsync("SaveAlpha");
|
||||
var created = await SchoolApiTests.CreateAsync(first, "Бесхозная", Start);
|
||||
|
||||
var directory = await SavesDirectoryAsync(first);
|
||||
var saveFiles = new[]
|
||||
{
|
||||
$"{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 == "Бесхозная");
|
||||
Assert.Null(orphan.Owner);
|
||||
|
||||
var second = await CreateUserAsync("SaveBeta");
|
||||
using var delete = await second.DeleteAsync($"/api/schools/{orphan.Id}", TestContext.Current.CancellationToken);
|
||||
Assert.Equal(HttpStatusCode.NoContent, delete.StatusCode);
|
||||
|
||||
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) =>
|
||||
await SchoolApiTests.CreateIsolatedClientAsync(fixture.App, userName);
|
||||
|
||||
private static async Task ResetUserAsync(HttpClient client)
|
||||
{
|
||||
await SchoolApiTests.LoginAsync(client);
|
||||
var state = await SchoolApiTests.GetSchoolsAsync(client);
|
||||
foreach (var school in state.Schools)
|
||||
{
|
||||
using var response = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
foreach (var other in state.Others)
|
||||
{
|
||||
using var response = await client.DeleteAsync($"/api/schools/{other.Id}", TestContext.Current.CancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetAllAsync()
|
||||
{
|
||||
using var client = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.WipeAllSavesAsync(client);
|
||||
}
|
||||
|
||||
private static async Task<string> SavesDirectoryAsync(HttpClient client) =>
|
||||
await SchoolApiTests.SavesDirectoryAsync(client);
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user