Files
h-school/tests/HSchool.AppHost.Tests/ServiceabilityTests.cs
T
Leonid PershinandCursor 1b66c89cd7 Replace selectable name sets with a country that owns names and climate presets.
Create picks a country; climate is rolled from the school seed and stored. Old Slavic saves lift as Russia.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 02:55:13 +03:00

223 lines
8.1 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 ServiceabilityTests(AppHostFixture fixture)
{
private static readonly DateTime TuesdayMorning = new(2012, 4, 3, 10, 20, 0, DateTimeKind.Utc);
[Fact]
public async Task FutureFormatSave_LeavesTheSchoolUnstartedAndTheFileUntouched()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var directory = await SavesDirectoryAsync(client);
try
{
InstallGolden(directory, "current");
var path = Path.Combine(directory, "1.json");
BumpFormat(path, 99);
var before = File.ReadAllBytes(path);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
Assert.Empty((await SchoolApiTests.GetSchoolsAsync(client)).Schools);
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 CurrentFormatSave_WithoutCountryId_LeavesTheSchoolUnstartedAndTheFileUntouched()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var directory = await SavesDirectoryAsync(client);
try
{
InstallGolden(directory, "current");
var path = Path.Combine(directory, "1.json");
BumpFormat(path, 3);
var before = File.ReadAllBytes(path);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
Assert.Empty((await SchoolApiTests.GetSchoolsAsync(client)).Schools);
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 CurrentFormatSave_LoadsAsBefore()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var directory = await SavesDirectoryAsync(client);
try
{
InstallGolden(directory, "current");
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
var school = Assert.Single((await SchoolApiTests.GetSchoolsAsync(client)).Schools);
Assert.Equal(1, school.Id);
Assert.Equal("Золотая", school.Name);
}
finally
{
await SchoolApiTests.ResetAsync(client);
}
}
[Fact]
public async Task Dump_ReturnsPeopleNodesAndTimetable()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Дамп", TuesdayMorning);
await HireMathAsync(client, school.Id);
var dump = await client.GetFromJsonAsync<SchoolDumpResponse>(
$"/api/dev/schools/{school.Id}/dump",
TestContext.Current.CancellationToken);
Assert.NotNull(dump);
Assert.Equal(school.Id, dump.Id);
Assert.NotEmpty(dump.People);
Assert.Contains(dump.People, person => person.NodeId is not null || person.Needs.Count > 0);
Assert.All(dump.People, person =>
{
Assert.False(string.IsNullOrWhiteSpace(person.Id));
Assert.False(string.IsNullOrWhiteSpace(person.FullName));
Assert.NotEmpty(person.Needs);
});
Assert.Contains(dump.Lessons, lesson => lesson.Subject == "Mathematics");
}
[Fact]
public async Task Dump_UnknownSchool_IsNotFound()
{
using var client = fixture.App.CreateHttpClient("server");
using var response = await client.GetAsync("/api/dev/schools/999999/dump", TestContext.Current.CancellationToken);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
Assert.Equal("unknown-school", await ProblemCodeAsync(response));
}
private static async Task HireMathAsync(HttpClient client, int schoolId)
{
var staffing = await client.GetFromJsonAsync<StaffingResponse>(
$"/api/schools/{schoolId}/staffing",
TestContext.Current.CancellationToken);
Assert.NotNull(staffing);
var applicant = Assert.Single(staffing.Applicants.Take(1));
using var hire = await client.PostAsJsonAsync(
$"/api/schools/{schoolId}/staff/hire",
new { personId = applicant.Id, position = "Teacher" },
TestContext.Current.CancellationToken);
hire.EnsureSuccessStatusCode();
using var assign = await client.PostAsJsonAsync(
$"/api/schools/{schoolId}/staff/{Uri.EscapeDataString(applicant.Id)}/subjects",
new { subject = "Mathematics" },
TestContext.Current.CancellationToken);
assign.EnsureSuccessStatusCode();
}
private static async Task<string> SavesDirectoryAsync(HttpClient client)
{
var payload = await client.GetFromJsonAsync<SavesDirectoryResponse>(
"/api/dev/saves-directory",
TestContext.Current.CancellationToken);
Assert.NotNull(payload);
Assert.False(string.IsNullOrWhiteSpace(payload.Path));
return payload.Path;
}
private static void InstallGolden(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 BumpFormat(string path, int format)
{
var node = JsonNode.Parse(File.ReadAllText(path))
?? throw new InvalidOperationException($"Save {path} parsed to nothing.");
node["format"] = format;
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<string?> ProblemCodeAsync(HttpResponseMessage response)
{
var problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
return problem?.Code;
}
private sealed record SavesDirectoryResponse(string Path);
private sealed record ProblemResponse(string? Code);
private sealed record StaffingResponse(IReadOnlyList<ApplicantResponse> Applicants);
private sealed record ApplicantResponse(string Id);
private sealed record SchoolDumpResponse(
int Id,
string Name,
DateTime GameTime,
bool Running,
IReadOnlyList<DumpPersonResponse> People,
IReadOnlyList<DumpLessonResponse> Now,
IReadOnlyList<DumpLessonResponse> Lessons);
private sealed record DumpPersonResponse(
string Id,
string FullName,
string? NodeId,
IReadOnlyDictionary<string, float> Needs);
private sealed record DumpLessonResponse(
string ClassId,
string Subject,
string TeacherId,
string RoomId,
int Day,
int Period);
}