#631: Derselbe Vorschlag (gleicher Agent, gleiches Werkzeug, gleiche Argumente unabhaengig von Schluesselreihenfolge) wird nicht erneut angelegt - der Agent erfaehrt die bestehende Nummer. Offene Vorschlaege verfallen nach 24 h (Status Expired). #609: Werkzeuge koennen den Freigabeschluessel aus dem Inhalt bestimmen (IAgentTool.StagingAktion). Database: SELECT/SHOW/DESCRIBE -> query, INSERT/UPDATE/DELETE -> write, DDL -> schema; write und schema sind freigabepflichtig. Ausgefuehrt wird nach Statement-Art, schreibende Statements melden die Zeilenzahl. #579: Database-Vorpruefung (Zugriffsebene, Tabellenfreigabe, SqlGuard) - was das Werkzeug ablehnen wuerde, kommt nicht mehr in die Freigabeliste. #610: SqlGuard ueberspringt IF [NOT] EXISTS und erkennt DESCRIBE/DESC <tabelle>. #632: WebFetch fetch mit filter liefert aus JSON/JSONP nur passende Eintraege; beim Blaettern durch grosses JSON empfiehlt der Hinweis den filter. Version 0.1.10. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Core.Tools;
|
||||
using ClawdDotNet.Tools.Database;
|
||||
using ClawdDotNet.Tools.Tests.FileRW;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Tools.Tests.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Vorpruefung (#579) und Freigabeschluessel (#609) des Database-Werkzeugs - beides ohne
|
||||
/// Datenbankverbindung pruefbar, weil es nur Konfiguration und Statement ansieht.
|
||||
/// </summary>
|
||||
public sealed class DatabaseVorpruefungTests
|
||||
{
|
||||
private static readonly DatabaseTool Tool = new();
|
||||
|
||||
private static AgentToolContext Kontext(string accessLevel, params string[] tabellen) => new(
|
||||
"agent", "inst",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["type"] = "mysql",
|
||||
["connectionString"] = "Server=nirgends",
|
||||
["accessLevel"] = accessLevel,
|
||||
["allowedTables"] = JsonSerializer.SerializeToElement(tabellen)
|
||||
},
|
||||
new NullStateStore(), NullLogger.Instance, CancellationToken.None);
|
||||
|
||||
private static JsonElement Sql(string sql, string action = "query")
|
||||
=> JsonSerializer.SerializeToElement(new { action, sql });
|
||||
|
||||
[Fact]
|
||||
public void Ein_Agent_mit_Leserechten_bekommt_sein_INSERT_sofort_abgelehnt_statt_zur_Freigabe()
|
||||
=> Tool.Preflight(Sql("INSERT INTO messwerte (ort) VALUES ('Berlin')", "insert"), Kontext("ReadOnly", "messwerte"))
|
||||
.ShouldNotBeNull().ShouldContain("ReadOnly");
|
||||
|
||||
[Fact]
|
||||
public void Strukturaenderung_ohne_Admin_wird_vorab_abgelehnt()
|
||||
=> Tool.Preflight(Sql("ALTER TABLE messwerte ADD COLUMN x INT"), Kontext("ReadWrite", "messwerte"))
|
||||
.ShouldNotBeNull().ShouldContain("DDL");
|
||||
|
||||
[Fact]
|
||||
public void Fremde_Tabelle_wird_vorab_abgelehnt()
|
||||
=> Tool.Preflight(Sql("SELECT * FROM protokoll"), Kontext("ReadOnly", "messwerte"))
|
||||
.ShouldNotBeNull().ShouldContain("protokoll");
|
||||
|
||||
[Theory]
|
||||
[InlineData("SELECT * FROM messwerte", "ReadOnly")]
|
||||
[InlineData("INSERT INTO messwerte (ort) VALUES ('Berlin')", "ReadWrite")]
|
||||
[InlineData("CREATE TABLE IF NOT EXISTS messwerte (id INT)", "Admin")]
|
||||
[InlineData("DESCRIBE messwerte", "ReadOnly")]
|
||||
public void Erlaubtes_passiert_die_Vorpruefung(string sql, string level)
|
||||
=> Tool.Preflight(Sql(sql), Kontext(level, "messwerte")).ShouldBeNull();
|
||||
|
||||
[Theory]
|
||||
[InlineData("SELECT * FROM messwerte", "query", "query")]
|
||||
[InlineData("INSERT INTO messwerte (ort) VALUES ('Berlin')", "query", "write")]
|
||||
[InlineData("UPDATE messwerte SET ort = 'X'", "insert", "write")]
|
||||
[InlineData("CREATE TABLE messwerte (id INT)", "query", "schema")]
|
||||
[InlineData("DROP TABLE IF EXISTS messwerte", "query", "schema")]
|
||||
public void Der_Freigabeschluessel_folgt_dem_Statement_nicht_dem_action_Argument(string sql, string action, string erwartet)
|
||||
=> Tool.StagingAktion(Sql(sql, action)).ShouldBe(erwartet);
|
||||
|
||||
[Fact]
|
||||
public void Ohne_sql_gilt_das_action_Argument_wie_bei_MongoDB()
|
||||
=> Tool.StagingAktion(JsonSerializer.SerializeToElement(new { action = "insert", collection = "c" })).ShouldBeNull();
|
||||
}
|
||||
@@ -22,6 +22,30 @@ public sealed class SqlGuardTests
|
||||
// Angriffe — alle müssen abgelehnt werden
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// ─── #610: wiederholbares Anlegen und Strukturabfragen ───
|
||||
|
||||
[Theory]
|
||||
[InlineData("CREATE TABLE IF NOT EXISTS prices (id INT)", SqlOperation.Schema)]
|
||||
[InlineData("DROP TABLE IF EXISTS prices", SqlOperation.Schema)]
|
||||
[InlineData("DESCRIBE prices", SqlOperation.Read)]
|
||||
[InlineData("DESC prices", SqlOperation.Read)]
|
||||
[InlineData("SHOW COLUMNS FROM prices", SqlOperation.Read)]
|
||||
public void IF_NOT_EXISTS_und_Strukturabfragen_nennen_die_richtige_Tabelle(string sql, SqlOperation operation)
|
||||
{
|
||||
var result = Inspect(sql);
|
||||
|
||||
result.IsValid.ShouldBeTrue(result.Error);
|
||||
result.Operation.ShouldBe(operation);
|
||||
result.Tables.ShouldBe(["prices"]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("CREATE TABLE IF NOT EXISTS users (id INT)")]
|
||||
[InlineData("DROP TABLE IF EXISTS users")]
|
||||
[InlineData("DESCRIBE users")]
|
||||
public void IF_EXISTS_und_DESCRIBE_oeffnen_keine_fremde_Tabelle(string sql)
|
||||
=> Inspect(sql).IsValid.ShouldBeFalse();
|
||||
|
||||
[Theory]
|
||||
[InlineData("DELETE FROM users -- prices")]
|
||||
[InlineData("DELETE FROM users /* prices */")]
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Text.Json;
|
||||
using ClawdDotNet.Tools.WebFetch;
|
||||
using Shouldly;
|
||||
|
||||
namespace ClawdDotNet.Tools.Tests.WebFetch;
|
||||
|
||||
/// <summary>
|
||||
/// #632: Die DWD-Unwetterwarnungen (JSONP, ganz Deutschland) wurden abschnittsweise
|
||||
/// vollstaendig gelesen - 160.000 Tokens fuer eine einzige Stadt. Mit filter kommt nur,
|
||||
/// was passt.
|
||||
/// </summary>
|
||||
public sealed class JsonFilterTests
|
||||
{
|
||||
private static readonly Uri Quelle = new("https://www.dwd.de/DWD/warnungen/warnapp/json/warnings.json");
|
||||
|
||||
private static string DwdAehnlich(int fuellWarnungen = 0)
|
||||
{
|
||||
var fuell = string.Join(",", Enumerable.Range(1, fuellWarnungen).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 : "")}}]
|
||||
},"vorabInformation":{},"copyright":"DWD"});
|
||||
""";
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JSONP_wird_ausgepackt_und_nur_die_passende_Warnung_geliefert()
|
||||
{
|
||||
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(), "application/javascript", Quelle, AbrufModus.Auto, null, filter: "berlin");
|
||||
|
||||
inhalt.Art.ShouldBe("json");
|
||||
inhalt.Bereich.ShouldBe("gefiltert");
|
||||
using var doc = JsonDocument.Parse(inhalt.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");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ein_passender_Schluesselname_liefert_seinen_Wert()
|
||||
{
|
||||
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(), "application/json", Quelle, AbrufModus.Auto, null, filter: "1110");
|
||||
|
||||
inhalt.Text.ShouldContain("STARKREGEN");
|
||||
inhalt.Text.ShouldNotContain("FROST");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Das_gefilterte_Ergebnis_ist_ein_Bruchteil_der_ganzen_Antwort()
|
||||
{
|
||||
var gross = DwdAehnlich(fuellWarnungen: 800);
|
||||
var inhalt = WebInhalt.Aufbereiten(gross, "application/json", Quelle, AbrufModus.Auto, null, filter: "Berlin");
|
||||
|
||||
gross.Length.ShouldBeGreaterThan(60_000);
|
||||
inhalt.Text.Length.ShouldBeLessThan(400);
|
||||
inhalt.NaechsterOffset.ShouldBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ohne_Treffer_sagt_der_Hinweis_das_ausdruecklich()
|
||||
{
|
||||
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(), "application/json", Quelle, AbrufModus.Auto, null, filter: "Muenchen");
|
||||
|
||||
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("Keine Eintraege");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Kein_gueltiges_JSON_wird_ungefiltert_mit_Hinweis_geliefert()
|
||||
{
|
||||
var inhalt = WebInhalt.Aufbereiten("Das ist Text, kein JSON.", "text/plain", Quelle, AbrufModus.Auto, null, filter: "Berlin");
|
||||
|
||||
inhalt.Text.ShouldBe("Das ist Text, kein JSON.");
|
||||
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("kein gueltiges JSON");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Blaettert_ein_Agent_durch_grosses_JSON_empfiehlt_der_Hinweis_den_filter()
|
||||
{
|
||||
var inhalt = WebInhalt.Aufbereiten(DwdAehnlich(fuellWarnungen: 800), "application/json", Quelle, AbrufModus.Auto, null);
|
||||
|
||||
inhalt.NaechsterOffset.ShouldNotBeNull();
|
||||
inhalt.Hinweis.ShouldNotBeNull().ShouldContain("filter");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user