Mit Roslyn umbenannt (Verweise folgen, Zeichenketten bleiben unberuehrt). Nicht umbenannt, weil gespeichert oder ueber die API gebunden (Stufe 4e): Properties, Record-Parameter, Enum-Member sowie die Parameter der Endpunkt-Handler (Routen-/Query-Bindung ueber den Namen). Die JSON-Felder anonymer Objekte bleiben ausdruecklich benannt (JsonFilter: pfad). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
97 lines
3.0 KiB
C#
97 lines
3.0 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 A_password_does_not_come_out()
|
|
{
|
|
var settings = new Dictionary<string, object?>
|
|
{
|
|
["host"] = "mail.example",
|
|
["port"] = 993,
|
|
["password"] = "sehr-geheim"
|
|
};
|
|
|
|
var dto = Mapping.ToDto("Mail", settings);
|
|
|
|
// 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 An_empty_secret_counts_as_not_set()
|
|
{
|
|
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 Every_known_secret_name_is_masked(string key)
|
|
{
|
|
// 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(key).ShouldBeTrue();
|
|
|
|
Mapping.ToDto("X", new Dictionary<string, object?> { [key] = "geheim" })
|
|
.Values.Keys.ShouldNotContain(key);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_spelling_of_the_key_does_not_matter()
|
|
{
|
|
Mapping.ToDto("X", new Dictionary<string, object?> { ["PASSWORD"] = "geheim" })
|
|
.Values.ShouldBeEmpty();
|
|
}
|
|
|
|
[Fact]
|
|
public void A_serialised_DTO_does_not_contain_the_secret()
|
|
{
|
|
// 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");
|
|
}
|
|
}
|