Stufe 4c (#764): Bezeichner auf Englisch - Typen, Methoden, Felder, Parameter, Locals, Testnamen

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>
This commit is contained in:
Richard
2026-10-04 13:18:43 +02:00
co-authored by Claude Opus 5.5
parent 837a634a22
commit 77062facc9
190 changed files with 4813 additions and 4813 deletions
@@ -8,11 +8,11 @@ namespace ClawdDotNet.Tools.Tests.WebFetch;
/// Stage 1 of WebFetch (#602): main content as compact Markdown instead of the whole page as
/// running text. The sample pages are self-written and reproduce typical structures.
/// </summary>
public sealed class HtmlExtraktorTests
public sealed class HtmlExtractorTests
{
private static readonly Uri Basis = new("https://www.beispiel.de/politik/artikel-123.html");
private const string Artikelseite = """
private const string ArticlePage = """
<!DOCTYPE html>
<html lang="de">
<head>
@@ -53,23 +53,23 @@ public sealed class HtmlExtraktorTests
""";
[Fact]
public void Artikel_liefert_den_Hauptinhalt_ohne_Navigation_Cookiehinweis_und_Skripte()
public void Article_returns_main_content_without_navigation_cookie_notice_and_scripts()
{
var inhalt = HtmlExtraktor.Extrahiere(Artikelseite, Basis);
var content = HtmlExtractor.Extract(ArticlePage, Basis);
inhalt.Bereich.ShouldBe("article");
inhalt.Markdown.ShouldContain("Der Stadtrat hat am Montag");
inhalt.Markdown.ShouldNotContain("Wirtschaft");
inhalt.Markdown.ShouldNotContain("Cookies");
inhalt.Markdown.ShouldNotContain("GEHEIMER-SKRIPTTEXT");
inhalt.Markdown.ShouldNotContain("Impressum");
inhalt.Markdown.ShouldNotContain("Zehn Tipps");
content.Bereich.ShouldBe("article");
content.Markdown.ShouldContain("Der Stadtrat hat am Montag");
content.Markdown.ShouldNotContain("Wirtschaft");
content.Markdown.ShouldNotContain("Cookies");
content.Markdown.ShouldNotContain("GEHEIMER-SKRIPTTEXT");
content.Markdown.ShouldNotContain("Impressum");
content.Markdown.ShouldNotContain("Zehn Tipps");
}
[Fact]
public void Struktur_bleibt_als_Markdown_erhalten()
public void Structure_is_kept_as_markdown()
{
var md = HtmlExtraktor.Extrahiere(Artikelseite, Basis).Markdown;
var md = HtmlExtractor.Extract(ArticlePage, Basis).Markdown;
md.ShouldContain("# Stadtrat beschliesst Radweg");
md.ShouldContain("## Kosten und Zeitplan");
@@ -82,84 +82,84 @@ public sealed class HtmlExtraktorTests
}
[Fact]
public void Die_Kopfzeile_des_Artikels_ist_Inhalt_nicht_Seitenrahmen()
=> HtmlExtraktor.Extrahiere(Artikelseite, Basis).Markdown.ShouldContain("Von Erika Beispiel");
public void The_article_header_is_content_not_page_frame()
=> HtmlExtractor.Extract(ArticlePage, Basis).Markdown.ShouldContain("Von Erika Beispiel");
[Fact]
public void Mit_Links_werden_relative_Ziele_absolut()
=> HtmlExtraktor.Extrahiere(Artikelseite, Basis, mitLinks: true).Markdown
public void With_links_relative_targets_become_absolute()
=> HtmlExtractor.Extract(ArticlePage, Basis, withLinks: true).Markdown
.ShouldContain("[Beschlussvorlage](https://www.beispiel.de/dokumente/beschluss.pdf)");
[Fact]
public void Ohne_Wunsch_nach_Links_bleibt_nur_der_Linktext()
public void Without_asking_for_links_only_the_link_text_stays()
{
var md = HtmlExtraktor.Extrahiere(Artikelseite, Basis).Markdown;
var md = HtmlExtractor.Extract(ArticlePage, Basis).Markdown;
md.ShouldContain("Laut Beschlussvorlage kostet");
md.ShouldNotContain("https://www.beispiel.de/dokumente");
}
[Fact]
public void Uebersichtsseite_mit_vielen_Teaser_Artikeln_nimmt_den_ganzen_Hauptbereich()
public void Overview_page_with_many_teaser_articles_takes_the_whole_main_area()
{
var teaser = string.Concat(Enumerable.Range(1, 6).Select(i =>
$"<article><h2>Meldung {i}</h2><p>Kurzfassung der Meldung {i} mit so viel Text, dass die Kachel fuer sich allein "
+ "die Mindestlaenge fuer einen Bereich erreicht und damit als Kandidat in Frage kaeme, obwohl sie nur einer von vielen ist.</p></article>"));
var html = $"<html><body><nav>Menue</nav><main>{teaser}</main></body></html>";
var inhalt = HtmlExtraktor.Extrahiere(html, Basis);
var content = HtmlExtractor.Extract(html, Basis);
inhalt.Bereich.ShouldBe("main");
inhalt.Markdown.ShouldContain("## Meldung 1");
inhalt.Markdown.ShouldContain("## Meldung 6");
content.Bereich.ShouldBe("main");
content.Markdown.ShouldContain("## Meldung 1");
content.Markdown.ShouldContain("## Meldung 6");
}
[Fact]
public void Metadaten_werden_mitgeliefert()
public void Metadata_is_included()
{
var inhalt = HtmlExtraktor.Extrahiere(Artikelseite, Basis);
var content = HtmlExtractor.Extract(ArticlePage, Basis);
inhalt.Titel.ShouldBe("Stadtrat beschliesst Radweg");
inhalt.Sprache.ShouldBe("de");
inhalt.KanonischeUrl.ShouldBe("https://www.beispiel.de/politik/radweg-123.html");
inhalt.Veroeffentlicht.ShouldBe(new DateTimeOffset(2026, 9, 14, 8, 30, 0, TimeSpan.FromHours(2)));
content.Titel.ShouldBe("Stadtrat beschliesst Radweg");
content.Sprache.ShouldBe("de");
content.KanonischeUrl.ShouldBe("https://www.beispiel.de/politik/radweg-123.html");
content.Veroeffentlicht.ShouldBe(new DateTimeOffset(2026, 9, 14, 8, 30, 0, TimeSpan.FromHours(2)));
}
[Fact]
public void Der_Markdown_Text_ist_deutlich_kuerzer_als_die_Seite()
=> HtmlExtraktor.Extrahiere(Artikelseite, Basis).Markdown.Length.ShouldBeLessThan(Artikelseite.Length / 2);
public void The_markdown_text_is_much_shorter_than_the_page()
=> HtmlExtractor.Extract(ArticlePage, Basis).Markdown.Length.ShouldBeLessThan(ArticlePage.Length / 2);
[Fact]
public void Ein_Selektor_holt_gezielt_nur_den_Teil()
public void A_selector_fetches_only_that_part()
{
var inhalt = HtmlExtraktor.Extrahiere(Artikelseite, Basis, "table");
var content = HtmlExtractor.Extract(ArticlePage, Basis, "table");
inhalt.Bereich.ShouldBe("selector");
inhalt.Markdown.ShouldStartWith("| Abschnitt | Länge |");
inhalt.Markdown.ShouldNotContain("Stadtrat");
inhalt.Hinweis.ShouldBeNull();
content.Bereich.ShouldBe("selector");
content.Markdown.ShouldStartWith("| Abschnitt | Länge |");
content.Markdown.ShouldNotContain("Stadtrat");
content.Hinweis.ShouldBeNull();
}
[Fact]
public void Ein_Selektor_ohne_Treffer_faellt_mit_Hinweis_auf_den_Hauptinhalt_zurueck()
public void A_selector_without_matches_falls_back_to_the_main_content_with_a_hint()
{
var inhalt = HtmlExtraktor.Extrahiere(Artikelseite, Basis, "#gibt-es-nicht");
var content = HtmlExtractor.Extract(ArticlePage, Basis, "#gibt-es-nicht");
inhalt.Bereich.ShouldBe("article");
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("matched nothing");
content.Bereich.ShouldBe("article");
content.Hinweis.ShouldNotBeNull().ShouldContain("matched nothing");
}
[Fact]
public void Ein_ungueltiger_Selektor_bringt_das_Werkzeug_nicht_zum_Absturz()
public void An_invalid_selector_does_not_crash_the_tool()
{
var inhalt = HtmlExtraktor.Extrahiere(Artikelseite, Basis, "div[[[");
var content = HtmlExtractor.Extract(ArticlePage, Basis, "div[[[");
inhalt.Markdown.ShouldContain("Der Stadtrat hat am Montag");
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("invalid");
content.Markdown.ShouldContain("Der Stadtrat hat am Montag");
content.Hinweis.ShouldNotBeNull().ShouldContain("invalid");
}
[Fact]
public void Ohne_article_und_main_findet_die_Textdichte_den_Inhaltsblock()
public void Without_article_and_main_text_density_finds_the_content_block()
{
const string html = """
<html><body>
@@ -174,16 +174,16 @@ public sealed class HtmlExtraktorTests
</body></html>
""";
var inhalt = HtmlExtraktor.Extrahiere(html, Basis);
var content = HtmlExtractor.Extract(html, Basis);
inhalt.Bereich.ShouldBe("hauptinhalt");
inhalt.Markdown.ShouldContain("Messstation im Stadtpark");
inhalt.Markdown.ShouldNotContain("Pollenkalender");
inhalt.Markdown.ShouldNotContain("Gamma");
content.Bereich.ShouldBe("hauptinhalt");
content.Markdown.ShouldContain("Messstation im Stadtpark");
content.Markdown.ShouldNotContain("Pollenkalender");
content.Markdown.ShouldNotContain("Gamma");
}
[Fact]
public void Verschachtelte_Listen_behalten_ihren_Einzug()
public void Nested_lists_keep_their_indent()
{
const string html = """
<html><body><main>
@@ -192,44 +192,44 @@ public sealed class HtmlExtraktorTests
</main></body></html>
""";
var md = HtmlExtraktor.Extrahiere(html, Basis).Markdown;
var md = HtmlExtractor.Extract(html, Basis).Markdown;
md.ShouldContain("1. Erster Schritt\n - Unterpunkt A\n - Unterpunkt B\n2. Zweiter Schritt");
}
[Fact]
public void Volltext_enthaelt_alles_Sichtbare_ausser_Skripten()
public void Full_text_contains_everything_visible_except_scripts()
{
var voll = HtmlExtraktor.Volltext(Artikelseite);
var full = HtmlExtractor.FullText(ArticlePage);
voll.ShouldContain("Wirtschaft");
voll.ShouldContain("Der Stadtrat hat am Montag");
voll.ShouldNotContain("GEHEIMER-SKRIPTTEXT");
full.ShouldContain("Wirtschaft");
full.ShouldContain("Der Stadtrat hat am Montag");
full.ShouldNotContain("GEHEIMER-SKRIPTTEXT");
}
}
/// <summary>What arrives at the model for which response (#602).</summary>
public sealed class WebInhaltTests
public sealed class WebContentTests
{
private static readonly Uri Quelle = new("https://api.beispiel.de/v1/wetter");
private static readonly Uri Source = new("https://api.beispiel.de/v1/wetter");
[Theory]
[InlineData("application/json")]
[InlineData("text/html")] // servers that deliver JSON as HTML do exist
[InlineData(null)]
public void Json_bleibt_unveraendert(string? mediaType)
public void Json_stays_unchanged(string? mediaType)
{
const string json = """{"current":{"time":"2026-09-15T12:00","temperature_2m":18.4}}""";
var inhalt = WebInhalt.Aufbereiten(json, mediaType, Quelle, AbrufModus.Auto, null);
var content = WebContent.Prepare(json, mediaType, Source, FetchMode.Auto, null);
inhalt.Art.ShouldBe("json");
inhalt.Bereich.ShouldBe("roh");
inhalt.Text.ShouldBe(json);
content.Art.ShouldBe("json");
content.Bereich.ShouldBe("roh");
content.Text.ShouldBe(json);
}
[Fact]
public void Zu_wenig_Hauptinhalt_faellt_mit_Hinweis_auf_den_Volltext_zurueck()
public void Too_little_main_content_falls_back_to_the_full_text_with_a_hint()
{
// Overview page: the content is a link list in the navigation, the body only holds a
// placeholder that JavaScript would fill. The extraction rightly removes the navigation
@@ -238,48 +238,48 @@ public sealed class WebInhaltTests
+ string.Concat(Enumerable.Range(1, 60).Select(i => $"<li><a href=\"/rubrik/{i}\">Rubrik {i}</a></li>"))
+ "</ul></nav><div id=\"app\">Lade …</div></body></html>";
var inhalt = WebInhalt.Aufbereiten(html, "text/html", Quelle, AbrufModus.Auto, null);
var content = WebContent.Prepare(html, "text/html", Source, FetchMode.Auto, null);
inhalt.Bereich.ShouldBe("vollstaendig");
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("JavaScript");
content.Bereich.ShouldBe("vollstaendig");
content.Hinweis.ShouldNotBeNull().ShouldContain("JavaScript");
}
[Fact]
public void Modus_voll_liefert_den_ganzen_Text()
public void Mode_full_returns_the_whole_text()
{
const string html = "<html><body><nav>Menue</nav><article><p>Inhalt</p></article></body></html>";
var inhalt = WebInhalt.Aufbereiten(html, "text/html", Quelle, AbrufModus.Voll, null);
var content = WebContent.Prepare(html, "text/html", Source, FetchMode.Voll, null);
inhalt.Bereich.ShouldBe("vollstaendig");
inhalt.Text.ShouldContain("Menue");
content.Bereich.ShouldBe("vollstaendig");
content.Text.ShouldContain("Menue");
}
[Fact]
public void Lange_Inhalte_kommen_in_Abschnitten_die_zusammen_den_ganzen_Text_ergeben()
public void Long_content_comes_in_sections_that_together_form_the_whole_text()
{
var text = string.Join("\n\n", Enumerable.Range(1, 400).Select(i => $"Absatz {i}: " + new string('x', 60)));
var teile = new List<string>();
var parts = new List<string>();
int? offset = 0;
while (offset is not null)
{
var inhalt = WebInhalt.Aufbereiten(text, "text/plain", Quelle, AbrufModus.Auto, null, offset.Value, 5_000);
teile.Add(inhalt.Text);
if (inhalt.NaechsterOffset is not null)
inhalt.Hinweis.ShouldNotBeNull().ShouldContain($"offset={inhalt.NaechsterOffset}");
offset = inhalt.NaechsterOffset;
var content = WebContent.Prepare(text, "text/plain", Source, FetchMode.Auto, null, offset.Value, 5_000);
parts.Add(content.Text);
if (content.NaechsterOffset is not null)
content.Hinweis.ShouldNotBeNull().ShouldContain($"offset={content.NaechsterOffset}");
offset = content.NaechsterOffset;
}
teile.Count.ShouldBeGreaterThan(1);
string.Concat(teile).ShouldBe(text);
teile[0].ShouldEndWith("\n\n", customMessage: "an einer Absatzgrenze geschnitten");
parts.Count.ShouldBeGreaterThan(1);
string.Concat(parts).ShouldBe(text);
parts[0].ShouldEndWith("\n\n", customMessage: "an einer Absatzgrenze geschnitten");
}
[Fact]
public void Umlaute_werden_in_der_Antwort_nicht_escaped()
public void Umlauts_are_not_escaped_in_the_answer()
{
var json = JsonSerializer.Serialize(new { text = "Grüße aus Köln – 18 °C" }, WebInhalt.JsonOptionen);
var json = JsonSerializer.Serialize(new { text = "Grüße aus Köln – 18 °C" }, WebContent.JsonOptionen);
json.ShouldContain("Grüße aus Köln – 18 °C");
json.ShouldNotContain("\\u00", Case.Sensitive);
@@ -290,6 +290,6 @@ public sealed class WebInhaltTests
[InlineData("text/csv", "a,b", "text")]
[InlineData(null, "<html><body>x</body></html>", "html")]
[InlineData(null, "<?xml version=\"1.0\"?><feed/>", "xml")]
public void Die_Art_des_Inhalts_wird_erkannt(string? mediaType, string body, string erwartet)
=> WebInhalt.Art(mediaType, body).ShouldBe(erwartet);
public void The_kind_of_content_is_recognised(string? mediaType, string body, string expected)
=> WebContent.KindOf(mediaType, body).ShouldBe(expected);
}
@@ -11,78 +11,78 @@ namespace ClawdDotNet.Tools.Tests.WebFetch;
/// </summary>
public sealed class JsonFilterTests
{
private static readonly Uri Quelle = new("https://www.dwd.de/DWD/warnungen/warnapp/json/warnings.json");
private static readonly Uri Source = new("https://www.dwd.de/DWD/warnungen/warnapp/json/warnings.json");
private static string DwdAehnlich(int fuellWarnungen = 0)
private static string DwdLike(int fillWarnings = 0)
{
var fuell = string.Join(",", Enumerable.Range(1, fuellWarnungen).Select(i =>
var fill = string.Join(",", Enumerable.Range(1, fillWarnings).Select(i =>
$$"""{"regionName":"Kreis {{i}}","event":"WINDBÖEN","level":2,"start":1726500000000}"""));
return $$"""
warnWetter.loadWarnings({"time":1726650000000,"warnings":{
"111000000":[{"regionName":"Berlin","event":"STARKREGEN","level":3,"start":1726650000000}],
"105000000":[{"regionName":"Hamburg","event":"FROST","level":1,"start":1726650000000}{{(fuell.Length > 0 ? "," + fuell : "")}}]
"105000000":[{"regionName":"Hamburg","event":"FROST","level":1,"start":1726650000000}{{(fill.Length > 0 ? "," + fill : "")}}]
},"vorabInformation":{},"copyright":"DWD"});
""";
}
[Fact]
public void JSONP_wird_ausgepackt_und_nur_die_passende_Warnung_geliefert()
public void JSONP_is_unwrapped_and_only_the_matching_warning_returned()
{
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(), "application/javascript", Quelle, AbrufModus.Auto, null, filter: "berlin");
var content = WebContent.Prepare(DwdLike(), "application/javascript", Source, FetchMode.Auto, null, filter: "berlin");
inhalt.Art.ShouldBe("json");
inhalt.Bereich.ShouldBe("gefiltert");
using var doc = JsonDocument.Parse(inhalt.Text);
content.Art.ShouldBe("json");
content.Bereich.ShouldBe("gefiltert");
using var doc = JsonDocument.Parse(content.Text);
doc.RootElement.GetProperty("treffer").GetInt32().ShouldBe(1);
var eintrag = doc.RootElement.GetProperty("eintraege")[0];
eintrag.GetProperty("pfad").GetString().ShouldBe("$.warnings.111000000[0]");
eintrag.GetProperty("wert").GetProperty("event").GetString().ShouldBe("STARKREGEN");
inhalt.Text.ShouldNotContain("Hamburg");
var entry = doc.RootElement.GetProperty("eintraege")[0];
entry.GetProperty("pfad").GetString().ShouldBe("$.warnings.111000000[0]");
entry.GetProperty("wert").GetProperty("event").GetString().ShouldBe("STARKREGEN");
content.Text.ShouldNotContain("Hamburg");
}
[Fact]
public void Ein_passender_Schluesselname_liefert_seinen_Wert()
public void A_matching_key_name_returns_its_value()
{
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(), "application/json", Quelle, AbrufModus.Auto, null, filter: "1110");
var content = WebContent.Prepare(DwdLike(), "application/json", Source, FetchMode.Auto, null, filter: "1110");
inhalt.Text.ShouldContain("STARKREGEN");
inhalt.Text.ShouldNotContain("FROST");
content.Text.ShouldContain("STARKREGEN");
content.Text.ShouldNotContain("FROST");
}
[Fact]
public void Das_gefilterte_Ergebnis_ist_ein_Bruchteil_der_ganzen_Antwort()
public void The_filtered_result_is_a_fraction_of_the_whole_answer()
{
var gross = DwdAehnlich(fuellWarnungen: 800);
var inhalt = WebInhalt.Aufbereiten(gross, "application/json", Quelle, AbrufModus.Auto, null, filter: "Berlin");
var large = DwdLike(fillWarnings: 800);
var content = WebContent.Prepare(large, "application/json", Source, FetchMode.Auto, null, filter: "Berlin");
gross.Length.ShouldBeGreaterThan(60_000);
inhalt.Text.Length.ShouldBeLessThan(400);
inhalt.NaechsterOffset.ShouldBeNull();
large.Length.ShouldBeGreaterThan(60_000);
content.Text.Length.ShouldBeLessThan(400);
content.NaechsterOffset.ShouldBeNull();
}
[Fact]
public void Ohne_Treffer_sagt_der_Hinweis_das_ausdruecklich()
public void Without_matches_the_hint_says_so_explicitly()
{
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(), "application/json", Quelle, AbrufModus.Auto, null, filter: "Muenchen");
var content = WebContent.Prepare(DwdLike(), "application/json", Source, FetchMode.Auto, null, filter: "Muenchen");
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("No entries");
content.Hinweis.ShouldNotBeNull().ShouldContain("No entries");
}
[Fact]
public void Kein_gueltiges_JSON_wird_ungefiltert_mit_Hinweis_geliefert()
public void Invalid_JSON_is_returned_unfiltered_with_a_hint()
{
var inhalt = WebInhalt.Aufbereiten("Das ist Text, kein JSON.", "text/plain", Quelle, AbrufModus.Auto, null, filter: "Berlin");
var content = WebContent.Prepare("Das ist Text, kein JSON.", "text/plain", Source, FetchMode.Auto, null, filter: "Berlin");
inhalt.Text.ShouldBe("Das ist Text, kein JSON.");
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("not valid JSON");
content.Text.ShouldBe("Das ist Text, kein JSON.");
content.Hinweis.ShouldNotBeNull().ShouldContain("not valid JSON");
}
[Fact]
public void Blaettert_ein_Agent_durch_grosses_JSON_empfiehlt_der_Hinweis_den_filter()
public void If_an_agent_pages_through_large_JSON_the_hint_recommends_the_filter()
{
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(fuellWarnungen: 800), "application/json", Quelle, AbrufModus.Auto, null);
var content = WebContent.Prepare(DwdLike(fillWarnings: 800), "application/json", Source, FetchMode.Auto, null);
inhalt.NaechsterOffset.ShouldNotBeNull();
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("filter");
content.NaechsterOffset.ShouldNotBeNull();
content.Hinweis.ShouldNotBeNull().ShouldContain("filter");
}
}
@@ -10,10 +10,10 @@ namespace ClawdDotNet.Tools.Tests.WebFetch;
/// and resolves to 127.0.0.1 - in the test run WebFetch thus opened a connection
/// to loopback. That's why the resolved address is checked, without a real network.
/// </summary>
public sealed class UrlGuardAufloesungTests
public sealed class UrlGuardResolutionTests
{
private static Func<string, CancellationToken, Task<IPAddress[]>> Aufloesung(params string[] adressen)
=> (_, _) => Task.FromResult(adressen.Select(IPAddress.Parse).ToArray());
private static Func<string, CancellationToken, Task<IPAddress[]>> Resolution(params string[] addresses)
=> (_, _) => Task.FromResult(addresses.Select(IPAddress.Parse).ToArray());
[Theory]
[InlineData("127.0.0.1")]
@@ -21,47 +21,47 @@ public sealed class UrlGuardAufloesungTests
[InlineData("169.254.169.254")]
[InlineData("::1")]
[InlineData("::ffff:10.0.0.5")]
public async Task Ein_Name_der_ins_lokale_Netz_aufloest_wird_gesperrt(string adresse)
public async Task A_name_resolving_into_the_local_network_is_blocked(string address)
{
var ex = await Should.ThrowAsync<HttpRequestException>(
() => UrlGuard.ResolveAllowedAsync("localtest.me", Aufloesung(adresse), default));
() => UrlGuard.ResolveAllowedAsync("localtest.me", Resolution(address), default));
ex.Message.ShouldContain("localtest.me");
ex.Message.ShouldContain("blocked");
}
[Fact]
public async Task Ein_Name_mit_oeffentlicher_und_interner_Adresse_wird_gesperrt()
public async Task A_name_with_a_public_and_an_internal_address_is_blocked()
{
// Exactly the pattern with which you outwit a sampling check.
await Should.ThrowAsync<HttpRequestException>(
() => UrlGuard.ResolveAllowedAsync("gemischt.example", Aufloesung("93.184.215.14", "127.0.0.1"), default));
() => UrlGuard.ResolveAllowedAsync("gemischt.example", Resolution("93.184.215.14", "127.0.0.1"), default));
}
[Fact]
public async Task Ein_Name_mit_oeffentlichen_Adressen_wird_zugelassen()
public async Task A_name_with_public_addresses_is_allowed()
{
var adressen = await UrlGuard.ResolveAllowedAsync(
"example.com", Aufloesung("93.184.215.14", "2606:2800:21f:cb07:6820:80da:af6b:8b2c"), default);
var addresses = await UrlGuard.ResolveAllowedAsync(
"example.com", Resolution("93.184.215.14", "2606:2800:21f:cb07:6820:80da:af6b:8b2c"), default);
adressen.Length.ShouldBe(2);
addresses.Length.ShouldBe(2);
}
[Fact]
public async Task Ein_IP_Literal_wird_ohne_Aufloesung_geprueft()
public async Task An_IP_literal_is_checked_without_resolution()
{
var aufgerufen = false;
var called = false;
await Should.ThrowAsync<HttpRequestException>(() => UrlGuard.ResolveAllowedAsync(
"[::1]",
(_, _) => { aufgerufen = true; return Task.FromResult(Array.Empty<IPAddress>()); },
(_, _) => { called = true; return Task.FromResult(Array.Empty<IPAddress>()); },
default));
aufgerufen.ShouldBeFalse();
called.ShouldBeFalse();
}
[Fact]
public async Task Ein_Name_ohne_Adresse_wird_nicht_verbunden()
public async Task A_name_without_address_is_not_connected()
=> await Should.ThrowAsync<HttpRequestException>(
() => UrlGuard.ResolveAllowedAsync("nirgends.example", Aufloesung(), default));
() => UrlGuard.ResolveAllowedAsync("nirgends.example", Resolution(), default));
}
@@ -33,7 +33,7 @@ public sealed class UrlGuardTests
[InlineData("http://[fd00::1]/")]
[InlineData("http://nas.local/")]
[InlineData("http://dienst.internal/")]
public void Adressen_im_lokalen_oder_privaten_Netz_werden_gesperrt(string url)
public void Addresses_in_the_local_or_private_network_are_blocked(string url)
{
Validate(url, out var error).ShouldBeFalse($"'{url}' muss gesperrt sein");
error.ShouldNotBeNull();
@@ -44,7 +44,7 @@ public sealed class UrlGuardTests
[InlineData("172.32.0.1")] // just above
[InlineData("11.0.0.1")]
[InlineData("8.8.8.8")]
public void Oeffentliche_Adressen_gelten_nicht_als_privat(string host)
public void Public_addresses_do_not_count_as_private(string host)
{
UrlGuard.IsPrivateOrLocal(host).ShouldBeFalse();
}
@@ -57,7 +57,7 @@ public sealed class UrlGuardTests
[InlineData("file:///C:/Windows/win.ini")]
[InlineData("ftp://example.com/datei")]
[InlineData("gopher://example.com/")]
public void Nur_http_und_https_sind_erlaubt(string url)
public void Only_http_and_https_are_allowed(string url)
{
Validate(url, out _).ShouldBeFalse();
}
@@ -72,7 +72,7 @@ public sealed class UrlGuardTests
[InlineData("https://news.example.com/artikel")]
[InlineData("https://a.b.example.com/x")]
[InlineData("https://reuters.com/")]
public void Erlaubte_Domains_und_ihre_Subdomains_gehen_durch(string url)
public void Allowed_domains_and_their_subdomains_go_through(string url)
{
Validate(url, out var error).ShouldBeTrue(error);
}
@@ -82,13 +82,13 @@ public sealed class UrlGuardTests
[InlineData("https://notexample.com/x")]
[InlineData("https://evil.com/x")]
[InlineData("https://exampleXcom/x")]
public void Aehnlich_aussehende_Domains_werden_abgelehnt(string url)
public void Lookalike_domains_are_rejected(string url)
{
Validate(url, out _).ShouldBeFalse();
}
[Fact]
public void Ohne_Whitelist_wird_nichts_durchgelassen()
public void Without_whitelist_nothing_is_let_through()
{
UrlGuard.TryValidate("https://example.com", [], out _, out _).ShouldBeFalse();
}
@@ -98,14 +98,14 @@ public sealed class UrlGuardTests
/// "mywww.example.com" became "myexample.com".
/// </summary>
[Fact]
public void Die_www_Behandlung_verstuemmelt_keine_anderen_Hostnamen()
public void The_www_handling_does_not_mangle_other_host_names()
{
UrlGuard.IsDomainAllowed("mywww.example.com", Allowed).ShouldBeTrue();
UrlGuard.IsDomainAllowed("wwwxexample.com", Allowed).ShouldBeFalse();
}
[Fact]
public void Ein_abschliessender_Punkt_im_Host_umgeht_die_Whitelist_nicht()
public void A_trailing_dot_in_the_host_does_not_bypass_the_whitelist()
{
// "example.com." is the same host in DNS terms.
UrlGuard.IsDomainAllowed("evil.com.", Allowed).ShouldBeFalse();
@@ -113,7 +113,7 @@ public sealed class UrlGuardTests
}
[Fact]
public void Grossschreibung_im_Host_spielt_keine_Rolle()
public void Case_in_the_host_does_not_matter()
{
Validate("https://EXAMPLE.COM/x", out var error).ShouldBeTrue(error);
}
@@ -123,7 +123,7 @@ public sealed class UrlGuardTests
[InlineData("")]
[InlineData("kein-url")]
[InlineData("/nur/ein/pfad")]
public void Ungueltige_Eingaben_werden_abgelehnt(string? url)
public void Invalid_inputs_are_rejected(string? url)
{
UrlGuard.TryValidate(url, Allowed, out _, out _).ShouldBeFalse();
}