- ClawdDotNet.Web: Kestrel im Prozess der Anwendung, Token-Absicherung (TokenGate mit FixedTimeEquals), Endpunkte unter /api, Ereignisstrom unter /events (Server-Sent Events) und fortlaufendes Log-Streaming. - Weboberflaeche (wwwroot/): Vollstaendiges Single-Page-Frontend mit nativem HTML5, CSS3 und ES-Modulen ohne Buildschritt (strikt textContent, #-Routing, sofortiger Login-Sprung bei fehlendem Token, Light/Dark-Theme, lokale Barlow-Schriften). - ClawdDotNet.App: Fachlogik aus den Avalonia-ViewModels in Dienste herausgeloest (AgentAdministration, JobAdministration, ServiceAdministration, RunCoordinator) und ueber InstanceChange synchronisiert. - Tests: ClawdDotNet.Web.Tests und ClawdDotNet.App.Tests hinzugefuegt, alle 684 Tests erfolgreich.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
using ClawdDotNet.Core.Config;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Web.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Die Weboberfläche macht aus einer lokalen Anwendung einen Netzwerkdienst, der Agenten
|
||||
/// mit Datei-, Mail-, FTP- und Datenbankzugriff steuert. Was hier geprüft wird, sind
|
||||
/// keine Feinheiten der Konfiguration, sondern die Bedingungen, unter denen sie
|
||||
/// überhaupt hochkommen darf.
|
||||
/// </summary>
|
||||
public sealed class WebUiOptionsTests
|
||||
{
|
||||
private static InstanceConfig Instanz(
|
||||
bool enabled = true,
|
||||
string binding = "loopback",
|
||||
string token = "ein-hinreichend-langes-token",
|
||||
int port = 8080)
|
||||
=> new()
|
||||
{
|
||||
WebUiEnabled = enabled,
|
||||
WebUiBinding = binding,
|
||||
WebUiToken = token,
|
||||
WebServerPort = port
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Sie_ist_in_der_Vorbelegung_aus()
|
||||
{
|
||||
// Wer einen Netzwerkdienst will, schaltet ihn ein. Nicht andersherum.
|
||||
new InstanceConfig().WebUiEnabled.ShouldBeFalse();
|
||||
new InstanceConfig().WebUiBinding.ShouldBe("loopback");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Abgeschaltet_ist_kein_Fehler()
|
||||
{
|
||||
WebUiOptions.TryCreate(Instanz(enabled: false), out var problem).ShouldBeNull();
|
||||
problem.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ohne_Token_startet_sie_nicht_einmal_auf_Loopback()
|
||||
{
|
||||
// Auf einer Maschine, auf der Agenten Browser-Werkzeuge bedienen, ist
|
||||
// „localhost" keine Vertrauensgrenze.
|
||||
WebUiOptions.TryCreate(Instanz(token: ""), out var problem).ShouldBeNull();
|
||||
problem.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alle_Adressen_ohne_Token_werden_abgelehnt_und_nicht_bemaengelt()
|
||||
{
|
||||
var options = WebUiOptions.TryCreate(Instanz(binding: "any", token: ""), out var problem);
|
||||
|
||||
options.ShouldBeNull();
|
||||
problem.ShouldNotBeNull();
|
||||
problem!.ShouldContain("So wird sie nicht gestartet");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_zu_kurzes_Token_zaehlt_nicht()
|
||||
{
|
||||
WebUiOptions.TryCreate(Instanz(token: "kurz"), out var problem).ShouldBeNull();
|
||||
problem.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Loopback_bindet_an_127_0_0_1()
|
||||
{
|
||||
var options = WebUiOptions.TryCreate(Instanz(port: 9001), out _);
|
||||
|
||||
options.ShouldNotBeNull();
|
||||
options!.Url.ShouldBe("http://127.0.0.1:9001");
|
||||
options.LoopbackOnly.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Alles_ausser_any_bleibt_Loopback()
|
||||
{
|
||||
// Ein Tippfehler in der Instanzdatei darf die Oberflaeche nicht ins Netz stellen.
|
||||
WebUiOptions.TryCreate(Instanz(binding: "alle"), out _)!.LoopbackOnly.ShouldBeTrue();
|
||||
WebUiOptions.TryCreate(Instanz(binding: ""), out _)!.LoopbackOnly.ShouldBeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mit_Token_darf_sie_an_alle_Adressen()
|
||||
{
|
||||
var options = WebUiOptions.TryCreate(Instanz(binding: "ANY"), out _);
|
||||
|
||||
options.ShouldNotBeNull();
|
||||
options!.LoopbackOnly.ShouldBeFalse();
|
||||
options.Url.ShouldStartWith("http://0.0.0.0:");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(-1)]
|
||||
[InlineData(70000)]
|
||||
public void Ein_unmoeglicher_Port_wird_abgelehnt(int port)
|
||||
{
|
||||
WebUiOptions.TryCreate(Instanz(port: port), out var problem).ShouldBeNull();
|
||||
problem.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Das_Token_steht_geschuetzt_in_der_Instanzdatei()
|
||||
{
|
||||
// Ohne den Eintrag in ConfigSecrets staende es im Klartext neben dem
|
||||
// geschuetzten OpenRouter-Schluessel.
|
||||
Core.Security.ConfigSecrets.IsSecretKey("webUiToken").ShouldBeTrue();
|
||||
|
||||
var config = Instanz(token: "ein-hinreichend-langes-token");
|
||||
Core.Security.ConfigSecrets.Protect(config);
|
||||
|
||||
config.WebUiToken.ShouldNotBe("ein-hinreichend-langes-token");
|
||||
|
||||
Core.Security.ConfigSecrets.Unprotect(config);
|
||||
config.WebUiToken.ShouldBe("ein-hinreichend-langes-token");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user