Rund 2.600 Kommentarblöcke in 310 C#-Dateien übersetzt, Begründungen vollständig erhalten: blockweise gleiche Anzahl wie vorher, kein Block unter 60 % seiner Länge, der Code außerhalb der Kommentare per Tokenizer byte-identisch geprüft. Zitate deutscher Oberflächentexte und Testdaten bleiben deutsch. WebUI: Kommentare in JS/CSS/HTML übersetzt, JSDoc-@param an die in 4b umbenannten Parameter angeglichen. Dabei behoben: - 4b hatte "tag" (HTML-Tag) als deutsches "Tag" gelesen und zu "dayValue" gemacht (makeElement, svg, Tagesübersicht) - zurück auf tag/day. - Aufgaben "InProgress" bekamen die Plakettenart "laufend", für die es nie eine CSS-Klasse gab - jetzt "success" wie laufende Instanzen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.1 KiB
C#
97 lines
3.1 KiB
C#
using ClawdDotNet.Core.Security;
|
|
using ClawdDotNet.Web.Contracts;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Web.Tests;
|
|
|
|
/// <summary>
|
|
/// No credential leaves the API.
|
|
///
|
|
/// <para>This is the check nobody wants to do by hand: tool settings
|
|
/// are a free dictionary, and whether there is a password in it is decided by the name of the
|
|
/// key. A <c>return agent.Tools;</c> would have sent the whole tree including passwords onto the
|
|
/// network — the kind of error nobody notices, because everything works.</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 instead of ShouldContainKey: the DTOs hand out
|
|
// IReadOnlyDictionary, and Shouldly's dictionary extensions sit
|
|
// on IDictionary.
|
|
dto.Values.Keys.ShouldContain("host");
|
|
dto.Values.Keys.ShouldContain("port");
|
|
dto.Values.Keys.ShouldNotContain("password");
|
|
|
|
// What comes out is the information "something is there" — a display
|
|
// needs no more, and may know no more.
|
|
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)
|
|
{
|
|
// The list comes from ConfigSecrets and not from a second one here: a
|
|
// second would be the one forgotten with the next new tool.
|
|
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()
|
|
{
|
|
// The test that really counts: not the structure, but what goes over the
|
|
// line.
|
|
var dto = Mapping.ToDto("Mail", new Dictionary<string, object?>
|
|
{
|
|
["host"] = "mail.example",
|
|
["password"] = "sehr-geheim"
|
|
});
|
|
|
|
System.Text.Json.JsonSerializer.Serialize(dto).ShouldNotContain("sehr-geheim");
|
|
}
|
|
}
|