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:
shankar0123
2026-03-21 23:43:32 -04:00
parent aa183efdca
commit 690765b53e
12 changed files with 4991 additions and 8 deletions
+558
View File
@@ -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")
}
+631
View File
@@ -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")
}
}
+279
View File
@@ -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)
}
})
})
}
+699
View File
@@ -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")
}
}
+329
View File
@@ -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)
}
}
+601
View File
@@ -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)
}
}
+814
View File
@@ -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))
}
}
+691
View File
@@ -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")
}
}