using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Nexus.Api.Data; using Nexus.Api.Models; using Nexus.Api.Services; using Xunit; namespace Nexus.Api.Tests; public sealed class OpenClawWriteGateTests { [Fact] public async Task Adopted_primary_profile_allows_matching_write_boundary() { var connector = ConnectedConnector(); await using var fixture = await GateFixture.CreateAsync(connector); var result = await fixture.Gate.EvaluateAsync("agents.create"); Assert.True(result.Allowed); } [Fact] public async Task Endpoint_change_after_adoption_blocks_write() { var connector = ConnectedConnector(); await using var fixture = await GateFixture.CreateAsync(connector); connector.ActiveEndpoint = "wss://other-openclaw.example.test:18789/"; var result = await fixture.Gate.EvaluateAsync("agents.create"); Assert.False(result.Allowed); Assert.Equal("endpoint_trust_mismatch", result.State); } [Fact] public async Task Device_change_after_adoption_blocks_write() { var connector = ConnectedConnector(); await using var fixture = await GateFixture.CreateAsync(connector); connector.DeviceId = "unexpected-device"; var result = await fixture.Gate.EvaluateAsync("agents.create"); Assert.False(result.Allowed); Assert.Equal("device_trust_mismatch", result.State); } [Fact] public async Task Wss_profile_without_tls_fingerprint_blocks_write() { var connector = ConnectedConnector(); connector.ActiveTlsFingerprint = null; await using var fixture = await GateFixture.CreateAsync(connector); var result = await fixture.Gate.EvaluateAsync("agents.create"); Assert.False(result.Allowed); Assert.Equal("tls_trust_missing", result.State); } [Fact] public async Task Internal_ws_profile_without_tls_fingerprint_can_write() { var connector = ConnectedConnector(); connector.ActiveEndpoint = "ws://openclaw-gateway:18789/"; connector.ActiveTlsFingerprint = null; await using var fixture = await GateFixture.CreateAsync(connector); var result = await fixture.Gate.EvaluateAsync("agents.create"); Assert.True(result.Allowed); } private static StubOpenClawConnector ConnectedConnector() => new() { ConnectionState = GatewayConnectionState.Connected, GatewayVersion = "2026.8.0", RequiredVersion = "2026.8.0", ProtocolVersion = 4, ActiveEndpoint = "wss://openclaw.example.test:18789/", ActiveTlsFingerprint = new string('A', 64), DeviceId = "nexus-device", GrantedScopes = new HashSet( ["operator.read", "operator.admin"], StringComparer.Ordinal), AdvertisedMethods = new HashSet( ["agents.create"], StringComparer.Ordinal), AdvertisedEvents = new HashSet( ["agent.updated"], StringComparer.Ordinal) }; private sealed class GateFixture( ServiceProvider provider, OpenClawWriteGate gate) : IAsyncDisposable { public OpenClawWriteGate Gate { get; } = gate; public static async Task CreateAsync( StubOpenClawConnector connector) { var services = new ServiceCollection(); var databaseName = $"write-gate-{Guid.NewGuid():N}"; services.AddDbContext(options => options.UseInMemoryDatabase(databaseName)); var provider = services.BuildServiceProvider(); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["OpenClawSetup:ExternalClientIdentitySupported"] = "true" }) .Build(); var options = Options.Create(new GatewayConnectorOptions { ClientId = "nexus", ClientMode = "backend", ExternalClientIdentitySupported = true, AllowReservedInternalClientIdentity = false }); await using (var scope = provider.CreateAsyncScope()) { var db = scope.ServiceProvider .GetRequiredService(); db.OpenClawConnectionProfiles.Add( new OpenClawConnectionProfile { Endpoint = connector.ActiveEndpoint!, DiscoverySource = "test", RequiredVersion = connector.GatewayVersion, TlsCertificateFingerprint = connector.ActiveTlsFingerprint, AdoptionState = OpenClawAdoptionStates.Adopted, ManagementEnabled = true, CapabilityHash = OpenClawWriteGate.BuildCapabilityHash(connector), DeviceId = connector.DeviceId, Revision = 1 }); await db.SaveChangesAsync(); } return new GateFixture( provider, new OpenClawWriteGate( provider.GetRequiredService(), connector, options, configuration)); } public ValueTask DisposeAsync() => provider.DisposeAsync(); } }