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>
96 lines
3.2 KiB
C#
96 lines
3.2 KiB
C#
using ClawdDotNet.Tools.DirectAPI;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.Tools.Tests.DirectAPI;
|
|
|
|
/// <summary>
|
|
/// S3 from the inventory: the fetched URL went to the model as "source" —
|
|
/// including apikey= in the query string. The key thus ended up in the context, was
|
|
/// sent again at every following step and written into ChatContext.json.
|
|
/// </summary>
|
|
public sealed class UrlSanitizerTests
|
|
{
|
|
private const string Secret = "GEHEIM-abc123XYZ";
|
|
|
|
[Theory]
|
|
[InlineData("https://api.twelvedata.com/quote?symbol=NVDA&apikey=GEHEIM-abc123XYZ")]
|
|
[InlineData("https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=X&apikey=GEHEIM-abc123XYZ")]
|
|
[InlineData("https://api.example.com/v1?api_key=GEHEIM-abc123XYZ&x=1")]
|
|
[InlineData("https://api.example.com/v1?token=GEHEIM-abc123XYZ")]
|
|
[InlineData("https://api.example.com/v1?access_token=GEHEIM-abc123XYZ")]
|
|
[InlineData("https://api.example.com/v1?KEY=GEHEIM-abc123XYZ")]
|
|
[InlineData("https://api.example.com/v1?ApiKey=GEHEIM-abc123XYZ&other=2")]
|
|
[InlineData("https://api.example.com/v1?secret=GEHEIM-abc123XYZ")]
|
|
[InlineData("https://api.example.com/v1?a=1&apikey=GEHEIM-abc123XYZ#fragment")]
|
|
public void Sensitive_parameters_are_masked(string url)
|
|
{
|
|
var sanitized = UrlSanitizer.Sanitize(url);
|
|
|
|
sanitized.ShouldNotContain(Secret);
|
|
sanitized.ShouldContain("***");
|
|
}
|
|
|
|
[Fact]
|
|
public void Harmless_parameters_stay_readable()
|
|
{
|
|
var sanitized = UrlSanitizer.Sanitize(
|
|
"https://api.twelvedata.com/quote?symbol=NVDA&interval=1day&apikey=GEHEIM-abc123XYZ");
|
|
|
|
sanitized.ShouldContain("symbol=NVDA");
|
|
sanitized.ShouldContain("interval=1day");
|
|
sanitized.ShouldContain("apikey=***");
|
|
}
|
|
|
|
[Fact]
|
|
public void The_path_is_kept()
|
|
{
|
|
var sanitized = UrlSanitizer.Sanitize("https://api.massive.com/v2/aggs/ticker/NVDA?apikey=x");
|
|
|
|
sanitized.ShouldStartWith("https://api.massive.com/v2/aggs/ticker/NVDA?");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_fragment_is_kept()
|
|
{
|
|
var sanitized = UrlSanitizer.Sanitize("https://x.de/a?apikey=geheim#abschnitt");
|
|
|
|
sanitized.ShouldEndWith("#abschnitt");
|
|
sanitized.ShouldNotContain("geheim");
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("https://api.example.com/v1")]
|
|
[InlineData("https://api.example.com/v1?symbol=NVDA")]
|
|
public void URLs_without_sensitive_parameters_stay_unchanged(string url)
|
|
{
|
|
UrlSanitizer.Sanitize(url).ShouldBe(url);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null)]
|
|
[InlineData("")]
|
|
[InlineData(" ")]
|
|
public void Empty_inputs_do_not_lead_to_an_error(string? url)
|
|
{
|
|
Should.NotThrow(() => UrlSanitizer.Sanitize(url));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_parameter_without_value_does_no_harm()
|
|
{
|
|
var sanitized = UrlSanitizer.Sanitize("https://x.de/a?flag&apikey=geheim");
|
|
|
|
sanitized.ShouldContain("flag");
|
|
sanitized.ShouldNotContain("geheim");
|
|
}
|
|
|
|
[Fact]
|
|
public void A_key_as_part_of_another_name_is_not_wrongly_masked()
|
|
{
|
|
// "monkey" contains "key", but is not a sensitive parameter.
|
|
var sanitized = UrlSanitizer.Sanitize("https://x.de/a?monkey=banane");
|
|
|
|
sanitized.ShouldBe("https://x.de/a?monkey=banane");
|
|
}
|
|
}
|