- Werkzeugformulare aus den typisierten Einstellungsklassen (L8): Geheimnisse
nur schreibend, nur geaenderte Schluessel werden ersetzt, Rechte bleiben
- Database.allowedTables las ein JSON-Array als rohen Text und haette es beim
naechsten Speichern zerlegt
- Automatische Sicherung der Instanz in der Sicherungsansicht (L10)
- Sicherung hochladen und als neue Instanz einspielen (L11): Pfade der alten
Maschine werden zurueckgesetzt, eine Kopie neben dem Original bekommt eine
eigene ID, Autostart aus; Upload bis 4 GB
- api.js schickte eine Datei als "{}"; Dialoge schliessen beim Seitenwechsel
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
4.5 KiB
C#
116 lines
4.5 KiB
C#
using System.Text.Json;
|
|
using ClawdDotNet.App.Services;
|
|
using ClawdDotNet.Core.Staging;
|
|
using Shouldly;
|
|
|
|
namespace ClawdDotNet.App.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// #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.
|
|
/// </summary>
|
|
public sealed class ToolSettingsFormTests
|
|
{
|
|
/// <summary>Settings as they are after loading: values as JsonElement, as from the file.</summary>
|
|
private static Dictionary<string, object?> Loaded(string json)
|
|
=> JsonSerializer.Deserialize<Dictionary<string, object?>>(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<string, string?> { ["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<string, string?> { ["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<string, string?> { [field] = value });
|
|
|
|
settings.ShouldBeNull();
|
|
error.ShouldNotBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void An_enum_outside_its_values_is_refused()
|
|
{
|
|
var (settings, _) = ToolSettingsForms.Apply("Database", Loaded("{}"),
|
|
new Dictionary<string, string?> { ["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<string, string?> { ["Host"] = "ftp.example.org" }).Ok.ShouldBeTrue();
|
|
|
|
var tool = host.Host.AgentAdmin.Find("scout")!.Tools["FTP"];
|
|
tool["host"].ShouldBe("ftp.example.org");
|
|
tool.ShouldContainKey(Werkzeugrechte.ErlaubtSchluessel);
|
|
tool.ShouldContainKey(Werkzeugrechte.VerbotenSchluessel);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_tool_without_a_form_has_none()
|
|
=> ToolSettingsForms.Describe("Memory", []).ShouldBeNull();
|
|
}
|