147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
package acmecert
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/rand"
|
||
|
|
"crypto/rsa"
|
||
|
|
"crypto/x509"
|
||
|
|
"crypto/x509/pkix"
|
||
|
|
"encoding/pem"
|
||
|
|
"math/big"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"github.com/go-acme/lego/v4/registration"
|
||
|
|
"gopkg.in/ini.v1"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestLoadOrCreateAccountGeneratesAndPersistsKey(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
|
||
|
|
user1, err := loadOrCreateAccount(dir, "admin@example.com")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if user1.Registration != nil {
|
||
|
|
t.Fatal("expected no registration on a brand-new account")
|
||
|
|
}
|
||
|
|
if user1.GetPrivateKey() == nil {
|
||
|
|
t.Fatal("expected a generated private key")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reload: must reuse the same key, not generate a new one.
|
||
|
|
user2, err := loadOrCreateAccount(dir, "admin@example.com")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
keyBytes1, _ := os.ReadFile(accountKeyPath(dir))
|
||
|
|
if len(keyBytes1) == 0 {
|
||
|
|
t.Fatal("expected a persisted key file")
|
||
|
|
}
|
||
|
|
// Re-reading shouldn't rewrite the file with different bytes.
|
||
|
|
keyBytes2, _ := os.ReadFile(accountKeyPath(dir))
|
||
|
|
if string(keyBytes1) != string(keyBytes2) {
|
||
|
|
t.Fatal("expected the same key to be reused across loads")
|
||
|
|
}
|
||
|
|
_ = user2
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSaveRegistrationRoundTrip(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
reg := ®istration.Resource{URI: "https://example.com/acme/acct/123"}
|
||
|
|
if err := saveRegistration(dir, reg); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
|
||
|
|
user, err := loadOrCreateAccount(dir, "admin@example.com")
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if user.Registration == nil || user.Registration.URI != reg.URI {
|
||
|
|
t.Fatalf("expected registration to round-trip, got %+v", user.Registration)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// writeFixtureCert writes a minimal self-signed cert with the given expiry to certFile
|
||
|
|
// (no matching key needed — NeedsRenewal only reads the cert).
|
||
|
|
func writeFixtureCert(t *testing.T, certFile string, notAfter time.Time) {
|
||
|
|
t.Helper()
|
||
|
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
tmpl := x509.Certificate{
|
||
|
|
SerialNumber: big.NewInt(1),
|
||
|
|
Subject: pkix.Name{CommonName: "test"},
|
||
|
|
NotBefore: time.Now().Add(-time.Hour),
|
||
|
|
NotAfter: notAfter,
|
||
|
|
}
|
||
|
|
der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||
|
|
if err := os.WriteFile(certFile, pemBytes, 0o644); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNeedsRenewal(t *testing.T) {
|
||
|
|
dir := t.TempDir()
|
||
|
|
mgr := &Manager{Cfg: ini.Empty(), CertFile: filepath.Join(dir, "server.crt")}
|
||
|
|
|
||
|
|
writeFixtureCert(t, mgr.CertFile, time.Now().Add(200*24*time.Hour))
|
||
|
|
if needs, err := mgr.NeedsRenewal(); err != nil || needs {
|
||
|
|
t.Fatalf("expected NeedsRenewal=false for a cert expiring in 200 days, got %v (err=%v)", needs, err)
|
||
|
|
}
|
||
|
|
|
||
|
|
writeFixtureCert(t, mgr.CertFile, time.Now().Add(5*24*time.Hour))
|
||
|
|
if needs, err := mgr.NeedsRenewal(); err != nil || !needs {
|
||
|
|
t.Fatalf("expected NeedsRenewal=true for a cert expiring in 5 days, got %v (err=%v)", needs, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNeedsRenewalMissingCertIsTrue(t *testing.T) {
|
||
|
|
mgr := &Manager{Cfg: ini.Empty(), CertFile: filepath.Join(t.TempDir(), "does-not-exist.crt")}
|
||
|
|
needs, err := mgr.NeedsRenewal()
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if !needs {
|
||
|
|
t.Fatal("expected NeedsRenewal=true when no certificate exists yet")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestBuildDNSProviderUnknownName(t *testing.T) {
|
||
|
|
cfg := ini.Empty()
|
||
|
|
cfg.Section("LetsEncrypt").Key("dns_provider").SetValue("not-a-real-provider")
|
||
|
|
if _, err := buildDNSProvider(cfg); err == nil {
|
||
|
|
t.Fatal("expected an error for an unknown DNS provider name")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestBuildDNSProviderDigitalOceanRequiresToken(t *testing.T) {
|
||
|
|
cfg := ini.Empty()
|
||
|
|
cfg.Section("LetsEncrypt").Key("dns_provider").SetValue("digitalocean")
|
||
|
|
// AuthToken deliberately left blank — DigitalOcean's constructor validates this
|
||
|
|
// locally (no network call) and errors immediately.
|
||
|
|
if _, err := buildDNSProvider(cfg); err == nil {
|
||
|
|
t.Fatal("expected an error when digitalocean_api_token is blank")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestBuildDNSProviderCloudflare(t *testing.T) {
|
||
|
|
cfg := ini.Empty()
|
||
|
|
sec := cfg.Section("LetsEncrypt")
|
||
|
|
sec.Key("dns_provider").SetValue("cloudflare")
|
||
|
|
sec.Key("cloudflare_api_token").SetValue("fake-token-for-local-construction-only")
|
||
|
|
provider, err := buildDNSProvider(cfg)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("expected local provider construction to succeed without a network call, got: %v", err)
|
||
|
|
}
|
||
|
|
if provider == nil {
|
||
|
|
t.Fatal("expected a non-nil provider")
|
||
|
|
}
|
||
|
|
}
|