package main import ( "bytes" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/go-chi/chi/v5" ) // Closes R-005 for queryd: cmd-level tests for the /sql handler's // pre-DB paths (decode, body cap, empty SQL). The actual SQL execution // path needs DuckDB so it lives in the smoke chain + proof harness. // // We construct handlers with a nil *sql.DB — the tests only exercise // paths that return early before db.QueryContext. Tests that would // reach the db are covered by GOLAKE-040 in the proof harness. func mountedRouter() chi.Router { h := &handlers{db: nil} r := chi.NewRouter() h.register(r) return r } func TestRoutesMounted(t *testing.T) { r := mountedRouter() found := false chi.Walk(r, func(method, route string, _ http.Handler, _ ...func(http.Handler) http.Handler) error { if method == "POST" && route == "/sql" { found = true } return nil }) if !found { t.Error("POST /sql not mounted") } } func TestHandleSQL_BodyTooLarge(t *testing.T) { // 4xx range — see embedd's TestHandleEmbed_BodyTooLarge for the // 413-vs-400 detail. The contract is "client error, fails loud." r := mountedRouter() srv := httptest.NewServer(r) defer srv.Close() big := bytes.Repeat([]byte("x"), maxSQLBodyBytes+1024) resp, err := http.Post(srv.URL+"/sql", "application/json", bytes.NewReader(big)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode < 400 || resp.StatusCode >= 500 { t.Errorf("expected 4xx on oversize, got %d", resp.StatusCode) } } func TestHandleSQL_MalformedJSON_400(t *testing.T) { r := mountedRouter() srv := httptest.NewServer(r) defer srv.Close() resp, err := http.Post(srv.URL+"/sql", "application/json", strings.NewReader("not json")) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 on malformed, got %d", resp.StatusCode) } } // TestHandleSQL_WrongFieldName_400 locks the JSON tag on sqlRequest.SQL // against drift. The 2026-04-30 playbook_lift harness sent {"q": "..."} // — the Go decoder ignores unknown fields by default, so req.SQL stays // empty and the empty-check fires with "sql is empty". If anyone renames // the JSON tag, callers POSTing the new (wrong) shape would hit this // same path; this test makes the contract explicit so the failure mode // is documented rather than discovered during a reality run. func TestHandleSQL_WrongFieldName_400(t *testing.T) { r := mountedRouter() srv := httptest.NewServer(r) defer srv.Close() cases := []string{ `{"q":"SELECT 1"}`, // the actual 2026-04-30 harness shape `{"query":"SELECT 1"}`, // matrixd-style drift in the other direction `{"statement":"SELECT 1"}`, } for _, body := range cases { t.Run(body, func(t *testing.T) { resp, err := http.Post(srv.URL+"/sql", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 on wrong field name, got %d", resp.StatusCode) } rb, _ := io.ReadAll(resp.Body) if !strings.Contains(string(rb), "sql is empty") { t.Errorf("expected 'sql is empty' to anchor the contract, got %q", string(rb)) } }) } } func TestHandleSQL_EmptySQL_400(t *testing.T) { r := mountedRouter() srv := httptest.NewServer(r) defer srv.Close() cases := []string{ `{"sql":""}`, `{"sql":" "}`, `{"sql":"\n\t \n"}`, } for _, body := range cases { t.Run(body, func(t *testing.T) { resp, err := http.Post(srv.URL+"/sql", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("POST: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusBadRequest { t.Errorf("expected 400 on empty/whitespace SQL, got %d", resp.StatusCode) } }) } } func TestMaxSQLBodyBytes_Reasonable(t *testing.T) { // SQL strings shouldn't be huge — 64 KiB is generous for queryd // (DuckDB statements above 64 KiB are pathological). Locking the // constant prevents an accidental refactor from blowing this open. if maxSQLBodyBytes < 16<<10 { t.Errorf("maxSQLBodyBytes=%d below sane SQL minimum (16 KiB)", maxSQLBodyBytes) } if maxSQLBodyBytes > 1<<20 { t.Errorf("maxSQLBodyBytes=%d above sane SQL maximum (1 MiB)", maxSQLBodyBytes) } } func TestPrimaryBucket_Constant(t *testing.T) { // Locks the logical bucket name — secrets provider lookup keys // against this. Refactor that flips this would silently fail // secret resolution for queryd at startup. if primaryBucket != "primary" { t.Errorf("primaryBucket = %q, want %q", primaryBucket, "primary") } }