using System.Net; using System.Net.Http.Json; using System.Text.Json; namespace HSchool.AppHost.Tests; [Collection(AppHostCollection.Name)] public class PortraitApiTests(AppHostFixture fixture) { private static readonly DateTime Start = new(2012, 4, 3, 6, 0, 0, DateTimeKind.Utc); [Fact] public async Task GetPortrait_WithoutFile_Returns404() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Портреты", Start); var personId = await FirstPersonIdAsync(client, school.Id); using var response = await client.GetAsync( $"/api/schools/{school.Id}/people/{Uri.EscapeDataString(personId)}/portrait?kind=avatar", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); Assert.Equal("portrait-missing", await SchoolApiTests.ProblemCodeAsync(response)); } [Fact] public async Task GetPortrait_WithFile_ReturnsPngAndCardFlags() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Портрет на диске", Start); var personId = await FirstPersonIdAsync(client, school.Id); var directory = await SavesDirectoryAsync(client); var portraits = Path.Combine(directory, $"{school.Id}.portraits"); Directory.CreateDirectory(portraits); var png = Convert.FromBase64String("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="); await File.WriteAllBytesAsync(Path.Combine(portraits, $"{personId}.avatar.png"), png, TestContext.Current.CancellationToken); using var image = await client.GetAsync( $"/api/schools/{school.Id}/people/{Uri.EscapeDataString(personId)}/portrait?kind=avatar", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, image.StatusCode); Assert.Equal("image/png", image.Content.Headers.ContentType?.MediaType); var card = await client.GetFromJsonAsync( $"/api/schools/{school.Id}/people/{Uri.EscapeDataString(personId)}?lang=ru", TestContext.Current.CancellationToken); Assert.NotNull(card); Assert.True(card!.HasAvatar); Assert.False(card.HasFullBody); } [Fact] public async Task PostPortrait_WhenSwarmNotConfigured_Returns503() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Swarm выкл", Start); var personId = await FirstPersonIdAsync(client, school.Id); using var response = await client.PostAsync( $"/api/schools/{school.Id}/people/{Uri.EscapeDataString(personId)}/portrait?kind=avatar", null, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); Assert.Equal("swarmui-not-configured", await SchoolApiTests.ProblemCodeAsync(response)); } [Fact] public async Task GetPortraitPrompt_ReturnsBuiltPositiveAndNegative() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Промпт", Start); var personId = await FirstPersonIdAsync(client, school.Id); using var response = await client.GetAsync( $"/api/schools/{school.Id}/people/{Uri.EscapeDataString(personId)}/portrait/prompt?kind=avatar", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var payload = await response.Content.ReadFromJsonAsync(TestContext.Current.CancellationToken); Assert.NotNull(payload); Assert.Equal("avatar", payload!.Kind); Assert.Contains("age", payload.Positive, StringComparison.OrdinalIgnoreCase); Assert.False(string.IsNullOrWhiteSpace(payload.Negative)); Assert.False(string.IsNullOrWhiteSpace(payload.PresetId)); } [Fact] public async Task GetSwarmUiSettings_ReturnsPresets() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.LoginAsync(client); var settings = await client.GetFromJsonAsync( "/api/settings/swarmui", TestContext.Current.CancellationToken); Assert.NotNull(settings); Assert.NotEmpty(settings!.Presets); Assert.NotEmpty(settings.Models); Assert.False(string.IsNullOrWhiteSpace(settings.ActivePresetId)); } [Fact] public async Task CreateSchool_CopiesPortraitSettingsIntoTheSave() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Пресеты школы", Start); var directory = await SavesDirectoryAsync(client); var json = await File.ReadAllTextAsync( Path.Combine(directory, $"{school.Id}.json"), TestContext.Current.CancellationToken); using var document = JsonDocument.Parse(json); Assert.True(document.RootElement.TryGetProperty("portraitSettings", out var presets)); Assert.True(presets.TryGetProperty("presets", out var list)); Assert.True(list.GetArrayLength() > 0); Assert.True(presets.TryGetProperty("models", out var models)); Assert.True(models.GetArrayLength() > 0); } [Fact] public async Task CreateSchool_RejectsEmptyPortraitPresets() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); using var response = await client.PostAsJsonAsync( "/api/schools", new { name = "Плохие пресеты", startDate = Start, portraitSettings = new { activePresetId = "missing", presets = Array.Empty(), ageRules = Array.Empty() }, }, TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); Assert.Equal("invalid-portrait-settings", await SchoolApiTests.ProblemCodeAsync(response)); } [Fact] public async Task DeleteSchool_RemovesPortraitDirectory() { using var client = fixture.App.CreateHttpClient("server"); await SchoolApiTests.ResetAsync(client); var school = await SchoolApiTests.CreateAsync(client, "Удалить портреты", Start); var personId = await FirstPersonIdAsync(client, school.Id); var directory = await SavesDirectoryAsync(client); var portraits = Path.Combine(directory, $"{school.Id}.portraits"); Directory.CreateDirectory(portraits); await File.WriteAllBytesAsync(Path.Combine(portraits, $"{personId}.avatar.png"), [1, 2, 3], TestContext.Current.CancellationToken); using var deleted = await client.DeleteAsync($"/api/schools/{school.Id}", TestContext.Current.CancellationToken); Assert.Equal(HttpStatusCode.NoContent, deleted.StatusCode); Assert.False(Directory.Exists(portraits)); } private static async Task FirstPersonIdAsync(HttpClient client, int schoolId) { var page = await client.GetFromJsonAsync( $"/api/schools/{schoolId}/people?pageSize=1", TestContext.Current.CancellationToken); Assert.NotNull(page); Assert.NotEmpty(page!.People); return page.People[0].Id; } private static async Task SavesDirectoryAsync(HttpClient client) { var payload = await client.GetFromJsonAsync( "/api/dev/saves-directory", TestContext.Current.CancellationToken); return payload!.Path; } private sealed record PeopleListResponse(int Total, IReadOnlyList People); private sealed record PersonListItem(string Id); private sealed record PersonCardResponse( string Id, bool HasAvatar, bool HasCustom, bool HasFullBody, string? CustomPortraitPrompt); private sealed record PortraitPromptPayload( string Kind, string Positive, string Negative, string? PromptExtra, string PresetId, string PresetLabel); private sealed record SwarmUiSettingsPayload( string ActivePresetId, IReadOnlyList Presets, IReadOnlyList Models); private sealed record SwarmUiPresetPayload(string Id, string Label); private sealed record SwarmUiModelPayload(string Id); private sealed record SavesDirectoryResponse(string Path); }