using ClawdDotNet.Core.Security; using ClawdDotNet.Web.Contracts; using Shouldly; namespace ClawdDotNet.Web.Tests; /// /// No credential leaves the API. /// /// 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 return agent.Tools; would have sent the whole tree including passwords onto the /// network — the kind of error nobody notices, because everything works. /// public sealed class SecretMaskingTests { [Fact] public void A_password_does_not_come_out() { var settings = new Dictionary { ["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 { ["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 { [key] = "geheim" }) .Values.Keys.ShouldNotContain(key); } [Fact] public void The_spelling_of_the_key_does_not_matter() { Mapping.ToDto("X", new Dictionary { ["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 { ["host"] = "mail.example", ["password"] = "sehr-geheim" }); System.Text.Json.JsonSerializer.Serialize(dto).ShouldNotContain("sehr-geheim"); } }