using System.Text.Json; using ClawdDotNet.Core.Staging; 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; /// /// Vorpruefung (#579), Freigabeschluessel (#609) und Rechte (#641) des Database-Werkzeugs - /// alles ohne Datenbankverbindung pruefbar, weil es nur Konfiguration und Statement ansieht. /// public sealed class DatabaseVorpruefungTests { private static readonly DatabaseTool Tool = new(); private static AgentToolContext Kontext(string accessLevel, params string[] tabellen) => new( "agent", "inst", new Dictionary { ["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 Fremde_Tabelle_wird_vorab_abgelehnt() => Tool.Preflight(Sql("SELECT * FROM protokoll"), Kontext("ReadOnly", "messwerte")) .ShouldNotBeNull().ShouldContain("protokoll"); [Fact] public void Kommentare_werden_vorab_abgelehnt() => Tool.Preflight(Sql("DELETE FROM messwerte -- messwerte"), Kontext("Admin", "messwerte")) .ShouldNotBeNull(); [Theory] [InlineData("SELECT * FROM messwerte", "ReadOnly")] [InlineData("INSERT INTO messwerte (ort) VALUES ('Berlin')", "ReadOnly")] [InlineData("ALTER TABLE messwerte ADD COLUMN x INT", "ReadWrite")] [InlineData("CREATE TABLE IF NOT EXISTS messwerte (id INT)", "Admin")] [InlineData("DESCRIBE messwerte", "ReadOnly")] public void Die_Vorpruefung_urteilt_nicht_mehr_ueber_die_Operation(string sql, string level) { // #641: Ob ein INSERT eines lesenden Agenten laeuft, vorgelegt oder verboten wird, // entscheidet die Freigabestelle - nicht mehr eine stille Ablehnung im Werkzeug. Tool.Preflight(Sql(sql), Kontext(level, "messwerte")).ShouldBeNull(); } [Theory] [InlineData("SELECT * FROM messwerte", "query", "select")] [InlineData("DESCRIBE messwerte", "query", "select")] [InlineData("SHOW COLUMNS FROM messwerte", "query", "select")] [InlineData("INSERT INTO messwerte (ort) VALUES ('Berlin')", "query", "insert")] [InlineData("UPDATE messwerte SET ort = 'X'", "insert", "update")] [InlineData("DELETE FROM messwerte WHERE id = 1", "query", "delete")] [InlineData("CREATE TABLE messwerte (id INT)", "query", "create")] [InlineData("ALTER TABLE messwerte ADD COLUMN x INT", "query", "alter")] [InlineData("DROP TABLE IF EXISTS messwerte", "query", "drop")] [InlineData("insert into messwerte (ort) values ('select')", "query", "insert")] public void Der_Freigabeschluessel_ist_die_SQL_Operation_nicht_das_action_Argument(string sql, string action, string erwartet) => Tool.StagingAktion(Sql(sql, action)).ShouldBe(erwartet); [Theory] [InlineData("query", "select")] [InlineData("insert", "insert")] [InlineData("upsert", "update")] public void MongoDB_Aktionen_werden_auf_dieselben_Operationen_abgebildet(string action, string erwartet) => Tool.StagingAktion(JsonSerializer.SerializeToElement(new { action, collection = "c" })).ShouldBe(erwartet); [Fact] public void Die_Operationen_sind_fuer_die_Oberflaeche_aufgezaehlt() => Tool.Operationen.ShouldBe(["select", "insert", "update", "delete", "replace", "create", "alter", "drop", "truncate"]); [Theory] [InlineData("select", StagingDecision.Auto)] [InlineData("insert", StagingDecision.Approve)] [InlineData("alter", StagingDecision.Approve)] public void Ohne_erlaubt_liest_der_Agent_frei_und_legt_alles_andere_vor(string operation, StagingDecision erwartet) => Tool.Vorgabe(operation, new Dictionary()).ShouldBe(erwartet); [Fact] public async Task Ohne_Freigabestelle_bleibt_die_Zugriffsstufe_eine_harte_Grenze() { // Kein Gate, keine Pruefung: dann muss das Werkzeug selbst verweigern, bevor es // ueberhaupt eine Verbindung aufbaut. var ergebnis = await Tool.ExecuteAsync( Sql("INSERT INTO messwerte (ort) VALUES ('Berlin')"), Kontext("ReadOnly", "messwerte"), default); ergebnis.Success.ShouldBeFalse(); ergebnis.ErrorMessage.ShouldNotBeNull().ShouldContain("ReadOnly"); } }