Add comprehensive ACME test suite (60 tests, 2100 lines)

Test coverage for the entire ACME server implementation:

- helpers_test.go: memBarrier, key generation, JWS/EAB signing, test fixtures
- nonce_test.go: issue/consume lifecycle, reuse rejection, concurrency
- jws_test.go: JWS parsing/verification (ES256, ES384, RS256), JWK parsing,
  RFC 7638 thumbprints, EAB HMAC verification, key authorization
- eab_test.go: EAB credential CRUD, account/order listing
- validate_test.go: HTTP-01 challenge validation with httptest servers,
  authorization/order state machine transitions
- handlers_test.go: full ACME protocol flow via chi router — directory,
  nonce, account creation with EAB, order creation, authorization retrieval,
  challenge triggering, finalize (order-not-ready), cert retrieval/revocation,
  CSR identifier validation

One production change: extract dnsResolver variable in validate.go for
DNS-01 test injection (no behavior change).

All 60 tests pass with -race. Full project vet and test clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 21:01:23 -07:00
parent 7f9e7f433f
commit 7749c035ae
8 changed files with 2101 additions and 3 deletions

176
internal/acme/eab_test.go Normal file
View File

@@ -0,0 +1,176 @@
package acme
import (
"context"
"encoding/json"
"testing"
"time"
)
func TestCreateEAB(t *testing.T) {
h := testHandler(t)
ctx := context.Background()
cred, err := h.CreateEAB(ctx, "alice")
if err != nil {
t.Fatalf("CreateEAB() error: %v", err)
}
if cred.KID == "" {
t.Fatalf("expected non-empty KID")
}
if len(cred.HMACKey) != 32 {
t.Fatalf("expected 32-byte HMAC key, got %d bytes", len(cred.HMACKey))
}
if cred.Used {
t.Fatalf("expected Used=false for new credential")
}
if cred.CreatedBy != "alice" {
t.Fatalf("expected CreatedBy=alice, got %s", cred.CreatedBy)
}
if cred.CreatedAt.IsZero() {
t.Fatalf("expected non-zero CreatedAt")
}
}
func TestGetEAB(t *testing.T) {
h := testHandler(t)
ctx := context.Background()
created, err := h.CreateEAB(ctx, "bob")
if err != nil {
t.Fatalf("CreateEAB() error: %v", err)
}
got, err := h.GetEAB(ctx, created.KID)
if err != nil {
t.Fatalf("GetEAB() error: %v", err)
}
if got.KID != created.KID {
t.Fatalf("KID mismatch: got %s, want %s", got.KID, created.KID)
}
if got.CreatedBy != "bob" {
t.Fatalf("CreatedBy mismatch: got %s, want bob", got.CreatedBy)
}
if len(got.HMACKey) != 32 {
t.Fatalf("expected 32-byte HMAC key, got %d bytes", len(got.HMACKey))
}
if got.Used != false {
t.Fatalf("expected Used=false, got true")
}
}
func TestGetEABNotFound(t *testing.T) {
h := testHandler(t)
ctx := context.Background()
_, err := h.GetEAB(ctx, "nonexistent-kid")
if err == nil {
t.Fatalf("expected error for non-existent KID, got nil")
}
}
func TestMarkEABUsed(t *testing.T) {
h := testHandler(t)
ctx := context.Background()
cred, err := h.CreateEAB(ctx, "carol")
if err != nil {
t.Fatalf("CreateEAB() error: %v", err)
}
if cred.Used {
t.Fatalf("expected Used=false before marking")
}
if err := h.MarkEABUsed(ctx, cred.KID); err != nil {
t.Fatalf("MarkEABUsed() error: %v", err)
}
got, err := h.GetEAB(ctx, cred.KID)
if err != nil {
t.Fatalf("GetEAB() after mark error: %v", err)
}
if !got.Used {
t.Fatalf("expected Used=true after marking, got false")
}
}
func TestListAccountsEmpty(t *testing.T) {
h := testHandler(t)
ctx := context.Background()
accounts, err := h.ListAccounts(ctx)
if err != nil {
t.Fatalf("ListAccounts() error: %v", err)
}
if len(accounts) != 0 {
t.Fatalf("expected 0 accounts, got %d", len(accounts))
}
}
func TestListAccounts(t *testing.T) {
h := testHandler(t)
ctx := context.Background()
// Store two accounts directly in the barrier.
for i, name := range []string{"user-a", "user-b"} {
acc := &Account{
ID: name,
Status: StatusValid,
Contact: []string{"mailto:" + name + "@example.com"},
JWK: []byte(`{"kty":"EC"}`),
CreatedAt: time.Now(),
MCIASUsername: name,
}
data, err := json.Marshal(acc)
if err != nil {
t.Fatalf("marshal account %d: %v", i, err)
}
path := h.barrierPrefix() + "accounts/" + name + ".json"
if err := h.barrier.Put(ctx, path, data); err != nil {
t.Fatalf("store account %d: %v", i, err)
}
}
accounts, err := h.ListAccounts(ctx)
if err != nil {
t.Fatalf("ListAccounts() error: %v", err)
}
if len(accounts) != 2 {
t.Fatalf("expected 2 accounts, got %d", len(accounts))
}
}
func TestListOrders(t *testing.T) {
h := testHandler(t)
ctx := context.Background()
// Store two orders directly in the barrier.
for i, id := range []string{"order-1", "order-2"} {
order := &Order{
ID: id,
AccountID: "test-account",
Status: StatusPending,
Identifiers: []Identifier{{Type: IdentifierDNS, Value: "example.com"}},
AuthzIDs: []string{"authz-1"},
ExpiresAt: time.Now().Add(24 * time.Hour),
CreatedAt: time.Now(),
IssuerName: "test-issuer",
}
data, err := json.Marshal(order)
if err != nil {
t.Fatalf("marshal order %d: %v", i, err)
}
path := h.barrierPrefix() + "orders/" + id + ".json"
if err := h.barrier.Put(ctx, path, data); err != nil {
t.Fatalf("store order %d: %v", i, err)
}
}
orders, err := h.ListOrders(ctx)
if err != nil {
t.Fatalf("ListOrders() error: %v", err)
}
if len(orders) != 2 {
t.Fatalf("expected 2 orders, got %d", len(orders))
}
}