mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-08 23:08:53 +00:00
36e722ba12
Uncommitted migration work at the time of branch cleanup. Tagged as checkpoint/m1-migration-wip so the commit survives git gc --prune=now. Session context: Phase 3 Part B+C of the M-1 sentinel error migration was in progress. 38 modified files, 4 new files (errors.go + errors_test.go in internal/service/ and internal/api/handler/). Resume from this commit via 'git checkout checkpoint/m1-migration-wip'.
277 lines
7.7 KiB
Go
277 lines
7.7 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
"github.com/shankar0123/certctl/internal/service"
|
|
)
|
|
|
|
// mockSCEPService implements SCEPService for testing.
|
|
type mockSCEPService struct {
|
|
CACaps string
|
|
CACertPEM string
|
|
CACertErr error
|
|
EnrollResult *domain.SCEPEnrollResult
|
|
EnrollErr error
|
|
}
|
|
|
|
func (m *mockSCEPService) GetCACaps(ctx context.Context) string {
|
|
if m.CACaps != "" {
|
|
return m.CACaps
|
|
}
|
|
return "POSTPKIOperation\nSHA-256\nAES\nSCEPStandard\n"
|
|
}
|
|
|
|
func (m *mockSCEPService) GetCACert(ctx context.Context) (string, error) {
|
|
return m.CACertPEM, m.CACertErr
|
|
}
|
|
|
|
func (m *mockSCEPService) PKCSReq(ctx context.Context, csrPEM string, challengePassword string, transactionID string) (*domain.SCEPEnrollResult, error) {
|
|
return m.EnrollResult, m.EnrollErr
|
|
}
|
|
|
|
func TestSCEP_GetCACaps_Success(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/scep?operation=GetCACaps", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "text/plain" {
|
|
t.Errorf("expected text/plain, got %s", ct)
|
|
}
|
|
body := w.Body.String()
|
|
if !strings.Contains(body, "POSTPKIOperation") {
|
|
t.Errorf("expected POSTPKIOperation in response, got: %s", body)
|
|
}
|
|
if !strings.Contains(body, "SHA-256") {
|
|
t.Errorf("expected SHA-256 in response, got: %s", body)
|
|
}
|
|
}
|
|
|
|
func TestSCEP_GetCACaps_MethodNotAllowed(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/scep?operation=GetCACaps", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("expected 405, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestSCEP_GetCACert_Success_SingleCert(t *testing.T) {
|
|
certPEM := generateTestCertPEM(t)
|
|
svc := &mockSCEPService{
|
|
CACertPEM: certPEM,
|
|
}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/scep?operation=GetCACert", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "application/x-x509-ca-cert" {
|
|
t.Errorf("expected application/x-x509-ca-cert, got %s", ct)
|
|
}
|
|
if w.Body.Len() == 0 {
|
|
t.Error("expected non-empty body")
|
|
}
|
|
}
|
|
|
|
func TestSCEP_GetCACert_MethodNotAllowed(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/scep?operation=GetCACert", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("expected 405, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestSCEP_GetCACert_ServiceError(t *testing.T) {
|
|
svc := &mockSCEPService{
|
|
CACertErr: errors.New("CA unavailable"),
|
|
}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/scep?operation=GetCACert", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("expected 500, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestSCEP_PKIOperation_MethodNotAllowed(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/scep?operation=PKIOperation", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusMethodNotAllowed {
|
|
t.Errorf("expected 405, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestSCEP_PKIOperation_EmptyBody(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(""))
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestSCEP_PKIOperation_InvalidBody(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader("not-valid-asn1-or-csr"))
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSCEP_PKIOperation_ServiceError(t *testing.T) {
|
|
svc := &mockSCEPService{
|
|
EnrollErr: errors.New("enrollment failed"),
|
|
}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
// Generate a valid raw CSR DER to send as body (fallback path)
|
|
csrPEM := generateTestCSRPEM(t)
|
|
block, _ := pem.Decode([]byte(csrPEM))
|
|
if block == nil {
|
|
t.Fatal("failed to decode CSR PEM")
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(string(block.Bytes)))
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusInternalServerError {
|
|
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSCEP_PKIOperation_Success_RawCSR(t *testing.T) {
|
|
certPEM := generateTestCertPEM(t)
|
|
svc := &mockSCEPService{
|
|
EnrollResult: &domain.SCEPEnrollResult{
|
|
CertPEM: certPEM,
|
|
ChainPEM: "",
|
|
},
|
|
}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
csrPEM := generateTestCSRPEM(t)
|
|
block, _ := pem.Decode([]byte(csrPEM))
|
|
if block == nil {
|
|
t.Fatal("failed to decode CSR PEM")
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(string(block.Bytes)))
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
ct := w.Header().Get("Content-Type")
|
|
if ct != "application/x-pki-message" {
|
|
t.Errorf("expected application/x-pki-message, got %s", ct)
|
|
}
|
|
}
|
|
|
|
// TestSCEP_PKIOperation_ChallengePasswordRejected pins the M-1 (P2) dispatch
|
|
// contract: when the service wraps the failure via `fmt.Errorf("%w: ...",
|
|
// service.ErrUnauthenticated)` the handler's errToStatus choke point must
|
|
// return 401 Unauthorized, NOT 403 Forbidden.
|
|
//
|
|
// This is a deliberate semantic correction. Pre-M-1 the handler inspected
|
|
// err.Error() for the "challenge password" substring and returned 403, which
|
|
// misclassified the RFC 7235 condition (the caller presented no valid
|
|
// application-layer credential — that is auth failure, not authorization
|
|
// denial). The errToStatus doc explicitly cites this code path as the
|
|
// canonical ErrUnauthenticated consumer; see handler/errors.go and the
|
|
// symmetric M-1 comment block at handler/scep.go in the pkiOperation arm.
|
|
func TestSCEP_PKIOperation_ChallengePasswordRejected(t *testing.T) {
|
|
svc := &mockSCEPService{
|
|
EnrollErr: fmt.Errorf("%w: invalid challenge password", service.ErrUnauthenticated),
|
|
}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
csrPEM := generateTestCSRPEM(t)
|
|
block, _ := pem.Decode([]byte(csrPEM))
|
|
if block == nil {
|
|
t.Fatal("failed to decode CSR PEM")
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(string(block.Bytes)))
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401 Unauthorized (M-1 dispatch of service.ErrUnauthenticated), got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSCEP_UnknownOperation(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/scep?operation=UnknownOp", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestSCEP_MissingOperation(t *testing.T) {
|
|
svc := &mockSCEPService{}
|
|
h := NewSCEPHandler(svc)
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/scep", nil)
|
|
w := httptest.NewRecorder()
|
|
h.HandleSCEP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d", w.Code)
|
|
}
|
|
}
|