using System.Net; using System.Text; using System.Text.Json; using HSchool.Server.Game; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; namespace HSchool.Server.Tests; public class SwarmUiClientTests { [Fact] public async Task GenerateAsync_UsesSessionAndReturnsImageBytes() { var handler = new FakeHandler(); var http = new HttpClient(handler) { BaseAddress = new Uri("http://swarm.test/") }; var client = new SwarmUiClient( http, Options.Create(new SwarmUiOptions { BaseUrl = "http://swarm.test", TimeoutSeconds = 30 }), NullLogger.Instance); var preset = SwarmUiPresetDefinition.CreateDefault(); var profile = preset.ToProfile(PortraitKind.Avatar); var bytes = await client.GenerateAsync("a student", "bad", profile, CancellationToken.None); Assert.Equal([0x89, 0x50, 0x4E, 0x47], bytes.Take(4)); Assert.Contains("/API/GetNewSession", handler.Requests[0]); Assert.Contains("/API/GenerateText2Image", handler.Requests[1]); } [Fact] public async Task ProbeAsync_ReturnsTrueWhenSessionOpens() { var handler = new FakeHandler(); var http = new HttpClient(handler) { BaseAddress = new Uri("http://swarm.test/") }; var client = new SwarmUiClient( http, Options.Create(new SwarmUiOptions { BaseUrl = "http://swarm.test", TimeoutSeconds = 30 }), NullLogger.Instance); var ok = await client.ProbeAsync(TimeSpan.FromSeconds(5), CancellationToken.None); Assert.True(ok); Assert.Single(handler.Requests); Assert.Contains("/API/GetNewSession", handler.Requests[0]); } [Fact] public async Task ProbeAsync_ReturnsFalseWhenUnreachable() { var handler = new FailingHandler(); var http = new HttpClient(handler) { BaseAddress = new Uri("http://swarm.test/") }; var client = new SwarmUiClient( http, Options.Create(new SwarmUiOptions { BaseUrl = "http://swarm.test", TimeoutSeconds = 30 }), NullLogger.Instance); var ok = await client.ProbeAsync(TimeSpan.FromSeconds(5), CancellationToken.None); Assert.False(ok); } private sealed class FailingHandler : HttpMessageHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable)); } [Fact] public async Task GenerateAsync_SendsClipStopAtLayerForClipSkip() { var handler = new CapturingHandler(); var http = new HttpClient(handler) { BaseAddress = new Uri("http://swarm.test/") }; var client = new SwarmUiClient( http, Options.Create(new SwarmUiOptions { BaseUrl = "http://swarm.test", TimeoutSeconds = 30 }), NullLogger.Instance); var preset = SwarmUiPresetDefinition.CreateDefault(); preset.Steps = 4; preset.CfgScale = 2; preset.ClipSkip = 2; preset.Sampler = "dpmpp_sde"; preset.Scheduler = "karras"; preset.PositiveLoras = [new SwarmUiLoraEntry { Name = "style.safetensors", Weight = 0.75 }]; var profile = preset.ToProfile(PortraitKind.Avatar); await client.GenerateAsync("a student", "bad", profile, CancellationToken.None); using var document = JsonDocument.Parse(handler.GenerateBody!); Assert.Equal(-2, document.RootElement.GetProperty("clipstopatlayer").GetInt32()); Assert.False(document.RootElement.TryGetProperty("clipskip", out _)); Assert.Equal("style.safetensors", document.RootElement.GetProperty("loras").GetString()); Assert.Equal("0.75", document.RootElement.GetProperty("loraweights").GetString()); } [Fact] public void Registration_StripsStandardResilienceHandler() { var services = new ServiceCollection(); services.AddLogging(); services.ConfigureHttpClientDefaults(http => http.AddStandardResilienceHandler()); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["SwarmUi:BaseUrl"] = "http://127.0.0.1:7801", ["SwarmUi:TimeoutSeconds"] = "180", }) .Build(); services.AddSwarmUi(configuration); using var provider = services.BuildServiceProvider(); var factory = provider.GetRequiredService(); using var handler = factory.CreateHandler(nameof(SwarmUiClient)); Assert.DoesNotContain( HandlerTypeNames(handler), name => name.Contains("Resilience", StringComparison.Ordinal)); } private static IEnumerable HandlerTypeNames(HttpMessageHandler handler) { for (HttpMessageHandler? current = handler; current is not null; current = (current as DelegatingHandler)?.InnerHandler) { yield return current.GetType().FullName ?? current.GetType().Name; } } private sealed class CapturingHandler : HttpMessageHandler { public string? GenerateBody { get; private set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (request.RequestUri!.AbsolutePath.Contains("GetNewSession", StringComparison.Ordinal)) { var session = JsonSerializer.Serialize(new { session_id = "sess-1" }); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(session, Encoding.UTF8, "application/json"), }; } if (request.RequestUri.AbsolutePath.Contains("GenerateText2Image", StringComparison.Ordinal)) { GenerateBody = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken); var payload = JsonSerializer.Serialize(new { images = new[] { "data:image/png;base64,iVBORw0KGgo=" } }); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(payload, Encoding.UTF8, "application/json"), }; } return new HttpResponseMessage(HttpStatusCode.NotFound); } } private sealed class FakeHandler : HttpMessageHandler { public List Requests { get; } = []; protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Requests.Add(request.RequestUri!.AbsolutePath); if (request.RequestUri.AbsolutePath.Contains("GetNewSession", StringComparison.Ordinal)) { var session = JsonSerializer.Serialize(new { session_id = "sess-1" }); return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(session, Encoding.UTF8, "application/json"), }); } if (request.RequestUri.AbsolutePath.Contains("GenerateText2Image", StringComparison.Ordinal)) { var payload = JsonSerializer.Serialize(new { images = new[] { "data:image/png;base64,iVBORw0KGgo=" } }); return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(payload, Encoding.UTF8, "application/json"), }); } return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound)); } } }