package httpapi import ( "net/http" "net/http/httptest" "os" "strings" "testing" ) func readSourceFile(t *testing.T, name string) string { t.Helper() b, err := os.ReadFile(name) if err != nil { t.Fatalf("read %s: %v", name, err) } return string(b) } // Filter parsing must reject bad input before touching the database, so a typo in // the admin UI cannot turn into a full-table scan or a 500. func TestHandleAdminListAICalls_rejectsBadFilters(t *testing.T) { t.Parallel() s := &Server{} cases := map[string]string{ "invalid company_id": "?company_id=not-a-uuid", "invalid user_id": "?user_id=123", "invalid job_id": "?job_id=abc", "invalid raw_product_id": "?raw_product_id=xyz", "invalid outcome": "?outcome=maybe", "invalid before cursor": "?before=yesterday", } for name, query := range cases { t.Run(name, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/admin/ai-calls"+query, nil) rec := httptest.NewRecorder() s.handleAdminListAICalls(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d want 400 body=%s", rec.Code, rec.Body.String()) } }) } } func TestHandleAdminGetAICall_rejectsBadID(t *testing.T) { t.Parallel() s := &Server{} req := httptest.NewRequest(http.MethodGet, "/api/admin/ai-calls/nope", nil) rec := httptest.NewRecorder() s.handleAdminGetAICall(rec, req) if rec.Code != http.StatusBadRequest { t.Fatalf("status=%d want 400 body=%s", rec.Code, rec.Body.String()) } } // The list endpoint returns previews, never the full bodies — one unfiltered // request must not stream a tenant's whole prompt corpus out of the admin API. func TestAdminAICallsListSQL_selectsPreviewsNotFullBodies(t *testing.T) { t.Parallel() sql := adminAICallsListSQL("TRUE", 1) selectList := sql[strings.Index(sql, "SELECT"):strings.Index(sql, "FROM ai_call_logs")] // A bare column followed by a comma is the whole body; the length()/left() // wrappers put a ")" in between. for _, bare := range []string{" l.system_prompt,", " l.user_prompt,", " l.response_text,"} { if strings.Contains(selectList, bare) { t.Fatalf("list selects a full body (%q):\n%s", strings.TrimSpace(bare), selectList) } } for _, want := range []string{ "left(l.user_prompt, 240)", "left(l.response_text, 240)", "length(l.system_prompt)", } { if !strings.Contains(selectList, want) { t.Fatalf("list should select %s:\n%s", want, selectList) } } if !strings.Contains(sql, "ORDER BY l.created_at DESC") || !strings.Contains(sql, "LIMIT $1") { t.Fatalf("list must page newest-first with a bound limit:\n%s", sql) } } // The detail endpoint is the only place full bodies come from. func TestAdminAICallsDetail_returnsFullBodies(t *testing.T) { t.Parallel() src := readSourceFile(t, "admin_ai_calls_handlers.go") detail := src[strings.Index(src, "func (s *Server) handleAdminGetAICall"):] if !strings.Contains(detail, "l.system_prompt, l.user_prompt, l.response_text") { t.Fatal("detail handler must select the full bodies") } }