fix(health): report configured application version
/health read the APP_VERSION environment variable directly, but docker-compose passes App__Version, which binds to the App:Version configuration key. The variable under that name never existed in the container, so the endpoint always reported "unknown". Read App:Version through IConfiguration, the approach AdminSystemController already used for the same value. The resolution rule (configured version, else assembly version) moves to a shared BuildMetadata helper rather than being written twice; AdminSystemController now calls it, so the admin page and /health cannot drift apart. Local development is unaffected: nothing sets App:Version there, and the assembly-version fallback still applies. Tests pin the configuration KEY, not just the behaviour, including that an App__Version environment variable binds to App:Version. The original bug failed silently, so a behavioural test alone would not have caught it. Verified against a running backend: App__Version=9.9.9-test reports 9.9.9-test; unset reports the assembly version rather than "unknown". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
// Pins the configuration KEY, not just the behaviour. /health previously read the
|
||||
// APP_VERSION environment variable while docker-compose passed App__Version, so the
|
||||
// endpoint silently reported "unknown" in every container. Nothing failed; the value
|
||||
// was just wrong. These tests fail if the key drifts again.
|
||||
public sealed class BuildMetadataTests
|
||||
{
|
||||
private static IConfiguration Config(params (string Key, string Value)[] values) =>
|
||||
new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(values.Select(v => new KeyValuePair<string, string?>(v.Key, v.Value)))
|
||||
.Build();
|
||||
|
||||
[Fact]
|
||||
public void Reads_the_App_Version_configuration_key()
|
||||
{
|
||||
var version = BuildMetadata.ResolveVersion(Config(("App:Version", "1.2.3")));
|
||||
|
||||
Assert.Equal("1.2.3", version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void App__Version_environment_variable_binds_to_the_same_key()
|
||||
{
|
||||
// This is the exact path docker-compose uses: App__Version in the container
|
||||
// environment must reach App:Version in configuration.
|
||||
var cfg = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>())
|
||||
.Add(new FakeEnvironmentSource(("App__Version", "42")))
|
||||
.Build();
|
||||
|
||||
Assert.Equal("42", BuildMetadata.ResolveVersion(cfg));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Falls_back_to_the_assembly_version_when_unconfigured()
|
||||
{
|
||||
// Local development sets nothing. "unknown" would be a regression, not a default.
|
||||
var version = BuildMetadata.ResolveVersion(Config());
|
||||
|
||||
Assert.False(string.IsNullOrWhiteSpace(version));
|
||||
Assert.NotEqual("unknown", version);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("$(git rev-parse --short HEAD)")]
|
||||
[InlineData("${APP_COMMIT_SHA}")]
|
||||
[InlineData(" ")]
|
||||
[InlineData(null)]
|
||||
public void Unresolved_placeholders_and_blanks_are_rejected(string? raw)
|
||||
{
|
||||
Assert.Null(BuildMetadata.Normalize(raw));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_real_value_survives_normalization_trimmed()
|
||||
{
|
||||
Assert.Equal("abc1234", BuildMetadata.Normalize(" abc1234 "));
|
||||
}
|
||||
|
||||
// Minimal stand-in for the environment-variable provider, so the test does not
|
||||
// mutate the real process environment and race other tests.
|
||||
private sealed class FakeEnvironmentSource : IConfigurationSource
|
||||
{
|
||||
private readonly (string Key, string Value)[] _values;
|
||||
|
||||
public FakeEnvironmentSource(params (string Key, string Value)[] values) => _values = values;
|
||||
|
||||
public IConfigurationProvider Build(IConfigurationBuilder builder) => new Provider(_values);
|
||||
|
||||
private sealed class Provider : ConfigurationProvider
|
||||
{
|
||||
private readonly (string Key, string Value)[] _values;
|
||||
|
||||
public Provider((string Key, string Value)[] values) => _values = values;
|
||||
|
||||
public override void Load()
|
||||
{
|
||||
foreach (var (key, value) in _values)
|
||||
{
|
||||
// "__" is the environment-variable spelling of the ":" section separator.
|
||||
Data[key.Replace("__", ":")] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user