rdev/internal/handlers/project_management_test.go
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
Major changes:
- Add internal/logging package with field constants, context propagation,
  sensitive data auto-redaction, and per-component log levels
- Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.)
- Extend SDLC with callback handlers, generate endpoints, and executor
- Add new cookbook trees for aeries and slackpath progression
- Add skeleton templates for queue, realtime, and microservices
- Add worker component template with async job processing
- Refactor services and handlers to use new logging infrastructure
- Split component.go into component_infra.go and component_listing.go

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:56:04 -07:00

195 lines
6.1 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/service"
)
func TestProjectManagementHandler_NilService(t *testing.T) {
h := NewProjectManagementHandler(nil)
r := chi.NewRouter()
r.Use(testAdminAuth)
h.Mount(r)
tests := []struct {
name string
method string
path string
body string
}{
{"create", "POST", "/project", `{"name":"test"}`},
{"list", "GET", "/project", ""},
{"status", "GET", "/project/test", ""},
{"delete", "DELETE", "/project/test", ""},
{"list templates", "GET", "/templates", ""},
{"list component templates", "GET", "/templates/components", ""},
{"get template", "GET", "/templates/default", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var req *http.Request
if tt.body != "" {
req = httptest.NewRequest(tt.method, tt.path, bytes.NewReader([]byte(tt.body)))
} else {
req = httptest.NewRequest(tt.method, tt.path, nil)
}
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Errorf("%s: status = %d, want %d", tt.name, rec.Code, http.StatusInternalServerError)
}
})
}
}
func TestProjectManagementHandler_CreateValidation(t *testing.T) {
// With nil service, the handler returns 500 before reaching validation.
// This tests that the nil check takes precedence.
h := NewProjectManagementHandler(nil)
r := chi.NewRouter()
r.Use(testAdminAuth)
h.Mount(r)
t.Run("nil service returns 500 even with missing name", func(t *testing.T) {
body, _ := json.Marshal(CreateRequest{Name: ""})
req := httptest.NewRequest("POST", "/project", bytes.NewReader(body))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
}
})
t.Run("nil service returns 500 even with invalid json", func(t *testing.T) {
req := httptest.NewRequest("POST", "/project", bytes.NewReader([]byte("not json")))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
}
})
}
// Test removed: logger field removed from handler
func TestProjectManagementHandler_SetOperationService(t *testing.T) {
h := NewProjectManagementHandler(nil)
t.Run("sets non-nil service", func(t *testing.T) {
repo := newMockOperationRepo()
opSvc := service.NewOperationService(repo)
result := h.SetOperationService(opSvc)
if h.operationService != opSvc {
t.Error("expected operation service to be set")
}
if result != h {
t.Error("expected fluent return of handler")
}
})
t.Run("ignores nil service", func(t *testing.T) {
repo := newMockOperationRepo()
opSvc := service.NewOperationService(repo)
h.SetOperationService(opSvc) // set first
h.SetOperationService(nil) // should not clear
if h.operationService != opSvc {
t.Error("nil should not clear operation service")
}
})
}
func TestProjectManagementHandler_CreateTracksOperation(t *testing.T) {
// This test verifies that the Create handler starts and completes operations
// when an operationService is configured. Since we can't easily mock
// ProjectInfraService (concrete struct), we test that nil infraService
// still doesn't panic when operationService is set.
repo := newMockOperationRepo()
opSvc := service.NewOperationService(repo)
h := NewProjectManagementHandler(nil).
SetOperationService(opSvc)
r := chi.NewRouter()
r.Use(testAdminAuth)
h.Mount(r)
body, _ := json.Marshal(CreateRequest{Name: "test-project"})
req := httptest.NewRequest("POST", "/project", bytes.NewReader(body))
req.Header.Set("X-Request-ID", "req-123")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
// Handler returns 500 because infraService is nil, but operation should NOT
// have been started because the nil-service check happens before operation tracking.
if rec.Code != http.StatusInternalServerError {
t.Errorf("status = %d, want %d", rec.Code, http.StatusInternalServerError)
}
// Operation should not be created because infraService nil check comes first
if repo.count() != 0 {
t.Errorf("expected no operations (nil service returns early), got %d", repo.count())
}
}
func TestProjectManagementHandler_OperationIDInResponse(t *testing.T) {
// Verify the response shape includes operation_id field when it would be set.
// We test the response structure by examining what Create() writes.
// Since we can't mock the concrete ProjectInfraService, this is a structural test
// verifying the handler properly sets the operationService field.
h := NewProjectManagementHandler(nil)
if h.operationService != nil {
t.Error("operationService should be nil by default")
}
repo := newMockOperationRepo()
opSvc := service.NewOperationService(repo)
h.SetOperationService(opSvc)
if h.operationService == nil {
t.Error("operationService should be set after SetOperationService")
}
}
func TestProjectManagementHandler_OperationFailsOnError(t *testing.T) {
// When operationService is set but create fails, the operation should be marked failed.
// Since nil infraService returns 500 before reaching operation tracking, we verify
// that operations are not leaked when the handler exits early.
repo := newMockOperationRepo()
opSvc := service.NewOperationService(repo)
h := &ProjectManagementHandler{
infraService: nil, // will cause 500
operationService: opSvc,
}
r := chi.NewRouter()
r.Post("/project", h.Create)
body, _ := json.Marshal(CreateRequest{Name: "fail-project"})
req := httptest.NewRequest("POST", "/project", bytes.NewReader(body))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
// The nil infraService check happens before operation tracking starts,
// so no operation should exist
if repo.count() != 0 {
// If an operation was created, verify it was marked as failed
for _, op := range repo.operations {
if op.Status != domain.OperationStatusFailed {
t.Errorf("expected operation status failed, got %s", op.Status)
}
}
}
}