93 lines
3.0 KiB
Go
93 lines
3.0 KiB
Go
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")
|
|
}
|
|
}
|