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>
346 lines
9.9 KiB
Go
346 lines
9.9 KiB
Go
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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"])
|
|
}
|
|
}
|