package handlers import ( "bytes" "context" "encoding/json" "log/slog" "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, nil) t.Run("sets non-nil service", func(t *testing.T) { repo := newMockOperationRepo() opSvc := service.NewOperationService(repo, slog.Default()) 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, slog.Default()) 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, slog.Default()) 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, slog.Default()) 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, slog.Default()) mock := &mockComponentService{ addComponent: func(ctx context.Context, projectID string, req port.AddComponentRequest) (*domain.Component, error) { return nil, domain.ErrInvalidComponentType }, } handler := NewComponentsHandler(mock, slog.Default()) 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") } } }