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
+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)