Files
h-school/tests/HSchool.AppHost.Tests/InventoryApiTests.cs
T
Leonid PershinandCursor 37501c506a Dress people at generation from a separate apparel stream.
Wardrobes live on the person in people.json: everyday layers, textbooks per parallel, and chance-based bags from catalog data. Hauling and the carry cap are defs, not constants. The golden roster gains an items column; later family members shift because Hauling consumes extra appearance rolls. Old saves without items are dressed on load.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 03:24:54 +03:00

155 lines
6.2 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 InventoryApiTests(AppHostFixture fixture)
{
private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task Card_IncludesWornAndCarried_ListDoesNot()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Гардероб карточка", Start, seed: 31);
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&year=5&pageSize=20",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
var girl = page.People.First(person => person.Female);
using var listResponse = await client.GetAsync(
$"/api/schools/{school.Id}/people?pageSize=1",
TestContext.Current.CancellationToken);
var listJson = await listResponse.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
Assert.DoesNotContain("carryMass", listJson, StringComparison.Ordinal);
Assert.DoesNotContain("\"worn\"", listJson, StringComparison.Ordinal);
Assert.DoesNotContain("\"carried\"", listJson, StringComparison.Ordinal);
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(girl.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.NotEmpty(card.Worn);
Assert.Contains(card.Worn, item => item.Layers.Count > 0 && item.ColorLabel is { Length: > 0 });
Assert.True(card.CarryCapacity > 0);
Assert.True(card.CarryMass <= card.CarryCapacity);
Assert.Contains(card.Carried, item => item.Mass > 0);
}
[Fact]
public async Task SameSeed_YieldsTheSameShirt()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var first = await SchoolApiTests.CreateAsync(client, "Синяя рубашка 1", Start, seed: 31);
var shirt = await FirstGirlTopAsync(client, first.Id);
await SchoolApiTests.ResetAsync(client);
var second = await SchoolApiTests.CreateAsync(client, "Синяя рубашка 2", Start, seed: 31);
var again = await FirstGirlTopAsync(client, second.Id);
Assert.Equal(shirt.DefName, again.DefName);
Assert.Equal(shirt.Color, again.Color);
}
[Fact]
public async Task PeopleJsonWithoutInventory_IsDressedOnReloadAndRewritten()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await SchoolApiTests.CreateAsync(client, "Старый сейв без одежды", Start, seed: 31);
var before = await FirstGirlTopAsync(client, school.Id);
var directory = await SavesDirectoryAsync(client);
var path = Path.Combine(directory, $"{school.Id}.people.json");
StripItems(path);
Assert.DoesNotContain("\"items\"", File.ReadAllText(path), StringComparison.Ordinal);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
var after = await FirstGirlTopAsync(client, school.Id);
Assert.Equal(before.DefName, after.DefName);
Assert.Equal(before.Color, after.Color);
Assert.Contains("\"items\"", File.ReadAllText(path), StringComparison.Ordinal);
}
private static async Task<WornItem> FirstGirlTopAsync(HttpClient client, int schoolId)
{
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{schoolId}/people?role=student&sex=female&pageSize=20",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
Assert.NotEmpty(page.People);
var girl = page.People[0];
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{schoolId}/people/{Uri.EscapeDataString(girl.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
var top = card.Worn.FirstOrDefault(item =>
item.Layers.Any(layer => layer.DefName.Equals("Top", StringComparison.Ordinal)));
Assert.NotNull(top);
return top;
}
private static async Task<string> SavesDirectoryAsync(HttpClient client)
{
var payload = await client.GetFromJsonAsync<SavesDirectoryResponse>(
"/api/dev/saves-directory",
TestContext.Current.CancellationToken);
Assert.NotNull(payload);
return payload.Path;
}
private static void StripItems(string path)
{
var root = JsonNode.Parse(File.ReadAllText(path))
?? throw new InvalidOperationException("People file parsed to nothing.");
foreach (var person in root["people"]?.AsArray() ?? [])
{
person?.AsObject().Remove("items");
}
var applicants = root["applicants"]?["applicants"]?.AsArray();
if (applicants is not null)
{
foreach (var applicant in applicants)
{
applicant?["person"]?.AsObject()?.Remove("items");
}
}
File.WriteAllText(
path,
root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
}
private sealed record SavesDirectoryResponse(string Path);
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
private sealed record PersonRow(string Id, string FullName, bool Female);
private sealed record Card(
IReadOnlyList<WornItem> Worn,
IReadOnlyList<CarriedItem> Carried,
float CarryMass,
float CarryCapacity);
private sealed record WornItem(
string DefName,
string Label,
string? Color,
string? ColorLabel,
IReadOnlyList<Layer> Layers);
private sealed record Layer(string DefName, string Label);
private sealed record CarriedItem(string DefName, string Label, float Mass);
}