server-monitor-manager/tests/ServerMonitorManager.Control.Tests/SchemaCompatibilityTests.cs

137 lines
6.4 KiB
C#

using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Xunit;
namespace ServerMonitorManager.Control.Tests;
public sealed class SchemaCompatibilityTests : IAsyncDisposable
{
private readonly string _directory = Path.Combine(
Path.GetTempPath(), $"smm-schema-compatibility-{Guid.NewGuid():N}");
[Fact]
public async Task PublishedAlpha7DatabaseRemainsReadableAndRestorable()
{
var cancellationToken = TestContext.Current.CancellationToken;
Directory.CreateDirectory(_directory);
var fixturePath = Path.Combine(
FindRepositoryRoot(), "tests", "fixtures", "control-v0.1.0-alpha.7.db");
Assert.True(File.Exists(fixturePath), $"Published database fixture is missing: {fixturePath}");
var options = new ControlOptions
{
DatabasePath = Path.Combine(_directory, "control.db"),
CertificateAuthorityPath = Path.Combine(_directory, "control-ca.pfx"),
BackupDirectory = Path.Combine(_directory, "backups")
};
File.Copy(fixturePath, options.DatabasePath);
await File.WriteAllBytesAsync(
options.CertificateAuthorityPath, [1, 2, 3, 4], cancellationToken);
var schemaVersionBefore = await ReadUserVersionAsync(options.DatabasePath, cancellationToken);
Assert.Equal(8L, schemaVersionBefore);
var store = new ControlStore(Options.Create(options));
await store.InitializeAsync(cancellationToken);
var schemaVersionAfter = await ReadUserVersionAsync(options.DatabasePath, cancellationToken);
Assert.Equal(schemaVersionBefore, schemaVersionAfter);
var agents = await store.ListAgentsAsync(cancellationToken);
Assert.Equal(["source-node", "target-node"], agents.Select(agent => agent.NodeId));
Assert.Equal("0.1.0-alpha.7", agents[0].AgentVersion);
Assert.Equal(
new ControlIdentity("source-node", "Agent"),
await store.ResolveIdentityAsync("AGENT-THUMBPRINT", cancellationToken));
Assert.Equal(
new ControlIdentity("operator-device", "Operator"),
await store.ResolveIdentityAsync("DEVICE-THUMBPRINT", cancellationToken));
Assert.Equal(
new ControlIdentity("automation-one", "Automation", "source-node"),
await store.ResolveIdentityAsync("AUTOMATION-THUMBPRINT", cancellationToken));
var link = Assert.Single(await store.ListLinksAsync(cancellationToken));
Assert.Equal("source-node", link.SourceNodeId);
Assert.Equal("target-node", link.TargetNodeId);
Assert.Equal("Active", link.ActualState);
var job = await store.GetProvisioningJobAsync(
"22222222222222222222222222222222", cancellationToken);
Assert.NotNull(job);
Assert.Equal("source-node", job.NodeId);
Assert.Equal("Completed", job.State);
var provisioningEvent = Assert.Single((await store.ListProvisioningEventsAsync(
job.Id, 100, cancellationToken))!);
Assert.Equal("job.completed", provisioningEvent.EventType);
await using (var connection = new SqliteConnection($"Data Source={options.DatabasePath};Pooling=False"))
{
await connection.OpenAsync(cancellationToken);
var audit = connection.CreateCommand();
audit.CommandText = "SELECT actor, action, subject, details_json FROM audit;";
await using var reader = await audit.ExecuteReaderAsync(cancellationToken);
Assert.True(await reader.ReadAsync(cancellationToken));
Assert.Equal("operator-device", reader.GetString(0));
Assert.Equal("fixture.created", reader.GetString(1));
Assert.Equal("source-node", reader.GetString(2));
Assert.Contains("v0.1.0-alpha.7", reader.GetString(3), StringComparison.Ordinal);
Assert.False(await reader.ReadAsync(cancellationToken));
}
var backups = new ControlBackupService(
store, Options.Create(options), NullLogger<ControlBackupService>.Instance);
var backupPath = await backups.CreateAsync(DateTimeOffset.UtcNow, cancellationToken);
await using (var connection = new SqliteConnection($"Data Source={options.DatabasePath};Pooling=False"))
{
await connection.OpenAsync(cancellationToken);
var mutate = connection.CreateCommand();
mutate.CommandText = "UPDATE agents SET name = 'mutated';";
await mutate.ExecuteNonQueryAsync(cancellationToken);
}
await File.WriteAllBytesAsync(options.CertificateAuthorityPath, [9, 9], cancellationToken);
SqliteConnection.ClearAllPools();
await backups.RestoreAsync(backupPath, cancellationToken);
var restoredStore = new ControlStore(Options.Create(options));
await restoredStore.InitializeAsync(cancellationToken);
Assert.Equal(schemaVersionBefore, await ReadUserVersionAsync(options.DatabasePath, cancellationToken));
Assert.Equal("Source Node", (await restoredStore.ListAgentsAsync(cancellationToken))[0].Name);
Assert.Equal([1, 2, 3, 4], await File.ReadAllBytesAsync(
options.CertificateAuthorityPath, cancellationToken));
}
public ValueTask DisposeAsync()
{
SqliteConnection.ClearAllPools();
if (Directory.Exists(_directory))
{
Directory.Delete(_directory, recursive: true);
}
return ValueTask.CompletedTask;
}
private static async Task<long> ReadUserVersionAsync(
string databasePath,
CancellationToken cancellationToken)
{
await using var connection = new SqliteConnection($"Data Source={databasePath};Mode=ReadOnly;Pooling=False");
await connection.OpenAsync(cancellationToken);
var command = connection.CreateCommand();
command.CommandText = "PRAGMA user_version;";
return (long)(await command.ExecuteScalarAsync(cancellationToken))!;
}
private static string FindRepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "ServerMonitorManager.slnx")))
{
directory = directory.Parent;
}
return directory?.FullName
?? throw new DirectoryNotFoundException("Repository root was not found.");
}
}