server-monitor-manager/tests/ServerMonitorManager.Control.Tests/LinkPolicyApplierIntegrationTests.cs
Ochenstarik ee7d89c1d0 feat(control): reconcile links from factual state
Add fact-first Link reconciliation, duplicate and orphan cleanup, generation-aware scheduling, retention, Desktop drift visibility, and strict helper contracts. Preserve exact B-3R batching and lock-safe finalization with Linux/native trimmed evidence.
2026-08-09 12:48:45 +07:00

259 lines
11 KiB
C#

using System.Runtime.Versioning;
using System.Text;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Options;
using ServerMonitorManager.Control;
using ServerMonitorManager.Core;
using Xunit;
namespace ServerMonitorManager.Control.Tests;
public sealed class LinkPolicyApplierIntegrationTests : IAsyncDisposable
{
private readonly string _directory = Path.Combine(
Path.GetTempPath(), $"smm-helper-tests-{Guid.NewGuid():N}");
[Fact]
public async Task LinuxHelperFailureKeepsKillSwitchPendingAcrossControlRestart()
{
if (!OperatingSystem.IsLinux())
{
return;
}
var cancellationToken = TestContext.Current.CancellationToken;
Directory.CreateDirectory(_directory);
var sudoPath = Path.Combine(_directory, "sudo");
var helperPath = Path.Combine(_directory, "policy-helper");
var failureMarkerPath = Path.Combine(_directory, "fail-disconnect");
var firewallUnavailableMarkerPath = Path.Combine(_directory, "firewall-unavailable");
var connectedMarkerPath = Path.Combine(_directory, "connected");
var orphanMarkerPath = Path.Combine(_directory, "orphan-present");
var invocationLogPath = Path.Combine(_directory, "helper.log");
await WriteExecutableAsync(
sudoPath,
"""
#!/bin/sh
set -eu
if [ "${1:-}" != "-n" ]; then
echo "sudo must be non-interactive" >&2
exit 90
fi
shift
exec "$@"
""",
cancellationToken);
await WriteExecutableAsync(
helperPath,
$$"""
#!/bin/sh
set -eu
printf '%s\n' "$*" >> '{{ShellQuote(invocationLogPath)}}'
if [ "${1:-}" = "link-connect" ]; then
touch '{{ShellQuote(connectedMarkerPath)}}'
fi
if [ "${1:-}" = "link-disconnect" ] && [ -f '{{ShellQuote(failureMarkerPath)}}' ]; then
echo "nftables validation failed" >&2
exit 23
fi
if [ "${1:-}" = "link-disconnect" ]; then
if [ "${2:-}" = "orphan" ]; then
rm -f '{{ShellQuote(orphanMarkerPath)}}'
else
rm -f '{{ShellQuote(connectedMarkerPath)}}'
fi
fi
if [ "${1:-}" = "link-list" ]; then
if [ -f '{{ShellQuote(firewallUnavailableMarkerPath)}}' ]; then
echo "mesh.firewall-unavailable" >&2
exit 79
fi
if [ -f '{{ShellQuote(connectedMarkerPath)}}' ]; then
printf 'ai-agent\thome\ttcp\t22\n'
fi
if [ -f '{{ShellQuote(orphanMarkerPath)}}' ]; then
printf 'orphan\tmissing\ttcp\t23\n'
fi
fi
if [ "${1:-}" = "link-status" ]; then
if [ -f '{{ShellQuote(firewallUnavailableMarkerPath)}}' ]; then
echo "mesh.firewall-unavailable" >&2
exit 79
fi
if [ -f '{{ShellQuote(connectedMarkerPath)}}' ]; then
printf '%s\n' active
else
printf '%s\n' disabled
fi
fi
""",
cancellationToken);
var store = CreateStore();
await store.InitializeAsync(cancellationToken);
await EnrollAgentAsync(store, "ai-agent", "AABB", cancellationToken);
await EnrollAgentAsync(store, "home", "CCDD", cancellationToken);
var service = CreateLinkService(store, sudoPath, helperPath);
var active = await service.CreateAsync(
new LinkPolicyCreateRequest(
"ai-agent", "home", "tcp", 22, 60, "integration", Guid.NewGuid().ToString()),
"windows-pc",
cancellationToken);
Assert.Equal("Active", active.ActualState);
File.Delete(connectedMarkerPath);
await File.WriteAllTextAsync(invocationLogPath, string.Empty, cancellationToken);
var restored = await service.ReconcileAllAsync(cancellationToken);
Assert.False(restored.FirewallUnavailable);
Assert.Equal("Active", (await store.GetLinkAsync(active.Id, cancellationToken))!.ActualState);
Assert.Equal(
[
"link-list",
"link-connect ai-agent home tcp 22 60",
"link-list"
],
await File.ReadAllLinesAsync(invocationLogPath, cancellationToken));
await File.WriteAllTextAsync(invocationLogPath, string.Empty, cancellationToken);
var noDrift = await service.ReconcileAllAsync(cancellationToken);
Assert.Equal((1, 1, 0), (noDrift.Examined, noDrift.Converged, noDrift.Failed));
Assert.Equal(["link-list"], await File.ReadAllLinesAsync(invocationLogPath, cancellationToken));
await File.WriteAllTextAsync(invocationLogPath, string.Empty, cancellationToken);
await File.WriteAllTextAsync(firewallUnavailableMarkerPath, "fail", cancellationToken);
var unavailable = await service.ReconcileAllAsync(cancellationToken);
Assert.True(unavailable.FirewallUnavailable);
Assert.Equal(["link-list"], await File.ReadAllLinesAsync(invocationLogPath, cancellationToken));
Assert.Equal(LinkService.FirewallUnavailableCode,
(await store.GetLinkAsync(active.Id, cancellationToken))!.LastError);
File.Delete(firewallUnavailableMarkerPath);
Assert.False((await service.ReconcileAllAsync(cancellationToken)).FirewallUnavailable);
File.Delete(connectedMarkerPath);
await File.WriteAllTextAsync(invocationLogPath, string.Empty, cancellationToken);
var orphanStore = new ControlStore(Options.Create(new ControlOptions
{
DatabasePath = Path.Combine(_directory, "orphan-control.db"),
CertificateAuthorityPath = Path.Combine(_directory, "unused.pfx")
}));
await orphanStore.InitializeAsync(cancellationToken);
var orphanService = CreateLinkService(orphanStore, sudoPath, helperPath);
await File.WriteAllTextAsync(orphanMarkerPath, "present", cancellationToken);
var orphanResult = await orphanService.ReconcileAllAsync(cancellationToken);
Assert.Equal((1, 1, 0), (orphanResult.Examined, orphanResult.Converged, orphanResult.Failed));
Assert.Equal(
["link-list", "link-disconnect orphan missing tcp 23", "link-list"],
await File.ReadAllLinesAsync(invocationLogPath, cancellationToken));
await File.WriteAllTextAsync(connectedMarkerPath, "connected", cancellationToken);
await File.WriteAllTextAsync(invocationLogPath, string.Empty, cancellationToken);
await File.WriteAllTextAsync(failureMarkerPath, "fail", cancellationToken);
var partial = await service.DisableAsync(
active.Id,
new LinkPolicyDisableRequest(Guid.NewGuid().ToString()),
"windows-pc",
cancellationToken);
Assert.NotNull(partial);
Assert.Equal("Disabled", partial.DesiredState);
Assert.Equal("Partial", partial.ActualState);
Assert.Contains("nftables validation failed", partial.LastError);
var restartedStore = CreateStore();
await restartedStore.InitializeAsync(cancellationToken);
var restartedService = CreateLinkService(restartedStore, sudoPath, helperPath);
var failedReconciliation = await restartedService.ReconcileLinksForNodeAsync(
"home", cancellationToken);
Assert.Equal((1, 0, 1, 0),
(failedReconciliation.Examined, failedReconciliation.Converged,
failedReconciliation.Failed, failedReconciliation.Deferred));
File.Delete(failureMarkerPath);
var secondRestartStore = CreateStore();
await secondRestartStore.InitializeAsync(cancellationToken);
var secondRestartService = CreateLinkService(secondRestartStore, sudoPath, helperPath);
var successfulReconciliation = await secondRestartService.ReconcileLinksForNodeAsync(
"home", cancellationToken);
Assert.Equal((1, 1, 0, 0),
(successfulReconciliation.Examined, successfulReconciliation.Converged,
successfulReconciliation.Failed, successfulReconciliation.Deferred));
var persisted = Assert.Single(await secondRestartStore.ListEffectiveLinksForNodeAsync(
"home", cancellationToken));
Assert.Equal("Disabled", persisted.DesiredState);
Assert.Equal("Disabled", persisted.ActualState);
var invocations = await File.ReadAllLinesAsync(invocationLogPath, cancellationToken);
Assert.Equal(7, invocations.Length);
Assert.Equal("link-list", invocations[0]);
Assert.Equal("link-disconnect ai-agent home tcp 22", invocations[1]);
Assert.Equal("link-list", invocations[2]);
Assert.Equal("link-disconnect ai-agent home tcp 22", invocations[3]);
Assert.Equal("link-list", invocations[4]);
Assert.Equal("link-disconnect ai-agent home tcp 22", invocations[5]);
Assert.Equal("link-list", invocations[6]);
}
public ValueTask DisposeAsync()
{
SqliteConnection.ClearAllPools();
if (Directory.Exists(_directory))
{
Directory.Delete(_directory, recursive: true);
}
return ValueTask.CompletedTask;
}
private ControlStore CreateStore()
=> new(Options.Create(new ControlOptions
{
DatabasePath = Path.Combine(_directory, "control.db"),
CertificateAuthorityPath = Path.Combine(_directory, "unused.pfx")
}));
private static LinkService CreateLinkService(
ControlStore store,
string sudoPath,
string helperPath)
{
var applier = new LinkPolicyApplier(Options.Create(new ControlOptions
{
HubHelperPath = helperPath,
PrivilegeEscalationPath = sudoPath
}));
return new LinkService(store, applier, new ControlEventBroker());
}
private static async Task EnrollAgentAsync(
ControlStore store,
string nodeId,
string thumbprint,
CancellationToken cancellationToken)
{
var token = await store.CreateEnrollmentTokenAsync(
nodeId, TimeSpan.FromMinutes(10), cancellationToken);
var enrolled = await store.EnrollAsync(
new EnrollmentRequest(nodeId, token, "csr", Guid.NewGuid().ToString()),
() => new IssuedCertificate(
"certificate", "ca", thumbprint, DateTimeOffset.UtcNow.AddYears(1)),
cancellationToken);
Assert.NotNull(enrolled);
}
[SupportedOSPlatform("linux")]
private static async Task WriteExecutableAsync(
string path,
string contents,
CancellationToken cancellationToken)
{
await File.WriteAllTextAsync(
path,
contents.Replace("\r\n", "\n"),
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
cancellationToken);
File.SetUnixFileMode(
path,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
}
private static string ShellQuote(string value)
=> value.Replace("'", "'\\''", StringComparison.Ordinal);
}