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>
177 lines
4.1 KiB
Go
177 lines
4.1 KiB
Go
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))
|
|
}
|
|
}
|