| new file mode 100644 |
| index 000000000..528ff18ae |
| --- /dev/null |
| +++ b/internal/domains/reconcile_internal_test.go |
| @@ -0,0 +1,104 @@ |
| +package domains |
| + |
| +// Internal-package tests for wildcardParent/hostCertExpiry: reconcile_test.go |
| +// is package domains_test and cannot reach these unexported helpers, so this |
| +// file follows slugname_test.go's existing convention of a same-package test |
| +// file living alongside it. |
| + |
| +import ( |
| + "bytes" |
| + "crypto/ecdsa" |
| + "crypto/elliptic" |
| + "crypto/rand" |
| + "crypto/x509" |
| + "crypto/x509/pkix" |
| + "encoding/pem" |
| + "math/big" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + "time" |
| +) |
| + |
| +// writeWildcardPEM writes a self-signed cert for name valid until notAfter, |
| +// in the acme-cache bundle layout certExpiry parses: key block first, then |
| +// the certificate. |
| +func writeWildcardPEM(t *testing.T, path, name string, notAfter time.Time) { |
| + 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: name}, |
| + DNSNames: []string{name}, |
| + NotBefore: time.Now().Add(-time.Hour), |
| + NotAfter: notAfter, |
| + } |
| + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + keyDER, err := x509.MarshalECPrivateKey(key) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + var buf bytes.Buffer |
| + if err := pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestWildcardParent(t *testing.T) { |
| + cases := []struct { |
| + host, appsDomain, want string |
| + }{ |
| + {"kass.abs.oncarlos.com", "oncarlos.com", "abs.oncarlos.com"}, |
| + {"canary.kass.abs.oncarlos.com", "oncarlos.com", ""}, // canary depth: not covered |
| + {"abs.oncarlos.com", "oncarlos.com", ""}, // apex of a parent |
| + {"custom.example.com", "oncarlos.com", ""}, // foreign domain |
| + {"kass.abs.oncarlos.com", "", ""}, // no apps domain configured |
| + {"Kass.ABS.oncarlos.com", "oncarlos.com", "abs.oncarlos.com"}, |
| + } |
| + for _, c := range cases { |
| + if got := wildcardParent(c.host, c.appsDomain); got != c.want { |
| + t.Errorf("wildcardParent(%q, %q) = %q, want %q", c.host, c.appsDomain, got, c.want) |
| + } |
| + } |
| +} |
| + |
| +// A wildcard-covered host has no per-host acme cache entry — its expiry must |
| +// come from the covering wildcard-<parent>.pem instead of reporting 0 |
| +// (certless) forever. |
| +func TestCertExpiryWildcardFallback(t *testing.T) { |
| + dir := t.TempDir() |
| + // certExpiry parses "key PEM block(s) first, then CERTIFICATE" — build a |
| + // minimal self-signed cert the same way the edge's wildcard PEM is laid |
| + // out. Reuse/adapt the cert-building helper if reconcile_test.go already |
| + // has one; otherwise inline ecdsa+x509 self-sign exactly as in |
| + // internal/edge/wildcard_test.go's selfSignedWildcard. |
| + notAfter := time.Now().Add(45 * 24 * time.Hour).Truncate(time.Second) |
| + writeWildcardPEM(t, filepath.Join(dir, "wildcard-abs.oncarlos.com.pem"), "*.abs.oncarlos.com", notAfter) |
| + |
| + host := "kass.abs.oncarlos.com" |
| + if got := hostCertExpiry(dir, host, "oncarlos.com"); got != notAfter.Unix() { |
| + t.Fatalf("hostCertExpiry = %d, want %d (wildcard fallback)", got, notAfter.Unix()) |
| + } |
| + // A per-host entry, when present, still wins. |
| + perHost := time.Now().Add(80 * 24 * time.Hour).Truncate(time.Second) |
| + writeWildcardPEM(t, filepath.Join(dir, host), host, perHost) |
| + if got := hostCertExpiry(dir, host, "oncarlos.com"); got != perHost.Unix() { |
| + t.Fatalf("hostCertExpiry with per-host file = %d, want %d", got, perHost.Unix()) |
| + } |
| + // Nothing at all → 0, unknown, never a guess. |
| + if got := hostCertExpiry(dir, "kass.zzz.oncarlos.com", "oncarlos.com"); got != 0 { |
| + t.Fatalf("hostCertExpiry with no files = %d, want 0", got) |
| + } |
| +} |