WIP phase 39: school owners, my/others menu, guest restrictions.
This commit is contained in:
@@ -22,7 +22,7 @@ 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]
|
||||
@@ -387,6 +387,39 @@ 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()
|
||||
{
|
||||
|
||||
@@ -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,21 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
_ = await LoginAndGetCookieAsync(client, userName);
|
||||
}
|
||||
|
||||
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> LoginAndGetCookieAsync(HttpClient client, string userName = TestUserName)
|
||||
{
|
||||
using var first = await client.PostAsJsonAsync(
|
||||
@@ -585,6 +601,62 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
using var response = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
foreach (var school in state.Others)
|
||||
{
|
||||
using var response = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken);
|
||||
if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task WipeAllSavesAsync(HttpClient client)
|
||||
{
|
||||
await LoginAsync(client);
|
||||
|
||||
for (var pass = 0; pass < 4; pass++)
|
||||
{
|
||||
var state = await GetSchoolsAsync(client);
|
||||
if (state.Schools.Count == 0 && state.Others.Count == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
var directory = await SavesDirectoryAsync(client);
|
||||
foreach (var path in Directory.EnumerateFiles(directory))
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -649,7 +721,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",
|
||||
@@ -659,7 +731,7 @@ public class SchoolApiTests(AppHostFixture fixture)
|
||||
return payload.Path;
|
||||
}
|
||||
|
||||
private sealed record SchoolSaveFile(string? CountryId, string? ClimatePresetId, string? NativeLanguage);
|
||||
private sealed record SchoolSaveFile(string? CountryId, string? ClimatePresetId, string? NativeLanguage, string? Owner);
|
||||
|
||||
internal static readonly object SimpleCustomMap = new
|
||||
{
|
||||
@@ -673,7 +745,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,
|
||||
@@ -696,14 +768,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,164 @@
|
||||
using System.Net.Http.Json;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[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));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task OwnerlessSave_AppearsInOthers_AndCanBeDeletedByAnotherUser()
|
||||
{
|
||||
using var wipe = fixture.App.CreateHttpClient("server");
|
||||
await SchoolApiTests.WipeAllSavesAsync(wipe);
|
||||
|
||||
var first = await CreateUserAsync("SaveAlpha");
|
||||
var directory = await SavesDirectoryAsync(first);
|
||||
var golden = Path.Combine(AppContext.BaseDirectory, "golden", "current");
|
||||
foreach (var file in Directory.EnumerateFiles(golden))
|
||||
{
|
||||
File.Copy(file, Path.Combine(directory, Path.GetFileName(file)), overwrite: 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user