| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721 |
- package httpserver
- import (
- "bytes"
- "compress/gzip"
- "encoding/json"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "time"
- "github.com/danfragoso/pizzasql-next/pkg/executor"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- func setupTestServer(t *testing.T) (*Server, *storage.KVPool) {
- pool, err := storage.NewKVPool("localhost:8085", 5, 5*time.Second)
- if err != nil {
- t.Skip("PizzaKV not available, skipping HTTP server tests")
- }
- schema := storage.NewSchemaManager(pool, "test_http_db")
- table := storage.NewTableManager(pool, schema, "test_http_db")
- exec := executor.New(schema, table)
- config := DefaultConfig()
- config.EnableAuth = false
- server := New(config, exec, schema)
- return server, pool
- }
- func TestQueryEndpoint(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Create test table
- req := QueryRequest{
- SQL: "CREATE TABLE test_users (id INTEGER PRIMARY KEY, name TEXT)",
- }
- body, _ := json.Marshal(req)
- r := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w := httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- // Insert data
- req = QueryRequest{
- SQL: "INSERT INTO test_users (id, name) VALUES (1, 'Alice')",
- }
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- var resp QueryResponse
- json.NewDecoder(w.Body).Decode(&resp)
- if resp.RowsAffected != 1 {
- t.Errorf("expected 1 row affected, got %d", resp.RowsAffected)
- }
- // Query data
- req = QueryRequest{
- SQL: "SELECT * FROM test_users WHERE id = 1",
- }
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- json.NewDecoder(w.Body).Decode(&resp)
- if len(resp.Rows) != 1 {
- t.Errorf("expected 1 row, got %d", len(resp.Rows))
- }
- // Cleanup
- req = QueryRequest{SQL: "DROP TABLE test_users"}
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- }
- func TestExecuteEndpoint(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Create table first
- createReq := QueryRequest{
- SQL: "CREATE TABLE test_batch (id INTEGER PRIMARY KEY, value TEXT)",
- }
- body, _ := json.Marshal(createReq)
- r := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w := httptest.NewRecorder()
- server.handleQuery(w, r)
- // Batch insert
- req := ExecuteRequest{
- Statements: []QueryRequest{
- {SQL: "INSERT INTO test_batch (id, value) VALUES (1, 'first')"},
- {SQL: "INSERT INTO test_batch (id, value) VALUES (2, 'second')"},
- },
- Transaction: true,
- }
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/execute", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleExecute(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- var resp ExecuteResponse
- json.NewDecoder(w.Body).Decode(&resp)
- if len(resp.Results) != 2 {
- t.Errorf("expected 2 results, got %d", len(resp.Results))
- }
- // Cleanup
- dropReq := QueryRequest{SQL: "DROP TABLE test_batch"}
- body, _ = json.Marshal(dropReq)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- }
- func TestSchemaEndpoints(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Create test table
- createReq := QueryRequest{
- SQL: "CREATE TABLE test_schema (id INTEGER PRIMARY KEY, name TEXT)",
- }
- body, _ := json.Marshal(createReq)
- r := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w := httptest.NewRecorder()
- server.handleQuery(w, r)
- // List tables
- r = httptest.NewRequest(http.MethodGet, "/schema/tables", nil)
- w = httptest.NewRecorder()
- server.handleSchemaTables(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- var tablesResp map[string]interface{}
- json.NewDecoder(w.Body).Decode(&tablesResp)
- tables := tablesResp["tables"].([]interface{})
- found := false
- for _, table := range tables {
- if table.(string) == "test_schema" {
- found = true
- break
- }
- }
- if !found {
- t.Error("test_schema table not found in list")
- }
- // Get table schema
- r = httptest.NewRequest(http.MethodGet, "/schema/tables/test_schema", nil)
- w = httptest.NewRecorder()
- server.handleSchemaTable(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- // Cleanup
- dropReq := QueryRequest{SQL: "DROP TABLE test_schema"}
- body, _ = json.Marshal(dropReq)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- }
- func TestHealthEndpoint(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- r := httptest.NewRequest(http.MethodGet, "/health", nil)
- w := httptest.NewRecorder()
- server.handleHealth(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- var resp map[string]interface{}
- json.NewDecoder(w.Body).Decode(&resp)
- if resp["status"] != "ok" {
- t.Errorf("expected status 'ok', got '%v'", resp["status"])
- }
- }
- func TestStatsEndpoint(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- r := httptest.NewRequest(http.MethodGet, "/stats", nil)
- w := httptest.NewRecorder()
- server.handleStats(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- var resp map[string]interface{}
- json.NewDecoder(w.Body).Decode(&resp)
- if _, ok := resp["queriesExecuted"]; !ok {
- t.Error("expected queriesExecuted in response")
- }
- }
- func TestReadOnlyMode(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Try to insert in readonly mode
- req := QueryRequest{
- SQL: "INSERT INTO test (id) VALUES (1)",
- }
- body, _ := json.Marshal(req)
- r := httptest.NewRequest(http.MethodPost, "/query?readonly=true", bytes.NewReader(body))
- w := httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusForbidden {
- t.Errorf("expected status 403, got %d", w.Code)
- }
- }
- func TestCORSMiddleware(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- r := httptest.NewRequest(http.MethodOptions, "/query", nil)
- w := httptest.NewRecorder()
- handler := server.corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
- handler.ServeHTTP(w, r)
- if w.Header().Get("Access-Control-Allow-Origin") != "*" {
- t.Error("CORS headers not set correctly")
- }
- }
- func TestParameterizedQuery(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Create test table
- createReq := QueryRequest{
- SQL: "CREATE TABLE test_params (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)",
- }
- body, _ := json.Marshal(createReq)
- r := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w := httptest.NewRecorder()
- server.handleQuery(w, r)
- // Insert with parameters
- req := QueryRequest{
- SQL: "INSERT INTO test_params (id, name, age) VALUES (?, ?, ?)",
- Params: []interface{}{1, "Alice", 30},
- }
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String())
- }
- // Query with parameters
- req = QueryRequest{
- SQL: "SELECT * FROM test_params WHERE name = ?",
- Params: []interface{}{"Alice"},
- }
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- var resp QueryResponse
- json.NewDecoder(w.Body).Decode(&resp)
- if len(resp.Rows) != 1 {
- t.Errorf("expected 1 row, got %d", len(resp.Rows))
- }
- // Cleanup
- dropReq := QueryRequest{SQL: "DROP TABLE test_params"}
- body, _ = json.Marshal(dropReq)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- }
- func TestSubstituteParams(t *testing.T) {
- tests := []struct {
- sql string
- params []interface{}
- expected string
- }{
- {"SELECT * FROM users WHERE id = ?", []interface{}{42}, "SELECT * FROM users WHERE id = 42"},
- {"SELECT * FROM users WHERE name = ?", []interface{}{"Alice"}, "SELECT * FROM users WHERE name = 'Alice'"},
- {"SELECT * FROM users WHERE name = ?", []interface{}{"O'Brien"}, "SELECT * FROM users WHERE name = 'O''Brien'"},
- {"INSERT INTO t (a, b) VALUES (?, ?)", []interface{}{1, "test"}, "INSERT INTO t (a, b) VALUES (1, 'test')"},
- {"SELECT * FROM t WHERE x = ?", []interface{}{nil}, "SELECT * FROM t WHERE x = NULL"},
- {"SELECT * FROM t WHERE x = ?", []interface{}{true}, "SELECT * FROM t WHERE x = 1"},
- {"SELECT * FROM t WHERE x = ?", []interface{}{false}, "SELECT * FROM t WHERE x = 0"},
- {"SELECT * FROM t WHERE x = ?", []interface{}{3.14}, "SELECT * FROM t WHERE x = 3.14"},
- }
- for _, tt := range tests {
- result := substituteParams(tt.sql, tt.params)
- if result != tt.expected {
- t.Errorf("substituteParams(%q, %v) = %q, want %q", tt.sql, tt.params, result, tt.expected)
- }
- }
- }
- func TestCompressionMiddleware(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Create a handler that returns some JSON
- testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "application/json")
- w.Write([]byte(`{"message": "hello world"}`))
- })
- // Wrap with compression middleware
- handler := server.compressionMiddleware(testHandler)
- // Request with gzip accept header
- r := httptest.NewRequest(http.MethodGet, "/test", nil)
- r.Header.Set("Accept-Encoding", "gzip")
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, r)
- // Check that response is gzip encoded
- if w.Header().Get("Content-Encoding") != "gzip" {
- t.Error("expected gzip Content-Encoding header")
- }
- // Decompress and verify content
- gr, err := gzip.NewReader(w.Body)
- if err != nil {
- t.Fatalf("failed to create gzip reader: %v", err)
- }
- defer gr.Close()
- body, err := io.ReadAll(gr)
- if err != nil {
- t.Fatalf("failed to read gzip body: %v", err)
- }
- if string(body) != `{"message": "hello world"}` {
- t.Errorf("unexpected body: %s", string(body))
- }
- }
- func TestCompressionMiddlewareNoGzip(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Create a handler that returns some text
- testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("hello world"))
- })
- // Wrap with compression middleware
- handler := server.compressionMiddleware(testHandler)
- // Request WITHOUT gzip accept header
- r := httptest.NewRequest(http.MethodGet, "/test", nil)
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, r)
- // Check that response is NOT gzip encoded
- if w.Header().Get("Content-Encoding") == "gzip" {
- t.Error("should not have gzip encoding without Accept-Encoding header")
- }
- if w.Body.String() != "hello world" {
- t.Errorf("unexpected body: %s", w.Body.String())
- }
- }
- func TestMetricsEndpoint(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- r := httptest.NewRequest(http.MethodGet, "/metrics", nil)
- w := httptest.NewRecorder()
- server.handleMetrics(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected status 200, got %d", w.Code)
- }
- body := w.Body.String()
- // Check for expected Prometheus metrics
- expectedMetrics := []string{
- "pizzasql_queries_total",
- "pizzasql_queries_executed_total",
- "pizzasql_tables_count",
- "pizzasql_uptime_seconds",
- "pizzasql_info",
- }
- for _, metric := range expectedMetrics {
- if !strings.Contains(body, metric) {
- t.Errorf("expected metric %s in response", metric)
- }
- }
- // Check content type
- contentType := w.Header().Get("Content-Type")
- if !strings.Contains(contentType, "text/plain") {
- t.Errorf("expected text/plain content type, got %s", contentType)
- }
- }
- func TestInferType(t *testing.T) {
- tests := []struct {
- value interface{}
- expected string
- }{
- {nil, "NULL"},
- {int64(42), "INTEGER"},
- {int(42), "INTEGER"},
- {3.14, "REAL"},
- {"hello", "TEXT"},
- {[]byte{1, 2, 3}, "BLOB"},
- {true, "INTEGER"},
- {false, "INTEGER"},
- }
- for _, tt := range tests {
- result := inferType(tt.value)
- if result != tt.expected {
- t.Errorf("inferType(%v) = %s, want %s", tt.value, result, tt.expected)
- }
- }
- }
- func TestTransactionEndpoints(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Test BEGIN transaction
- r := httptest.NewRequest(http.MethodPost, "/transaction/begin", nil)
- w := httptest.NewRecorder()
- server.handleTransactionBegin(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("BEGIN: expected status 200, got %d", w.Code)
- }
- var beginResp map[string]interface{}
- json.NewDecoder(w.Body).Decode(&beginResp)
- if beginResp["status"] != "started" {
- t.Errorf("BEGIN: expected status 'started', got '%v'", beginResp["status"])
- }
- // Test COMMIT transaction
- r = httptest.NewRequest(http.MethodPost, "/transaction/commit", nil)
- w = httptest.NewRecorder()
- server.handleTransactionCommit(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("COMMIT: expected status 200, got %d", w.Code)
- }
- var commitResp map[string]interface{}
- json.NewDecoder(w.Body).Decode(&commitResp)
- if commitResp["status"] != "committed" {
- t.Errorf("COMMIT: expected status 'committed', got '%v'", commitResp["status"])
- }
- // Test BEGIN again for rollback test
- r = httptest.NewRequest(http.MethodPost, "/transaction/begin", nil)
- w = httptest.NewRecorder()
- server.handleTransactionBegin(w, r)
- // Test ROLLBACK transaction
- r = httptest.NewRequest(http.MethodPost, "/transaction/rollback", nil)
- w = httptest.NewRecorder()
- server.handleTransactionRollback(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("ROLLBACK: expected status 200, got %d", w.Code)
- }
- var rollbackResp map[string]interface{}
- json.NewDecoder(w.Body).Decode(&rollbackResp)
- if rollbackResp["status"] != "rolled back" {
- t.Errorf("ROLLBACK: expected status 'rolled back', got '%v'", rollbackResp["status"])
- }
- }
- func TestTransactionEndpointsMethodNotAllowed(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Test GET on transaction endpoints (should fail)
- endpoints := []struct {
- path string
- handler func(http.ResponseWriter, *http.Request)
- }{
- {"/transaction/begin", server.handleTransactionBegin},
- {"/transaction/commit", server.handleTransactionCommit},
- {"/transaction/rollback", server.handleTransactionRollback},
- }
- for _, ep := range endpoints {
- r := httptest.NewRequest(http.MethodGet, ep.path, nil)
- w := httptest.NewRecorder()
- ep.handler(w, r)
- if w.Code != http.StatusMethodNotAllowed {
- t.Errorf("%s: expected status 405 for GET, got %d", ep.path, w.Code)
- }
- }
- }
- func TestAuthMiddleware(t *testing.T) {
- pool, err := storage.NewKVPool("localhost:8085", 5, 5*time.Second)
- if err != nil {
- t.Skip("PizzaKV not available, skipping auth tests")
- }
- defer pool.Close()
- schema := storage.NewSchemaManager(pool, "test_auth_db")
- table := storage.NewTableManager(pool, schema, "test_auth_db")
- exec := executor.New(schema, table)
- config := DefaultConfig()
- config.EnableAuth = true
- config.APIKeys = []string{"test-api-key-123", "another-key-456"}
- server := New(config, exec, schema)
- testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
- w.Write([]byte("OK"))
- })
- handler := server.authMiddleware(testHandler)
- // Test without Authorization header
- r := httptest.NewRequest(http.MethodGet, "/test", nil)
- w := httptest.NewRecorder()
- handler.ServeHTTP(w, r)
- if w.Code != http.StatusUnauthorized {
- t.Errorf("expected 401 without auth header, got %d", w.Code)
- }
- // Test with invalid API key
- r = httptest.NewRequest(http.MethodGet, "/test", nil)
- r.Header.Set("Authorization", "Bearer invalid-key")
- w = httptest.NewRecorder()
- handler.ServeHTTP(w, r)
- if w.Code != http.StatusForbidden {
- t.Errorf("expected 403 with invalid key, got %d", w.Code)
- }
- // Test with valid API key
- r = httptest.NewRequest(http.MethodGet, "/test", nil)
- r.Header.Set("Authorization", "Bearer test-api-key-123")
- w = httptest.NewRecorder()
- handler.ServeHTTP(w, r)
- if w.Code != http.StatusOK {
- t.Errorf("expected 200 with valid key, got %d", w.Code)
- }
- }
- func TestQueryEndpointErrors(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- // Test with missing SQL
- req := QueryRequest{
- SQL: "",
- }
- body, _ := json.Marshal(req)
- r := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w := httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for empty SQL, got %d", w.Code)
- }
- // Test with invalid JSON
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader([]byte("invalid json")))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for invalid JSON, got %d", w.Code)
- }
- // Test with syntax error
- req = QueryRequest{
- SQL: "SELEC * FORM users",
- }
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- if w.Code != http.StatusBadRequest {
- t.Errorf("expected 400 for SQL syntax error, got %d", w.Code)
- }
- }
- func TestPrettyPrintOption(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- req := QueryRequest{
- SQL: "SELECT 1 as num",
- }
- body, _ := json.Marshal(req)
- // Without pretty
- r := httptest.NewRequest(http.MethodPost, "/query", bytes.NewReader(body))
- w := httptest.NewRecorder()
- server.handleQuery(w, r)
- normalResponse := w.Body.String()
- // With pretty
- body, _ = json.Marshal(req)
- r = httptest.NewRequest(http.MethodPost, "/query?pretty=true", bytes.NewReader(body))
- w = httptest.NewRecorder()
- server.handleQuery(w, r)
- prettyResponse := w.Body.String()
- // Pretty response should be longer due to formatting
- if len(prettyResponse) <= len(normalResponse) {
- t.Error("pretty response should be longer than normal response")
- }
- // Pretty response should contain newlines
- if !strings.Contains(prettyResponse, "\n") {
- t.Error("pretty response should contain newlines")
- }
- }
- func TestSchemaTableNotFound(t *testing.T) {
- server, pool := setupTestServer(t)
- defer pool.Close()
- r := httptest.NewRequest(http.MethodGet, "/schema/tables/nonexistent_table_xyz", nil)
- w := httptest.NewRecorder()
- server.handleSchemaTable(w, r)
- if w.Code != http.StatusNotFound {
- t.Errorf("expected 404 for nonexistent table, got %d", w.Code)
- }
- }
|