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>
158 lines
4.5 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|