Refuse newer save formats and add a gated dump of a live school.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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_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);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Arch's native allocator is not safe across parallel test classes in this suite.
|
||||
[assembly: CollectionBehavior(DisableTestParallelization = true)]
|
||||
Reference in New Issue
Block a user