doctor: complete runtime check documentation sprint
Signed-off-by: master <>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Database.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Database.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class DatabaseCheckRunbookTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("connection", "docs/doctor/articles/postgres/db-connection.md")]
|
||||
[InlineData("pending", "docs/doctor/articles/postgres/db-migrations-pending.md")]
|
||||
[InlineData("failed", "docs/doctor/articles/postgres/db-migrations-failed.md")]
|
||||
[InlineData("schema", "docs/doctor/articles/postgres/db-schema-version.md")]
|
||||
[InlineData("pool-health", "docs/doctor/articles/postgres/db-pool-health.md")]
|
||||
[InlineData("pool-size", "docs/doctor/articles/postgres/db-pool-size.md")]
|
||||
[InlineData("latency", "docs/doctor/articles/postgres/db-latency.md")]
|
||||
[InlineData("permissions", "docs/doctor/articles/postgres/db-permissions.md")]
|
||||
public async Task RunAsync_WhenConnectionFails_UsesExpectedRunbook(string checkName, string expectedRunbook)
|
||||
{
|
||||
var check = CreateCheck(checkName);
|
||||
var context = CreateContext();
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.NotNull(result.Remediation);
|
||||
Assert.Equal(expectedRunbook, result.Remediation!.RunbookUrl);
|
||||
}
|
||||
|
||||
private static IDoctorCheck CreateCheck(string checkName) => checkName switch
|
||||
{
|
||||
"connection" => new DatabaseConnectionCheck(),
|
||||
"pending" => new PendingMigrationsCheck(),
|
||||
"failed" => new FailedMigrationsCheck(),
|
||||
"schema" => new SchemaVersionCheck(),
|
||||
"pool-health" => new ConnectionPoolHealthCheck(),
|
||||
"pool-size" => new ConnectionPoolSizeCheck(),
|
||||
"latency" => new QueryLatencyCheck(),
|
||||
"permissions" => new DatabasePermissionsCheck(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(checkName), checkName, "Unknown check")
|
||||
};
|
||||
|
||||
private static DoctorPluginContext CreateContext()
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["ConnectionStrings:DefaultConnection"] = "Host=127.0.0.1;Port=1;Database=stellaops;Username=stellaops;Password=stellaops;Timeout=1;Command Timeout=1;Pooling=false"
|
||||
})
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new EmptyServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins:Database")
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class EmptyServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.ServiceGraph.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.ServiceGraph.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class ServiceGraphCheckRunbookTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task BackendConnectivityCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new BackendConnectivityCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["StellaOps:BackendUrl"] = "http://127.0.0.1:1"
|
||||
}, includeHttpClientFactory: true);
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-backend.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CircuitBreakerStatusCheck_Warning_UsesRunbook()
|
||||
{
|
||||
var check = new CircuitBreakerStatusCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Resilience:Enabled"] = "true",
|
||||
["Resilience:CircuitBreaker:BreakDurationSeconds"] = "1"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Warn, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-circuitbreaker.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServiceEndpointsCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new ServiceEndpointsCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["StellaOps:AuthorityUrl"] = "http://127.0.0.1:1"
|
||||
}, includeHttpClientFactory: true);
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-endpoints.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MessageQueueCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new MessageQueueCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["RabbitMQ:Host"] = "127.0.0.1",
|
||||
["RabbitMQ:Port"] = "1"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-mq.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ServiceTimeoutCheck_Warning_UsesRunbook()
|
||||
{
|
||||
var check = new ServiceTimeoutCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["HttpClient:Timeout"] = "301"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Warn, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-timeouts.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValkeyConnectivityCheck_Failure_UsesRunbook()
|
||||
{
|
||||
var check = new ValkeyConnectivityCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Valkey:ConnectionString"] = ":6379"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/servicegraph/servicegraph-valkey.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> values, bool includeHttpClientFactory = false)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(values)
|
||||
.Build();
|
||||
|
||||
var services = new ServiceCollection();
|
||||
if (includeHttpClientFactory)
|
||||
{
|
||||
services.AddHttpClient();
|
||||
}
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = services.BuildServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins:ServiceGraph")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Moq" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor\StellaOps.Doctor.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.Doctor.Plugins.Verification\StellaOps.Doctor.Plugins.Verification.csproj" />
|
||||
<ProjectReference Include="..\..\StellaOps.TestKit\StellaOps.TestKit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,80 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Doctor.Models;
|
||||
using StellaOps.Doctor.Plugins;
|
||||
using StellaOps.Doctor.Plugins.Verification.Checks;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Doctor.Plugins.Verification.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class VerificationCheckRunbookTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("artifact", "docs/doctor/articles/verification/verification-artifact-pull.md")]
|
||||
[InlineData("signature", "docs/doctor/articles/verification/verification-signature.md")]
|
||||
[InlineData("sbom", "docs/doctor/articles/verification/verification-sbom-validation.md")]
|
||||
[InlineData("vex", "docs/doctor/articles/verification/verification-vex-validation.md")]
|
||||
[InlineData("policy", "docs/doctor/articles/verification/verification-policy-engine.md")]
|
||||
public async Task RunAsync_WhenOfflineBundleMissing_UsesExpectedRunbook(string checkName, string expectedRunbook)
|
||||
{
|
||||
var check = CreateCheck(checkName);
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Doctor:Plugins:Verification:Enabled"] = "true",
|
||||
["Doctor:Plugins:Verification:TestArtifact:OfflineBundlePath"] = Path.Combine(Path.GetTempPath(), $"missing-{Guid.NewGuid():N}.json")
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Fail, result.Severity);
|
||||
Assert.Equal(expectedRunbook, result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RunAsync_WhenArtifactNotConfigured_UsesBaseRunbook()
|
||||
{
|
||||
var check = new SignatureVerificationCheck();
|
||||
var context = CreateContext(new Dictionary<string, string?>
|
||||
{
|
||||
["Doctor:Plugins:Verification:Enabled"] = "true"
|
||||
});
|
||||
|
||||
var result = await check.RunAsync(context, CancellationToken.None);
|
||||
|
||||
Assert.Equal(DoctorSeverity.Skip, result.Severity);
|
||||
Assert.Equal("docs/doctor/articles/verification/verification-signature.md", result.Remediation?.RunbookUrl);
|
||||
}
|
||||
|
||||
private static IDoctorCheck CreateCheck(string checkName) => checkName switch
|
||||
{
|
||||
"artifact" => new TestArtifactPullCheck(),
|
||||
"signature" => new SignatureVerificationCheck(),
|
||||
"sbom" => new SbomValidationCheck(),
|
||||
"vex" => new VexValidationCheck(),
|
||||
"policy" => new PolicyEngineCheck(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(checkName), checkName, "Unknown check")
|
||||
};
|
||||
|
||||
private static DoctorPluginContext CreateContext(Dictionary<string, string?> values)
|
||||
{
|
||||
var config = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(values)
|
||||
.Build();
|
||||
|
||||
return new DoctorPluginContext
|
||||
{
|
||||
Services = new EmptyServiceProvider(),
|
||||
Configuration = config,
|
||||
TimeProvider = TimeProvider.System,
|
||||
Logger = NullLogger.Instance,
|
||||
EnvironmentName = "Test",
|
||||
PluginConfig = config.GetSection("Doctor:Plugins:Verification")
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class EmptyServiceProvider : IServiceProvider
|
||||
{
|
||||
public object? GetService(Type serviceType) => null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user