749 lines
26 KiB
C#
749 lines
26 KiB
C#
using System.Text.Json;
|
|
using EntKube.Clusters.Features.AdoptCluster.Components.LetsEncrypt;
|
|
using FluentAssertions;
|
|
|
|
namespace EntKube.Clusters.Tests.Features;
|
|
|
|
/// <summary>
|
|
/// Tests for the LetsEncrypt component check — specifically the DNS01 solver
|
|
/// configuration extraction logic. When a cluster has ACME ClusterIssuers using
|
|
/// DNS01 challenges, the check should extract the provider type, secret references,
|
|
/// hosted zones, and domain selectors so the UI can display them properly.
|
|
/// </summary>
|
|
public class LetsEncryptCheckTests
|
|
{
|
|
// ─── DNS01 Solver Parsing ─────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ParseDns01Details_WithCloudflareSolver_ExtractsProviderAndSecret()
|
|
{
|
|
// Arrange — A ClusterIssuer with a Cloudflare DNS01 solver. The solver
|
|
// specifies an API token stored in a Kubernetes Secret.
|
|
|
|
string json = """
|
|
{
|
|
"cloudflare": {
|
|
"apiTokenSecretRef": {
|
|
"name": "cloudflare-api-token",
|
|
"key": "api-token"
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement dns01Element = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act — Extract DNS01 details from the solver configuration.
|
|
|
|
Dns01Details details = LetsEncryptCheck.ParseDns01Details(dns01Element);
|
|
|
|
// Assert — Should identify Cloudflare as the provider and capture the secret name.
|
|
|
|
details.Provider.Should().Be("cloudflare");
|
|
details.SecretName.Should().Be("cloudflare-api-token");
|
|
details.HostedZone.Should().BeNull();
|
|
details.Project.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDns01Details_WithRoute53Solver_ExtractsProviderAndHostedZone()
|
|
{
|
|
// Arrange — A ClusterIssuer with an AWS Route53 DNS01 solver. Route53
|
|
// solvers include a hosted zone ID and region.
|
|
|
|
string json = """
|
|
{
|
|
"route53": {
|
|
"region": "eu-north-1",
|
|
"hostedZoneID": "Z1234567890ABC"
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement dns01Element = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Dns01Details details = LetsEncryptCheck.ParseDns01Details(dns01Element);
|
|
|
|
// Assert
|
|
|
|
details.Provider.Should().Be("route53");
|
|
details.HostedZone.Should().Be("Z1234567890ABC");
|
|
details.SecretName.Should().BeNull();
|
|
details.Project.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDns01Details_WithRoute53AndSecretAccessKey_ExtractsSecretName()
|
|
{
|
|
// Arrange — A Route53 DNS01 solver using explicit credentials stored in
|
|
// a Kubernetes Secret via secretAccessKeySecretRef. This is the most common
|
|
// setup for non-IRSA (IAM Roles for Service Accounts) Route53 configurations.
|
|
|
|
string json = """
|
|
{
|
|
"route53": {
|
|
"region": "eu-north-1",
|
|
"hostedZoneID": "Z1234567890ABC",
|
|
"accessKeyID": "AKIAIOSFODNN7EXAMPLE",
|
|
"secretAccessKeySecretRef": {
|
|
"name": "aws-route53-credentials",
|
|
"key": "secret-access-key"
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement dns01Element = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Dns01Details details = LetsEncryptCheck.ParseDns01Details(dns01Element);
|
|
|
|
// Assert — Should capture both the hosted zone and the credential secret.
|
|
|
|
details.Provider.Should().Be("route53");
|
|
details.HostedZone.Should().Be("Z1234567890ABC");
|
|
details.SecretName.Should().Be("aws-route53-credentials");
|
|
details.Project.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDns01Details_WithAzureDnsServicePrincipal_ExtractsAllFields()
|
|
{
|
|
// Arrange — An Azure DNS solver using a service principal for authentication.
|
|
// This is the most common Azure DNS setup: the ClusterIssuer authenticates
|
|
// to Azure using a clientID + clientSecret (stored in a K8s Secret),
|
|
// scoped to a specific subscription, tenant, resource group, and hosted zone.
|
|
|
|
string json = """
|
|
{
|
|
"azureDNS": {
|
|
"clientID": "app-id-12345",
|
|
"clientSecretSecretRef": {
|
|
"name": "azuredns-sp-secret",
|
|
"key": "client-secret"
|
|
},
|
|
"subscriptionID": "sub-aaaa-bbbb-cccc",
|
|
"tenantID": "tenant-xxxx-yyyy",
|
|
"resourceGroupName": "dns-rg",
|
|
"hostedZoneName": "example.com",
|
|
"environment": "AzurePublicCloud"
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement dns01Element = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Dns01Details details = LetsEncryptCheck.ParseDns01Details(dns01Element);
|
|
|
|
// Assert — All Azure DNS fields should be extracted.
|
|
|
|
details.Provider.Should().Be("azuredns");
|
|
details.HostedZone.Should().Be("example.com");
|
|
details.SecretName.Should().Be("azuredns-sp-secret");
|
|
details.ClientId.Should().Be("app-id-12345");
|
|
details.SubscriptionId.Should().Be("sub-aaaa-bbbb-cccc");
|
|
details.TenantId.Should().Be("tenant-xxxx-yyyy");
|
|
details.ResourceGroup.Should().Be("dns-rg");
|
|
details.Project.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDns01Details_WithAzureDnsManagedIdentity_ExtractsHostedZone()
|
|
{
|
|
// Arrange — An Azure DNS solver using managed identity (no clientSecret).
|
|
// Only the hosted zone and resource group are present.
|
|
|
|
string json = """
|
|
{
|
|
"azureDNS": {
|
|
"hostedZoneName": "example.com",
|
|
"resourceGroupName": "dns-rg",
|
|
"subscriptionID": "sub-123",
|
|
"environment": "AzurePublicCloud"
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement dns01Element = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Dns01Details details = LetsEncryptCheck.ParseDns01Details(dns01Element);
|
|
|
|
// Assert — Should capture what's available, leave missing fields null.
|
|
|
|
details.Provider.Should().Be("azuredns");
|
|
details.HostedZone.Should().Be("example.com");
|
|
details.ResourceGroup.Should().Be("dns-rg");
|
|
details.SubscriptionId.Should().Be("sub-123");
|
|
details.SecretName.Should().BeNull();
|
|
details.ClientId.Should().BeNull();
|
|
details.TenantId.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDns01Details_WithCloudDnsSolver_ExtractsProviderAndProject()
|
|
{
|
|
// Arrange — A Google Cloud DNS solver with a project and service account.
|
|
|
|
string json = """
|
|
{
|
|
"cloudDNS": {
|
|
"project": "my-gcp-project",
|
|
"serviceAccountSecretRef": {
|
|
"name": "clouddns-service-account",
|
|
"key": "key.json"
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement dns01Element = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Dns01Details details = LetsEncryptCheck.ParseDns01Details(dns01Element);
|
|
|
|
// Assert
|
|
|
|
details.Provider.Should().Be("clouddns");
|
|
details.Project.Should().Be("my-gcp-project");
|
|
details.SecretName.Should().Be("clouddns-service-account");
|
|
details.HostedZone.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDns01Details_WithEmptyDns01_ReturnsUnknownProvider()
|
|
{
|
|
// Arrange — A DNS01 solver with no recognized provider (unusual but possible
|
|
// with custom webhook solvers).
|
|
|
|
string json = """
|
|
{
|
|
"webhook": {
|
|
"solverName": "custom-solver"
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement dns01Element = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Dns01Details details = LetsEncryptCheck.ParseDns01Details(dns01Element);
|
|
|
|
// Assert — Should report unknown when no recognized provider is found.
|
|
|
|
details.Provider.Should().BeNull();
|
|
details.SecretName.Should().BeNull();
|
|
}
|
|
|
|
// ─── Domain Selector Parsing ──────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ParseDnsZones_WithDnsZonesSelector_ExtractsZones()
|
|
{
|
|
// Arrange — A solver with a selector that scopes it to specific DNS zones.
|
|
// This is common when using DNS01 for wildcard certificates on specific domains.
|
|
|
|
string json = """
|
|
{
|
|
"selector": {
|
|
"dnsZones": ["example.com", "internal.example.com"]
|
|
},
|
|
"dns01": {
|
|
"cloudflare": {
|
|
"apiTokenSecretRef": {
|
|
"name": "cf-token",
|
|
"key": "api-token"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement solverElement = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
List<string> zones = LetsEncryptCheck.ParseDnsZones(solverElement);
|
|
|
|
// Assert
|
|
|
|
zones.Should().BeEquivalentTo(new[] { "example.com", "internal.example.com" });
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseDnsZones_WithNoSelector_ReturnsEmptyList()
|
|
{
|
|
// Arrange — A solver with no selector (applies to all domains).
|
|
|
|
string json = """
|
|
{
|
|
"dns01": {
|
|
"cloudflare": {
|
|
"apiTokenSecretRef": {
|
|
"name": "cf-token",
|
|
"key": "api-token"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement solverElement = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
List<string> zones = LetsEncryptCheck.ParseDnsZones(solverElement);
|
|
|
|
// Assert
|
|
|
|
zones.Should().BeEmpty();
|
|
}
|
|
|
|
// ─── Per-Issuer Config Values ─────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void BuildConfigValues_WithMultipleIssuers_IncludesPerIssuerDetails()
|
|
{
|
|
// Arrange — Two ACME issuers: one staging with HTTP01, one production
|
|
// with DNS01 using Cloudflare. The config values should include both
|
|
// global summary and per-issuer detail so the UI can display each issuer.
|
|
|
|
List<DetectedIssuer> issuers = new()
|
|
{
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-staging",
|
|
Email: "admin@example.com",
|
|
AcmeServer: "https://acme-staging-v02.api.letsencrypt.org/directory",
|
|
SolverType: "HTTP01",
|
|
IsReady: true,
|
|
Dns01Details: null,
|
|
DnsZones: new List<string>()),
|
|
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-prod",
|
|
Email: "admin@example.com",
|
|
AcmeServer: "https://acme-v02.api.letsencrypt.org/directory",
|
|
SolverType: "DNS01",
|
|
IsReady: true,
|
|
Dns01Details: new Dns01Details(
|
|
Provider: "cloudflare",
|
|
SecretName: "cloudflare-api-token",
|
|
HostedZone: null,
|
|
Project: null),
|
|
DnsZones: new List<string> { "example.com" })
|
|
};
|
|
|
|
// Act — Build the configuration values dictionary from the detected issuers.
|
|
|
|
Dictionary<string, string> configValues = LetsEncryptCheck.BuildConfigValues(issuers);
|
|
|
|
// Assert — Global summary values.
|
|
|
|
configValues["issuers"].Should().Be("letsencrypt-staging,letsencrypt-prod");
|
|
configValues["issuerCount"].Should().Be("2");
|
|
configValues["email"].Should().Be("admin@example.com");
|
|
configValues["readyCount"].Should().Be("2");
|
|
configValues["httpSolverEnabled"].Should().Be("true");
|
|
configValues["dnsSolverEnabled"].Should().Be("true");
|
|
|
|
// Assert — Per-issuer detail for the DNS01 issuer.
|
|
|
|
configValues["issuer.letsencrypt-prod.solverType"].Should().Be("DNS01");
|
|
configValues["issuer.letsencrypt-prod.acmeServer"].Should().Be("https://acme-v02.api.letsencrypt.org/directory");
|
|
configValues["issuer.letsencrypt-prod.dnsSolverProvider"].Should().Be("cloudflare");
|
|
configValues["issuer.letsencrypt-prod.dnsSolverSecretName"].Should().Be("cloudflare-api-token");
|
|
configValues["issuer.letsencrypt-prod.dnsZones"].Should().Be("example.com");
|
|
|
|
// Assert — Per-issuer detail for the HTTP01 issuer.
|
|
|
|
configValues["issuer.letsencrypt-staging.solverType"].Should().Be("HTTP01");
|
|
configValues["issuer.letsencrypt-staging.acmeServer"].Should().Be("https://acme-staging-v02.api.letsencrypt.org/directory");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildConfigValues_WithDns01Solver_PopulatesGlobalDnsSolverFields()
|
|
{
|
|
// Arrange — A single issuer with DNS01. The global config values should
|
|
// include the DNS solver details so the "Current Configuration" view
|
|
// shows them without needing to expand per-issuer details.
|
|
|
|
List<DetectedIssuer> issuers = new()
|
|
{
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-production",
|
|
Email: "certs@company.com",
|
|
AcmeServer: "https://acme-v02.api.letsencrypt.org/directory",
|
|
SolverType: "DNS01",
|
|
IsReady: true,
|
|
Dns01Details: new Dns01Details(
|
|
Provider: "route53",
|
|
SecretName: null,
|
|
HostedZone: "Z1234567890ABC",
|
|
Project: null),
|
|
DnsZones: new List<string> { "company.com", "internal.company.com" })
|
|
};
|
|
|
|
// Act
|
|
|
|
Dictionary<string, string> configValues = LetsEncryptCheck.BuildConfigValues(issuers);
|
|
|
|
// Assert — Global DNS solver values populated from the first DNS01 issuer found.
|
|
|
|
configValues["httpSolverEnabled"].Should().Be("false");
|
|
configValues["dnsSolverEnabled"].Should().Be("true");
|
|
configValues["dnsSolverProvider"].Should().Be("route53");
|
|
configValues["dnsSolverHostedZone"].Should().Be("Z1234567890ABC");
|
|
configValues["dnsZones"].Should().Be("company.com,internal.company.com");
|
|
configValues.Should().NotContainKey("dnsSolverProject");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildConfigValues_WithCloudDnsSolver_IncludesProject()
|
|
{
|
|
// Arrange — A GCP Cloud DNS solver should include the project.
|
|
|
|
List<DetectedIssuer> issuers = new()
|
|
{
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-prod",
|
|
Email: "ops@company.com",
|
|
AcmeServer: "https://acme-v02.api.letsencrypt.org/directory",
|
|
SolverType: "DNS01",
|
|
IsReady: true,
|
|
Dns01Details: new Dns01Details(
|
|
Provider: "clouddns",
|
|
SecretName: "gcp-dns-sa",
|
|
HostedZone: null,
|
|
Project: "my-gcp-project"),
|
|
DnsZones: new List<string>())
|
|
};
|
|
|
|
// Act
|
|
|
|
Dictionary<string, string> configValues = LetsEncryptCheck.BuildConfigValues(issuers);
|
|
|
|
// Assert
|
|
|
|
configValues["dnsSolverProvider"].Should().Be("clouddns");
|
|
configValues["dnsSolverProject"].Should().Be("my-gcp-project");
|
|
configValues["dnsSolverSecretName"].Should().Be("gcp-dns-sa");
|
|
configValues["dnsSolverSecretNameGcp"].Should().Be("gcp-dns-sa");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildConfigValues_WithHttp01Only_DoesNotIncludeDnsFields()
|
|
{
|
|
// Arrange — An HTTP01-only issuer should not produce DNS solver fields.
|
|
|
|
List<DetectedIssuer> issuers = new()
|
|
{
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-prod",
|
|
Email: "admin@example.com",
|
|
AcmeServer: "https://acme-v02.api.letsencrypt.org/directory",
|
|
SolverType: "HTTP01",
|
|
IsReady: true,
|
|
Dns01Details: null,
|
|
DnsZones: new List<string>())
|
|
};
|
|
|
|
// Act
|
|
|
|
Dictionary<string, string> configValues = LetsEncryptCheck.BuildConfigValues(issuers);
|
|
|
|
// Assert
|
|
|
|
configValues["httpSolverEnabled"].Should().Be("true");
|
|
configValues["dnsSolverEnabled"].Should().Be("false");
|
|
configValues.Should().NotContainKey("dnsSolverProvider");
|
|
configValues.Should().NotContainKey("dnsSolverSecretName");
|
|
configValues.Should().NotContainKey("dnsSolverHostedZone");
|
|
configValues.Should().NotContainKey("dnsSolverProject");
|
|
configValues.Should().NotContainKey("dnsSolverClientId");
|
|
configValues.Should().NotContainKey("dnsSolverSubscriptionId");
|
|
configValues.Should().NotContainKey("dnsSolverTenantId");
|
|
configValues.Should().NotContainKey("dnsSolverResourceGroup");
|
|
configValues.Should().NotContainKey("dnsZones");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildConfigValues_WithAzureDnsServicePrincipal_IncludesAllAzureFields()
|
|
{
|
|
// Arrange — An Azure DNS solver with full service principal configuration.
|
|
// All Azure-specific fields should appear in both global and per-issuer config.
|
|
|
|
List<DetectedIssuer> issuers = new()
|
|
{
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-prod",
|
|
Email: "certs@company.com",
|
|
AcmeServer: "https://acme-v02.api.letsencrypt.org/directory",
|
|
SolverType: "DNS01",
|
|
IsReady: true,
|
|
Dns01Details: new Dns01Details(
|
|
Provider: "azuredns",
|
|
SecretName: "azuredns-sp-secret",
|
|
HostedZone: "company.com",
|
|
Project: null,
|
|
ClientId: "app-id-12345",
|
|
SubscriptionId: "sub-aaaa-bbbb",
|
|
TenantId: "tenant-xxxx",
|
|
ResourceGroup: "dns-rg"),
|
|
DnsZones: new List<string>())
|
|
};
|
|
|
|
// Act
|
|
|
|
Dictionary<string, string> configValues = LetsEncryptCheck.BuildConfigValues(issuers);
|
|
|
|
// Assert — Global DNS solver values include all Azure fields.
|
|
|
|
configValues["dnsSolverProvider"].Should().Be("azuredns");
|
|
configValues["dnsSolverSecretName"].Should().Be("azuredns-sp-secret");
|
|
configValues["dnsSolverSecretNameAzure"].Should().Be("azuredns-sp-secret");
|
|
configValues["dnsSolverHostedZone"].Should().Be("company.com");
|
|
configValues["dnsSolverClientId"].Should().Be("app-id-12345");
|
|
configValues["dnsSolverSubscriptionId"].Should().Be("sub-aaaa-bbbb");
|
|
configValues["dnsSolverTenantId"].Should().Be("tenant-xxxx");
|
|
configValues["dnsSolverResourceGroup"].Should().Be("dns-rg");
|
|
|
|
// Assert — Per-issuer values also include Azure fields.
|
|
|
|
configValues["issuer.letsencrypt-prod.dnsSolverProvider"].Should().Be("azuredns");
|
|
configValues["issuer.letsencrypt-prod.dnsSolverSecretName"].Should().Be("azuredns-sp-secret");
|
|
configValues["issuer.letsencrypt-prod.dnsSolverClientId"].Should().Be("app-id-12345");
|
|
configValues["issuer.letsencrypt-prod.dnsSolverSubscriptionId"].Should().Be("sub-aaaa-bbbb");
|
|
configValues["issuer.letsencrypt-prod.dnsSolverTenantId"].Should().Be("tenant-xxxx");
|
|
configValues["issuer.letsencrypt-prod.dnsSolverResourceGroup"].Should().Be("dns-rg");
|
|
}
|
|
|
|
// ─── HTTP01 Solver Mode Detection ─────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void BuildConfigValues_WithGatewayApiHttp01_ReportsGatewayMode()
|
|
{
|
|
// Arrange — An issuer using HTTP01 via gatewayHTTPRoute (Gateway API)
|
|
// instead of traditional Ingress. The config should report the mode
|
|
// so the UI can show Gateway API-specific fields.
|
|
|
|
List<DetectedIssuer> issuers = new()
|
|
{
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-prod",
|
|
Email: "admin@example.com",
|
|
AcmeServer: "https://acme-v02.api.letsencrypt.org/directory",
|
|
SolverType: "HTTP01",
|
|
IsReady: true,
|
|
Dns01Details: null,
|
|
DnsZones: new List<string>(),
|
|
Http01Mode: "gatewayHTTPRoute",
|
|
Http01GatewayName: "internal",
|
|
Http01GatewayNamespace: "internal-ingress")
|
|
};
|
|
|
|
// Act
|
|
|
|
Dictionary<string, string> configValues = LetsEncryptCheck.BuildConfigValues(issuers);
|
|
|
|
// Assert — Global config should reflect gateway mode.
|
|
|
|
configValues["httpSolverEnabled"].Should().Be("true");
|
|
configValues["httpSolverMode"].Should().Be("gatewayHTTPRoute");
|
|
configValues["httpSolverGatewayName"].Should().Be("internal");
|
|
configValues["httpSolverGatewayNamespace"].Should().Be("internal-ingress");
|
|
configValues.Should().NotContainKey("httpSolverIngressClass");
|
|
}
|
|
|
|
[Fact]
|
|
public void BuildConfigValues_WithIngressHttp01_ReportsIngressMode()
|
|
{
|
|
// Arrange — A traditional ingress-based HTTP01 solver. The config
|
|
// should report "ingress" mode with the ingress class.
|
|
|
|
List<DetectedIssuer> issuers = new()
|
|
{
|
|
new DetectedIssuer(
|
|
Name: "letsencrypt-prod",
|
|
Email: "admin@example.com",
|
|
AcmeServer: "https://acme-v02.api.letsencrypt.org/directory",
|
|
SolverType: "HTTP01",
|
|
IsReady: true,
|
|
Dns01Details: null,
|
|
DnsZones: new List<string>(),
|
|
Http01Mode: "ingress",
|
|
Http01IngressClass: "traefik")
|
|
};
|
|
|
|
// Act
|
|
|
|
Dictionary<string, string> configValues = LetsEncryptCheck.BuildConfigValues(issuers);
|
|
|
|
// Assert — Global config should reflect ingress mode.
|
|
|
|
configValues["httpSolverEnabled"].Should().Be("true");
|
|
configValues["httpSolverMode"].Should().Be("ingress");
|
|
configValues["httpSolverIngressClass"].Should().Be("traefik");
|
|
configValues.Should().NotContainKey("httpSolverGatewayName");
|
|
}
|
|
|
|
// ─── HTTP01 Solver Parsing ────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void ParseHttp01Details_WithGatewayHTTPRoute_ExtractsParentRef()
|
|
{
|
|
// Arrange — An HTTP01 solver configured with gatewayHTTPRoute (for
|
|
// clusters using Gateway API instead of Ingress). The solver specifies
|
|
// which Gateway to attach the challenge HTTPRoute to.
|
|
|
|
string json = """
|
|
{
|
|
"http01": {
|
|
"gatewayHTTPRoute": {
|
|
"parentRefs": [
|
|
{
|
|
"name": "internal",
|
|
"namespace": "internal-ingress",
|
|
"kind": "Gateway"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement solverElement = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Http01Details details = LetsEncryptCheck.ParseHttp01Details(solverElement);
|
|
|
|
// Assert — Should identify gateway mode and extract parent ref.
|
|
|
|
details.Mode.Should().Be("gatewayHTTPRoute");
|
|
details.GatewayName.Should().Be("internal");
|
|
details.GatewayNamespace.Should().Be("internal-ingress");
|
|
details.IngressClass.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHttp01Details_WithIngress_ExtractsIngressClass()
|
|
{
|
|
// Arrange — A traditional ingress-based HTTP01 solver.
|
|
|
|
string json = """
|
|
{
|
|
"http01": {
|
|
"ingress": {
|
|
"ingressClassName": "traefik"
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement solverElement = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Http01Details details = LetsEncryptCheck.ParseHttp01Details(solverElement);
|
|
|
|
// Assert
|
|
|
|
details.Mode.Should().Be("ingress");
|
|
details.IngressClass.Should().Be("traefik");
|
|
details.GatewayName.Should().BeNull();
|
|
details.GatewayNamespace.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHttp01Details_WithIngressClassField_ExtractsClass()
|
|
{
|
|
// Arrange — Some older issuers use "class" instead of "ingressClassName".
|
|
|
|
string json = """
|
|
{
|
|
"http01": {
|
|
"ingress": {
|
|
"class": "nginx"
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement solverElement = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Http01Details details = LetsEncryptCheck.ParseHttp01Details(solverElement);
|
|
|
|
// Assert
|
|
|
|
details.Mode.Should().Be("ingress");
|
|
details.IngressClass.Should().Be("nginx");
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHttp01Details_WithNoHttp01_ReturnsNull()
|
|
{
|
|
// Arrange — A solver that only has dns01.
|
|
|
|
string json = """
|
|
{
|
|
"dns01": {
|
|
"cloudflare": {}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement solverElement = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Http01Details? details = LetsEncryptCheck.ParseHttp01Details(solverElement);
|
|
|
|
// Assert
|
|
|
|
details.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseHttp01Details_WithGatewayHTTPRouteNoNamespace_OmitsNamespace()
|
|
{
|
|
// Arrange — A gatewayHTTPRoute solver where the parentRef doesn't
|
|
// specify a namespace (uses the issuer's namespace by default).
|
|
|
|
string json = """
|
|
{
|
|
"http01": {
|
|
"gatewayHTTPRoute": {
|
|
"parentRefs": [
|
|
{
|
|
"name": "default-gateway"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
""";
|
|
|
|
JsonElement solverElement = JsonDocument.Parse(json).RootElement;
|
|
|
|
// Act
|
|
|
|
Http01Details details = LetsEncryptCheck.ParseHttp01Details(solverElement);
|
|
|
|
// Assert
|
|
|
|
details.Mode.Should().Be("gatewayHTTPRoute");
|
|
details.GatewayName.Should().Be("default-gateway");
|
|
details.GatewayNamespace.Should().BeNull();
|
|
}
|
|
}
|