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); }