231 lines
6.6 KiB
Go
231 lines
6.6 KiB
Go
package certificate
|
|
|
|
import (
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"helix-proxy/internal/config"
|
|
"helix-proxy/internal/store"
|
|
)
|
|
|
|
func TestGenerateSelfSignedTestCert(t *testing.T) {
|
|
domains := []string{"foo.example.com", "bar.example.com"}
|
|
certPEM, keyPEM, err := generateSelfSignedTestCert(domains)
|
|
if err != nil {
|
|
t.Fatalf("generate: %v", err)
|
|
}
|
|
if !strings.Contains(string(certPEM), "BEGIN CERTIFICATE") {
|
|
t.Error("no cert pem marker")
|
|
}
|
|
if !strings.Contains(string(keyPEM), "BEGIN RSA PRIVATE KEY") {
|
|
t.Error("no key pem marker")
|
|
}
|
|
// parse and check
|
|
block, _ := pem.Decode(certPEM)
|
|
if block == nil {
|
|
t.Fatal("decode failed")
|
|
}
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if cert.NotAfter.Sub(cert.NotBefore) < 89*24*time.Hour {
|
|
t.Error("validity too short")
|
|
}
|
|
found := false
|
|
for _, d := range cert.DNSNames {
|
|
if d == "foo.example.com" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("dns names not in cert")
|
|
}
|
|
}
|
|
|
|
func TestIssueTestCertPath(t *testing.T) {
|
|
// use temp data dir, reset config
|
|
config.ResetForTest()
|
|
tmp := t.TempDir()
|
|
t.Setenv("DATA_DIR", tmp)
|
|
// ensure dirs so writes don't fail
|
|
_ = config.EnsureDataDirs()
|
|
|
|
// minimal store: use New which is bbolt in the tmp (via reset+env)
|
|
st, err := store.New()
|
|
if err != nil {
|
|
t.Fatalf("new store: %v", err)
|
|
}
|
|
if c, ok := st.(interface{ Close() error }); ok {
|
|
defer c.Close()
|
|
}
|
|
|
|
cm := NewManager(st)
|
|
|
|
// create a cert record (simulating what API would do)
|
|
cert := store.Certificate{
|
|
Provider: "letsencrypt",
|
|
NiceName: "test local",
|
|
DomainNames: []string{"mytest.example.com"}, // any domain; in dev mode all LE become test certs
|
|
Meta: map[string]any{},
|
|
}
|
|
created, err := st.CreateCertificate(cert)
|
|
if err != nil {
|
|
t.Fatalf("create cert: %v", err)
|
|
}
|
|
|
|
// dev mode => all letsencrypt certs are test/self-signed
|
|
t.Setenv("PROXY_MODE", "development")
|
|
email := cm.GetEmailForCert(created)
|
|
if err := cm.Issue(created, email); err != nil {
|
|
t.Fatalf("Issue test cert: %v", err)
|
|
}
|
|
|
|
// reload from store
|
|
got, ok := st.GetCertificate(created.ID)
|
|
if !ok {
|
|
t.Fatal("cert not found after issue")
|
|
}
|
|
if got.ExpiresOn == "" {
|
|
t.Error("no expires set")
|
|
}
|
|
if v, _ := got.Meta["test_cert"].(bool); !v {
|
|
t.Error("test_cert marker not set")
|
|
}
|
|
if _, ok := got.Meta["certificate"]; !ok {
|
|
t.Error("certificate pem not in meta")
|
|
}
|
|
if _, ok := got.Meta["key"]; !ok {
|
|
t.Error("key not in meta")
|
|
}
|
|
|
|
// file written?
|
|
if _, err := os.Stat(config.Resolve("certs")); err != nil {
|
|
t.Error("certs dir missing")
|
|
}
|
|
// the per-id dir
|
|
perID := fmt.Sprintf("%s/%d", config.Resolve("certs"), got.ID)
|
|
if _, err := os.Stat(perID); err != nil {
|
|
t.Logf("per-id cert dir %s not present (may be ok): %v", perID, err)
|
|
}
|
|
|
|
// exercise non-local LE path meta parsing (staging/key_type from meta) in *production* mode
|
|
// (real LE path; will fail on obtain due to no DNS/reach, but pre-LE branches + meta covered)
|
|
t.Setenv("PROXY_MODE", "")
|
|
c2 := store.Certificate{
|
|
Provider: "letsencrypt",
|
|
DomainNames: []string{"nonlocal.example.com"},
|
|
Meta: map[string]any{"staging": "true", "key_type": "rsa4096"},
|
|
}
|
|
created2, _ := st.CreateCertificate(c2)
|
|
email2 := cm.GetEmailForCert(created2)
|
|
if err2 := cm.Issue(created2, email2); err2 == nil || strings.Contains(err2.Error(), "not a letsencrypt") {
|
|
t.Logf("non-local LE Issue err (expected without public reach/DNS; meta parse covered): %v", err2)
|
|
}
|
|
}
|
|
|
|
func TestIssueRejectsNonLE(t *testing.T) {
|
|
config.ResetForTest()
|
|
tmp := t.TempDir()
|
|
t.Setenv("DATA_DIR", tmp)
|
|
_ = config.EnsureDataDirs()
|
|
st, err := store.New()
|
|
if err != nil {
|
|
t.Fatalf("store: %v", err)
|
|
}
|
|
if c, ok := st.(interface{ Close() error }); ok {
|
|
defer c.Close()
|
|
}
|
|
cm := NewManager(st)
|
|
c := store.Certificate{Provider: "other", DomainNames: []string{"ex.com"}}
|
|
created, _ := st.CreateCertificate(c)
|
|
err = cm.Issue(created, "e@e.com")
|
|
if err == nil || !strings.Contains(err.Error(), "not a letsencrypt") {
|
|
t.Errorf("expected reject non-letsencrypt: got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestShouldRenew(t *testing.T) {
|
|
config.ResetForTest()
|
|
tmp := t.TempDir()
|
|
t.Setenv("DATA_DIR", tmp)
|
|
_ = config.EnsureDataDirs()
|
|
st, err := store.New()
|
|
if err != nil {
|
|
t.Fatalf("store: %v", err)
|
|
}
|
|
if c, ok := st.(interface{ Close() error }); ok {
|
|
defer c.Close()
|
|
}
|
|
cm := NewManager(st)
|
|
now := time.Now().UTC()
|
|
tests := []struct {
|
|
name string
|
|
exp string
|
|
want bool
|
|
}{
|
|
{"empty", "", true},
|
|
{"past", now.Add(-40 * 24 * time.Hour).Format("2006-01-02 15:04:05"), true},
|
|
{"near 29d", now.Add(29 * 24 * time.Hour).Format("2006-01-02 15:04:05"), true},
|
|
{"far 31d", now.Add(31 * 24 * time.Hour).Format("2006-01-02 15:04:05"), false},
|
|
{"rfc3339 near", now.Add(10 * 24 * time.Hour).Format(time.RFC3339), true},
|
|
{"bad format", "not-a-date", true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := store.Certificate{Provider: "letsencrypt", ExpiresOn: tt.exp}
|
|
if got := cm.ShouldRenew(c); got != tt.want {
|
|
t.Errorf("ShouldRenew(%s) = %v want %v", tt.exp, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProcessRenewals(t *testing.T) {
|
|
config.ResetForTest()
|
|
tmp := t.TempDir()
|
|
t.Setenv("DATA_DIR", tmp)
|
|
_ = config.EnsureDataDirs()
|
|
st, err := store.New()
|
|
if err != nil {
|
|
t.Fatalf("store: %v", err)
|
|
}
|
|
if c, ok := st.(interface{ Close() error }); ok {
|
|
defer c.Close()
|
|
}
|
|
cm := NewManager(st)
|
|
|
|
// dev mode: *all* letsencrypt certs (any domain) use test/self-signed path in Issue/renew
|
|
t.Setenv("PROXY_MODE", "development")
|
|
|
|
// LE due soon
|
|
dueExp := time.Now().Add(5 * 24 * time.Hour).Format("2006-01-02 15:04:05")
|
|
leDue := store.Certificate{Provider: "letsencrypt", DomainNames: []string{"due.example.com"}, ExpiresOn: dueExp}
|
|
createdDue, _ := st.CreateCertificate(leDue)
|
|
|
|
// LE far future (no renew)
|
|
farExp := time.Now().Add(40 * 24 * time.Hour).Format("2006-01-02 15:04:05")
|
|
leFar := store.Certificate{Provider: "letsencrypt", DomainNames: []string{"far.example.com"}, ExpiresOn: farExp}
|
|
createdFar, _ := st.CreateCertificate(leFar)
|
|
|
|
// non-LE (ignored even if "due")
|
|
nonLE := store.Certificate{Provider: "other", DomainNames: []string{"x.com"}, ExpiresOn: time.Now().Add(-1 * time.Hour).Format("2006-01-02 15:04:05")}
|
|
st.CreateCertificate(nonLE)
|
|
|
|
cm.ProcessRenewals()
|
|
|
|
gDue, _ := st.GetCertificate(createdDue.ID)
|
|
if tDue, _ := time.Parse("2006-01-02 15:04:05", gDue.ExpiresOn); time.Until(tDue) < 80*24*time.Hour {
|
|
t.Error("due LE cert was not renewed (expires not extended)")
|
|
}
|
|
gFar, _ := st.GetCertificate(createdFar.ID)
|
|
if gFar.ExpiresOn != farExp {
|
|
t.Error("far LE should not have been touched")
|
|
}
|
|
}
|