Complete phase 47 romance pack on generic orientation and affinity hooks.

Vanilla catalogs stay without crushes; a content pack ships the overlay so later mods can reuse it.
This commit is contained in:
Leonid Pershin
2026-08-20 10:42:42 +03:00
parent cb869e4977
commit d2911c3daf
43 changed files with 1840 additions and 29 deletions
@@ -0,0 +1,236 @@
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace HSchool.AppHost.Tests;
[Collection(AppHostCollection.Name)]
public class RomancePackTests(AppHostFixture fixture)
{
private static readonly DateTime Start = new(2012, 3, 31, 6, 0, 0, DateTimeKind.Utc);
[Fact]
public async Task Catalog_WithoutRomance_HasNoRomanticTopics()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.LoginAsync(client);
var vanilla = await client.GetFromJsonAsync<CatalogResponse>("/api/catalog?lang=ru", TestContext.Current.CancellationToken);
var example = await client.GetFromJsonAsync<CatalogResponse>(
"/api/catalog?lang=ru&mods=example",
TestContext.Current.CancellationToken);
Assert.NotNull(vanilla);
Assert.NotNull(example);
Assert.DoesNotContain(vanilla.Topics, topic => topic.DefName is "TopicCrush" or "TopicCouple" or "TopicTease");
Assert.DoesNotContain(vanilla.Topics, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
Assert.DoesNotContain(example.Topics, topic => topic.DefName is "TopicCrush" or "TopicCouple" or "TopicTease");
Assert.DoesNotContain(example.Topics, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
}
[Fact]
public async Task Catalog_WithRomance_AddsPackTopics()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.LoginAsync(client);
var catalog = await client.GetFromJsonAsync<CatalogResponse>(
"/api/catalog?lang=ru&mods=romance",
TestContext.Current.CancellationToken);
Assert.NotNull(catalog);
Assert.Contains(catalog.Topics, topic => topic.DefName == "TopicCrush");
Assert.Contains(catalog.Topics, topic => topic.Tags.Contains("romance", StringComparer.Ordinal));
}
[Fact]
public async Task Create_WithPack_WritesOrientation_WithoutPack_OmitsField()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var vanilla = await SchoolApiTests.CreateAsync(client, "Без романтики", Start, seed: 47);
var vanillaJson = await ReadPeopleJsonAsync(client, vanilla.Id);
Assert.DoesNotContain("\"orientation\"", vanillaJson, StringComparison.Ordinal);
var packed = await CreateWithModsAsync(client, "С романтикой", ["romance"]);
var packedJson = await ReadPeopleJsonAsync(client, packed.Id);
Assert.Contains("\"orientation\"", packedJson, StringComparison.Ordinal);
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{packed.Id}/people?role=student&pageSize=5",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{packed.Id}/people/{Uri.EscapeDataString(page.People[0].Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.False(string.IsNullOrWhiteSpace(card.Orientation?.DefName));
}
[Fact]
public async Task SixteenYearOlds_DoNotPair_WhenOpinionsAreMax()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await CreateWithModsAsync(client, "Шестнадцать", ["romance"]);
var page = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&year=10&pageSize=40",
TestContext.Current.CancellationToken);
Assert.NotNull(page);
Assert.True(page.People.Count >= 2, "need two tenth-years");
var first = page.People[0];
var second = page.People.First(person => person.Id != first.Id);
await PatchPeopleAsync(client, school.Id, people =>
{
Link(people, first.Id, second.Id, 100);
Link(people, second.Id, first.Id, 100);
});
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(first.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.Null(card.Connections?.Pair);
Assert.DoesNotContain("\"partnerId\"", await ReadPeopleJsonAsync(client, school.Id), StringComparison.Ordinal);
}
[Fact]
public async Task Teacher_DoesNotGainCrushOnStudent()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await CreateWithModsAsync(client, "Учитель", ["romance"]);
var staffing = await client.GetFromJsonAsync<StaffingResponse>(
$"/api/schools/{school.Id}/staffing",
TestContext.Current.CancellationToken);
Assert.NotNull(staffing);
var applicant = staffing.Applicants.First(row => !row.IsParent);
using var hire = await client.PostAsJsonAsync(
$"/api/schools/{school.Id}/staff/hire",
new { personId = applicant.Id, position = "Teacher" },
TestContext.Current.CancellationToken);
hire.EnsureSuccessStatusCode();
var pupils = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&pageSize=5",
TestContext.Current.CancellationToken);
Assert.NotNull(pupils);
var pupilId = pupils.People[0].Id;
await PatchPeopleAsync(client, school.Id, people => Link(people, applicant.Id, pupilId, 100));
var json = await ReadPeopleJsonAsync(client, school.Id);
Assert.DoesNotContain($"\"partnerId\": \"{pupilId}\"", json, StringComparison.Ordinal);
var teacher = JsonNode.Parse(json)!["people"]!.AsArray()
.First(node => node!["id"]?.GetValue<string>() == applicant.Id);
var crushes = teacher!["bonds"]?["crushes"]?.AsArray() ?? [];
Assert.DoesNotContain(crushes, node => node?.GetValue<string>() == pupilId);
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(applicant.Id)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.DoesNotContain(card.Connections?.Crushes ?? [], row => row.Id == pupilId);
}
[Fact]
public async Task FirstYear_DoesNotCrushEleventh()
{
using var client = fixture.App.CreateHttpClient("server");
await SchoolApiTests.ResetAsync(client);
var school = await CreateWithModsAsync(client, "Параллели", ["romance"]);
var first = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&year=1&pageSize=5",
TestContext.Current.CancellationToken);
var eleventh = await client.GetFromJsonAsync<PeoplePage>(
$"/api/schools/{school.Id}/people?role=student&year=11&pageSize=5",
TestContext.Current.CancellationToken);
Assert.NotNull(first);
Assert.NotNull(eleventh);
Assert.NotEmpty(first.People);
Assert.NotEmpty(eleventh.People);
var youngId = first.People[0].Id;
var oldId = eleventh.People[0].Id;
await PatchPeopleAsync(client, school.Id, people => Link(people, youngId, oldId, 100));
var card = await client.GetFromJsonAsync<Card>(
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(youngId)}?lang=ru",
TestContext.Current.CancellationToken);
Assert.NotNull(card);
Assert.DoesNotContain(card.Connections?.Crushes ?? [], row => row.Id == oldId);
}
private static async Task<SchoolApiTests.SchoolResponse> CreateWithModsAsync(
HttpClient client,
string name,
string[] modIds)
{
using var response = await client.PostAsJsonAsync(
"/api/schools",
new { name, startDate = Start, modIds, seed = 47 },
TestContext.Current.CancellationToken);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<SchoolApiTests.SchoolResponse>(TestContext.Current.CancellationToken);
Assert.NotNull(created);
return created;
}
private static async Task<string> ReadPeopleJsonAsync(HttpClient client, int schoolId)
{
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
return await File.ReadAllTextAsync(
Path.Combine(directory, $"{schoolId}.people.json"),
TestContext.Current.CancellationToken);
}
private static async Task PatchPeopleAsync(HttpClient client, int schoolId, Action<JsonArray> patch)
{
var directory = await SchoolApiTests.SavesDirectoryAsync(client);
var path = Path.Combine(directory, $"{schoolId}.people.json");
var doc = JsonNode.Parse(await File.ReadAllTextAsync(path, TestContext.Current.CancellationToken))!;
var people = doc["people"]!.AsArray();
patch(people);
await File.WriteAllTextAsync(path, doc.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), TestContext.Current.CancellationToken);
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
reload.EnsureSuccessStatusCode();
}
private static void Link(JsonArray people, string fromId, string toId, int opinion)
{
var from = people.First(node => node!["id"]?.GetValue<string>() == fromId)!.AsObject();
from["orientation"] = "Bisexual";
var opinions = from["opinions"] as JsonObject ?? new JsonObject();
opinions[toId] = opinion;
from["opinions"] = opinions;
var bonds = from["bonds"] as JsonObject ?? new JsonObject();
bonds["crushes"] = new JsonArray();
from["bonds"] = bonds;
}
private sealed record CatalogResponse(IReadOnlyList<TopicInfo> Topics);
private sealed record TopicInfo(string DefName, string Label, IReadOnlyList<string> Tags);
private sealed record PeoplePage(IReadOnlyList<PersonRow> People);
private sealed record PersonRow(string Id);
private sealed record Card(string Id, DefLabel? Orientation, PersonConnections? Connections);
private sealed record DefLabel(string DefName, string Label);
private sealed record PersonConnections(
IReadOnlyList<PersonLink>? Crushes,
PersonLink? Pair);
private sealed record PersonLink(string Id);
private sealed record StaffingResponse(IReadOnlyList<ApplicantRow> Applicants);
private sealed record ApplicantRow(string Id, bool IsParent);
}