package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/adapter/memory" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/service" ) func TestVerifyHandler_Submit_Success(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) body := SubmitVerifyRequest{ ProjectID: "my-project", URL: "https://example.com", Viewports: []string{"1920x1080", "375x667"}, } bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/verify", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusCreated { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusCreated, rec.Body.String()) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } data, ok := resp["data"].(map[string]any) if !ok { t.Fatalf("expected data to be map, got %T", resp["data"]) } if data["task_id"] == nil || data["task_id"] == "" { t.Error("expected task_id in response") } if data["status_url"] == nil { t.Error("expected status_url in response") } if data["stream_url"] == nil { t.Error("expected stream_url in response") } } func TestVerifyHandler_Submit_MissingURL(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) body := SubmitVerifyRequest{ ProjectID: "my-project", // URL missing } bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/verify", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusBadRequest { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusBadRequest, rec.Body.String()) } } func TestVerifyHandler_Submit_InvalidURL(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) body := SubmitVerifyRequest{ ProjectID: "my-project", URL: "not-a-valid-url", } bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/verify", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { // Invalid URL scheme gets caught as an internal error after service processes t.Logf("got status %d; body: %s", rec.Code, rec.Body.String()) } } func TestVerifyHandler_Get_Found(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) // Pre-populate a verify task queue.tasks["verify-task-1"] = &domain.WorkTask{ ID: "verify-task-1", ProjectID: "my-project", Type: domain.WorkTaskTypeVerify, Status: domain.WorkTaskStatusCompleted, Spec: map[string]any{ "url": "https://example.com", "viewports": []string{"1920x1080"}, }, Result: &domain.WorkResult{ Artifacts: map[string]string{ "screenshot_1920x1080": "/captures/verify-task-1/1920_1080.png", "duration_ms": "2500", }, }, CreatedAt: time.Now(), } router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) req := httptest.NewRequest(http.MethodGet, "/verify/verify-task-1", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } data, ok := resp["data"].(map[string]any) if !ok { t.Fatalf("expected data to be map, got %T", resp["data"]) } if data["id"] != "verify-task-1" { t.Errorf("got id=%v, want verify-task-1", data["id"]) } if data["status"] != "completed" { t.Errorf("got status=%v, want completed", data["status"]) } if data["url"] != "https://example.com" { t.Errorf("got url=%v, want https://example.com", data["url"]) } screenshots, ok := data["screenshots"].(map[string]any) if !ok { t.Fatalf("expected screenshots to be map, got %T", data["screenshots"]) } if screenshots["1920x1080"] == nil { t.Error("expected screenshot for 1920x1080 viewport") } } func TestVerifyHandler_Get_NotFound(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) req := httptest.NewRequest(http.MethodGet, "/verify/nonexistent", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusNotFound { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusNotFound, rec.Body.String()) } } func TestVerifyHandler_Cancel_Pending(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) // Pre-populate a pending verify task queue.tasks["verify-task-2"] = &domain.WorkTask{ ID: "verify-task-2", ProjectID: "my-project", Type: domain.WorkTaskTypeVerify, Status: domain.WorkTaskStatusPending, Spec: map[string]any{ "url": "https://example.com", }, CreatedAt: time.Now(), } router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) req := httptest.NewRequest(http.MethodDelete, "/verify/verify-task-2", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String()) } // Verify task was cancelled if queue.tasks["verify-task-2"].Status != domain.WorkTaskStatusCancelled { t.Errorf("expected task status to be cancelled, got %s", queue.tasks["verify-task-2"].Status) } } func TestVerifyHandler_Cancel_NotPending(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) // Pre-populate a running verify task (can't be cancelled) queue.tasks["verify-task-3"] = &domain.WorkTask{ ID: "verify-task-3", ProjectID: "my-project", Type: domain.WorkTaskTypeVerify, Status: domain.WorkTaskStatusRunning, Spec: map[string]any{ "url": "https://example.com", }, CreatedAt: time.Now(), } router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) req := httptest.NewRequest(http.MethodDelete, "/verify/verify-task-3", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) // Should fail because task is not pending if rec.Code != http.StatusNotFound && rec.Code != http.StatusBadRequest { t.Errorf("got status %d, want 404 or 400; body: %s", rec.Code, rec.Body.String()) } } func TestVerifyHandler_ListByProject(t *testing.T) { queue := newMockWorkQueue() verifyService := service.NewVerifyService(queue, nil) streams := memory.NewStreamPublisher() handler := NewVerifyHandler(verifyService, streams) // Pre-populate verify tasks for different projects queue.tasks["verify-1"] = &domain.WorkTask{ ID: "verify-1", ProjectID: "project-a", Type: domain.WorkTaskTypeVerify, Status: domain.WorkTaskStatusCompleted, Spec: map[string]any{"url": "https://example.com/1"}, CreatedAt: time.Now(), } queue.tasks["verify-2"] = &domain.WorkTask{ ID: "verify-2", ProjectID: "project-a", Type: domain.WorkTaskTypeVerify, Status: domain.WorkTaskStatusPending, Spec: map[string]any{"url": "https://example.com/2"}, CreatedAt: time.Now(), } queue.tasks["verify-3"] = &domain.WorkTask{ ID: "verify-3", ProjectID: "project-b", Type: domain.WorkTaskTypeVerify, Status: domain.WorkTaskStatusCompleted, Spec: map[string]any{"url": "https://example.com/3"}, CreatedAt: time.Now(), } // Non-verify task should be filtered out queue.tasks["build-1"] = &domain.WorkTask{ ID: "build-1", ProjectID: "project-a", Type: domain.WorkTaskTypeBuild, Status: domain.WorkTaskStatusCompleted, Spec: map[string]any{"prompt": "build something"}, CreatedAt: time.Now(), } router := chi.NewRouter() router.Use(testAdminAuth) handler.Mount(router) req := httptest.NewRequest(http.MethodGet, "/projects/project-a/verify", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Errorf("got status %d, want %d; body: %s", rec.Code, http.StatusOK, rec.Body.String()) } var resp map[string]any if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil { t.Fatalf("failed to unmarshal response: %v", err) } data, ok := resp["data"].(map[string]any) if !ok { t.Fatalf("expected data to be map, got %T", resp["data"]) } tasks, ok := data["tasks"].([]any) if !ok { t.Fatalf("expected tasks to be array, got %T", data["tasks"]) } // Should only get 2 verify tasks for project-a (build task filtered out) if len(tasks) != 2 { t.Errorf("got %d tasks, want 2", len(tasks)) } if data["project_id"] != "project-a" { t.Errorf("got project_id=%v, want project-a", data["project_id"]) } }