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>
212 lines
7.9 KiB
C#
212 lines
7.9 KiB
C#
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);
|
|
}
|