WitNote / internal /handlers /handlers_test.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
package handlers
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/chromedp/cdproto/target"
"github.com/chromedp/chromedp"
"github.com/pinchtab/pinchtab/internal/bridge"
"github.com/pinchtab/pinchtab/internal/config"
)
type mockBridge struct {
bridge.BridgeAPI
failTab bool
}
func (m *mockBridge) TabContext(tabID string) (context.Context, string, error) {
if m.failTab {
return nil, "", fmt.Errorf("tab not found")
}
// We need a context that chromedp.Run won't complain about,
// even if it's not fully functional for real CDP commands.
ctx, _ := chromedp.NewContext(context.Background())
return ctx, "tab1", nil
}
func (m *mockBridge) ListTargets() ([]*target.Info, error) {
return []*target.Info{{TargetID: "tab1", Type: "page"}}, nil
}
func (m *mockBridge) AvailableActions() []string {
return []string{bridge.ActionClick, bridge.ActionType}
}
func (m *mockBridge) ExecuteAction(ctx context.Context, kind string, req bridge.ActionRequest) (map[string]any, error) {
return map[string]any{"success": true}, nil
}
func (m *mockBridge) CreateTab(url string) (string, context.Context, context.CancelFunc, error) {
ctx, cancel := chromedp.NewContext(context.Background())
return "tab_abc12345", ctx, cancel, nil
}
func (m *mockBridge) CloseTab(tabID string) error {
if tabID == "fail" {
return fmt.Errorf("close failed")
}
return nil
}
func (m *mockBridge) FocusTab(tabID string) error {
if tabID == "fail" {
return fmt.Errorf("tab not found")
}
return nil
}
func (m *mockBridge) EnsureChrome(cfg *config.RuntimeConfig) error {
// Mock implementation - just return nil
return nil
}
func (m *mockBridge) DeleteRefCache(tabID string) {}
func (m *mockBridge) TabLockInfo(tabID string) *bridge.LockInfo { return nil }
func (m *mockBridge) GetMemoryMetrics(tabID string) (*bridge.MemoryMetrics, error) {
return &bridge.MemoryMetrics{JSHeapUsedMB: 10}, nil
}
func (m *mockBridge) GetBrowserMemoryMetrics() (*bridge.MemoryMetrics, error) {
return &bridge.MemoryMetrics{JSHeapUsedMB: 50}, nil
}
func (m *mockBridge) GetAggregatedMemoryMetrics() (*bridge.MemoryMetrics, error) {
return &bridge.MemoryMetrics{JSHeapUsedMB: 50, Nodes: 500}, nil
}
func (m *mockBridge) GetCrashLogs() []string {
return nil
}
func (m *mockBridge) Execute(ctx context.Context, tabID string, task func(ctx context.Context) error) error {
return task(ctx)
}
func TestHandlers(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{}, nil, nil, nil)
mux := http.NewServeMux()
h.RegisterRoutes(mux, nil)
req := httptest.NewRequest("GET", "/help", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 from /help, got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "endpoints") {
t.Fatalf("expected /help response to include endpoints")
}
req = httptest.NewRequest("GET", "/openapi.json", nil)
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 from /openapi.json, got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "openapi") {
t.Fatalf("expected /openapi.json response to include openapi")
}
req = httptest.NewRequest("GET", "/metrics", nil)
w = httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 from /metrics, got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "metrics") {
t.Fatalf("expected /metrics response to include metrics")
}
}
func TestHelpIncludesSecurityStatus(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{}, nil, nil, nil)
req := httptest.NewRequest("GET", "/help", nil)
w := httptest.NewRecorder()
h.HandleHelp(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 from /help, got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "\"security\"") {
t.Fatalf("expected /help response to include security status")
}
if !strings.Contains(w.Body.String(), "security.allowEvaluate") {
t.Fatalf("expected /help response to include locked setting names")
}
}
func TestOpenAPIIncludesSensitiveEndpointStatus(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{AllowDownload: true}, nil, nil, nil)
req := httptest.NewRequest("GET", "/openapi.json", nil)
w := httptest.NewRecorder()
h.HandleOpenAPI(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 from /openapi.json, got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "\"x-pinchtab-security\"") {
t.Fatalf("expected /openapi.json response to include security metadata")
}
if !strings.Contains(w.Body.String(), "\"x-pinchtab-enabled\":true") {
t.Fatalf("expected /openapi.json response to mark enabled sensitive endpoints")
}
}
func TestHandleNavigate(t *testing.T) {
cfg := &config.RuntimeConfig{}
m := &mockBridge{}
h := New(m, cfg, nil, nil, nil)
// 1. Valid POST request
body := `{"url": "https://pinchtab.com"}`
req := httptest.NewRequest("POST", "/navigate", bytes.NewReader([]byte(body)))
w := httptest.NewRecorder()
h.HandleNavigate(w, req)
// Even with mock context, it might fail inside chromedp.Run if no browser is attached,
// but we're testing the handler logic around it.
if w.Code != 200 && w.Code != 500 {
t.Errorf("unexpected status %d: %s", w.Code, w.Body.String())
}
// 2. Valid GET request (ergonomic alias path style)
req = httptest.NewRequest("GET", "/nav?url=https%3A%2F%2Fpinchtab.com", nil)
w = httptest.NewRecorder()
h.HandleNavigate(w, req)
if w.Code != 200 && w.Code != 500 {
t.Errorf("unexpected status for GET navigate %d: %s", w.Code, w.Body.String())
}
// 3. Missing URL
req = httptest.NewRequest("POST", "/navigate", bytes.NewReader([]byte(`{}`)))
w = httptest.NewRecorder()
h.HandleNavigate(w, req)
if w.Code != 400 {
t.Errorf("expected 400 for missing url, got %d", w.Code)
}
}
func TestHandleTab(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{}, nil, nil, nil)
// New Tab
body := `{"action": "new", "url": "about:blank"}`
req := httptest.NewRequest("POST", "/tab", bytes.NewReader([]byte(body)))
w := httptest.NewRecorder()
h.HandleTab(w, req)
if w.Code != 200 && w.Code != 500 {
t.Errorf("unexpected status %d", w.Code)
}
// Close Tab
body = `{"action": "close", "tabId": "tab1"}`
req = httptest.NewRequest("POST", "/tab", bytes.NewReader([]byte(body)))
w = httptest.NewRecorder()
h.HandleTab(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}
func TestHandleTabFocus(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{}, nil, nil, nil)
t.Run("focus success", func(t *testing.T) {
body := `{"action": "focus", "tabId": "tab1"}`
req := httptest.NewRequest("POST", "/tab", bytes.NewReader([]byte(body)))
w := httptest.NewRecorder()
h.HandleTab(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
var resp map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp["focused"] != true {
t.Error("expected focused=true")
}
if resp["tabId"] != "tab1" {
t.Errorf("expected tabId=tab1, got %v", resp["tabId"])
}
})
t.Run("focus missing tabId", func(t *testing.T) {
body := `{"action": "focus"}`
req := httptest.NewRequest("POST", "/tab", bytes.NewReader([]byte(body)))
w := httptest.NewRecorder()
h.HandleTab(w, req)
if w.Code != 400 {
t.Errorf("expected 400, got %d", w.Code)
}
})
t.Run("focus not found", func(t *testing.T) {
body := `{"action": "focus", "tabId": "fail"}`
req := httptest.NewRequest("POST", "/tab", bytes.NewReader([]byte(body)))
w := httptest.NewRecorder()
h.HandleTab(w, req)
if w.Code != 404 {
t.Errorf("expected 404, got %d", w.Code)
}
})
t.Run("invalid action", func(t *testing.T) {
body := `{"action": "invalid"}`
req := httptest.NewRequest("POST", "/tab", bytes.NewReader([]byte(body)))
w := httptest.NewRecorder()
h.HandleTab(w, req)
if w.Code != 400 {
t.Errorf("expected 400, got %d", w.Code)
}
})
}
func TestRoutesRegistration(t *testing.T) {
b := &mockBridge{}
cfg := &config.RuntimeConfig{}
h := New(b, cfg, nil, nil, nil)
mux := http.NewServeMux()
h.RegisterRoutes(mux, func() {})
tests := []struct {
method string
path string
code int
}{
{"GET", "/health", 200},
{"GET", "/tabs", 200},
{"GET", "/welcome", 200},
{"POST", "/navigate", 400}, // missing body
}
for _, tt := range tests {
req := httptest.NewRequest(tt.method, tt.path, nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != tt.code {
t.Errorf("%s %s expected %d, got %d", tt.method, tt.path, tt.code, w.Code)
}
}
}
func TestEvaluateRouteLockedByDefault(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{}, nil, nil, nil)
mux := http.NewServeMux()
h.RegisterRoutes(mux, nil)
req := httptest.NewRequest("POST", "/evaluate", bytes.NewReader([]byte(`{"expression":"1+1"}`)))
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != 403 {
t.Fatalf("expected 403 when evaluate is disabled, got %d", w.Code)
}
if !strings.Contains(w.Body.String(), "security.allowEvaluate") {
t.Fatalf("expected evaluate lock response to include the setting name, got %s", w.Body.String())
}
}
func TestEvaluateRouteRegisteredWhenEnabled(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{AllowEvaluate: true}, nil, nil, nil)
mux := http.NewServeMux()
h.RegisterRoutes(mux, nil)
req := httptest.NewRequest("POST", "/evaluate", bytes.NewReader([]byte(`not json`)))
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != 400 {
t.Fatalf("expected evaluate route to be active, got %d", w.Code)
}
}
func TestSensitiveTabRouteLockedByDefault(t *testing.T) {
h := New(&mockBridge{}, &config.RuntimeConfig{}, nil, nil, nil)
mux := http.NewServeMux()
h.RegisterRoutes(mux, nil)
req := httptest.NewRequest("POST", "/tabs/tab1/evaluate", bytes.NewReader([]byte(`{"expression":"1+1"}`)))
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != 403 {
t.Fatalf("expected 403 when tab evaluate is disabled, got %d", w.Code)
}
var payload map[string]any
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
t.Fatalf("failed to parse response: %v", err)
}
if payload["code"] != "evaluate_disabled" {
t.Fatalf("expected evaluate_disabled code, got %v", payload["code"])
}
}