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; /// /// Pre-check (#579), approval key (#609) and permissions (#641) of the Database tool - /// all checkable without a database connection, because it only looks at configuration and statement. /// 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: Whether an INSERT of a reading agent runs, is presented or is forbidden // is decided by the approval point - no longer by a silent refusal in the tool. 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() { // No gate, no check: then the tool itself has to refuse before it // even establishes a connection. var ergebnis = await Tool.ExecuteAsync( Sql("INSERT INTO messwerte (ort) VALUES ('Berlin')"), Kontext("ReadOnly", "messwerte"), default); ergebnis.Success.ShouldBeFalse(); ergebnis.ErrorMessage.ShouldNotBeNull().ShouldContain("ReadOnly"); } }