Files

171 lines
6.6 KiB
C#

using ItWorx.KnowledgeHub.Api.Operations;
using ItWorx.KnowledgeHub.Application.Operations;
using Microsoft.Extensions.DependencyInjection;
namespace ItWorx.KnowledgeHub.Api.Tests;
/// <summary>
/// <see cref="RestoreModeRunner"/> is deliberately pure/testable without booting a real
/// web host or database — no <c>Environment.Exit</c> call inside the tested unit
/// itself (only Program.cs's thin top-level wrapper performs the real process exit),
/// so a fake <see cref="IRestoreService"/> is enough to prove the CLI contract.
/// </summary>
public sealed class RestoreModeRunnerTests
{
[Fact]
public void IsRestoreMode_recognizes_the_flag_alone()
{
Assert.True(RestoreModeRunner.IsRestoreMode(["--restore"]));
}
[Fact]
public void IsRestoreMode_recognizes_the_flag_alongside_other_arguments()
{
Assert.True(RestoreModeRunner.IsRestoreMode(["--restore", "--backup-id=x"]));
}
[Fact]
public void IsRestoreMode_is_false_for_ordinary_startup_args()
{
Assert.False(RestoreModeRunner.IsRestoreMode([]));
}
[Fact]
public async Task Missing_required_arguments_returns_usage_exit_code_without_touching_the_restore_service()
{
var services = new ServiceCollection()
.AddSingleton<IRestoreService>(new FakeRestoreService(shouldSucceed: true))
.BuildServiceProvider();
using var output = new StringWriter();
var exitCode = await RestoreModeRunner.RunAsync(["--restore"], services, output, CancellationToken.None);
Assert.Equal(2, exitCode);
}
[Fact]
public async Task A_successful_restore_returns_exit_code_zero()
{
var connectionString = "Host=restore-db;Username=restore;Password=public-fixture";
var connectionFile = await CreateConnectionFileAsync(connectionString);
string? receivedConnectionString = null;
var services = new ServiceCollection()
.AddSingleton<IRestoreService>(new FakeRestoreService(shouldSucceed: true,
target => receivedConnectionString = target.ConnectionString))
.BuildServiceProvider();
using var output = new StringWriter();
int exitCode;
try
{
exitCode = await RestoreModeRunner.RunAsync(
["--restore", "--backup-id=abc", $"--target-connection-file={connectionFile}", "--target-attachments=/tmp/x"],
services, output, CancellationToken.None);
}
finally
{
File.Delete(connectionFile);
}
Assert.Equal(0, exitCode);
Assert.Contains("GESLAAGD", output.ToString(), StringComparison.Ordinal);
Assert.Equal(connectionString, receivedConnectionString);
Assert.DoesNotContain(connectionString, output.ToString(), StringComparison.Ordinal);
}
[Fact]
public async Task A_failed_restore_returns_a_non_zero_exit_code_and_reports_the_mismatch()
{
var connectionFile = await CreateConnectionFileAsync("Host=restore-db;Username=restore;Password=public-fixture");
var services = new ServiceCollection()
.AddSingleton<IRestoreService>(new FakeRestoreService(shouldSucceed: false))
.BuildServiceProvider();
using var output = new StringWriter();
int exitCode;
try
{
exitCode = await RestoreModeRunner.RunAsync(
["--restore", "--backup-id=abc", $"--target-connection-file={connectionFile}", "--target-attachments=/tmp/x"],
services, output, CancellationToken.None);
}
finally
{
File.Delete(connectionFile);
}
Assert.Equal(1, exitCode);
Assert.Contains("MISLUKT", output.ToString(), StringComparison.Ordinal);
Assert.Contains("AFWIJKING", output.ToString(), StringComparison.Ordinal);
}
[Fact]
public async Task Plaintext_connection_argument_is_not_accepted_or_echoed()
{
const string secret = "Password=public-fixture";
var services = new ServiceCollection()
.AddSingleton<IRestoreService>(new FakeRestoreService(shouldSucceed: true))
.BuildServiceProvider();
using var output = new StringWriter();
var exitCode = await RestoreModeRunner.RunAsync(
["--restore", "--backup-id=abc", $"--target-connection={secret}", "--target-attachments=/tmp/x"],
services, output, CancellationToken.None);
Assert.Equal(2, exitCode);
Assert.DoesNotContain(secret, output.ToString(), StringComparison.Ordinal);
}
[Fact]
public async Task Restore_failure_does_not_print_exception_details_that_may_contain_credentials()
{
const string secret = "Password=public-fixture";
var connectionFile = await CreateConnectionFileAsync("Host=restore-db;" + secret);
var services = new ServiceCollection()
.AddSingleton<IRestoreService>(new FakeRestoreService(exception: new InvalidOperationException(secret)))
.BuildServiceProvider();
using var output = new StringWriter();
int exitCode;
try
{
exitCode = await RestoreModeRunner.RunAsync(
["--restore", "--backup-id=abc", $"--target-connection-file={connectionFile}", "--target-attachments=/tmp/x"],
services, output, CancellationToken.None);
}
finally
{
File.Delete(connectionFile);
}
Assert.Equal(1, exitCode);
Assert.DoesNotContain(secret, output.ToString(), StringComparison.Ordinal);
}
private static async Task<string> CreateConnectionFileAsync(string connectionString)
{
var path = Path.Combine(Path.GetTempPath(), "ikh-restore-connection-" + Guid.NewGuid().ToString("N"));
await File.WriteAllTextAsync(path, connectionString);
if (!OperatingSystem.IsWindows())
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
return path;
}
private sealed class FakeRestoreService(
bool shouldSucceed = true,
Action<RestoreTarget>? onRestore = null,
Exception? exception = null) : IRestoreService
{
public Task<RestoreReport> RestoreAsync(string backupId, RestoreTarget target, CancellationToken cancellationToken)
{
if (exception is not null) throw exception;
onRestore?.Invoke(target);
var tables = shouldSucceed
? new List<RestoreTableReport> { new("pages", 3, 3) }
: new List<RestoreTableReport> { new("pages", 2, 3) };
return Task.FromResult(new RestoreReport(backupId, shouldSucceed, tables, shouldSucceed ? 1 : 0, 1, []));
}
}
}