using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; namespace HSchool.AppHost.Tests; /// /// Phase 25 goldens. The folders stay historic (format 2, no country): that shape must not start. /// Load tests stamp today's format, country and owner onto a copy so missing-field defaults and /// seed-equals-id still have a host assertion after later slices raised CanStart. /// [Collection(AppHostCollection.Name)] public class GoldenSaveTests(AppHostFixture fixture) { [Fact] public async Task HistoricFormat2Golden_LeavesTheSchoolUnstartedAndTheFileUntouched() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var directory = await SchoolApiTests.SavesDirectoryAsync(client); try { InstallFolder(directory, "current"); var path = Path.Combine(directory, "1.json"); var before = File.ReadAllBytes(path); using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); reload.EnsureSuccessStatusCode(); var state = await SchoolApiTests.GetSchoolsAsync(client); Assert.Empty(state.Schools); var broken = Assert.Single(state.Others); Assert.Equal(1, broken.Id); Assert.Equal("Золотая", broken.Name); Assert.Equal(1, broken.Seed); 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 { DeleteSchoolFiles(directory, 1); using var cleanup = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); cleanup.EnsureSuccessStatusCode(); } } [Fact] public async Task SaveWithoutNativeLanguage_LoadsWithoutReshuffling() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var directory = await SchoolApiTests.SavesDirectoryAsync(client); try { await InstallRunnableAsync(client, directory, "legacy-no-native", includeNative: false); 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()); Assert.Contains(people, person => person.Roles.Contains("student")); Assert.Equal(new DateTime(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc), school.GameTime); } finally { DeleteSchoolFiles(directory, 1); using var cleanup = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); cleanup.EnsureSuccessStatusCode(); } } private static async Task InstallRunnableAsync( HttpClient client, string directory, string folder, bool includeNative) { InstallFolder(directory, folder); StampRunnable(Path.Combine(directory, "1.json"), includeNative); using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken); reload.EnsureSuccessStatusCode(); } private static void InstallFolder(string directory, string folder) { 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); } } private static void StampRunnable(string path, bool includeNative) { var node = JsonNode.Parse(File.ReadAllText(path)) ?? throw new InvalidOperationException($"Save {path} parsed to nothing."); node["format"] = 3; node["countryId"] = "Russia"; node["climatePresetId"] = "TemperateContinental"; node["owner"] = SchoolApiTests.TestUserName; node.AsObject().Remove("nameSetId"); if (includeNative) { node["nativeLanguage"] = "RussianLanguage"; } else { node.AsObject().Remove("nativeLanguage"); } File.WriteAllText(path, node.ToJsonString(new JsonSerializerOptions { WriteIndented = true })); } private static void DeleteSchoolFiles(string directory, int id) { foreach (var name in new[] { $"{id}.json", $"{id}.people.json", $"{id}.timetable.json" }) { var path = Path.Combine(directory, name); if (File.Exists(path)) { File.Delete(path); } } } private static async Task> PeopleAsync(HttpClient client, int schoolId) { var page = await client.GetFromJsonAsync( $"/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 PeoplePage(IReadOnlyList People); private sealed record PersonRow(string Id, string FullName, IReadOnlyList Roles); }