mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-09 18:48:52 +00:00
test: comprehensive test expansion — 330+ to 525+ tests, close M11b coverage gaps
Add 195+ new tests across service, handler, connector, and integration layers: - Service tests: team (23), owner (21), agent_group (25), issuer (18), issuer_adapter (6) - Handler tests: teams (26), owners (21) - NGINX target connector tests (13): config validation, deployment, reload - Integration tests: 19 M11b endpoint subtests (teams, owners, agent groups CRUD) - CI pipeline: add ./internal/connector/target/... to test coverage path - Docs: update test counts to 525+ across README, architecture, CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,558 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/api/middleware"
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// MockOwnerService is a mock implementation of OwnerService interface.
|
||||
type MockOwnerService struct {
|
||||
ListOwnersFn func(page, perPage int) ([]domain.Owner, int64, error)
|
||||
GetOwnerFn func(id string) (*domain.Owner, error)
|
||||
CreateOwnerFn func(owner domain.Owner) (*domain.Owner, error)
|
||||
UpdateOwnerFn func(id string, owner domain.Owner) (*domain.Owner, error)
|
||||
DeleteOwnerFn func(id string) error
|
||||
}
|
||||
|
||||
func (m *MockOwnerService) ListOwners(page, perPage int) ([]domain.Owner, int64, error) {
|
||||
if m.ListOwnersFn != nil {
|
||||
return m.ListOwnersFn(page, perPage)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *MockOwnerService) GetOwner(id string) (*domain.Owner, error) {
|
||||
if m.GetOwnerFn != nil {
|
||||
return m.GetOwnerFn(id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockOwnerService) CreateOwner(owner domain.Owner) (*domain.Owner, error) {
|
||||
if m.CreateOwnerFn != nil {
|
||||
return m.CreateOwnerFn(owner)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockOwnerService) UpdateOwner(id string, owner domain.Owner) (*domain.Owner, error) {
|
||||
if m.UpdateOwnerFn != nil {
|
||||
return m.UpdateOwnerFn(id, owner)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockOwnerService) DeleteOwner(id string) error {
|
||||
if m.DeleteOwnerFn != nil {
|
||||
return m.DeleteOwnerFn(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestListOwners_Success lists owners with pagination, verify data fields.
|
||||
func TestListOwners_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
o1 := domain.Owner{
|
||||
ID: "o-alice",
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "t-platform",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
o2 := domain.Owner{
|
||||
ID: "o-bob",
|
||||
Name: "Bob",
|
||||
Email: "bob@example.com",
|
||||
TeamID: "t-ops",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
mock := &MockOwnerService{
|
||||
ListOwnersFn: func(page, perPage int) ([]domain.Owner, int64, error) {
|
||||
return []domain.Owner{o1, o2}, 2, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListOwners(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp PagedResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if resp.Total != 2 {
|
||||
t.Errorf("expected total 2, got %d", resp.Total)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListOwners_Pagination verifies pagination parameters are passed to service.
|
||||
func TestListOwners_Pagination(t *testing.T) {
|
||||
var capturedPage, capturedPerPage int
|
||||
mock := &MockOwnerService{
|
||||
ListOwnersFn: func(page, perPage int) ([]domain.Owner, int64, error) {
|
||||
capturedPage = page
|
||||
capturedPerPage = perPage
|
||||
return []domain.Owner{}, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners?page=3&per_page=25", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListOwners(w, req)
|
||||
|
||||
if capturedPage != 3 {
|
||||
t.Errorf("expected page 3, got %d", capturedPage)
|
||||
}
|
||||
if capturedPerPage != 25 {
|
||||
t.Errorf("expected per_page 25, got %d", capturedPerPage)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListOwners_ServiceError returns 500 on service error.
|
||||
func TestListOwners_ServiceError(t *testing.T) {
|
||||
mock := &MockOwnerService{
|
||||
ListOwnersFn: func(page, perPage int) ([]domain.Owner, int64, error) {
|
||||
return nil, 0, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListOwners(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListOwners_MethodNotAllowed returns 405 for non-GET methods.
|
||||
func TestListOwners_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/owners", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListOwners(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetOwner_Success returns owner with email and team_id.
|
||||
func TestGetOwner_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
mock := &MockOwnerService{
|
||||
GetOwnerFn: func(id string) (*domain.Owner, error) {
|
||||
return &domain.Owner{
|
||||
ID: id,
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "t-platform",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners/o-alice", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var owner domain.Owner
|
||||
if err := json.NewDecoder(w.Body).Decode(&owner); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if owner.Email != "alice@example.com" {
|
||||
t.Errorf("expected email 'alice@example.com', got '%s'", owner.Email)
|
||||
}
|
||||
if owner.TeamID != "t-platform" {
|
||||
t.Errorf("expected team_id 't-platform', got '%s'", owner.TeamID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetOwner_NotFound returns 404 when owner not found.
|
||||
func TestGetOwner_NotFound(t *testing.T) {
|
||||
mock := &MockOwnerService{
|
||||
GetOwnerFn: func(id string) (*domain.Owner, error) {
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners/nonexistent", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected status 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetOwner_EmptyID returns 400 for empty ID.
|
||||
func TestGetOwner_EmptyID(t *testing.T) {
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetOwner_MethodNotAllowed returns 405 for non-GET methods.
|
||||
func TestGetOwner_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/owners/o-alice", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateOwner_Success returns 201 with email and team_id.
|
||||
func TestCreateOwner_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
mock := &MockOwnerService{
|
||||
CreateOwnerFn: func(owner domain.Owner) (*domain.Owner, error) {
|
||||
owner.ID = "o-new"
|
||||
owner.CreatedAt = now
|
||||
owner.UpdatedAt = now
|
||||
return &owner, nil
|
||||
},
|
||||
}
|
||||
|
||||
body := domain.Owner{
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "t-platform",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/owners", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected status 201, got %d", w.Code)
|
||||
}
|
||||
|
||||
var owner domain.Owner
|
||||
if err := json.NewDecoder(w.Body).Decode(&owner); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if owner.Email != "alice@example.com" {
|
||||
t.Errorf("expected email 'alice@example.com', got '%s'", owner.Email)
|
||||
}
|
||||
if owner.TeamID != "t-platform" {
|
||||
t.Errorf("expected team_id 't-platform', got '%s'", owner.TeamID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateOwner_InvalidJSON returns 400 for malformed JSON.
|
||||
func TestCreateOwner_InvalidJSON(t *testing.T) {
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/owners", bytes.NewReader([]byte("not json")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateOwner_MissingName returns 400 when name is required.
|
||||
func TestCreateOwner_MissingName(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"email": "alice@example.com",
|
||||
"team_id": "t-platform",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/owners", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateOwner_NameTooLong returns 400 for name exceeding 255 chars.
|
||||
func TestCreateOwner_NameTooLong(t *testing.T) {
|
||||
longName := ""
|
||||
for i := 0; i < 256; i++ {
|
||||
longName += "x"
|
||||
}
|
||||
body := domain.Owner{
|
||||
Name: longName,
|
||||
Email: "alice@example.com",
|
||||
TeamID: "t-platform",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/owners", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateOwner_ServiceError returns 500 on service error.
|
||||
func TestCreateOwner_ServiceError(t *testing.T) {
|
||||
mock := &MockOwnerService{
|
||||
CreateOwnerFn: func(owner domain.Owner) (*domain.Owner, error) {
|
||||
return nil, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
body := domain.Owner{
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "t-platform",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/owners", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateOwner_MethodNotAllowed returns 405 for non-POST methods.
|
||||
func TestCreateOwner_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateOwner_Success returns 200 with updated data.
|
||||
func TestUpdateOwner_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
mock := &MockOwnerService{
|
||||
UpdateOwnerFn: func(id string, owner domain.Owner) (*domain.Owner, error) {
|
||||
return &domain.Owner{
|
||||
ID: id,
|
||||
Name: owner.Name,
|
||||
Email: owner.Email,
|
||||
TeamID: owner.TeamID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
body := domain.Owner{
|
||||
Name: "Alice Updated",
|
||||
Email: "alice.updated@example.com",
|
||||
TeamID: "t-platform",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/owners/o-alice", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var owner domain.Owner
|
||||
if err := json.NewDecoder(w.Body).Decode(&owner); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if owner.Name != "Alice Updated" {
|
||||
t.Errorf("expected name 'Alice Updated', got '%s'", owner.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateOwner_ServiceError returns 500 on service error.
|
||||
func TestUpdateOwner_ServiceError(t *testing.T) {
|
||||
mock := &MockOwnerService{
|
||||
UpdateOwnerFn: func(id string, owner domain.Owner) (*domain.Owner, error) {
|
||||
return nil, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
body := domain.Owner{
|
||||
Name: "Alice Updated",
|
||||
Email: "alice.updated@example.com",
|
||||
TeamID: "t-platform",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/owners/o-alice", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateOwner_EmptyID returns 400 for empty ID.
|
||||
func TestUpdateOwner_EmptyID(t *testing.T) {
|
||||
body := domain.Owner{
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "t-platform",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/owners/", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteOwner_Success returns 204 No Content.
|
||||
func TestDeleteOwner_Success(t *testing.T) {
|
||||
var deletedID string
|
||||
mock := &MockOwnerService{
|
||||
DeleteOwnerFn: func(id string) error {
|
||||
deletedID = id
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/owners/o-alice", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected status 204, got %d", w.Code)
|
||||
}
|
||||
if deletedID != "o-alice" {
|
||||
t.Errorf("expected deleted ID 'o-alice', got '%s'", deletedID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteOwner_ServiceError returns 500 on service error.
|
||||
func TestDeleteOwner_ServiceError(t *testing.T) {
|
||||
mock := &MockOwnerService{
|
||||
DeleteOwnerFn: func(id string) error {
|
||||
return ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewOwnerHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/owners/o-alice", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteOwner_EmptyID returns 400 for empty ID.
|
||||
func TestDeleteOwner_EmptyID(t *testing.T) {
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/owners/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteOwner_MethodNotAllowed returns 405 for non-DELETE methods.
|
||||
func TestDeleteOwner_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewOwnerHandler(&MockOwnerService{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/owners/o-alice", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteOwner(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// contextWithRequestID returns a context with a test request ID for use in tests.
|
||||
func contextWithRequestID() context.Context {
|
||||
return context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id-123")
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/api/middleware"
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// MockTeamService is a mock implementation of TeamService interface.
|
||||
type MockTeamService struct {
|
||||
ListTeamsFn func(page, perPage int) ([]domain.Team, int64, error)
|
||||
GetTeamFn func(id string) (*domain.Team, error)
|
||||
CreateTeamFn func(team domain.Team) (*domain.Team, error)
|
||||
UpdateTeamFn func(id string, team domain.Team) (*domain.Team, error)
|
||||
DeleteTeamFn func(id string) error
|
||||
}
|
||||
|
||||
func (m *MockTeamService) ListTeams(page, perPage int) ([]domain.Team, int64, error) {
|
||||
if m.ListTeamsFn != nil {
|
||||
return m.ListTeamsFn(page, perPage)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *MockTeamService) GetTeam(id string) (*domain.Team, error) {
|
||||
if m.GetTeamFn != nil {
|
||||
return m.GetTeamFn(id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTeamService) CreateTeam(team domain.Team) (*domain.Team, error) {
|
||||
if m.CreateTeamFn != nil {
|
||||
return m.CreateTeamFn(team)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTeamService) UpdateTeam(id string, team domain.Team) (*domain.Team, error) {
|
||||
if m.UpdateTeamFn != nil {
|
||||
return m.UpdateTeamFn(id, team)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockTeamService) DeleteTeam(id string) error {
|
||||
if m.DeleteTeamFn != nil {
|
||||
return m.DeleteTeamFn(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestListTeams_Success tests listing teams with default pagination.
|
||||
func TestListTeams_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
t1 := domain.Team{
|
||||
ID: "t-platform",
|
||||
Name: "Platform Team",
|
||||
Description: "Infrastructure team",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
t2 := domain.Team{
|
||||
ID: "t-security",
|
||||
Name: "Security Team",
|
||||
Description: "Security operations",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
mock := &MockTeamService{
|
||||
ListTeamsFn: func(page, perPage int) ([]domain.Team, int64, error) {
|
||||
return []domain.Team{t1, t2}, 2, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListTeams(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var resp PagedResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if resp.Total != 2 {
|
||||
t.Errorf("expected total 2, got %d", resp.Total)
|
||||
}
|
||||
if resp.Page != 1 {
|
||||
t.Errorf("expected page 1, got %d", resp.Page)
|
||||
}
|
||||
if resp.PerPage != 50 {
|
||||
t.Errorf("expected per_page 50, got %d", resp.PerPage)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListTeams_WithQueryParams tests listing with custom pagination parameters.
|
||||
func TestListTeams_WithQueryParams(t *testing.T) {
|
||||
var capturedPage, capturedPerPage int
|
||||
mock := &MockTeamService{
|
||||
ListTeamsFn: func(page, perPage int) ([]domain.Team, int64, error) {
|
||||
capturedPage = page
|
||||
capturedPerPage = perPage
|
||||
return []domain.Team{}, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams?page=3&per_page=25", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListTeams(w, req)
|
||||
|
||||
if capturedPage != 3 {
|
||||
t.Errorf("expected page 3, got %d", capturedPage)
|
||||
}
|
||||
if capturedPerPage != 25 {
|
||||
t.Errorf("expected per_page 25, got %d", capturedPerPage)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListTeams_PerPageMaxLimit tests that per_page is capped at 500.
|
||||
func TestListTeams_PerPageMaxLimit(t *testing.T) {
|
||||
var capturedPerPage int
|
||||
mock := &MockTeamService{
|
||||
ListTeamsFn: func(page, perPage int) ([]domain.Team, int64, error) {
|
||||
capturedPerPage = perPage
|
||||
return []domain.Team{}, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams?per_page=1000", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListTeams(w, req)
|
||||
|
||||
if capturedPerPage != 500 {
|
||||
t.Errorf("expected per_page capped at 500, got %d", capturedPerPage)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListTeams_ServiceError tests error handling when service fails.
|
||||
func TestListTeams_ServiceError(t *testing.T) {
|
||||
mock := &MockTeamService{
|
||||
ListTeamsFn: func(page, perPage int) ([]domain.Team, int64, error) {
|
||||
return nil, 0, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListTeams(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListTeams_MethodNotAllowed tests that non-GET requests are rejected.
|
||||
func TestListTeams_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListTeams(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTeam_Success tests retrieving a team by ID.
|
||||
func TestGetTeam_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
mock := &MockTeamService{
|
||||
GetTeamFn: func(id string) (*domain.Team, error) {
|
||||
return &domain.Team{
|
||||
ID: id,
|
||||
Name: "Platform Team",
|
||||
Description: "Infrastructure team",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams/t-platform", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var team domain.Team
|
||||
if err := json.NewDecoder(w.Body).Decode(&team); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if team.ID != "t-platform" {
|
||||
t.Errorf("expected ID t-platform, got %s", team.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTeam_NotFound tests 404 response when team does not exist.
|
||||
func TestGetTeam_NotFound(t *testing.T) {
|
||||
mock := &MockTeamService{
|
||||
GetTeamFn: func(id string) (*domain.Team, error) {
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams/nonexistent", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("expected status 404, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTeam_EmptyID tests 400 response when team ID is empty.
|
||||
func TestGetTeam_EmptyID(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTeam_MethodNotAllowed tests that non-GET requests are rejected.
|
||||
func TestGetTeam_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/teams/t-platform", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTeam_Success tests successful team creation.
|
||||
func TestCreateTeam_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
mock := &MockTeamService{
|
||||
CreateTeamFn: func(team domain.Team) (*domain.Team, error) {
|
||||
team.ID = "t-new"
|
||||
team.CreatedAt = now
|
||||
team.UpdatedAt = now
|
||||
return &team, nil
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "New Team",
|
||||
"description": "A new team",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("expected status 201, got %d", w.Code)
|
||||
}
|
||||
|
||||
var team domain.Team
|
||||
if err := json.NewDecoder(w.Body).Decode(&team); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if team.ID != "t-new" {
|
||||
t.Errorf("expected ID t-new, got %s", team.ID)
|
||||
}
|
||||
if team.Name != "New Team" {
|
||||
t.Errorf("expected name 'New Team', got %s", team.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTeam_InvalidJSON tests 400 response for malformed JSON.
|
||||
func TestCreateTeam_InvalidJSON(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", bytes.NewReader([]byte("not json")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTeam_MissingName tests 400 response when name is required but missing.
|
||||
func TestCreateTeam_MissingName(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"description": "Team without name",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTeam_NameTooLong tests 400 response when name exceeds max length.
|
||||
func TestCreateTeam_NameTooLong(t *testing.T) {
|
||||
longName := ""
|
||||
for i := 0; i < 256; i++ {
|
||||
longName += "x"
|
||||
}
|
||||
body := map[string]interface{}{
|
||||
"name": longName,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTeam_ServiceError tests error handling when service fails.
|
||||
func TestCreateTeam_ServiceError(t *testing.T) {
|
||||
mock := &MockTeamService{
|
||||
CreateTeamFn: func(team domain.Team) (*domain.Team, error) {
|
||||
return nil, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "New Team",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTeam_MethodNotAllowed tests that non-POST requests are rejected.
|
||||
func TestCreateTeam_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateTeam_Success tests successful team update.
|
||||
func TestUpdateTeam_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
mock := &MockTeamService{
|
||||
UpdateTeamFn: func(id string, team domain.Team) (*domain.Team, error) {
|
||||
return &domain.Team{
|
||||
ID: id,
|
||||
Name: team.Name,
|
||||
Description: team.Description,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "Updated Team",
|
||||
"description": "Updated description",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/t-platform", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var team domain.Team
|
||||
if err := json.NewDecoder(w.Body).Decode(&team); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
if team.Name != "Updated Team" {
|
||||
t.Errorf("expected name 'Updated Team', got %s", team.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateTeam_InvalidJSON tests 400 response for malformed JSON.
|
||||
func TestUpdateTeam_InvalidJSON(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/t-platform", bytes.NewReader([]byte("bad json")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateTeam_EmptyID tests 400 response when team ID is empty.
|
||||
func TestUpdateTeam_EmptyID(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"name": "Updated Team",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateTeam_ServiceError tests error handling when service fails.
|
||||
func TestUpdateTeam_ServiceError(t *testing.T) {
|
||||
mock := &MockTeamService{
|
||||
UpdateTeamFn: func(id string, team domain.Team) (*domain.Team, error) {
|
||||
return nil, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
body := map[string]interface{}{
|
||||
"name": "Updated Team",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/teams/t-platform", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateTeam_MethodNotAllowed tests that non-PUT requests are rejected.
|
||||
func TestUpdateTeam_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams/t-platform", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteTeam_Success tests successful team deletion.
|
||||
func TestDeleteTeam_Success(t *testing.T) {
|
||||
mock := &MockTeamService{
|
||||
DeleteTeamFn: func(id string) error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/teams/t-platform", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Fatalf("expected status 204, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteTeam_EmptyID tests 400 response when team ID is empty.
|
||||
func TestDeleteTeam_EmptyID(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/teams/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteTeam_ServiceError tests error handling when service fails.
|
||||
func TestDeleteTeam_ServiceError(t *testing.T) {
|
||||
mock := &MockTeamService{
|
||||
DeleteTeamFn: func(id string) error {
|
||||
return ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/teams/t-platform", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("expected status 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteTeam_MethodNotAllowed tests that non-DELETE requests are rejected.
|
||||
func TestDeleteTeam_MethodNotAllowed(t *testing.T) {
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams/t-platform", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DeleteTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected status 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateTeam_EmptyNameString tests 400 response when name is empty string.
|
||||
func TestCreateTeam_EmptyNameString(t *testing.T) {
|
||||
body := map[string]interface{}{
|
||||
"name": "",
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(body)
|
||||
|
||||
handler := NewTeamHandler(&MockTeamService{})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/teams", bytes.NewReader(bodyBytes))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateTeam(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// TestListTeams_InvalidPagination tests handling of invalid pagination parameters.
|
||||
func TestListTeams_InvalidPagination(t *testing.T) {
|
||||
var capturedPage, capturedPerPage int
|
||||
mock := &MockTeamService{
|
||||
ListTeamsFn: func(page, perPage int) ([]domain.Team, int64, error) {
|
||||
capturedPage = page
|
||||
capturedPerPage = perPage
|
||||
return []domain.Team{}, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewTeamHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/teams?page=invalid&per_page=bad", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListTeams(w, req)
|
||||
|
||||
// Should use defaults when parsing fails
|
||||
if capturedPage != 1 {
|
||||
t.Errorf("expected default page 1, got %d", capturedPage)
|
||||
}
|
||||
if capturedPerPage != 50 {
|
||||
t.Errorf("expected default per_page 50, got %d", capturedPerPage)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package nginx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/connector/target"
|
||||
"github.com/shankar0123/certctl/internal/connector/target/nginx"
|
||||
)
|
||||
|
||||
func TestNginxConnector_ValidateConfig_Success(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := nginx.Config{
|
||||
CertPath: filepath.Join(tmpDir, "cert.pem"),
|
||||
KeyPath: filepath.Join(tmpDir, "key.pem"),
|
||||
ChainPath: filepath.Join(tmpDir, "chain.pem"),
|
||||
ReloadCommand: "true",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(&cfg, logger)
|
||||
rawConfig, _ := json.Marshal(cfg)
|
||||
err := connector.ValidateConfig(ctx, rawConfig)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateConfig failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_ValidateConfig_InvalidJSON(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
connector := nginx.New(&nginx.Config{}, logger)
|
||||
err := connector.ValidateConfig(ctx, json.RawMessage(`{invalid}`))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_ValidateConfig_MissingCertPath(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := nginx.Config{
|
||||
ChainPath: filepath.Join(tmpDir, "chain.pem"),
|
||||
ReloadCommand: "true",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(&cfg, logger)
|
||||
rawConfig, _ := json.Marshal(cfg)
|
||||
err := connector.ValidateConfig(ctx, rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing cert_path")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_ValidateConfig_MissingReloadCommand(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := nginx.Config{
|
||||
CertPath: filepath.Join(tmpDir, "cert.pem"),
|
||||
ChainPath: filepath.Join(tmpDir, "chain.pem"),
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(&cfg, logger)
|
||||
rawConfig, _ := json.Marshal(cfg)
|
||||
err := connector.ValidateConfig(ctx, rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing reload_command")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_ValidateConfig_DirectoryNotExists(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
cfg := nginx.Config{
|
||||
CertPath: "/nonexistent/directory/cert.pem",
|
||||
ChainPath: "/tmp/chain.pem",
|
||||
ReloadCommand: "true",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(&cfg, logger)
|
||||
rawConfig, _ := json.Marshal(cfg)
|
||||
err := connector.ValidateConfig(ctx, rawConfig)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-existent cert directory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_DeployCertificate_Success(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &nginx.Config{
|
||||
CertPath: filepath.Join(tmpDir, "cert.pem"),
|
||||
KeyPath: filepath.Join(tmpDir, "key.pem"),
|
||||
ChainPath: filepath.Join(tmpDir, "chain.pem"),
|
||||
ReloadCommand: "true",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
req := target.DeploymentRequest{
|
||||
CertPEM: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
|
||||
ChainPEM: "-----BEGIN CERTIFICATE-----\nchain\n-----END CERTIFICATE-----",
|
||||
}
|
||||
|
||||
result, err := connector.DeployCertificate(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("DeployCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
if !result.Success {
|
||||
t.Fatalf("expected success, got: %s", result.Message)
|
||||
}
|
||||
|
||||
// Verify cert file was written
|
||||
certData, err := os.ReadFile(cfg.CertPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read cert file: %v", err)
|
||||
}
|
||||
if string(certData) != req.CertPEM {
|
||||
t.Errorf("cert content mismatch")
|
||||
}
|
||||
|
||||
// Verify chain file was written
|
||||
chainData, err := os.ReadFile(cfg.ChainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read chain file: %v", err)
|
||||
}
|
||||
if string(chainData) != req.ChainPEM {
|
||||
t.Errorf("chain content mismatch")
|
||||
}
|
||||
|
||||
// Verify cert has correct permissions (0644)
|
||||
info, err := os.Stat(cfg.CertPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat cert file: %v", err)
|
||||
}
|
||||
if info.Mode().Perm() != 0644 {
|
||||
t.Errorf("expected cert permissions 0644, got %v", info.Mode().Perm())
|
||||
}
|
||||
|
||||
// Verify chain has correct permissions (0644)
|
||||
info, err = os.Stat(cfg.ChainPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat chain file: %v", err)
|
||||
}
|
||||
if info.Mode().Perm() != 0644 {
|
||||
t.Errorf("expected chain permissions 0644, got %v", info.Mode().Perm())
|
||||
}
|
||||
|
||||
// Verify metadata is populated
|
||||
if result.Metadata == nil {
|
||||
t.Fatal("expected metadata in result")
|
||||
}
|
||||
if result.Metadata["cert_path"] != cfg.CertPath {
|
||||
t.Errorf("expected cert_path in metadata")
|
||||
}
|
||||
if result.Metadata["chain_path"] != cfg.ChainPath {
|
||||
t.Errorf("expected chain_path in metadata")
|
||||
}
|
||||
if _, ok := result.Metadata["duration_ms"]; !ok {
|
||||
t.Errorf("expected duration_ms in metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_DeployCertificate_CertWriteFail(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
cfg := &nginx.Config{
|
||||
CertPath: "/nonexistent/directory/cert.pem",
|
||||
ChainPath: "/tmp/chain.pem",
|
||||
ReloadCommand: "true",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
req := target.DeploymentRequest{
|
||||
CertPEM: "cert",
|
||||
ChainPEM: "chain",
|
||||
}
|
||||
|
||||
result, err := connector.DeployCertificate(ctx, req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when cert write fails")
|
||||
}
|
||||
if result.Success {
|
||||
t.Fatal("expected failure result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_DeployCertificate_ChainWriteFail(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &nginx.Config{
|
||||
CertPath: filepath.Join(tmpDir, "cert.pem"),
|
||||
ChainPath: "/nonexistent/directory/chain.pem",
|
||||
ReloadCommand: "true",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
req := target.DeploymentRequest{
|
||||
CertPEM: "cert",
|
||||
ChainPEM: "chain",
|
||||
}
|
||||
|
||||
result, err := connector.DeployCertificate(ctx, req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when chain write fails")
|
||||
}
|
||||
if result.Success {
|
||||
t.Fatal("expected failure result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_DeployCertificate_ValidateCommandFails(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &nginx.Config{
|
||||
CertPath: filepath.Join(tmpDir, "cert.pem"),
|
||||
ChainPath: filepath.Join(tmpDir, "chain.pem"),
|
||||
ReloadCommand: "true",
|
||||
ValidateCommand: "false",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
req := target.DeploymentRequest{
|
||||
CertPEM: "cert",
|
||||
ChainPEM: "chain",
|
||||
}
|
||||
|
||||
result, err := connector.DeployCertificate(ctx, req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when validate command fails")
|
||||
}
|
||||
if result.Success {
|
||||
t.Fatal("expected failure result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_DeployCertificate_ReloadCommandFails(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
cfg := &nginx.Config{
|
||||
CertPath: filepath.Join(tmpDir, "cert.pem"),
|
||||
ChainPath: filepath.Join(tmpDir, "chain.pem"),
|
||||
ReloadCommand: "false",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
req := target.DeploymentRequest{
|
||||
CertPEM: "cert",
|
||||
ChainPEM: "chain",
|
||||
}
|
||||
|
||||
result, err := connector.DeployCertificate(ctx, req)
|
||||
if err == nil {
|
||||
t.Fatal("expected error when reload command fails")
|
||||
}
|
||||
if result.Success {
|
||||
t.Fatal("expected failure result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_ValidateDeployment_Success(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
certPath := filepath.Join(tmpDir, "cert.pem")
|
||||
os.WriteFile(certPath, []byte("cert"), 0644)
|
||||
|
||||
cfg := &nginx.Config{
|
||||
CertPath: certPath,
|
||||
ChainPath: filepath.Join(tmpDir, "chain.pem"),
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
result, err := connector.ValidateDeployment(ctx, target.ValidationRequest{
|
||||
CertificateID: "mc-test",
|
||||
Serial: "123",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateDeployment failed: %v", err)
|
||||
}
|
||||
if !result.Valid {
|
||||
t.Fatal("expected valid deployment")
|
||||
}
|
||||
|
||||
// Verify metadata is populated
|
||||
if result.Metadata == nil {
|
||||
t.Fatal("expected metadata in result")
|
||||
}
|
||||
if _, ok := result.Metadata["duration_ms"]; !ok {
|
||||
t.Errorf("expected duration_ms in metadata")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_ValidateDeployment_CertNotFound(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
cfg := &nginx.Config{
|
||||
CertPath: "/nonexistent/cert.pem",
|
||||
ValidateCommand: "true",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
result, err := connector.ValidateDeployment(ctx, target.ValidationRequest{
|
||||
CertificateID: "mc-test",
|
||||
Serial: "123",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing cert file")
|
||||
}
|
||||
if result.Valid {
|
||||
t.Fatal("expected invalid result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNginxConnector_ValidateDeployment_ValidateCommandFails(t *testing.T) {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
certPath := filepath.Join(tmpDir, "cert.pem")
|
||||
os.WriteFile(certPath, []byte("cert"), 0644)
|
||||
|
||||
cfg := &nginx.Config{
|
||||
CertPath: certPath,
|
||||
ValidateCommand: "false",
|
||||
}
|
||||
|
||||
connector := nginx.New(cfg, logger)
|
||||
|
||||
result, err := connector.ValidateDeployment(ctx, target.ValidationRequest{
|
||||
CertificateID: "mc-test",
|
||||
Serial: "123",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when validate command fails")
|
||||
}
|
||||
if result.Valid {
|
||||
t.Fatal("expected invalid result")
|
||||
}
|
||||
}
|
||||
@@ -392,3 +392,282 @@ func TestCertificateLifecycleWithExpiredCert(t *testing.T) {
|
||||
t.Logf("Renewal trigger on expired cert returned status: %d", resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// TestM11bEndpoints exercises the M11b endpoints: teams, owners, agent groups.
|
||||
// Tests M11b feature coverage through the HTTP API.
|
||||
func TestM11bEndpoints(t *testing.T) {
|
||||
server, _, _, _ := setupTestServer(t)
|
||||
|
||||
// ========================
|
||||
// Teams API
|
||||
// ========================
|
||||
t.Run("Teams", func(t *testing.T) {
|
||||
t.Run("CreateTeam_Success", func(t *testing.T) {
|
||||
payload := map[string]string{"name": "Platform", "description": "Platform team"}
|
||||
body, _ := json.Marshal(payload)
|
||||
resp, err := http.Post(server.URL+"/api/v1/teams", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("expected 201, got %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
var team domain.Team
|
||||
json.NewDecoder(resp.Body).Decode(&team)
|
||||
if team.Name != "Platform" {
|
||||
t.Errorf("expected name=Platform, got %s", team.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateTeam_MissingName", func(t *testing.T) {
|
||||
payload := map[string]string{"description": "No name team"}
|
||||
body, _ := json.Marshal(payload)
|
||||
resp, err := http.Post(server.URL+"/api/v1/teams", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateTeam_NameTooLong", func(t *testing.T) {
|
||||
longName := ""
|
||||
for i := 0; i < 256; i++ {
|
||||
longName += "a"
|
||||
}
|
||||
payload := map[string]string{"name": longName}
|
||||
body, _ := json.Marshal(payload)
|
||||
resp, err := http.Post(server.URL+"/api/v1/teams", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateTeam_InvalidJSON", func(t *testing.T) {
|
||||
resp, err := http.Post(server.URL+"/api/v1/teams", "application/json", bytes.NewReader([]byte("not json")))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetTeam_NotFound", func(t *testing.T) {
|
||||
resp, err := http.Get(server.URL + "/api/v1/teams/t-nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListTeams_Empty", func(t *testing.T) {
|
||||
resp, err := http.Get(server.URL + "/api/v1/teams")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteTeam_Success", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/api/v1/teams/t-platform", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Errorf("expected 204, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListTeams_MethodNotAllowed", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/api/v1/teams", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected 405, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ========================
|
||||
// Owners API
|
||||
// ========================
|
||||
t.Run("Owners", func(t *testing.T) {
|
||||
t.Run("CreateOwner_Success", func(t *testing.T) {
|
||||
payload := map[string]string{"name": "Alice", "email": "alice@example.com", "team_id": "t-platform"}
|
||||
body, _ := json.Marshal(payload)
|
||||
resp, err := http.Post(server.URL+"/api/v1/owners", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("expected 201, got %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
var owner domain.Owner
|
||||
json.NewDecoder(resp.Body).Decode(&owner)
|
||||
if owner.Name != "Alice" {
|
||||
t.Errorf("expected name=Alice, got %s", owner.Name)
|
||||
}
|
||||
if owner.Email != "alice@example.com" {
|
||||
t.Errorf("expected email=alice@example.com, got %s", owner.Email)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateOwner_MissingName", func(t *testing.T) {
|
||||
payload := map[string]string{"email": "bob@example.com"}
|
||||
body, _ := json.Marshal(payload)
|
||||
resp, err := http.Post(server.URL+"/api/v1/owners", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetOwner_NotFound", func(t *testing.T) {
|
||||
resp, err := http.Get(server.URL + "/api/v1/owners/o-nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListOwners_Empty", func(t *testing.T) {
|
||||
resp, err := http.Get(server.URL + "/api/v1/owners")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteOwner_Success", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/api/v1/owners/o-alice", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Errorf("expected 204, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// ========================
|
||||
// Agent Groups API
|
||||
// ========================
|
||||
t.Run("AgentGroups", func(t *testing.T) {
|
||||
t.Run("CreateAgentGroup_Success", func(t *testing.T) {
|
||||
payload := map[string]interface{}{
|
||||
"name": "Linux Servers",
|
||||
"description": "All linux-based agents",
|
||||
"match_os": "linux",
|
||||
"match_architecture": "amd64",
|
||||
"enabled": true,
|
||||
}
|
||||
body, _ := json.Marshal(payload)
|
||||
resp, err := http.Post(server.URL+"/api/v1/agent-groups", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
t.Errorf("expected 201, got %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
var group domain.AgentGroup
|
||||
json.NewDecoder(resp.Body).Decode(&group)
|
||||
if group.Name != "Linux Servers" {
|
||||
t.Errorf("expected name=Linux Servers, got %s", group.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("CreateAgentGroup_MissingName", func(t *testing.T) {
|
||||
payload := map[string]string{"description": "No name group"}
|
||||
body, _ := json.Marshal(payload)
|
||||
resp, err := http.Post(server.URL+"/api/v1/agent-groups", "application/json", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("expected 400, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetAgentGroup_NotFound", func(t *testing.T) {
|
||||
resp, err := http.Get(server.URL + "/api/v1/agent-groups/ag-nonexistent")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNotFound {
|
||||
t.Errorf("expected 404, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAgentGroups_Empty", func(t *testing.T) {
|
||||
resp, err := http.Get(server.URL + "/api/v1/agent-groups")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteAgentGroup_Success", func(t *testing.T) {
|
||||
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/api/v1/agent-groups/ag-linux", nil)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
t.Errorf("expected 204, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ListAgentGroupMembers_Empty", func(t *testing.T) {
|
||||
resp, err := http.Get(server.URL + "/api/v1/agent-groups/ag-linux/members")
|
||||
if err != nil {
|
||||
t.Fatalf("request failed: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,699 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// mockAgentGroupRepo is a test implementation of AgentGroupRepository
|
||||
type mockAgentGroupRepo struct {
|
||||
groups map[string]*domain.AgentGroup
|
||||
members map[string][]*domain.Agent
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
DeleteErr error
|
||||
GetErr error
|
||||
ListErr error
|
||||
ListMembersErr error
|
||||
AddMemberErr error
|
||||
RemoveMemberErr error
|
||||
}
|
||||
|
||||
func newMockAgentGroupRepository() *mockAgentGroupRepo {
|
||||
return &mockAgentGroupRepo{
|
||||
groups: make(map[string]*domain.AgentGroup),
|
||||
members: make(map[string][]*domain.Agent),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) List(ctx context.Context) ([]*domain.AgentGroup, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var groups []*domain.AgentGroup
|
||||
for _, g := range m.groups {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) Get(ctx context.Context, id string) (*domain.AgentGroup, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
group, ok := m.groups[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) Create(ctx context.Context, group *domain.AgentGroup) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.groups[group.ID] = group
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) Update(ctx context.Context, group *domain.AgentGroup) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.groups[group.ID] = group
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) Delete(ctx context.Context, id string) error {
|
||||
if m.DeleteErr != nil {
|
||||
return m.DeleteErr
|
||||
}
|
||||
delete(m.groups, id)
|
||||
delete(m.members, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) ListMembers(ctx context.Context, groupID string) ([]*domain.Agent, error) {
|
||||
if m.ListMembersErr != nil {
|
||||
return nil, m.ListMembersErr
|
||||
}
|
||||
members := m.members[groupID]
|
||||
if members == nil {
|
||||
return make([]*domain.Agent, 0), nil
|
||||
}
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) AddMember(ctx context.Context, groupID, agentID, membershipType string) error {
|
||||
if m.AddMemberErr != nil {
|
||||
return m.AddMemberErr
|
||||
}
|
||||
// For testing purposes, we'll assume a simple mock agent
|
||||
agent := &domain.Agent{
|
||||
ID: agentID,
|
||||
Name: "test-agent-" + agentID,
|
||||
}
|
||||
m.members[groupID] = append(m.members[groupID], agent)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) RemoveMember(ctx context.Context, groupID, agentID string) error {
|
||||
if m.RemoveMemberErr != nil {
|
||||
return m.RemoveMemberErr
|
||||
}
|
||||
members := m.members[groupID]
|
||||
var filtered []*domain.Agent
|
||||
for _, m := range members {
|
||||
if m.ID != agentID {
|
||||
filtered = append(filtered, m)
|
||||
}
|
||||
}
|
||||
m.members[groupID] = filtered
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) AddGroup(group *domain.AgentGroup) {
|
||||
m.groups[group.ID] = group
|
||||
}
|
||||
|
||||
func (m *mockAgentGroupRepo) AddGroupMembers(groupID string, agents []*domain.Agent) {
|
||||
m.members[groupID] = agents
|
||||
}
|
||||
|
||||
// Test: ListAgentGroups returns groups
|
||||
func TestAgentGroupService_ListAgentGroups(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group1 := &domain.AgentGroup{
|
||||
ID: "ag-test-1",
|
||||
Name: "Linux Servers",
|
||||
}
|
||||
group2 := &domain.AgentGroup{
|
||||
ID: "ag-test-2",
|
||||
Name: "Windows Servers",
|
||||
}
|
||||
repo.AddGroup(group1)
|
||||
repo.AddGroup(group2)
|
||||
|
||||
groups, total, err := svc.ListAgentGroups(1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 2 {
|
||||
t.Errorf("expected total=2, got %d", total)
|
||||
}
|
||||
if len(groups) != 2 {
|
||||
t.Errorf("expected 2 groups, got %d", len(groups))
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ListAgentGroups with default pagination
|
||||
func TestAgentGroupService_ListAgentGroups_DefaultPagination(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := &domain.AgentGroup{
|
||||
ID: "ag-test-1",
|
||||
Name: "Test Group",
|
||||
}
|
||||
repo.AddGroup(group)
|
||||
|
||||
// page < 1 should default to 1, perPage < 1 should default to 50
|
||||
groups, total, err := svc.ListAgentGroups(-1, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 1 {
|
||||
t.Errorf("expected total=1, got %d", total)
|
||||
}
|
||||
if len(groups) != 1 {
|
||||
t.Errorf("expected 1 group, got %d", len(groups))
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ListAgentGroups with repository error
|
||||
func TestAgentGroupService_ListAgentGroups_RepositoryError(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
repo.ListErr = errors.New("database error")
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
_, _, err := svc.ListAgentGroups(1, 50)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to list agent groups") {
|
||||
t.Errorf("expected 'failed to list agent groups' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ListAgentGroups with empty result
|
||||
func TestAgentGroupService_ListAgentGroups_EmptyResult(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
groups, total, err := svc.ListAgentGroups(1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 0 {
|
||||
t.Errorf("expected total=0, got %d", total)
|
||||
}
|
||||
if len(groups) != 0 {
|
||||
t.Errorf("expected 0 groups, got %d", len(groups))
|
||||
}
|
||||
}
|
||||
|
||||
// Test: GetAgentGroup success
|
||||
func TestAgentGroupService_GetAgentGroup(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := &domain.AgentGroup{
|
||||
ID: "ag-test-1",
|
||||
Name: "Test Group",
|
||||
}
|
||||
repo.AddGroup(group)
|
||||
|
||||
retrieved, err := svc.GetAgentGroup("ag-test-1")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if retrieved == nil {
|
||||
t.Fatal("expected group, got nil")
|
||||
}
|
||||
if retrieved.ID != "ag-test-1" {
|
||||
t.Errorf("expected ID 'ag-test-1', got %s", retrieved.ID)
|
||||
}
|
||||
if retrieved.Name != "Test Group" {
|
||||
t.Errorf("expected name 'Test Group', got %s", retrieved.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: GetAgentGroup not found
|
||||
func TestAgentGroupService_GetAgentGroup_NotFound(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
_, err := svc.GetAgentGroup("ag-nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !errors.Is(err, errNotFound) {
|
||||
t.Errorf("expected errNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: CreateAgentGroup success with ID generated and timestamps
|
||||
func TestAgentGroupService_CreateAgentGroup(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := domain.AgentGroup{
|
||||
Name: "Test Group",
|
||||
}
|
||||
before := time.Now()
|
||||
|
||||
created, err := svc.CreateAgentGroup(group)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if created == nil {
|
||||
t.Fatal("expected group, got nil")
|
||||
}
|
||||
|
||||
// ID should be generated
|
||||
if created.ID == "" {
|
||||
t.Fatal("expected ID to be generated, got empty string")
|
||||
}
|
||||
if !strings.HasPrefix(created.ID, "ag-") {
|
||||
t.Errorf("expected ID to start with 'ag-', got %s", created.ID)
|
||||
}
|
||||
|
||||
// Timestamps should be set
|
||||
if created.CreatedAt.IsZero() {
|
||||
t.Fatal("expected CreatedAt to be set")
|
||||
}
|
||||
if created.UpdatedAt.IsZero() {
|
||||
t.Fatal("expected UpdatedAt to be set")
|
||||
}
|
||||
if created.CreatedAt.Before(before) {
|
||||
t.Errorf("expected CreatedAt >= before, got %v < %v", created.CreatedAt, before)
|
||||
}
|
||||
|
||||
// Should be in repository
|
||||
retrieved, err := repo.Get(context.Background(), created.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if retrieved.ID != created.ID {
|
||||
t.Errorf("expected ID %s, got %s", created.ID, retrieved.ID)
|
||||
}
|
||||
|
||||
// Audit event should be recorded
|
||||
if len(auditRepo.Events) == 0 {
|
||||
t.Fatal("expected audit event to be recorded")
|
||||
}
|
||||
if auditRepo.Events[0].Action != "create_agent_group" {
|
||||
t.Errorf("expected action 'create_agent_group', got %s", auditRepo.Events[0].Action)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: CreateAgentGroup with empty name
|
||||
func TestAgentGroupService_CreateAgentGroup_EmptyName(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := domain.AgentGroup{
|
||||
Name: "",
|
||||
}
|
||||
|
||||
_, err := svc.CreateAgentGroup(group)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "agent group name is required") {
|
||||
t.Errorf("expected 'agent group name is required' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: CreateAgentGroup with name too long
|
||||
func TestAgentGroupService_CreateAgentGroup_NameTooLong(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := domain.AgentGroup{
|
||||
Name: strings.Repeat("a", 256),
|
||||
}
|
||||
|
||||
_, err := svc.CreateAgentGroup(group)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "exceeds 255 characters") {
|
||||
t.Errorf("expected 'exceeds 255 characters' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: CreateAgentGroup with existing ID preserves ID
|
||||
func TestAgentGroupService_CreateAgentGroup_WithExistingID(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := domain.AgentGroup{
|
||||
ID: "ag-custom-id",
|
||||
Name: "Test Group",
|
||||
}
|
||||
|
||||
created, err := svc.CreateAgentGroup(group)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if created.ID != "ag-custom-id" {
|
||||
t.Errorf("expected ID 'ag-custom-id', got %s", created.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: CreateAgentGroup with dynamic criteria
|
||||
func TestAgentGroupService_CreateAgentGroup_WithDynamicCriteria(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := domain.AgentGroup{
|
||||
Name: "Linux x86_64 Servers",
|
||||
MatchOS: "linux",
|
||||
MatchArchitecture: "amd64",
|
||||
}
|
||||
|
||||
created, err := svc.CreateAgentGroup(group)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if created.MatchOS != "linux" {
|
||||
t.Errorf("expected MatchOS 'linux', got %s", created.MatchOS)
|
||||
}
|
||||
if created.MatchArchitecture != "amd64" {
|
||||
t.Errorf("expected MatchArchitecture 'amd64', got %s", created.MatchArchitecture)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: CreateAgentGroup with repository error
|
||||
func TestAgentGroupService_CreateAgentGroup_RepositoryError(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
repo.CreateErr = errors.New("database error")
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := domain.AgentGroup{
|
||||
Name: "Test Group",
|
||||
}
|
||||
|
||||
_, err := svc.CreateAgentGroup(group)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to create agent group") {
|
||||
t.Errorf("expected 'failed to create agent group' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: UpdateAgentGroup success
|
||||
func TestAgentGroupService_UpdateAgentGroup(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
existing := &domain.AgentGroup{
|
||||
ID: "ag-test-1",
|
||||
Name: "Old Name",
|
||||
}
|
||||
repo.AddGroup(existing)
|
||||
|
||||
updated := domain.AgentGroup{
|
||||
Name: "New Name",
|
||||
}
|
||||
|
||||
result, err := svc.UpdateAgentGroup("ag-test-1", updated)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if result.ID != "ag-test-1" {
|
||||
t.Errorf("expected ID 'ag-test-1', got %s", result.ID)
|
||||
}
|
||||
if result.Name != "New Name" {
|
||||
t.Errorf("expected name 'New Name', got %s", result.Name)
|
||||
}
|
||||
|
||||
// Audit event should be recorded
|
||||
if len(auditRepo.Events) == 0 {
|
||||
t.Fatal("expected audit event to be recorded")
|
||||
}
|
||||
if auditRepo.Events[0].Action != "update_agent_group" {
|
||||
t.Errorf("expected action 'update_agent_group', got %s", auditRepo.Events[0].Action)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: UpdateAgentGroup with empty name validation error
|
||||
func TestAgentGroupService_UpdateAgentGroup_EmptyName(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
updated := domain.AgentGroup{
|
||||
Name: "",
|
||||
}
|
||||
|
||||
_, err := svc.UpdateAgentGroup("ag-test-1", updated)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "agent group name is required") {
|
||||
t.Errorf("expected 'agent group name is required' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: UpdateAgentGroup with repository error
|
||||
func TestAgentGroupService_UpdateAgentGroup_RepositoryError(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
repo.UpdateErr = errors.New("database error")
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
updated := domain.AgentGroup{
|
||||
Name: "Valid Name",
|
||||
}
|
||||
|
||||
_, err := svc.UpdateAgentGroup("ag-test-1", updated)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to update agent group") {
|
||||
t.Errorf("expected 'failed to update agent group' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: DeleteAgentGroup success with audit
|
||||
func TestAgentGroupService_DeleteAgentGroup(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
group := &domain.AgentGroup{
|
||||
ID: "ag-test-1",
|
||||
Name: "Test Group",
|
||||
}
|
||||
repo.AddGroup(group)
|
||||
|
||||
err := svc.DeleteAgentGroup("ag-test-1")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Group should be deleted from repository
|
||||
_, err = repo.Get(context.Background(), "ag-test-1")
|
||||
if !errors.Is(err, errNotFound) {
|
||||
t.Errorf("expected errNotFound after delete, got %v", err)
|
||||
}
|
||||
|
||||
// Audit event should be recorded
|
||||
if len(auditRepo.Events) == 0 {
|
||||
t.Fatal("expected audit event to be recorded")
|
||||
}
|
||||
if auditRepo.Events[0].Action != "delete_agent_group" {
|
||||
t.Errorf("expected action 'delete_agent_group', got %s", auditRepo.Events[0].Action)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: DeleteAgentGroup with repository error
|
||||
func TestAgentGroupService_DeleteAgentGroup_RepositoryError(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
repo.DeleteErr = errors.New("database error")
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
err := svc.DeleteAgentGroup("ag-test-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to delete agent group") {
|
||||
t.Errorf("expected 'failed to delete agent group' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ListMembers returns agents
|
||||
func TestAgentGroupService_ListMembers(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
agents := []*domain.Agent{
|
||||
{
|
||||
ID: "agent-1",
|
||||
Name: "Agent 1",
|
||||
},
|
||||
{
|
||||
ID: "agent-2",
|
||||
Name: "Agent 2",
|
||||
},
|
||||
}
|
||||
repo.AddGroupMembers("ag-test-1", agents)
|
||||
|
||||
result, total, err := svc.ListMembers("ag-test-1")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 2 {
|
||||
t.Errorf("expected total=2, got %d", total)
|
||||
}
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected 2 agents, got %d", len(result))
|
||||
}
|
||||
if result[0].ID != "agent-1" {
|
||||
t.Errorf("expected first agent ID 'agent-1', got %s", result[0].ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ListMembers returns empty when no agents
|
||||
func TestAgentGroupService_ListMembers_Empty(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
result, total, err := svc.ListMembers("ag-test-1")
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if total != 0 {
|
||||
t.Errorf("expected total=0, got %d", total)
|
||||
}
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected 0 agents, got %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ListMembers with repository error
|
||||
func TestAgentGroupService_ListMembers_RepositoryError(t *testing.T) {
|
||||
repo := newMockAgentGroupRepository()
|
||||
repo.ListMembersErr = errors.New("database error")
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
svc := NewAgentGroupService(repo, auditSvc)
|
||||
|
||||
_, _, err := svc.ListMembers("ag-test-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to list group members") {
|
||||
t.Errorf("expected 'failed to list group members' in error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test: AgentGroup.MatchesAgent with all criteria matching
|
||||
func TestAgentGroup_MatchesAgent(t *testing.T) {
|
||||
group := &domain.AgentGroup{
|
||||
MatchOS: "linux",
|
||||
MatchArchitecture: "amd64",
|
||||
MatchVersion: "1.0.0",
|
||||
}
|
||||
agent := &domain.Agent{
|
||||
OS: "linux",
|
||||
Architecture: "amd64",
|
||||
Version: "1.0.0",
|
||||
}
|
||||
|
||||
matches := group.MatchesAgent(agent)
|
||||
if !matches {
|
||||
t.Fatal("expected agent to match all criteria")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: AgentGroup.MatchesAgent with OS mismatch
|
||||
func TestAgentGroup_MatchesAgent_OSMismatch(t *testing.T) {
|
||||
group := &domain.AgentGroup{
|
||||
MatchOS: "linux",
|
||||
MatchArchitecture: "amd64",
|
||||
}
|
||||
agent := &domain.Agent{
|
||||
OS: "windows",
|
||||
Architecture: "amd64",
|
||||
}
|
||||
|
||||
matches := group.MatchesAgent(agent)
|
||||
if matches {
|
||||
t.Fatal("expected agent NOT to match due to OS mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: AgentGroup.MatchesAgent with empty criteria matches any agent
|
||||
func TestAgentGroup_MatchesAgent_EmptyCriteria(t *testing.T) {
|
||||
group := &domain.AgentGroup{
|
||||
// All criteria empty (wildcards)
|
||||
}
|
||||
agent := &domain.Agent{
|
||||
OS: "linux",
|
||||
Architecture: "arm64",
|
||||
Version: "2.0.0",
|
||||
}
|
||||
|
||||
matches := group.MatchesAgent(agent)
|
||||
if !matches {
|
||||
t.Fatal("expected agent to match empty criteria (wildcard)")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: AgentGroup.HasDynamicCriteria returns true when criteria set
|
||||
func TestAgentGroup_HasDynamicCriteria(t *testing.T) {
|
||||
group := &domain.AgentGroup{
|
||||
MatchOS: "linux",
|
||||
}
|
||||
|
||||
if !group.HasDynamicCriteria() {
|
||||
t.Fatal("expected HasDynamicCriteria to return true")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: AgentGroup.HasDynamicCriteria returns false when empty
|
||||
func TestAgentGroup_HasDynamicCriteria_Empty(t *testing.T) {
|
||||
group := &domain.AgentGroup{
|
||||
// All criteria empty
|
||||
}
|
||||
|
||||
if group.HasDynamicCriteria() {
|
||||
t.Fatal("expected HasDynamicCriteria to return false")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/connector/issuer"
|
||||
)
|
||||
|
||||
// mockConnectorLayerIssuer is a test implementation of issuer.Connector
|
||||
type mockConnectorLayerIssuer struct {
|
||||
issueResult *issuer.IssuanceResult
|
||||
issueErr error
|
||||
renewResult *issuer.IssuanceResult
|
||||
renewErr error
|
||||
lastIssueReq *issuer.IssuanceRequest
|
||||
lastRenewReq *issuer.RenewalRequest
|
||||
validateErr error
|
||||
revokeErr error
|
||||
orderStatusErr error
|
||||
orderStatus *issuer.OrderStatus
|
||||
}
|
||||
|
||||
func (m *mockConnectorLayerIssuer) ValidateConfig(ctx context.Context, config []byte) error {
|
||||
return m.validateErr
|
||||
}
|
||||
|
||||
func (m *mockConnectorLayerIssuer) IssueCertificate(ctx context.Context, request issuer.IssuanceRequest) (*issuer.IssuanceResult, error) {
|
||||
m.lastIssueReq = &request
|
||||
if m.issueErr != nil {
|
||||
return nil, m.issueErr
|
||||
}
|
||||
if m.issueResult != nil {
|
||||
return m.issueResult, nil
|
||||
}
|
||||
// Return default result
|
||||
now := time.Now()
|
||||
return &issuer.IssuanceResult{
|
||||
CertPEM: "-----BEGIN CERTIFICATE-----\ndefault-cert\n-----END CERTIFICATE-----",
|
||||
ChainPEM: "-----BEGIN CERTIFICATE-----\ndefault-chain\n-----END CERTIFICATE-----",
|
||||
Serial: "default-serial-123",
|
||||
NotBefore: now,
|
||||
NotAfter: now.AddDate(1, 0, 0),
|
||||
OrderID: "order-default",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockConnectorLayerIssuer) RenewCertificate(ctx context.Context, request issuer.RenewalRequest) (*issuer.IssuanceResult, error) {
|
||||
m.lastRenewReq = &request
|
||||
if m.renewErr != nil {
|
||||
return nil, m.renewErr
|
||||
}
|
||||
if m.renewResult != nil {
|
||||
return m.renewResult, nil
|
||||
}
|
||||
// Return default result
|
||||
now := time.Now()
|
||||
return &issuer.IssuanceResult{
|
||||
CertPEM: "-----BEGIN CERTIFICATE-----\ndefault-renewed-cert\n-----END CERTIFICATE-----",
|
||||
ChainPEM: "-----BEGIN CERTIFICATE-----\ndefault-renewed-chain\n-----END CERTIFICATE-----",
|
||||
Serial: "default-renewed-serial-456",
|
||||
NotBefore: now,
|
||||
NotAfter: now.AddDate(1, 0, 0),
|
||||
OrderID: "order-renewed",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockConnectorLayerIssuer) RevokeCertificate(ctx context.Context, request issuer.RevocationRequest) error {
|
||||
return m.revokeErr
|
||||
}
|
||||
|
||||
func (m *mockConnectorLayerIssuer) GetOrderStatus(ctx context.Context, orderID string) (*issuer.OrderStatus, error) {
|
||||
if m.orderStatusErr != nil {
|
||||
return nil, m.orderStatusErr
|
||||
}
|
||||
if m.orderStatus != nil {
|
||||
return m.orderStatus, nil
|
||||
}
|
||||
status := "pending"
|
||||
return &issuer.OrderStatus{
|
||||
OrderID: orderID,
|
||||
Status: status,
|
||||
UpdatedAt: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Tests for IssueCertificate
|
||||
|
||||
func TestIssuerConnectorAdapter_IssueCertificate_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
notAfter := now.AddDate(1, 0, 0)
|
||||
|
||||
mock := &mockConnectorLayerIssuer{
|
||||
issueResult: &issuer.IssuanceResult{
|
||||
CertPEM: "-----BEGIN CERTIFICATE-----\ntest-cert\n-----END CERTIFICATE-----",
|
||||
ChainPEM: "-----BEGIN CERTIFICATE-----\ntest-chain\n-----END CERTIFICATE-----",
|
||||
Serial: "test-serial-001",
|
||||
NotBefore: now,
|
||||
NotAfter: notAfter,
|
||||
OrderID: "order-123",
|
||||
},
|
||||
}
|
||||
|
||||
adapter := NewIssuerConnectorAdapter(mock)
|
||||
|
||||
result, err := adapter.IssueCertificate(ctx, "example.com", []string{"www.example.com"}, "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("IssueCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
if result.Serial != "test-serial-001" {
|
||||
t.Errorf("expected serial test-serial-001, got %s", result.Serial)
|
||||
}
|
||||
|
||||
if result.CertPEM != "-----BEGIN CERTIFICATE-----\ntest-cert\n-----END CERTIFICATE-----" {
|
||||
t.Errorf("expected CertPEM test-cert, got %s", result.CertPEM)
|
||||
}
|
||||
|
||||
if result.ChainPEM != "-----BEGIN CERTIFICATE-----\ntest-chain\n-----END CERTIFICATE-----" {
|
||||
t.Errorf("expected ChainPEM test-chain, got %s", result.ChainPEM)
|
||||
}
|
||||
|
||||
if !result.NotBefore.Equal(now) {
|
||||
t.Errorf("expected NotBefore %v, got %v", now, result.NotBefore)
|
||||
}
|
||||
|
||||
if !result.NotAfter.Equal(notAfter) {
|
||||
t.Errorf("expected NotAfter %v, got %v", notAfter, result.NotAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuerConnectorAdapter_IssueCertificate_Error(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("issuer connection failed")
|
||||
|
||||
mock := &mockConnectorLayerIssuer{
|
||||
issueErr: testErr,
|
||||
}
|
||||
|
||||
adapter := NewIssuerConnectorAdapter(mock)
|
||||
|
||||
result, err := adapter.IssueCertificate(ctx, "example.com", []string{}, "csr")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, testErr) {
|
||||
t.Errorf("expected error %v, got %v", testErr, err)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("expected nil result, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuerConnectorAdapter_IssueCertificate_RequestTranslation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mock := &mockConnectorLayerIssuer{
|
||||
issueResult: &issuer.IssuanceResult{
|
||||
CertPEM: "cert",
|
||||
ChainPEM: "chain",
|
||||
Serial: "serial",
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||
},
|
||||
}
|
||||
|
||||
adapter := NewIssuerConnectorAdapter(mock)
|
||||
|
||||
commonName := "test.example.com"
|
||||
sans := []string{"www.test.example.com", "api.test.example.com"}
|
||||
csrPEM := "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----"
|
||||
|
||||
_, err := adapter.IssueCertificate(ctx, commonName, sans, csrPEM)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("IssueCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify request was passed through correctly
|
||||
if mock.lastIssueReq == nil {
|
||||
t.Fatal("expected request to be recorded")
|
||||
}
|
||||
|
||||
if mock.lastIssueReq.CommonName != commonName {
|
||||
t.Errorf("expected CommonName %s, got %s", commonName, mock.lastIssueReq.CommonName)
|
||||
}
|
||||
|
||||
if len(mock.lastIssueReq.SANs) != len(sans) {
|
||||
t.Errorf("expected %d SANs, got %d", len(sans), len(mock.lastIssueReq.SANs))
|
||||
}
|
||||
|
||||
for i, san := range sans {
|
||||
if mock.lastIssueReq.SANs[i] != san {
|
||||
t.Errorf("expected SAN[%d] %s, got %s", i, san, mock.lastIssueReq.SANs[i])
|
||||
}
|
||||
}
|
||||
|
||||
if mock.lastIssueReq.CSRPEM != csrPEM {
|
||||
t.Errorf("expected CSRPEM %s, got %s", csrPEM, mock.lastIssueReq.CSRPEM)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests for RenewCertificate
|
||||
|
||||
func TestIssuerConnectorAdapter_RenewCertificate_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
notAfter := now.AddDate(1, 0, 0)
|
||||
|
||||
mock := &mockConnectorLayerIssuer{
|
||||
renewResult: &issuer.IssuanceResult{
|
||||
CertPEM: "-----BEGIN CERTIFICATE-----\nrenewed-cert\n-----END CERTIFICATE-----",
|
||||
ChainPEM: "-----BEGIN CERTIFICATE-----\nrenewed-chain\n-----END CERTIFICATE-----",
|
||||
Serial: "renewed-serial-002",
|
||||
NotBefore: now,
|
||||
NotAfter: notAfter,
|
||||
OrderID: "order-456",
|
||||
},
|
||||
}
|
||||
|
||||
adapter := NewIssuerConnectorAdapter(mock)
|
||||
|
||||
result, err := adapter.RenewCertificate(ctx, "example.com", []string{"www.example.com"}, "-----BEGIN CERTIFICATE REQUEST-----\nCSR\n-----END CERTIFICATE REQUEST-----")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("RenewCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
if result.Serial != "renewed-serial-002" {
|
||||
t.Errorf("expected serial renewed-serial-002, got %s", result.Serial)
|
||||
}
|
||||
|
||||
if result.CertPEM != "-----BEGIN CERTIFICATE-----\nrenewed-cert\n-----END CERTIFICATE-----" {
|
||||
t.Errorf("expected CertPEM renewed-cert, got %s", result.CertPEM)
|
||||
}
|
||||
|
||||
if result.ChainPEM != "-----BEGIN CERTIFICATE-----\nrenewed-chain\n-----END CERTIFICATE-----" {
|
||||
t.Errorf("expected ChainPEM renewed-chain, got %s", result.ChainPEM)
|
||||
}
|
||||
|
||||
if !result.NotBefore.Equal(now) {
|
||||
t.Errorf("expected NotBefore %v, got %v", now, result.NotBefore)
|
||||
}
|
||||
|
||||
if !result.NotAfter.Equal(notAfter) {
|
||||
t.Errorf("expected NotAfter %v, got %v", notAfter, result.NotAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuerConnectorAdapter_RenewCertificate_Error(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testErr := errors.New("renewal failed")
|
||||
|
||||
mock := &mockConnectorLayerIssuer{
|
||||
renewErr: testErr,
|
||||
}
|
||||
|
||||
adapter := NewIssuerConnectorAdapter(mock)
|
||||
|
||||
result, err := adapter.RenewCertificate(ctx, "example.com", []string{}, "csr")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, testErr) {
|
||||
t.Errorf("expected error %v, got %v", testErr, err)
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("expected nil result, got %v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuerConnectorAdapter_RenewCertificate_RequestTranslation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
mock := &mockConnectorLayerIssuer{
|
||||
renewResult: &issuer.IssuanceResult{
|
||||
CertPEM: "cert",
|
||||
ChainPEM: "chain",
|
||||
Serial: "serial",
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||
},
|
||||
}
|
||||
|
||||
adapter := NewIssuerConnectorAdapter(mock)
|
||||
|
||||
commonName := "renew.example.com"
|
||||
sans := []string{"www.renew.example.com"}
|
||||
csrPEM := "-----BEGIN CERTIFICATE REQUEST-----\nRENEW-CSR\n-----END CERTIFICATE REQUEST-----"
|
||||
|
||||
_, err := adapter.RenewCertificate(ctx, commonName, sans, csrPEM)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("RenewCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify request was passed through correctly
|
||||
if mock.lastRenewReq == nil {
|
||||
t.Fatal("expected request to be recorded")
|
||||
}
|
||||
|
||||
if mock.lastRenewReq.CommonName != commonName {
|
||||
t.Errorf("expected CommonName %s, got %s", commonName, mock.lastRenewReq.CommonName)
|
||||
}
|
||||
|
||||
if len(mock.lastRenewReq.SANs) != len(sans) {
|
||||
t.Errorf("expected %d SANs, got %d", len(sans), len(mock.lastRenewReq.SANs))
|
||||
}
|
||||
|
||||
for i, san := range sans {
|
||||
if mock.lastRenewReq.SANs[i] != san {
|
||||
t.Errorf("expected SAN[%d] %s, got %s", i, san, mock.lastRenewReq.SANs[i])
|
||||
}
|
||||
}
|
||||
|
||||
if mock.lastRenewReq.CSRPEM != csrPEM {
|
||||
t.Errorf("expected CSRPEM %s, got %s", csrPEM, mock.lastRenewReq.CSRPEM)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// TestIssuerService_List tests listing issuers with pagination
|
||||
func TestIssuerService_List(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
issuer1 := &domain.Issuer{
|
||||
ID: "iss-1",
|
||||
Name: "ACME Provider",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Enabled: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
issuer2 := &domain.Issuer{
|
||||
ID: "iss-2",
|
||||
Name: "Step CA",
|
||||
Type: domain.IssuerTypeStepCA,
|
||||
Enabled: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
issuer3 := &domain.Issuer{
|
||||
ID: "iss-3",
|
||||
Name: "Internal CA",
|
||||
Type: domain.IssuerTypeGenericCA,
|
||||
Enabled: false,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
repo.AddIssuer(issuer1)
|
||||
repo.AddIssuer(issuer2)
|
||||
repo.AddIssuer(issuer3)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
issuers, total, err := service.List(ctx, 1, 2)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
|
||||
if total != 3 {
|
||||
t.Errorf("expected total 3, got %d", total)
|
||||
}
|
||||
|
||||
if len(issuers) != 2 {
|
||||
t.Errorf("expected 2 issuers on page 1, got %d", len(issuers))
|
||||
}
|
||||
|
||||
// Test page 2
|
||||
issuers2, _, err := service.List(ctx, 2, 2)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("List page 2 failed: %v", err)
|
||||
}
|
||||
|
||||
if len(issuers2) != 1 {
|
||||
t.Errorf("expected 1 issuer on page 2, got %d", len(issuers2))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_List_DefaultPagination tests list with default pagination values
|
||||
func TestIssuerService_List_DefaultPagination(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
// Call with invalid page and perPage
|
||||
issuers, total, err := service.List(ctx, 0, 0)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
|
||||
if total != 0 {
|
||||
t.Errorf("expected total 0, got %d", total)
|
||||
}
|
||||
|
||||
if len(issuers) != 0 {
|
||||
t.Errorf("expected 0 issuers, got %d", len(issuers))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_List_RepositoryError tests list when repository returns error
|
||||
func TestIssuerService_List_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
repo.ListErr = errors.New("database connection failed")
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
_, _, err := service.List(ctx, 1, 50)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, repo.ListErr) {
|
||||
t.Errorf("expected error %v, got %v", repo.ListErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_List_EmptyResult tests list returning empty list
|
||||
func TestIssuerService_List_EmptyResult(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
issuers, total, err := service.List(ctx, 1, 50)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
|
||||
if total != 0 {
|
||||
t.Errorf("expected total 0, got %d", total)
|
||||
}
|
||||
|
||||
if len(issuers) != 0 {
|
||||
t.Errorf("expected 0 issuers, got %d", len(issuers))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Get tests retrieving an issuer by ID
|
||||
func TestIssuerService_Get(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
issuer := &domain.Issuer{
|
||||
ID: "iss-acme-prod",
|
||||
Name: "ACME Production",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Enabled: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
repo.AddIssuer(issuer)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
retrieved, err := service.Get(ctx, "iss-acme-prod")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name != "ACME Production" {
|
||||
t.Errorf("expected name ACME Production, got %s", retrieved.Name)
|
||||
}
|
||||
|
||||
if retrieved.Type != domain.IssuerTypeACME {
|
||||
t.Errorf("expected type ACME, got %s", retrieved.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Get_NotFound tests Get when issuer doesn't exist
|
||||
func TestIssuerService_Get_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
_, err := service.Get(ctx, "nonexistent-issuer")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent issuer")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Create tests creating a new issuer
|
||||
func TestIssuerService_Create(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
config := map[string]interface{}{"endpoint": "https://acme.example.com/v2/new-account"}
|
||||
configJSON, _ := json.Marshal(config)
|
||||
|
||||
issuer := &domain.Issuer{
|
||||
Name: "Test ACME",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Config: configJSON,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
err := service.Create(ctx, issuer, "user-alice")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
if issuer.ID == "" {
|
||||
t.Error("expected ID to be generated")
|
||||
}
|
||||
|
||||
if issuer.CreatedAt.IsZero() {
|
||||
t.Error("expected CreatedAt to be set")
|
||||
}
|
||||
|
||||
if issuer.UpdatedAt.IsZero() {
|
||||
t.Error("expected UpdatedAt to be set")
|
||||
}
|
||||
|
||||
// Verify stored in repo
|
||||
retrieved, err := repo.Get(ctx, issuer.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve created issuer: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name != "Test ACME" {
|
||||
t.Errorf("expected name Test ACME, got %s", retrieved.Name)
|
||||
}
|
||||
|
||||
// Verify audit event recorded
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
|
||||
if auditRepo.Events[0].Action != "create_issuer" {
|
||||
t.Errorf("expected action create_issuer, got %s", auditRepo.Events[0].Action)
|
||||
}
|
||||
|
||||
if auditRepo.Events[0].Actor != "user-alice" {
|
||||
t.Errorf("expected actor user-alice, got %s", auditRepo.Events[0].Actor)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Create_EmptyName tests Create with empty name validation
|
||||
func TestIssuerService_Create_EmptyName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
issuer := &domain.Issuer{
|
||||
Name: "",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
err := service.Create(ctx, issuer, "user-bob")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty name")
|
||||
}
|
||||
|
||||
if err.Error() != "issuer name is required" {
|
||||
t.Errorf("expected 'issuer name is required', got '%v'", err)
|
||||
}
|
||||
|
||||
// Verify no audit event recorded on validation error
|
||||
if len(auditRepo.Events) != 0 {
|
||||
t.Errorf("expected 0 audit events on validation error, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Create_RepositoryError tests Create when repository fails
|
||||
func TestIssuerService_Create_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
repo.CreateErr = errors.New("database error")
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
issuer := &domain.Issuer{
|
||||
Name: "Test Issuer",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
err := service.Create(ctx, issuer, "user-charlie")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error from repository")
|
||||
}
|
||||
|
||||
if !errors.Is(err, repo.CreateErr) {
|
||||
t.Errorf("expected error %v, got %v", repo.CreateErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Update tests updating an existing issuer
|
||||
func TestIssuerService_Update(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
config := map[string]interface{}{"endpoint": "https://acme.example.com"}
|
||||
configJSON, _ := json.Marshal(config)
|
||||
|
||||
issuer := &domain.Issuer{
|
||||
Name: "Updated ACME",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Config: configJSON,
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
err := service.Update(ctx, "iss-acme-001", issuer, "user-dave")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
|
||||
if issuer.ID != "iss-acme-001" {
|
||||
t.Errorf("expected ID to be set to iss-acme-001, got %s", issuer.ID)
|
||||
}
|
||||
|
||||
// Verify audit event recorded
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
|
||||
if auditRepo.Events[0].Action != "update_issuer" {
|
||||
t.Errorf("expected action update_issuer, got %s", auditRepo.Events[0].Action)
|
||||
}
|
||||
|
||||
if auditRepo.Events[0].ResourceID != "iss-acme-001" {
|
||||
t.Errorf("expected ResourceID iss-acme-001, got %s", auditRepo.Events[0].ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Update_EmptyName tests Update with empty name validation
|
||||
func TestIssuerService_Update_EmptyName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
issuer := &domain.Issuer{
|
||||
Name: "",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
err := service.Update(ctx, "iss-acme-001", issuer, "user-eve")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty name")
|
||||
}
|
||||
|
||||
if err.Error() != "issuer name is required" {
|
||||
t.Errorf("expected 'issuer name is required', got '%v'", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Delete tests deleting an issuer
|
||||
func TestIssuerService_Delete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
err := service.Delete(ctx, "iss-to-delete", "user-frank")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify audit event recorded
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
|
||||
if auditRepo.Events[0].Action != "delete_issuer" {
|
||||
t.Errorf("expected action delete_issuer, got %s", auditRepo.Events[0].Action)
|
||||
}
|
||||
|
||||
if auditRepo.Events[0].ResourceID != "iss-to-delete" {
|
||||
t.Errorf("expected ResourceID iss-to-delete, got %s", auditRepo.Events[0].ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_Delete_RepositoryError tests Delete when repository fails
|
||||
func TestIssuerService_Delete_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
repo.DeleteErr = errors.New("delete failed")
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
err := service.Delete(ctx, "iss-bad-id", "user-grace")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error from repository")
|
||||
}
|
||||
|
||||
if !errors.Is(err, repo.DeleteErr) {
|
||||
t.Errorf("expected error %v, got %v", repo.DeleteErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_TestConnection_Success tests successful connection test
|
||||
func TestIssuerService_TestConnection_Success(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
issuer := &domain.Issuer{
|
||||
ID: "iss-test-conn",
|
||||
Name: "Test Connection",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Enabled: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
repo.AddIssuer(issuer)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
err := service.TestConnectionWithContext(ctx, "iss-test-conn")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("TestConnectionWithContext failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_TestConnection_NotFound tests connection test when issuer not found
|
||||
func TestIssuerService_TestConnection_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
err := service.TestConnectionWithContext(ctx, "nonexistent-issuer")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent issuer")
|
||||
}
|
||||
|
||||
if !errors.Is(err, errNotFound) {
|
||||
t.Errorf("expected not found error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_ListIssuers_HandlerInterface tests handler interface method
|
||||
func TestIssuerService_ListIssuers_HandlerInterface(t *testing.T) {
|
||||
issuer1 := &domain.Issuer{
|
||||
ID: "iss-handler-1",
|
||||
Name: "Handler Test 1",
|
||||
Type: domain.IssuerTypeACME,
|
||||
Enabled: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
issuer2 := &domain.Issuer{
|
||||
ID: "iss-handler-2",
|
||||
Name: "Handler Test 2",
|
||||
Type: domain.IssuerTypeStepCA,
|
||||
Enabled: true,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
repo := newMockIssuerRepository()
|
||||
repo.AddIssuer(issuer1)
|
||||
repo.AddIssuer(issuer2)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
issuers, total, err := service.ListIssuers(1, 50)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("ListIssuers failed: %v", err)
|
||||
}
|
||||
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got %d", total)
|
||||
}
|
||||
|
||||
if len(issuers) != 2 {
|
||||
t.Errorf("expected 2 issuers, got %d", len(issuers))
|
||||
}
|
||||
|
||||
if issuers[0].Name != "Handler Test 1" && issuers[1].Name != "Handler Test 1" {
|
||||
t.Error("expected to find Handler Test 1 in results")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_CreateIssuer_HandlerInterface tests handler interface create method
|
||||
func TestIssuerService_CreateIssuer_HandlerInterface(t *testing.T) {
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
config := map[string]interface{}{"url": "https://example.com"}
|
||||
configJSON, _ := json.Marshal(config)
|
||||
|
||||
issuer := domain.Issuer{
|
||||
Name: "Handler Create Test",
|
||||
Type: domain.IssuerTypeGenericCA,
|
||||
Config: configJSON,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
result, err := service.CreateIssuer(issuer)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssuer failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if result.ID == "" {
|
||||
t.Error("expected ID to be generated")
|
||||
}
|
||||
|
||||
if result.Name != "Handler Create Test" {
|
||||
t.Errorf("expected name Handler Create Test, got %s", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssuerService_DeleteIssuer_HandlerInterface tests handler interface delete method
|
||||
func TestIssuerService_DeleteIssuer_HandlerInterface(t *testing.T) {
|
||||
repo := newMockIssuerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
service := NewIssuerService(repo, auditService)
|
||||
|
||||
err := service.DeleteIssuer("iss-handler-delete")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteIssuer failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,814 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// mockOwnerRepo is a test implementation of OwnerRepository
|
||||
type mockOwnerRepo struct {
|
||||
owners map[string]*domain.Owner
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
DeleteErr error
|
||||
GetErr error
|
||||
ListErr error
|
||||
}
|
||||
|
||||
func (m *mockOwnerRepo) List(ctx context.Context) ([]*domain.Owner, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var owners []*domain.Owner
|
||||
for _, o := range m.owners {
|
||||
owners = append(owners, o)
|
||||
}
|
||||
return owners, nil
|
||||
}
|
||||
|
||||
func (m *mockOwnerRepo) Get(ctx context.Context, id string) (*domain.Owner, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
owner, ok := m.owners[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return owner, nil
|
||||
}
|
||||
|
||||
func (m *mockOwnerRepo) Create(ctx context.Context, owner *domain.Owner) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.owners[owner.ID] = owner
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockOwnerRepo) Update(ctx context.Context, owner *domain.Owner) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.owners[owner.ID] = owner
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockOwnerRepo) Delete(ctx context.Context, id string) error {
|
||||
if m.DeleteErr != nil {
|
||||
return m.DeleteErr
|
||||
}
|
||||
delete(m.owners, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockOwnerRepo) AddOwner(owner *domain.Owner) {
|
||||
m.owners[owner.ID] = owner
|
||||
}
|
||||
|
||||
func newMockOwnerRepository() *mockOwnerRepo {
|
||||
return &mockOwnerRepo{
|
||||
owners: make(map[string]*domain.Owner),
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_List tests paginated listing of owners.
|
||||
func TestOwnerService_List(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
owner1 := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
owner2 := &domain.Owner{
|
||||
ID: "owner-002",
|
||||
Name: "Bob Jones",
|
||||
Email: "bob@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner1)
|
||||
ownerRepo.AddOwner(owner2)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owners, total, err := ownerService.List(ctx, 1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
|
||||
if len(owners) != 2 {
|
||||
t.Errorf("expected 2 owners, got %d", len(owners))
|
||||
}
|
||||
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_List_DefaultPagination tests that default pagination values are applied.
|
||||
func TestOwnerService_List_DefaultPagination(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
owner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
// Test with page < 1 (should default to 1)
|
||||
owners, total, err := ownerService.List(ctx, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
|
||||
if len(owners) != 1 {
|
||||
t.Errorf("expected 1 owner with default pagination, got %d", len(owners))
|
||||
}
|
||||
|
||||
if total != 1 {
|
||||
t.Errorf("expected total 1, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_List_RepositoryError tests handling of repository errors.
|
||||
func TestOwnerService_List_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.ListErr = errors.New("database error")
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
_, _, err := ownerService.List(ctx, 1, 50)
|
||||
if err == nil {
|
||||
t.Fatal("expected error from List, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_List_EmptyResult tests listing with no owners.
|
||||
func TestOwnerService_List_EmptyResult(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owners, total, err := ownerService.List(ctx, 1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
|
||||
if len(owners) != 0 {
|
||||
t.Errorf("expected 0 owners, got %d", len(owners))
|
||||
}
|
||||
|
||||
if total != 0 {
|
||||
t.Errorf("expected total 0, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_List_PageBeyondRange tests pagination when page exceeds available data.
|
||||
func TestOwnerService_List_PageBeyondRange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
owner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
// Request page 3 with only 1 owner
|
||||
owners, total, err := ownerService.List(ctx, 3, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("List failed: %v", err)
|
||||
}
|
||||
|
||||
if len(owners) != 0 {
|
||||
t.Errorf("expected 0 owners on page beyond range, got %d", len(owners))
|
||||
}
|
||||
|
||||
if total != 1 {
|
||||
t.Errorf("expected total 1, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Get tests retrieving a single owner by ID.
|
||||
func TestOwnerService_Get(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
owner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
retrieved, err := ownerService.Get(ctx, "owner-001")
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name != "Alice Smith" {
|
||||
t.Errorf("expected name Alice Smith, got %s", retrieved.Name)
|
||||
}
|
||||
|
||||
if retrieved.Email != "alice@example.com" {
|
||||
t.Errorf("expected email alice@example.com, got %s", retrieved.Email)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Get_NotFound tests Get with a nonexistent owner.
|
||||
func TestOwnerService_Get_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
_, err := ownerService.Get(ctx, "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent owner, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Create tests creating a new owner with audit recording.
|
||||
func TestOwnerService_Create(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owner := &domain.Owner{
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
}
|
||||
|
||||
err := ownerService.Create(ctx, owner, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
if owner.ID == "" {
|
||||
t.Fatal("expected non-empty owner ID after creation")
|
||||
}
|
||||
|
||||
if !owner.CreatedAt.IsZero() && owner.CreatedAt.After(time.Now().Add(-time.Second)) {
|
||||
// CreatedAt should have been set
|
||||
} else if owner.CreatedAt.IsZero() {
|
||||
t.Fatal("expected CreatedAt to be set")
|
||||
}
|
||||
|
||||
if len(ownerRepo.owners) != 1 {
|
||||
t.Errorf("expected 1 owner in repo, got %d", len(ownerRepo.owners))
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
|
||||
auditEvent := auditRepo.Events[0]
|
||||
if auditEvent.Action != "create_owner" {
|
||||
t.Errorf("expected action create_owner, got %s", auditEvent.Action)
|
||||
}
|
||||
|
||||
if auditEvent.ResourceType != "owner" {
|
||||
t.Errorf("expected resource type owner, got %s", auditEvent.ResourceType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Create_EmptyName tests that Create rejects empty name.
|
||||
func TestOwnerService_Create_EmptyName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owner := &domain.Owner{
|
||||
Name: "",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
}
|
||||
|
||||
err := ownerService.Create(ctx, owner, "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty owner name")
|
||||
}
|
||||
|
||||
if len(ownerRepo.owners) != 0 {
|
||||
t.Errorf("expected 0 owners in repo after validation failure, got %d", len(ownerRepo.owners))
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 0 {
|
||||
t.Errorf("expected 0 audit events after validation failure, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Create_WithExistingID tests that Create preserves existing ID.
|
||||
func TestOwnerService_Create_WithExistingID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owner := &domain.Owner{
|
||||
ID: "custom-id-123",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
}
|
||||
|
||||
err := ownerService.Create(ctx, owner, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
if owner.ID != "custom-id-123" {
|
||||
t.Errorf("expected ID custom-id-123, got %s", owner.ID)
|
||||
}
|
||||
|
||||
stored, ok := ownerRepo.owners["custom-id-123"]
|
||||
if !ok {
|
||||
t.Fatal("expected owner with custom ID in repo")
|
||||
}
|
||||
|
||||
if stored.Name != "Alice Smith" {
|
||||
t.Errorf("expected name Alice Smith, got %s", stored.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Create_RepositoryError tests Create with repository failure.
|
||||
func TestOwnerService_Create_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.CreateErr = errors.New("database error")
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owner := &domain.Owner{
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
}
|
||||
|
||||
err := ownerService.Create(ctx, owner, "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error from Create")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Update tests updating an existing owner.
|
||||
func TestOwnerService_Update(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
originalOwner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(originalOwner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
updatedOwner := &domain.Owner{
|
||||
Name: "Alice Johnson",
|
||||
Email: "alice.j@example.com",
|
||||
TeamID: "team-002",
|
||||
}
|
||||
|
||||
err := ownerService.Update(ctx, "owner-001", updatedOwner, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
|
||||
stored := ownerRepo.owners["owner-001"]
|
||||
if stored.Name != "Alice Johnson" {
|
||||
t.Errorf("expected updated name Alice Johnson, got %s", stored.Name)
|
||||
}
|
||||
|
||||
if stored.Email != "alice.j@example.com" {
|
||||
t.Errorf("expected updated email alice.j@example.com, got %s", stored.Email)
|
||||
}
|
||||
|
||||
if stored.ID != "owner-001" {
|
||||
t.Errorf("expected ID to remain owner-001, got %s", stored.ID)
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
|
||||
auditEvent := auditRepo.Events[0]
|
||||
if auditEvent.Action != "update_owner" {
|
||||
t.Errorf("expected action update_owner, got %s", auditEvent.Action)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Update_EmptyName tests that Update rejects empty name.
|
||||
func TestOwnerService_Update_EmptyName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
originalOwner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(originalOwner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
updatedOwner := &domain.Owner{
|
||||
Name: "",
|
||||
Email: "alice.j@example.com",
|
||||
TeamID: "team-002",
|
||||
}
|
||||
|
||||
err := ownerService.Update(ctx, "owner-001", updatedOwner, "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty owner name")
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 0 {
|
||||
t.Errorf("expected 0 audit events after validation failure, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Update_RepositoryError tests Update with repository failure.
|
||||
func TestOwnerService_Update_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.UpdateErr = errors.New("database error")
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
updatedOwner := &domain.Owner{
|
||||
Name: "Alice Johnson",
|
||||
Email: "alice.j@example.com",
|
||||
TeamID: "team-002",
|
||||
}
|
||||
|
||||
err := ownerService.Update(ctx, "owner-001", updatedOwner, "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error from Update")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Delete tests deleting an owner with audit recording.
|
||||
func TestOwnerService_Delete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
owner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
err := ownerService.Delete(ctx, "owner-001", "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Delete failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ownerRepo.owners) != 0 {
|
||||
t.Errorf("expected 0 owners in repo after delete, got %d", len(ownerRepo.owners))
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
|
||||
auditEvent := auditRepo.Events[0]
|
||||
if auditEvent.Action != "delete_owner" {
|
||||
t.Errorf("expected action delete_owner, got %s", auditEvent.Action)
|
||||
}
|
||||
|
||||
if auditEvent.ResourceID != "owner-001" {
|
||||
t.Errorf("expected resource ID owner-001, got %s", auditEvent.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_Delete_RepositoryError tests Delete with repository failure.
|
||||
func TestOwnerService_Delete_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.DeleteErr = errors.New("database error")
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
err := ownerService.Delete(ctx, "owner-001", "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error from Delete")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_ListOwners_HandlerInterface tests the handler interface method ListOwners.
|
||||
func TestOwnerService_ListOwners_HandlerInterface(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
owner1 := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
owner2 := &domain.Owner{
|
||||
ID: "owner-002",
|
||||
Name: "Bob Jones",
|
||||
Email: "bob@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner1)
|
||||
ownerRepo.AddOwner(owner2)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owners, total, err := ownerService.ListOwners(1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ListOwners failed: %v", err)
|
||||
}
|
||||
|
||||
if len(owners) != 2 {
|
||||
t.Errorf("expected 2 owners, got %d", len(owners))
|
||||
}
|
||||
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got %d", total)
|
||||
}
|
||||
|
||||
// Verify value type conversion worked
|
||||
if owners[0].ID == "" {
|
||||
t.Fatal("expected non-empty owner ID in result")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_GetOwner_HandlerInterface tests the handler interface method GetOwner.
|
||||
func TestOwnerService_GetOwner_HandlerInterface(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
owner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
retrieved, err := ownerService.GetOwner("owner-001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetOwner failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name != "Alice Smith" {
|
||||
t.Errorf("expected name Alice Smith, got %s", retrieved.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_CreateOwner_HandlerInterface tests the handler interface method CreateOwner.
|
||||
func TestOwnerService_CreateOwner_HandlerInterface(t *testing.T) {
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
owner := domain.Owner{
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
}
|
||||
|
||||
created, err := ownerService.CreateOwner(owner)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateOwner failed: %v", err)
|
||||
}
|
||||
|
||||
if created.ID == "" {
|
||||
t.Fatal("expected non-empty owner ID after creation")
|
||||
}
|
||||
|
||||
if created.Name != "Alice Smith" {
|
||||
t.Errorf("expected name Alice Smith, got %s", created.Name)
|
||||
}
|
||||
|
||||
if len(ownerRepo.owners) != 1 {
|
||||
t.Errorf("expected 1 owner in repo, got %d", len(ownerRepo.owners))
|
||||
}
|
||||
|
||||
// Note: handler interface method does NOT record audit events (no actor parameter)
|
||||
if len(auditRepo.Events) != 0 {
|
||||
t.Errorf("expected 0 audit events from handler interface method, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_UpdateOwner_HandlerInterface tests the handler interface method UpdateOwner.
|
||||
func TestOwnerService_UpdateOwner_HandlerInterface(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
originalOwner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(originalOwner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
updatedOwner := domain.Owner{
|
||||
Name: "Alice Johnson",
|
||||
Email: "alice.j@example.com",
|
||||
TeamID: "team-002",
|
||||
}
|
||||
|
||||
updated, err := ownerService.UpdateOwner("owner-001", updatedOwner)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateOwner failed: %v", err)
|
||||
}
|
||||
|
||||
if updated.ID != "owner-001" {
|
||||
t.Errorf("expected ID owner-001, got %s", updated.ID)
|
||||
}
|
||||
|
||||
if updated.Name != "Alice Johnson" {
|
||||
t.Errorf("expected updated name Alice Johnson, got %s", updated.Name)
|
||||
}
|
||||
|
||||
// Verify in repo
|
||||
stored := ownerRepo.owners["owner-001"]
|
||||
if stored.Email != "alice.j@example.com" {
|
||||
t.Errorf("expected updated email alice.j@example.com, got %s", stored.Email)
|
||||
}
|
||||
|
||||
// Note: handler interface method does NOT record audit events
|
||||
if len(auditRepo.Events) != 0 {
|
||||
t.Errorf("expected 0 audit events from handler interface method, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerService_DeleteOwner_HandlerInterface tests the handler interface method DeleteOwner.
|
||||
func TestOwnerService_DeleteOwner_HandlerInterface(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
owner := &domain.Owner{
|
||||
ID: "owner-001",
|
||||
Name: "Alice Smith",
|
||||
Email: "alice@example.com",
|
||||
TeamID: "team-001",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
ownerRepo := newMockOwnerRepository()
|
||||
ownerRepo.AddOwner(owner)
|
||||
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
ownerService := NewOwnerService(ownerRepo, auditService)
|
||||
|
||||
err := ownerService.DeleteOwner("owner-001")
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteOwner failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ownerRepo.owners) != 0 {
|
||||
t.Errorf("expected 0 owners in repo after delete, got %d", len(ownerRepo.owners))
|
||||
}
|
||||
|
||||
// Note: handler interface method does NOT record audit events
|
||||
if len(auditRepo.Events) != 0 {
|
||||
t.Errorf("expected 0 audit events from handler interface method, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
)
|
||||
|
||||
// mockTeamRepo is a test implementation of TeamRepository
|
||||
type mockTeamRepo struct {
|
||||
teams map[string]*domain.Team
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
DeleteErr error
|
||||
GetErr error
|
||||
ListErr error
|
||||
}
|
||||
|
||||
func (m *mockTeamRepo) List(ctx context.Context) ([]*domain.Team, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var teams []*domain.Team
|
||||
for _, t := range m.teams {
|
||||
teams = append(teams, t)
|
||||
}
|
||||
return teams, nil
|
||||
}
|
||||
|
||||
func (m *mockTeamRepo) Get(ctx context.Context, id string) (*domain.Team, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
team, ok := m.teams[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
func (m *mockTeamRepo) Create(ctx context.Context, team *domain.Team) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.teams[team.ID] = team
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTeamRepo) Update(ctx context.Context, team *domain.Team) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.teams[team.ID] = team
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTeamRepo) Delete(ctx context.Context, id string) error {
|
||||
if m.DeleteErr != nil {
|
||||
return m.DeleteErr
|
||||
}
|
||||
delete(m.teams, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTeamRepo) AddTeam(team *domain.Team) {
|
||||
m.teams[team.ID] = team
|
||||
}
|
||||
|
||||
func newMockTeamRepository() *mockTeamRepo {
|
||||
return &mockTeamRepo{
|
||||
teams: make(map[string]*domain.Team),
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_List tests retrieving teams with pagination
|
||||
func TestTeamService_List(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Add test teams
|
||||
for i := 0; i < 5; i++ {
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "team-" + string(rune(i)),
|
||||
Name: "Team " + string(rune(48+i)),
|
||||
})
|
||||
}
|
||||
|
||||
teams, total, err := teamService.List(ctx, 1, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if total != 5 {
|
||||
t.Errorf("expected total 5, got %d", total)
|
||||
}
|
||||
|
||||
if len(teams) != 2 {
|
||||
t.Errorf("expected 2 teams on page 1, got %d", len(teams))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_List_DefaultPagination tests default pagination values
|
||||
func TestTeamService_List_DefaultPagination(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Add test teams
|
||||
for i := 0; i < 10; i++ {
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "team-" + string(rune(i)),
|
||||
Name: "Team " + string(rune(48+i)),
|
||||
})
|
||||
}
|
||||
|
||||
// Test page < 1 defaults to 1
|
||||
teams, total, err := teamService.List(ctx, 0, 5)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if total != 10 {
|
||||
t.Errorf("expected total 10, got %d", total)
|
||||
}
|
||||
|
||||
if len(teams) != 5 {
|
||||
t.Errorf("expected 5 teams, got %d", len(teams))
|
||||
}
|
||||
|
||||
// Test perPage < 1 defaults to 50
|
||||
teams, total, err = teamService.List(ctx, 1, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if len(teams) != 10 {
|
||||
t.Errorf("expected 10 teams with perPage=50, got %d", len(teams))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_List_RepositoryError tests error handling from repo
|
||||
func TestTeamService_List_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
mockTeamRepo.ListErr = errors.New("database error")
|
||||
|
||||
_, _, err := teamService.List(ctx, 1, 50)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, errors.New("database error")) {
|
||||
t.Errorf("expected database error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_List_EmptyResult tests empty list response
|
||||
func TestTeamService_List_EmptyResult(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
teams, total, err := teamService.List(ctx, 1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if total != 0 {
|
||||
t.Errorf("expected total 0, got %d", total)
|
||||
}
|
||||
|
||||
if len(teams) != 0 {
|
||||
t.Errorf("expected empty slice, got %d teams", len(teams))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_List_PageBeyondRange tests pagination beyond available data
|
||||
func TestTeamService_List_PageBeyondRange(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Add only 3 teams
|
||||
for i := 0; i < 3; i++ {
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "team-" + string(rune(i)),
|
||||
Name: "Team " + string(rune(48+i)),
|
||||
})
|
||||
}
|
||||
|
||||
// Request page beyond range
|
||||
teams, total, err := teamService.List(ctx, 10, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if total != 3 {
|
||||
t.Errorf("expected total 3, got %d", total)
|
||||
}
|
||||
|
||||
if teams != nil && len(teams) != 0 {
|
||||
t.Errorf("expected empty slice for page beyond range, got %d teams", len(teams))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Get tests retrieving a single team
|
||||
func TestTeamService_Get(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
testTeam := &domain.Team{
|
||||
ID: "team-1",
|
||||
Name: "Test Team",
|
||||
}
|
||||
mockTeamRepo.AddTeam(testTeam)
|
||||
|
||||
team, err := teamService.Get(ctx, "team-1")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if team.ID != "team-1" || team.Name != "Test Team" {
|
||||
t.Errorf("expected team-1/Test Team, got %s/%s", team.ID, team.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Get_NotFound tests retrieval of nonexistent team
|
||||
func TestTeamService_Get_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
_, err := teamService.Get(ctx, "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for nonexistent team, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Create tests creating a new team
|
||||
func TestTeamService_Create(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
team := &domain.Team{
|
||||
Name: "New Team",
|
||||
Description: "A test team",
|
||||
}
|
||||
|
||||
err := teamService.Create(ctx, team, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify ID was generated
|
||||
if team.ID == "" {
|
||||
t.Errorf("expected ID to be generated, got empty")
|
||||
}
|
||||
|
||||
if !team.ID[:5] == "team-" {
|
||||
t.Logf("note: generated ID is %s", team.ID)
|
||||
}
|
||||
|
||||
// Verify timestamps were set
|
||||
if team.CreatedAt.IsZero() {
|
||||
t.Errorf("expected CreatedAt to be set")
|
||||
}
|
||||
|
||||
if team.UpdatedAt.IsZero() {
|
||||
t.Errorf("expected UpdatedAt to be set")
|
||||
}
|
||||
|
||||
// Verify team was stored
|
||||
stored, err := teamService.Get(ctx, team.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve created team: %v", err)
|
||||
}
|
||||
|
||||
if stored.Name != "New Team" {
|
||||
t.Errorf("expected name 'New Team', got %s", stored.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Create_EmptyName tests validation on empty name
|
||||
func TestTeamService_Create_EmptyName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
team := &domain.Team{
|
||||
Name: "",
|
||||
}
|
||||
|
||||
err := teamService.Create(ctx, team, "test-user")
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error for empty name, got nil")
|
||||
}
|
||||
|
||||
if !errors.Is(err, errors.New("team name is required")) {
|
||||
t.Logf("error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Create_WithExistingID tests preserving provided ID
|
||||
func TestTeamService_Create_WithExistingID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
team := &domain.Team{
|
||||
ID: "custom-team-id",
|
||||
Name: "Custom Team",
|
||||
}
|
||||
|
||||
err := teamService.Create(ctx, team, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if team.ID != "custom-team-id" {
|
||||
t.Errorf("expected ID to be preserved as custom-team-id, got %s", team.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Create_RepositoryError tests repo error handling
|
||||
func TestTeamService_Create_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
mockTeamRepo.CreateErr = errors.New("database insert failed")
|
||||
|
||||
team := &domain.Team{
|
||||
Name: "Test Team",
|
||||
}
|
||||
|
||||
err := teamService.Create(ctx, team, "test-user")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Create_AuditRecorded tests audit event recording
|
||||
func TestTeamService_Create_AuditRecorded(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
team := &domain.Team{
|
||||
ID: "audit-test-team",
|
||||
Name: "Audit Test Team",
|
||||
}
|
||||
|
||||
err := teamService.Create(ctx, team, "audit-user")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify audit event was recorded
|
||||
if len(mockAuditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(mockAuditRepo.Events))
|
||||
}
|
||||
|
||||
if mockAuditRepo.Events[0].Action != "create_team" {
|
||||
t.Errorf("expected action 'create_team', got %s", mockAuditRepo.Events[0].Action)
|
||||
}
|
||||
|
||||
if mockAuditRepo.Events[0].ResourceID != "audit-test-team" {
|
||||
t.Errorf("expected resource ID 'audit-test-team', got %s", mockAuditRepo.Events[0].ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Update tests updating an existing team
|
||||
func TestTeamService_Update(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Create initial team
|
||||
initialTeam := &domain.Team{
|
||||
ID: "team-update",
|
||||
Name: "Original Name",
|
||||
Description: "Original description",
|
||||
}
|
||||
mockTeamRepo.AddTeam(initialTeam)
|
||||
|
||||
// Update team
|
||||
updateTeam := &domain.Team{
|
||||
Name: "Updated Name",
|
||||
Description: "Updated description",
|
||||
}
|
||||
|
||||
err := teamService.Update(ctx, "team-update", updateTeam, "update-user")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify ID was set correctly
|
||||
if updateTeam.ID != "team-update" {
|
||||
t.Errorf("expected ID to be set to team-update, got %s", updateTeam.ID)
|
||||
}
|
||||
|
||||
// Verify team was updated
|
||||
updated, err := teamService.Get(ctx, "team-update")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to retrieve updated team: %v", err)
|
||||
}
|
||||
|
||||
if updated.Name != "Updated Name" {
|
||||
t.Errorf("expected name 'Updated Name', got %s", updated.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Update_EmptyName tests validation on update
|
||||
func TestTeamService_Update_EmptyName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "team-1",
|
||||
Name: "Original",
|
||||
})
|
||||
|
||||
updateTeam := &domain.Team{
|
||||
Name: "",
|
||||
}
|
||||
|
||||
err := teamService.Update(ctx, "team-1", updateTeam, "user")
|
||||
if err == nil {
|
||||
t.Fatalf("expected validation error for empty name, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Update_RepositoryError tests repo error handling
|
||||
func TestTeamService_Update_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
mockTeamRepo.UpdateErr = errors.New("database update failed")
|
||||
|
||||
updateTeam := &domain.Team{
|
||||
Name: "Updated",
|
||||
}
|
||||
|
||||
err := teamService.Update(ctx, "team-1", updateTeam, "user")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Delete tests deleting a team
|
||||
func TestTeamService_Delete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Create team to delete
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "team-delete",
|
||||
Name: "Team to Delete",
|
||||
})
|
||||
|
||||
err := teamService.Delete(ctx, "team-delete", "delete-user")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify team was deleted
|
||||
_, err = teamService.Get(ctx, "team-delete")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for deleted team, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_Delete_RepositoryError tests repo error handling
|
||||
func TestTeamService_Delete_RepositoryError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
mockTeamRepo.DeleteErr = errors.New("database delete failed")
|
||||
|
||||
err := teamService.Delete(ctx, "team-1", "user")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_ListTeams_HandlerInterface tests handler interface method
|
||||
func TestTeamService_ListTeams_HandlerInterface(t *testing.T) {
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Add test teams
|
||||
for i := 0; i < 3; i++ {
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "team-" + string(rune(i)),
|
||||
Name: "Team " + string(rune(48+i)),
|
||||
})
|
||||
}
|
||||
|
||||
teams, total, err := teamService.ListTeams(1, 2)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if total != 3 {
|
||||
t.Errorf("expected total 3, got %d", total)
|
||||
}
|
||||
|
||||
if len(teams) != 3 {
|
||||
t.Errorf("expected 3 teams (ListTeams doesn't paginate), got %d", len(teams))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_GetTeam_HandlerInterface tests handler interface method
|
||||
func TestTeamService_GetTeam_HandlerInterface(t *testing.T) {
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
testTeam := &domain.Team{
|
||||
ID: "handler-team",
|
||||
Name: "Handler Test Team",
|
||||
}
|
||||
mockTeamRepo.AddTeam(testTeam)
|
||||
|
||||
team, err := teamService.GetTeam("handler-team")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if team.ID != "handler-team" || team.Name != "Handler Test Team" {
|
||||
t.Errorf("expected handler-team/Handler Test Team, got %s/%s", team.ID, team.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_CreateTeam_HandlerInterface tests handler interface method
|
||||
func TestTeamService_CreateTeam_HandlerInterface(t *testing.T) {
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
team := domain.Team{
|
||||
Name: "Handler Create Team",
|
||||
Description: "Created via handler",
|
||||
}
|
||||
|
||||
result, err := teamService.CreateTeam(team)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.ID == "" {
|
||||
t.Errorf("expected ID to be generated")
|
||||
}
|
||||
|
||||
if result.Name != "Handler Create Team" {
|
||||
t.Errorf("expected name 'Handler Create Team', got %s", result.Name)
|
||||
}
|
||||
|
||||
if result.CreatedAt.IsZero() {
|
||||
t.Errorf("expected CreatedAt to be set")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_UpdateTeam_HandlerInterface tests handler interface method
|
||||
func TestTeamService_UpdateTeam_HandlerInterface(t *testing.T) {
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Create initial team
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "handler-update-team",
|
||||
Name: "Original",
|
||||
})
|
||||
|
||||
updateTeam := domain.Team{
|
||||
Name: "Updated via Handler",
|
||||
Description: "Handler update",
|
||||
}
|
||||
|
||||
result, err := teamService.UpdateTeam("handler-update-team", updateTeam)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.ID != "handler-update-team" {
|
||||
t.Errorf("expected ID handler-update-team, got %s", result.ID)
|
||||
}
|
||||
|
||||
if result.Name != "Updated via Handler" {
|
||||
t.Errorf("expected name 'Updated via Handler', got %s", result.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_DeleteTeam_HandlerInterface tests handler interface method
|
||||
func TestTeamService_DeleteTeam_HandlerInterface(t *testing.T) {
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
mockAuditRepo := newMockAuditRepository()
|
||||
auditService := NewAuditService(mockAuditRepo)
|
||||
teamService := NewTeamService(mockTeamRepo, auditService)
|
||||
|
||||
// Create team to delete
|
||||
mockTeamRepo.AddTeam(&domain.Team{
|
||||
ID: "handler-delete-team",
|
||||
Name: "To Delete",
|
||||
})
|
||||
|
||||
err := teamService.DeleteTeam("handler-delete-team")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify deletion
|
||||
_, err = mockTeamRepo.Get(context.Background(), "handler-delete-team")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for deleted team")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTeamService_NilAuditService tests behavior when audit service is nil
|
||||
func TestTeamService_NilAuditService(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mockTeamRepo := newMockTeamRepository()
|
||||
teamService := NewTeamService(mockTeamRepo, nil)
|
||||
|
||||
team := &domain.Team{
|
||||
Name: "Test Team",
|
||||
}
|
||||
|
||||
// Should not panic with nil audit service
|
||||
err := teamService.Create(ctx, team, "user")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if team.ID == "" {
|
||||
t.Errorf("expected ID to be generated")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user