initial commit
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user