Picker is Swarm intersect config, preset style/shot inherit model defaults, and living saves lift the old preset shape.
210 lines
8.9 KiB
C#
210 lines
8.9 KiB
C#
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<PersonCardResponse>(
|
|
$"/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<PortraitPromptPayload>(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<SwarmUiSettingsPayload>(
|
|
"/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<object>(), ageRules = Array.Empty<object>() },
|
|
},
|
|
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<string> FirstPersonIdAsync(HttpClient client, int schoolId)
|
|
{
|
|
var page = await client.GetFromJsonAsync<PeopleListResponse>(
|
|
$"/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<string> SavesDirectoryAsync(HttpClient client)
|
|
{
|
|
var payload = await client.GetFromJsonAsync<SavesDirectoryResponse>(
|
|
"/api/dev/saves-directory",
|
|
TestContext.Current.CancellationToken);
|
|
return payload!.Path;
|
|
}
|
|
|
|
private sealed record PeopleListResponse(int Total, IReadOnlyList<PersonListItem> 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<SwarmUiPresetPayload> Presets,
|
|
IReadOnlyList<SwarmUiModelPayload> Models);
|
|
|
|
private sealed record SwarmUiPresetPayload(string Id, string Label);
|
|
|
|
private sealed record SwarmUiModelPayload(string Id);
|
|
|
|
private sealed record SavesDirectoryResponse(string Path);
|
|
}
|