- 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.
97 lines
3.2 KiB
C#
97 lines
3.2 KiB
C#
using ClawdDotNet.Core.Security;
|
|
using ClawdDotNet.Web.Contracts;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Web.Tests;
|
|
|
|
/// <summary>
|
|
/// Kein Zugangsdatum verlässt die API.
|
|
///
|
|
/// <para>Das ist die Prüfung, die man nicht von Hand machen will: Werkzeug-Einstellungen
|
|
/// sind ein freies Wörterbuch, und ob darin ein Passwort steckt, entscheidet der Name des
|
|
/// Schlüssels. Ein <c>return agent.Tools;</c> hätte den ganzen Baum samt Passwörtern ins
|
|
/// Netz geschickt — die Sorte Fehler, die niemandem auffällt, weil alles funktioniert.</para>
|
|
/// </summary>
|
|
public sealed class SecretMaskingTests
|
|
{
|
|
[Fact]
|
|
public void Ein_Passwort_kommt_nicht_heraus()
|
|
{
|
|
var einstellungen = new Dictionary<string, object?>
|
|
{
|
|
["host"] = "mail.example",
|
|
["port"] = 993,
|
|
["password"] = "sehr-geheim"
|
|
};
|
|
|
|
var dto = Mapping.ToDto("Mail", einstellungen);
|
|
|
|
// Keys.ShouldContain statt ShouldContainKey: Die DTOs geben
|
|
// IReadOnlyDictionary heraus, und Shouldlys Woerterbuch-Erweiterungen sitzen
|
|
// auf IDictionary.
|
|
dto.Values.Keys.ShouldContain("host");
|
|
dto.Values.Keys.ShouldContain("port");
|
|
dto.Values.Keys.ShouldNotContain("password");
|
|
|
|
// Was herauskommt, ist die Auskunft „da steht etwas" — mehr braucht eine
|
|
// Anzeige nicht, und mehr darf sie nicht wissen.
|
|
dto.Secrets["password"].ShouldBeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public void Ein_leeres_Geheimnis_gilt_als_nicht_gesetzt()
|
|
{
|
|
var dto = Mapping.ToDto("Mail", new Dictionary<string, object?>
|
|
{
|
|
["password"] = "",
|
|
["apiKey"] = null
|
|
});
|
|
|
|
dto.Secrets["password"].ShouldBeFalse();
|
|
dto.Secrets["apiKey"].ShouldBeFalse();
|
|
dto.Values.ShouldBeEmpty();
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("password")]
|
|
[InlineData("apiKey")]
|
|
[InlineData("api_key")]
|
|
[InlineData("token")]
|
|
[InlineData("accessToken")]
|
|
[InlineData("botToken")]
|
|
[InlineData("clientSecret")]
|
|
[InlineData("connectionString")]
|
|
[InlineData("apiHash")]
|
|
[InlineData("webUiToken")]
|
|
public void Jeder_bekannte_Geheimnisname_wird_maskiert(string schluessel)
|
|
{
|
|
// Die Liste kommt aus ConfigSecrets und nicht aus einer zweiten hier: Eine
|
|
// zweite waere die, die beim naechsten neuen Werkzeug vergessen wird.
|
|
ConfigSecrets.IsSecretKey(schluessel).ShouldBeTrue();
|
|
|
|
Mapping.ToDto("X", new Dictionary<string, object?> { [schluessel] = "geheim" })
|
|
.Values.Keys.ShouldNotContain(schluessel);
|
|
}
|
|
|
|
[Fact]
|
|
public void Die_Schreibweise_des_Schluessels_spielt_keine_Rolle()
|
|
{
|
|
Mapping.ToDto("X", new Dictionary<string, object?> { ["PASSWORD"] = "geheim" })
|
|
.Values.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void Ein_serialisiertes_DTO_enthaelt_das_Geheimnis_nicht()
|
|
{
|
|
// Der Test, der wirklich zaehlt: nicht die Struktur, sondern das, was ueber die
|
|
// Leitung geht.
|
|
var dto = Mapping.ToDto("Mail", new Dictionary<string, object?>
|
|
{
|
|
["host"] = "mail.example",
|
|
["password"] = "sehr-geheim"
|
|
});
|
|
|
|
System.Text.Json.JsonSerializer.Serialize(dto).ShouldNotContain("sehr-geheim");
|
|
}
|
|
}
|