internal/server/caddy_logs_ari_test.go
Rama main · réplica de lectura del repositorio original.
// SPDX-License-Identifier: Apache-2.0
package server
import (
"path/filepath"
"testing"
"time"
appdb "github.com/X4Applegate/caddyui/internal/db"
"github.com/X4Applegate/caddyui/internal/models"
)
// Issue #151: a long-past ACME failure must not outlive the live evidence. The
// reporter's certificate failed one renewal-information update on 2026-10-11
// and was still shown as Issuance error on 2026-10-25 while serving fine.
func TestReconcileCertificateLifecycleHealsLongStaleFailure(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
serverID, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "edge", AdminURL: "http://edge:2019", Type: models.CaddyServerTypeManaged,
})
if err != nil {
t.Fatal(err)
}
if _, err := models.CreateCertificate(conn, serverID, 0, &models.Certificate{
Name: "wildcard", Domains: "*.example.com", Source: models.CertSourceManaged,
}); err != nil {
t.Fatal(err)
}
if err := models.UpsertCertificateLifecycle(conn, models.CertificateLifecycleStatus{
ServerID: serverID, ServerName: "edge", Identifier: "*.example.com",
Phase: "error", Level: "ERROR",
Message: "failed updating renewal info from ACME CA",
Error: "dial tcp: lookup acme-v02.api.letsencrypt.org: server misbehaving",
// Two weeks stale, as in the report.
UpdatedAt: time.Now().Add(-14 * 24 * time.Hour),
}); err != nil {
t.Fatal(err)
}
probeCalls := 0
s := &Server{DB: conn, certificateProbeFn: func(server models.CaddyServer, cert models.Certificate) managedCertificateServerStatus {
probeCalls++
return managedCertificateServerStatus{
ServerID: server.ID, ServerName: server.Name, Status: "healthy", Issuer: "Test CA",
}
}}
updated, err := s.reconcileCertificateLifecycle()
if err != nil {
t.Fatal(err)
}
if updated != 1 || probeCalls != 1 {
t.Fatalf("stale failure was not re-probed: updated=%d probeCalls=%d, want 1/1", updated, probeCalls)
}
states, err := models.ListCertificateLifecycle(conn, serverID)
if err != nil {
t.Fatal(err)
}
if len(states) != 1 || states[0].Phase != "active" {
t.Fatalf("healed state = %#v, want active", states)
}
if states[0].Error != "" {
t.Fatalf("stale error text survived a healthy probe: %q", states[0].Error)
}
}
// A failure must still be honoured while it is recent, so a real outage is not
// painted over by a probe against a certificate that is still valid on disk.
func TestReconcileCertificateLifecyclePreservesRecentFailure(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
serverID, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "edge", AdminURL: "http://edge:2019", Type: models.CaddyServerTypeManaged,
})
if err != nil {
t.Fatal(err)
}
if _, err := models.CreateCertificate(conn, serverID, 0, &models.Certificate{
Name: "wildcard", Domains: "*.example.com", Source: models.CertSourceManaged,
}); err != nil {
t.Fatal(err)
}
if err := models.UpsertCertificateLifecycle(conn, models.CertificateLifecycleStatus{
ServerID: serverID, ServerName: "edge", Identifier: "*.example.com",
Phase: "error", Level: "ERROR", Message: "could not get certificate from issuer",
Error: "challenge failed", UpdatedAt: time.Now().Add(-5 * time.Minute),
}); err != nil {
t.Fatal(err)
}
probeCalls := 0
s := &Server{DB: conn, certificateProbeFn: func(server models.CaddyServer, cert models.Certificate) managedCertificateServerStatus {
probeCalls++
return managedCertificateServerStatus{ServerID: server.ID, Status: "healthy"}
}}
updated, err := s.reconcileCertificateLifecycle()
if err != nil {
t.Fatal(err)
}
if updated != 0 || probeCalls != 0 {
t.Fatalf("recent failure was overwritten: updated=%d probeCalls=%d", updated, probeCalls)
}
states, err := models.ListCertificateLifecycle(conn, serverID)
if err != nil {
t.Fatal(err)
}
if len(states) != 1 || states[0].Phase != "error" {
t.Fatalf("recent failure state = %#v, want error retained", states)
}
}
// Revocation is a CA decision; no local TLS probe may clear it.
func TestReconcileCertificateLifecycleNeverHealsRevoked(t *testing.T) {
conn, err := appdb.Open(filepath.Join(t.TempDir(), "caddyui.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = conn.Close() })
serverID, err := models.CreateCaddyServer(conn, &models.CaddyServer{
Name: "edge", AdminURL: "http://edge:2019", Type: models.CaddyServerTypeManaged,
})
if err != nil {
t.Fatal(err)
}
if _, err := models.CreateCertificate(conn, serverID, 0, &models.Certificate{
Name: "wildcard", Domains: "*.example.com", Source: models.CertSourceManaged,
}); err != nil {
t.Fatal(err)
}
if err := models.UpsertCertificateLifecycle(conn, models.CertificateLifecycleStatus{
ServerID: serverID, ServerName: "edge", Identifier: "*.example.com",
Phase: "revoked", Level: "ERROR", Message: "certificate revoked",
UpdatedAt: time.Now().Add(-30 * 24 * time.Hour),
}); err != nil {
t.Fatal(err)
}
probeCalls := 0
s := &Server{DB: conn, certificateProbeFn: func(server models.CaddyServer, cert models.Certificate) managedCertificateServerStatus {
probeCalls++
return managedCertificateServerStatus{ServerID: server.ID, Status: "healthy"}
}}
if _, err := s.reconcileCertificateLifecycle(); err != nil {
t.Fatal(err)
}
if probeCalls != 0 {
t.Fatalf("revoked certificate was re-probed %d time(s)", probeCalls)
}
states, err := models.ListCertificateLifecycle(conn, serverID)
if err != nil {
t.Fatal(err)
}
if len(states) != 1 || states[0].Phase != "revoked" {
t.Fatalf("revoked state = %#v, want revoked retained", states)
}
}