Ctrl K
ComunidadGuías…

internal/server/caddy_logs_ari_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// 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)
	}
}