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(); 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 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 SavesDirectoryAsync(HttpClient client) => await SchoolApiTests.SavesDirectoryAsync(client); }