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 DatabasePreCheckTests { private static readonly DatabaseTool Tool = new(); private static AgentToolContext Context(string accessLevel, params string[] tables) => new( "agent", "inst", new Dictionary { ["type"] = "mysql", ["connectionString"] = "Server=nirgends", ["accessLevel"] = accessLevel, ["allowedTables"] = JsonSerializer.SerializeToElement(tables) }, new NullStateStore(), NullLogger.Instance, CancellationToken.None); private static JsonElement Sql(string sql, string action = "query") => JsonSerializer.SerializeToElement(new { action, sql }); [Fact] public void Foreign_table_is_rejected_up_front() => Tool.Preflight(Sql("SELECT * FROM protokoll"), Context("ReadOnly", "messwerte")) .ShouldNotBeNull().ShouldContain("protokoll"); [Fact] public void Comments_are_rejected_up_front() => Tool.Preflight(Sql("DELETE FROM messwerte -- messwerte"), Context("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 The_pre_check_no_longer_judges_the_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), Context(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 The_approval_key_is_the_SQL_operation_not_the_action_argument(string sql, string action, string expected) => Tool.StagingAction(Sql(sql, action)).ShouldBe(expected); [Theory] [InlineData("query", "select")] [InlineData("insert", "insert")] [InlineData("upsert", "update")] public void MongoDB_actions_are_mapped_to_the_same_operations(string action, string expected) => Tool.StagingAction(JsonSerializer.SerializeToElement(new { action, collection = "c" })).ShouldBe(expected); [Fact] public void The_operations_are_enumerated_for_the_UI() => 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 Without_allowed_the_agent_reads_freely_and_submits_everything_else(string operation, StagingDecision expected) => Tool.Vorgabe(operation, new Dictionary()).ShouldBe(expected); [Fact] public async Task Without_an_approver_the_access_tier_stays_a_hard_limit() { // No gate, no check: then the tool itself has to refuse before it // even establishes a connection. var result = await Tool.ExecuteAsync( Sql("INSERT INTO messwerte (ort) VALUES ('Berlin')"), Context("ReadOnly", "messwerte"), default); result.Success.ShouldBeFalse(); result.ErrorMessage.ShouldNotBeNull().ShouldContain("ReadOnly"); } }