feat(api): fail-fast env parsing for ints + ENCRYPTION_KEY base64
This commit is contained in:
@@ -11,24 +11,55 @@ using VolcanicCore.Persistence.Repositories;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
static string RequireOrThrow(string env, string raw, string reason)
|
||||
=> throw new InvalidOperationException($"env var {env} is invalid: {reason} (got: {raw})");
|
||||
|
||||
// Flat-name env-var overrides for the deploy contract. Only fill in a key if
|
||||
// nothing (e.g. WebFactory's UseSetting in tests) already set it — this lets
|
||||
// test fixtures own their own config regardless of process env state.
|
||||
foreach (var (env, prop) in new (string, string)[] {
|
||||
("ADMIN_PASSWORD", nameof(AppSettings.AdminPassword)),
|
||||
("DATA_DIRECTORY", nameof(AppSettings.DataDirectory)),
|
||||
("ENCRYPTION_KEY", nameof(AppSettings.EncryptionKeyBase64)),
|
||||
("EXECUTION_RETENTION_DAYS", nameof(AppSettings.ExecutionRetentionDays)),
|
||||
("EXECUTION_KEEP_PER_WORKFLOW", nameof(AppSettings.ExecutionKeepPerWorkflow)),
|
||||
("WEBHOOK_DEFAULT_TIMEOUT_SECONDS", nameof(AppSettings.WebhookDefaultTimeoutSeconds)),
|
||||
("MAX_CONCURRENT_EXECUTIONS", nameof(AppSettings.MaxConcurrentExecutions)),
|
||||
})
|
||||
var envMap = new (string Env, string Prop, string Kind)[] {
|
||||
("ADMIN_PASSWORD", nameof(AppSettings.AdminPassword), "string"),
|
||||
("DATA_DIRECTORY", nameof(AppSettings.DataDirectory), "string"),
|
||||
("ENCRYPTION_KEY", nameof(AppSettings.EncryptionKeyBase64), "base64-32"),
|
||||
("EXECUTION_RETENTION_DAYS", nameof(AppSettings.ExecutionRetentionDays), "int"),
|
||||
("EXECUTION_KEEP_PER_WORKFLOW", nameof(AppSettings.ExecutionKeepPerWorkflow), "int"),
|
||||
("WEBHOOK_DEFAULT_TIMEOUT_SECONDS", nameof(AppSettings.WebhookDefaultTimeoutSeconds), "int"),
|
||||
("MAX_CONCURRENT_EXECUTIONS", nameof(AppSettings.MaxConcurrentExecutions), "int"),
|
||||
};
|
||||
foreach (var (env, prop, _) in envMap)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(builder.Configuration[prop])) continue;
|
||||
var v = Environment.GetEnvironmentVariable(env);
|
||||
if (v is not null) builder.Configuration[prop] = v;
|
||||
}
|
||||
|
||||
// Fail-fast validation. Runs against whatever ended up in Configuration —
|
||||
// covers both env vars and test UseSetting overrides.
|
||||
foreach (var (env, prop, kind) in envMap)
|
||||
{
|
||||
var raw = builder.Configuration[prop];
|
||||
if (string.IsNullOrEmpty(raw)) continue;
|
||||
switch (kind)
|
||||
{
|
||||
case "int":
|
||||
if (!int.TryParse(raw, out _))
|
||||
RequireOrThrow(env, raw, "must be an integer");
|
||||
break;
|
||||
case "base64-32":
|
||||
try
|
||||
{
|
||||
var bytes = Convert.FromBase64String(raw);
|
||||
if (bytes.Length != 32)
|
||||
RequireOrThrow(env, raw, "must decode to 32 bytes");
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
RequireOrThrow(env, raw, "must be valid base64");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var settings = new AppSettings();
|
||||
builder.Configuration.Bind(settings);
|
||||
builder.Services.AddSingleton(settings);
|
||||
|
||||
40
tests/VolcanicCore.Tests/ProgramEnvParsingTests.cs
Normal file
40
tests/VolcanicCore.Tests/ProgramEnvParsingTests.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
|
||||
namespace VolcanicCore.Tests;
|
||||
|
||||
public class ProgramEnvParsingTests
|
||||
{
|
||||
private sealed class BadIntFactory : WebFactory
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
base.ConfigureWebHost(builder);
|
||||
builder.UseSetting(nameof(VolcanicCore.Api.AppSettings.ExecutionRetentionDays), "not-a-number");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BadKeyFactory : WebFactory
|
||||
{
|
||||
protected override void ConfigureWebHost(IWebHostBuilder builder)
|
||||
{
|
||||
base.ConfigureWebHost(builder);
|
||||
builder.UseSetting(nameof(VolcanicCore.Api.AppSettings.EncryptionKeyBase64), "!!!not-base64!!!");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bad_integer_env_fails_fast_with_named_error()
|
||||
{
|
||||
using var f = new BadIntFactory();
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => f.CreateClient());
|
||||
Assert.Contains("EXECUTION_RETENTION_DAYS", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bad_encryption_key_fails_fast_with_named_error()
|
||||
{
|
||||
using var f = new BadKeyFactory();
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => f.CreateClient());
|
||||
Assert.Contains("ENCRYPTION_KEY", ex.Message);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using Xunit;
|
||||
|
||||
namespace VolcanicCore.Tests;
|
||||
|
||||
public sealed class WebFactory : WebApplicationFactory<Program>
|
||||
public class WebFactory : WebApplicationFactory<Program>
|
||||
{
|
||||
public string DataDir { get; } = Path.Combine(Path.GetTempPath(), $"vc-{Guid.NewGuid():N}");
|
||||
public string AdminPassword { get; } = "hunter2";
|
||||
|
||||
Reference in New Issue
Block a user