Files
felhom-controller/controller/internal/report/web_certs_report_test.go
T
admin 7daf3d24a7
gates / gates (push) Successful in 34s
controller (unreleased): the report carries the web certificate's end date (felhom.eu R-1379, D77)
Names and end dates only, read from Traefik's store; no key material leaves the box.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-11-14 18:14:24 +01:00

99 lines
3.7 KiB
Go

package report
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"go/ast"
"go/parser"
"go/token"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// felhom.eu R-1379 (D77): the report carries the web certificate's end date from the SAME folder Traefik's stack
// lives in (`<stacks>/traefik/acme.json`), the field is `web_certs` on the wire, and a store that is broken costs the
// field, not the report. (The reader itself, and that it returns no key material: infra TestReadWebCerts_*.)
func TestWebCertsForReport_PathFieldAndBrokenStore(t *testing.T) {
stacks := t.TempDir()
if got := webCertsForReport(stacks, nil); got != nil {
t.Fatalf("no store: %+v", got)
}
if err := os.MkdirAll(filepath.Join(stacks, "traefik"), 0o755); err != nil {
t.Fatal(err)
}
store := filepath.Join(stacks, "traefik", "acme.json")
_ = os.WriteFile(store, []byte("{broken"), 0o600)
if got := webCertsForReport(stacks, nil); got != nil {
t.Fatalf("a broken store: %+v, want nil (and no panic with a nil logger)", got)
}
_ = os.WriteFile(store, []byte(`{"letsencrypt":{"Certificates":[]}}`), 0o600)
r := &Report{WebCerts: webCertsForReport(stacks, nil)}
wire, _ := json.Marshal(r)
if strings.Contains(string(wire), "web_certs") {
t.Fatalf("a box with no certificate must omit the field: %s", wire)
}
// a real certificate in Traefik's folder is read; one in ANOTHER folder (the stacks folder itself) is not
_ = os.WriteFile(store, testACMEStore(t, "*.example.hu", time.Date(2027, 1, 19, 1, 46, 0, 0, time.UTC)), 0o600)
_ = os.WriteFile(filepath.Join(stacks, "acme.json"), testACMEStore(t, "decoy.example.hu", time.Date(2026, 12, 1, 0, 0, 0, 0, time.UTC)), 0o600)
got := webCertsForReport(stacks, nil)
if len(got) != 1 || got[0].Domain != "*.example.hu" || got[0].NotAfter != "2027-01-19T01:46:00Z" {
t.Fatalf("certificates read for the report = %+v, want the one in <stacks>/traefik/acme.json", got)
}
wire, _ = json.Marshal(&Report{WebCerts: got})
if !strings.Contains(string(wire), `"web_certs":[{"domain":"*.example.hu","not_after":"2027-01-19T01:46:00Z"`) {
t.Fatalf("the field on the wire: %s", wire)
}
// BuildReport is what fills it (a helper nobody calls is the inert-seam defect)
f, err := parser.ParseFile(token.NewFileSet(), "builder.go", nil, 0)
if err != nil {
t.Fatal(err)
}
wired := 0
ast.Inspect(f, func(n ast.Node) bool {
as, ok := n.(*ast.AssignStmt)
if !ok || len(as.Lhs) != 1 || len(as.Rhs) != 1 {
return true
}
sel, ok := as.Lhs[0].(*ast.SelectorExpr)
call, ok2 := as.Rhs[0].(*ast.CallExpr)
if ok && ok2 && sel.Sel.Name == "WebCerts" {
if id, ok := call.Fun.(*ast.Ident); ok && id.Name == "webCertsForReport" {
wired++
}
}
return true
})
if wired != 1 {
t.Fatalf("BuildReport assigns r.WebCerts from webCertsForReport %d time(s), want 1", wired)
}
}
// testACMEStore is a Traefik store with one certificate (and its key, as the real file has).
func testACMEStore(t *testing.T, main string, notAfter time.Time) []byte {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
tmpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: main}, DNSNames: []string{main},
NotBefore: notAfter.Add(-90 * 24 * time.Hour), NotAfter: notAfter}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
leaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
b, _ := json.Marshal(map[string]any{"letsencrypt": map[string]any{"Certificates": []map[string]any{{
"domain": map[string]any{"main": main}, "certificate": base64.StdEncoding.EncodeToString(leaf), "key": "S0VZ"}}}})
return b
}