rdev/internal/handlers/components_operations_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

158 lines
4.5 KiB
Go

package handlers
import (
"bytes"
"context"
"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/port"
"github.com/orchard9/rdev/internal/service"
)
func TestComponentsHandler_SetOperationService(t *testing.T) {
h := NewComponentsHandler(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)
h.SetOperationService(nil)
if h.operationService != opSvc {
t.Error("nil should not clear operation service")
}
})
}
func TestComponentsHandler_AddTracksOperation(t *testing.T) {
opRepo := newMockOperationRepo()
opSvc := service.NewOperationService(opRepo)
mock := &mockComponentService{
addComponent: func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
return &domain.Component{
Type: domain.ComponentTypeService,
Name: "auth-api",
Path: "services/auth-api",
Port: 8001,
Template: "service",
Dependencies: []string{},
}, nil
},
}
handler := NewComponentsHandler(mock)
handler.SetOperationService(opSvc)
body, _ := json.Marshal(AddComponentRequest{Type: "service", Name: "auth-api"})
req := httptest.NewRequest(http.MethodPost, "/projects/my-project/components", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Request-ID", "req-456")
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "my-project")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
rec := httptest.NewRecorder()
handler.Add(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected status 201, got %d. Body: %s", rec.Code, rec.Body.String())
}
// Verify operation was created and completed
if opRepo.count() != 1 {
t.Fatalf("expected 1 operation, got %d", opRepo.count())
}
// Verify operation status is completed
for _, op := range opRepo.operations {
if op.Status != domain.OperationStatusCompleted {
t.Errorf("expected operation status completed, got %s", op.Status)
}
if op.ProjectID != "my-project" {
t.Errorf("expected project_id my-project, got %s", op.ProjectID)
}
if op.Type != domain.OperationTypeComponentAdd {
t.Errorf("expected operation type component.add, got %s", op.Type)
}
if op.RequestID != "req-456" {
t.Errorf("expected request_id req-456, got %s", op.RequestID)
}
}
// Verify response contains operation_id
var resp struct {
Data map[string]any `json:"data"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("failed to unmarshal response: %v", err)
}
opID, ok := resp.Data["operation_id"]
if !ok {
t.Error("response missing operation_id field")
}
if opID == "" {
t.Error("operation_id should not be empty")
}
}
func TestComponentsHandler_AddFailsOperationOnError(t *testing.T) {
opRepo := newMockOperationRepo()
opSvc := service.NewOperationService(opRepo)
mock := &mockComponentService{
addComponent: func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) {
return nil, domain.ErrInvalidComponentType
},
}
handler := NewComponentsHandler(mock)
handler.SetOperationService(opSvc)
body, _ := json.Marshal(AddComponentRequest{Type: "invalid", Name: "auth-api"})
req := httptest.NewRequest(http.MethodPost, "/projects/my-project/components", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rctx := chi.NewRouteContext()
rctx.URLParams.Add("id", "my-project")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
rec := httptest.NewRecorder()
handler.Add(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", rec.Code)
}
// Operation should be created and marked as failed
if opRepo.count() != 1 {
t.Fatalf("expected 1 operation, got %d", opRepo.count())
}
for _, op := range opRepo.operations {
if op.Status != domain.OperationStatusFailed {
t.Errorf("expected operation status failed, got %s", op.Status)
}
if op.Error == "" {
t.Error("expected error message on failed operation")
}
}
}