using MeterVault.Infrastructure.Options;
using MeterVault.Infrastructure.Update;
using Microsoft.Extensions.Logging.Abstractions;
namespace MeterVault.Integration.Tests;
///
/// The gate in front of the in-app update. Triggering it runs a build of whatever is on the branch,
/// as root on the LXC, so every one of these is a case where getting it wrong hands the host over.
///
///
/// None of these launch anything: they assert the conditions that must hold before a launch
/// is even attempted, which is the part worth pinning. The launch itself needs systemd and a real
/// /usr/bin/update.
///
public sealed class UpdateRunnerTests
{
private const string Key = "correct-horse-battery-staple";
[Fact]
public void Disabled_by_default()
{
// The single most important assertion here: an operator who has not thought about this does
// not get a remote-code-execution endpoint by upgrading.
Assert.False(new MeterVaultOptions().AllowInAppUpdate);
Assert.Equal(UpdateAvailability.NotEnabled, NewRunner(new MeterVaultOptions()).Availability);
}
[Fact]
public void Refuses_when_enabled_but_no_api_key_is_configured()
{
// Otherwise "enabled" would mean anyone at all, since there would be no key to present.
var runner = NewRunner(new MeterVaultOptions { AllowInAppUpdate = true });
Assert.Equal(UpdateAvailability.NoApiKeyConfigured, runner.Availability);
}
[Fact]
public void An_anonymous_api_deployment_cannot_reach_it()
{
// AllowAnonymousApi opens reads. It must not open root: with no keys configured this stays
// shut regardless of that flag.
var runner = NewRunner(new MeterVaultOptions
{
AllowInAppUpdate = true,
AllowAnonymousApi = true,
});
Assert.Equal(UpdateAvailability.NoApiKeyConfigured, runner.Availability);
Assert.False(runner.IsAuthorised(null));
Assert.False(runner.IsAuthorised(""));
Assert.False(runner.IsAuthorised("anything"));
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData("wrong")]
[InlineData("correct-horse-battery-stapl")] // prefix of a valid key
[InlineData("correct-horse-battery-staple ")] // trailing space
[InlineData("CORRECT-HORSE-BATTERY-STAPLE")] // case differs
public void Rejects_anything_that_is_not_exactly_a_configured_key(string? provided)
{
Assert.False(NewRunner(Enabled()).IsAuthorised(provided));
}
[Fact]
public void Accepts_an_exact_key_and_any_of_several()
{
var options = Enabled();
options.ApiKeys.Add("second-key");
var runner = NewRunner(options);
Assert.True(runner.IsAuthorised(Key));
Assert.True(runner.IsAuthorised("second-key"));
}
[Fact]
public async Task Refuses_to_launch_when_not_allowed_even_if_asked_directly()
{
// Defence in depth: LaunchAsync re-checks rather than trusting its caller to have done so.
var launch = await NewRunner(new MeterVaultOptions()).LaunchAsync();
Assert.False(launch.Started);
Assert.Contains("NotEnabled", launch.Message, StringComparison.Ordinal);
}
[Fact]
public void Reports_unsupported_where_there_is_no_in_place_updater()
{
// On a dev box or in a container there is no /usr/bin/update, so a fully configured runner
// still declines rather than half-running something.
if (UpdateRunner.IsSupportedHere)
{
return; // running on a provisioned LXC; the negative case cannot be observed here
}
Assert.Equal(UpdateAvailability.NotSupportedHere, NewRunner(Enabled()).Availability);
}
private static MeterVaultOptions Enabled()
{
var options = new MeterVaultOptions { AllowInAppUpdate = true };
options.ApiKeys.Add(Key);
return options;
}
private static UpdateRunner NewRunner(MeterVaultOptions options) =>
new(Microsoft.Extensions.Options.Options.Create(options), NullLogger.Instance);
}