using System.Text.Json; using ClawdDotNet.App.Services; using ClawdDotNet.Core.Staging; using Shouldly; namespace ClawdDotNet.App.Tests.Services; /// /// #764: Tool settings written from the WebUI — the desktop ToolSettingsView, without a /// window. The secrets go one way, and nothing the form does not touch may change. /// public sealed class ToolSettingsFormTests { /// Settings as they are after loading: values as JsonElement, as from the file. private static Dictionary Loaded(string json) => JsonSerializer.Deserialize>(json)!; [Fact] public void The_form_shows_no_secret() { var form = ToolSettingsForms.Describe("FTP", Loaded("""{"host":"ftp.example.org","password":"hunter2"}"""))!; var password = form.Single(f => f.Name == "Password"); password.Kind.ShouldBe(ToolSettingsForms.Secret); password.Value.ShouldBeNull(); password.IsSet.ShouldBeTrue(); JsonSerializer.Serialize(form).ShouldNotContain("hunter2"); } [Fact] public void A_connection_string_is_a_secret_even_without_a_password_mark() { // It carries the database password; ConfigSecrets protects it by name. var form = ToolSettingsForms.Describe("Database", Loaded("""{"connectionString":"Server=db;Password=x"}"""))!; form.Single(f => f.Name == "ConnectionString").Kind.ShouldBe(ToolSettingsForms.Secret); } [Fact] public void An_empty_secret_keeps_the_stored_one() { var (settings, error) = ToolSettingsForms.Apply("FTP", Loaded("""{"host":"old","password":"hunter2"}"""), new Dictionary { ["Host"] = "new", ["Password"] = "" }); error.ShouldBeNull(); settings!["host"].ShouldBe("new"); settings["password"]!.ToString().ShouldBe("hunter2"); } [Fact] public void Untouched_fields_stay_exactly_as_stored() { // allowedTables is a JSON array in the file, allowWrite an old key the class only reads, // "extra" a key no class knows. Saving another field must leave all three alone. var stored = Loaded("""{"type":"mysql","connectionString":"cs","allowedTables":["orders","stock"],"allowWrite":true,"extra":42}"""); var (settings, _) = ToolSettingsForms.Apply("Database", stored, new Dictionary { ["Type"] = "Postgres" }); settings!["type"].ShouldBe("postgres"); JsonSerializer.Serialize(settings["allowedTables"]).ShouldBe("""["orders","stock"]"""); JsonSerializer.Serialize(settings["allowWrite"]).ShouldBe("true"); JsonSerializer.Serialize(settings["extra"]).ShouldBe("42"); } [Fact] public void The_table_list_reads_back_as_names_not_as_json() { var form = ToolSettingsForms.Describe("Database", Loaded("""{"allowedTables":["orders","stock"]}"""))!; form.Single(f => f.Name == "AllowedTables").Value.ShouldBe("orders,stock"); } [Theory] [InlineData("Port", "twenty-one")] [InlineData("Nonsense", "x")] public void A_bad_input_is_refused(string field, string value) { var (settings, error) = ToolSettingsForms.Apply("FTP", Loaded("{}"), new Dictionary { [field] = value }); settings.ShouldBeNull(); error.ShouldNotBeNull(); } [Fact] public void An_enum_outside_its_values_is_refused() { var (settings, _) = ToolSettingsForms.Apply("Database", Loaded("{}"), new Dictionary { ["AccessLevel"] = "99" }); settings.ShouldBeNull(); } [Fact] public void Saving_through_the_agent_keeps_the_rights() { using var host = new TestHost(); host.Host.AgentAdmin.Add(new NewAgent("scout", "Scout", "model")).Ok.ShouldBeTrue(); host.Host.AgentAdmin.SetToolEnabled("scout", "FTP", true).Ok.ShouldBeTrue(); host.Host.AgentAdmin.SetToolRights("scout", "FTP", ["list"], ["delete"]).Ok.ShouldBeTrue(); host.Host.AgentAdmin.ApplyToolSettingsForm("scout", "FTP", new Dictionary { ["Host"] = "ftp.example.org" }).Ok.ShouldBeTrue(); var tool = host.Host.AgentAdmin.Find("scout")!.Tools["FTP"]; tool["host"].ShouldBe("ftp.example.org"); tool.ShouldContainKey(ToolRights.AllowedKey); tool.ShouldContainKey(ToolRights.ForbiddenKey); } [Fact] public void A_tool_without_a_form_has_none() => ToolSettingsForms.Describe("Memory", []).ShouldBeNull(); }