using ClawdDotNet.Core.Config; using Shouldly; namespace ClawdDotNet.Web.Tests; /// /// The web UI turns a local application into a network service that controls agents /// with file, mail, FTP and database access. What is checked here are /// not subtleties of configuration, but the conditions under which it /// may come up at all. /// public sealed class WebUiOptionsTests { private static InstanceConfig Instance( bool enabled = true, string binding = "loopback", string token = "ein-hinreichend-langes-token", int port = 7001) => new() { WebUiEnabled = enabled, WebUiBinding = binding, WebUiToken = token, WebServerPort = port }; [Fact] public void It_is_off_by_default() { // Whoever wants a network service switches it on. Not the other way round. new InstanceConfig().WebUiEnabled.ShouldBeFalse(); new InstanceConfig().WebUiBinding.ShouldBe("loopback"); new InstanceConfig().WebServerPort.ShouldBe(7001); } [Fact] public void Switched_off_is_not_an_error() { WebUiOptions.TryCreate(Instance(enabled: false), out var problem).ShouldBeNull(); problem.ShouldBeNull(); } [Fact] public void Without_token_it_does_not_start_even_on_loopback() { // On a machine on which agents operate browser tools, // "localhost" is no trust boundary. WebUiOptions.TryCreate(Instance(token: ""), out var problem).ShouldBeNull(); problem.ShouldNotBeNull(); } [Fact] public void All_addresses_without_token_are_rejected_and_not_flagged() { var options = WebUiOptions.TryCreate(Instance(binding: "any", token: ""), out var problem); options.ShouldBeNull(); problem.ShouldNotBeNull(); problem!.ShouldContain("So wird sie nicht gestartet"); } [Fact] public void A_too_short_token_does_not_count() { WebUiOptions.TryCreate(Instance(token: "kurz"), out var problem).ShouldBeNull(); problem.ShouldNotBeNull(); } [Fact] public void Loopback_binds_to_127_0_0_1() { var options = WebUiOptions.TryCreate(Instance(port: 9001), out _); options.ShouldNotBeNull(); options!.Url.ShouldBe("http://127.0.0.1:9001"); options.LoopbackOnly.ShouldBeTrue(); } [Fact] public void Everything_except_any_stays_loopback() { // A typo in the instance file must not put the user interface onto the network. WebUiOptions.TryCreate(Instance(binding: "alle"), out _)!.LoopbackOnly.ShouldBeTrue(); WebUiOptions.TryCreate(Instance(binding: ""), out _)!.LoopbackOnly.ShouldBeTrue(); } [Fact] public void With_token_it_may_reach_all_addresses() { var options = WebUiOptions.TryCreate(Instance(binding: "ANY"), out _); options.ShouldNotBeNull(); options!.LoopbackOnly.ShouldBeFalse(); options.Url.ShouldStartWith("http://0.0.0.0:"); } [Theory] [InlineData(0)] [InlineData(-1)] [InlineData(70000)] public void An_impossible_port_is_rejected(int port) { WebUiOptions.TryCreate(Instance(port: port), out var problem).ShouldBeNull(); problem.ShouldNotBeNull(); } [Fact] public void The_token_is_stored_protected_in_the_instance_file() { // Without the entry in ConfigSecrets it would be in plain text next to the // protected OpenRouter key. Core.Security.ConfigSecrets.IsSecretKey("webUiToken").ShouldBeTrue(); var config = Instance(token: "ein-hinreichend-langes-token"); Core.Security.ConfigSecrets.Protect(config); config.WebUiToken.ShouldNotBe("ein-hinreichend-langes-token"); Core.Security.ConfigSecrets.Unprotect(config); config.WebUiToken.ShouldBe("ein-hinreichend-langes-token"); } }