mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 12:08:54 +00:00
fix: return 409 on duplicate issuer name, improve error handling and onboarding defaults
Closes #7. The issuer create/update handlers swallowed all service errors as generic 500s. Now differentiates: 409 for UNIQUE constraint violations, 400 for unsupported issuer type, 404 for not-found on update, 500 for unknown errors. Adds structured error logging via slog. OnboardingWizard now pre-populates config field defaults when a type is selected (matching IssuersPage behavior), preventing empty required fields from causing silent failures. install-agent.sh hardened for curl|bash usage: --agent-id flag, =value syntax, /dev/tty stdin reopening, proper stderr routing in download_binary, non-interactive install examples in help text, and updated wizard commands. Adds adversarial security tests for EST, path traversal, and query injection handlers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,8 +3,10 @@ package handler
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -324,6 +326,122 @@ func TestCreateIssuer_NameTooLong(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateIssuer_DuplicateName(t *testing.T) {
|
||||
mock := &MockIssuerService{
|
||||
CreateIssuerFn: func(issuer domain.Issuer) (*domain.Issuer, error) {
|
||||
return nil, fmt.Errorf("failed to create issuer: duplicate key value violates unique constraint \"issuers_name_key\"")
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "ACME Issuer",
|
||||
"type": "ACME",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewIssuerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/issuers", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateIssuer(w, req)
|
||||
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Fatalf("expected status 409, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp ErrorResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Message, "already exists") {
|
||||
t.Errorf("expected message to contain 'already exists', got %q", resp.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateIssuer_UnsupportedType(t *testing.T) {
|
||||
mock := &MockIssuerService{
|
||||
CreateIssuerFn: func(issuer domain.Issuer) (*domain.Issuer, error) {
|
||||
return nil, fmt.Errorf("unsupported issuer type: FakeCA")
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "Fake Issuer",
|
||||
"type": "FakeCA",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewIssuerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/issuers", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateIssuer(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp ErrorResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if !strings.Contains(resp.Message, "unsupported issuer type") {
|
||||
t.Errorf("expected message to contain 'unsupported issuer type', got %q", resp.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateIssuer_GenericServiceError(t *testing.T) {
|
||||
mock := &MockIssuerService{
|
||||
CreateIssuerFn: func(issuer domain.Issuer) (*domain.Issuer, error) {
|
||||
return nil, fmt.Errorf("failed to encrypt config: cipher error")
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "Some Issuer",
|
||||
"type": "ACME",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewIssuerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/issuers", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateIssuer(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateIssuer_DuplicateName(t *testing.T) {
|
||||
mock := &MockIssuerService{
|
||||
UpdateIssuerFn: func(id string, issuer domain.Issuer) (*domain.Issuer, error) {
|
||||
return nil, fmt.Errorf("failed to update issuer: duplicate key value violates unique constraint")
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "Existing Name",
|
||||
"type": "ACME",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewIssuerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/issuers/iss-test", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateIssuer(w, req)
|
||||
|
||||
if w.Code != http.StatusConflict {
|
||||
t.Fatalf("expected status 409, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteIssuer_Success(t *testing.T) {
|
||||
var deletedID string
|
||||
mock := &MockIssuerService{
|
||||
|
||||
Reference in New Issue
Block a user