Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.23', '1.24']
go-version: ['1.27']

steps:
- name: Checkout code
Expand Down Expand Up @@ -48,7 +48,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
go-version-file: go.mod

- name: Run golangci-lint
uses: golangci/golangci-lint-action@v9
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ sql-http-proxy/

## Development Commands

Requires Go 1.27 or later: the project uses `encoding/json/v2` / `encoding/json/jsontext`
and generic methods.

```bash
# Build (all drivers)
go build ./cmd/sql-http-proxy
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Or download manually from [GitHub Releases](https://github.com/mpyw/sql-http-pro

## Go Install

Requires Go 1.27 or later.

```bash
go install github.com/mpyw/sql-http-proxy/cmd/sql-http-proxy@latest
```
Expand Down
14 changes: 14 additions & 0 deletions e2e/common_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package e2e

import (
json "encoding/json/v2"
"io"
"net/http"
"testing"

Expand Down Expand Up @@ -68,3 +70,15 @@ func createMutationHandler(t *testing.T, db *sqlx.DB, cfg config.Config) http.Ha
require.NoError(t, err)
return handler
}

// decodeJSON decodes a response body as T, failing the test if it does not
// parse. Decoding uses encoding/json/v2 defaults, so every assertion made
// through this helper also asserts that the response is RFC 7493 JSON: one
// complete value, no duplicate object members, valid UTF-8 throughout.
func decodeJSON[T any](t *testing.T, body io.Reader) T {
t.Helper()

var v T
require.NoError(t, json.UnmarshalRead(body, &v))
return v
}
4 changes: 1 addition & 3 deletions e2e/content_type_multipart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package e2e

import (
"bytes"
"encoding/json"
"mime/multipart"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -36,8 +35,7 @@ func TestContentTypeMultipart(t *testing.T) {

assert.Equal(t, http.StatusOK, rec.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(rec.Body).Decode(&result))
result := decodeJSON[map[string]any](t, rec.Body)
assert.Equal(t, float64(42), result["id"])
assert.Equal(t, "Multipart User", result["name"])
})
Expand Down
4 changes: 1 addition & 3 deletions e2e/csv_value_parser_file_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -26,8 +25,7 @@ func TestCSVValueParserFile(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result []map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[[]map[string]any](t, w.Body)
require.Len(t, result, 2)

// Custom parser uppercases strings
Expand Down
4 changes: 1 addition & 3 deletions e2e/csv_value_parser_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -27,8 +26,7 @@ func TestCSVValueParser(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result []map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[[]map[string]any](t, w.Body)
require.Len(t, result, 2)

// Row 1: 1,Alice,true,95.5,2024-01-15,
Expand Down
7 changes: 2 additions & 5 deletions e2e/database_init_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
Expand Down Expand Up @@ -37,8 +36,7 @@ func TestDatabaseInit_InlineSQL(t *testing.T) {

require.Equal(t, http.StatusOK, w.Code)

var result []map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[[]map[string]any](t, w.Body)

// Verify init SQL created table and inserted data
require.Len(t, result, 2)
Expand Down Expand Up @@ -104,8 +102,7 @@ queries:

require.Equal(t, http.StatusOK, w.Code)

var result []map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[[]map[string]any](t, w.Body)

// Verify init SQL file created table and inserted data
require.Len(t, result, 2)
Expand Down
106 changes: 37 additions & 69 deletions e2e/delay_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"testing/synctest"
"time"

"github.com/stretchr/testify/require"
Expand All @@ -12,80 +13,47 @@ import (
"github.com/mpyw/sql-http-proxy/internal/server"
)

// configuredDelay matches the `delay:` value on the delayed endpoints in
// delay_test.yaml.
const configuredDelay = 50 * time.Millisecond

func TestDelay(t *testing.T) {
cfg, err := config.ParseFile("delay_test.yaml")
require.NoError(t, err)

mux, err := server.NewServeMux(nil, cfg, ".")
require.NoError(t, err)

t.Run("object with delay", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/slow-user", nil)
rec := httptest.NewRecorder()

start := time.Now()
mux.ServeHTTP(rec, req)
elapsed := time.Since(start)

require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, `{"id":1,"name":"Slow User"}`, rec.Body.String())
// Should take at least 50ms
require.GreaterOrEqual(t, elapsed.Milliseconds(), int64(40), "expected delay of at least 40ms")
})

t.Run("array with delay", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/slow-users", nil)
rec := httptest.NewRecorder()

start := time.Now()
mux.ServeHTTP(rec, req)
elapsed := time.Since(start)

require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, `[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]`, rec.Body.String())
// Should take at least 50ms
require.GreaterOrEqual(t, elapsed.Milliseconds(), int64(40), "expected delay of at least 40ms")
})

t.Run("filtered array with delay", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/slow-filtered-user?id=1", nil)
rec := httptest.NewRecorder()

start := time.Now()
mux.ServeHTTP(rec, req)
elapsed := time.Since(start)

require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, `{"id":1,"name":"Alice"}`, rec.Body.String())
// Should take at least 50ms
require.GreaterOrEqual(t, elapsed.Milliseconds(), int64(40), "expected delay of at least 40ms")
})

t.Run("JS source with delay", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/slow-js-user?id=42", nil)
rec := httptest.NewRecorder()

start := time.Now()
mux.ServeHTTP(rec, req)
elapsed := time.Since(start)

require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, `{"id":42,"name":"JS User"}`, rec.Body.String())
// Should take at least 50ms
require.GreaterOrEqual(t, elapsed.Milliseconds(), int64(40), "expected delay of at least 40ms")
})

t.Run("no delay is fast", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/no-delay-user", nil)
rec := httptest.NewRecorder()

start := time.Now()
mux.ServeHTTP(rec, req)
elapsed := time.Since(start)

require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, `{"id":1,"name":"Fast User"}`, rec.Body.String())
// Should complete quickly (less than 30ms typically)
require.Less(t, elapsed.Milliseconds(), int64(30), "expected fast response without delay")
})
// Each case runs inside a synctest bubble, so the handler's time.Sleep is
// served by a fake clock: the assertions can require the exact delay
// instead of a tolerance window, and the test does not actually wait.
tests := []struct {
name string
path string
body string
delay time.Duration
}{
{"object with delay", "/slow-user", `{"id":1,"name":"Slow User"}`, configuredDelay},
{"array with delay", "/slow-users", `[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]`, configuredDelay},
{"filtered array with delay", "/slow-filtered-user?id=1", `{"id":1,"name":"Alice"}`, configuredDelay},
{"JS source with delay", "/slow-js-user?id=42", `{"id":42,"name":"JS User"}`, configuredDelay},
{"no delay", "/no-delay-user", `{"id":1,"name":"Fast User"}`, 0},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tt.path, nil)
rec := httptest.NewRecorder()

start := time.Now()
mux.ServeHTTP(rec, req)
elapsed := time.Since(start)

require.Equal(t, http.StatusOK, rec.Code)
require.JSONEq(t, tt.body, rec.Body.String())
require.Equal(t, tt.delay, elapsed)
})
})
}
}
4 changes: 1 addition & 3 deletions e2e/global_helpers_file_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -27,8 +26,7 @@ func TestGlobalHelpersFile(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[map[string]any](t, w.Body)
assert.Equal(t, float64(10), result["doubled"]) // double(5) from inline
assert.Equal(t, float64(15), result["tripled"]) // triple(5) from file
assert.Equal(t, "Hello, Test!", result["greeting"]) // greet() from file
Expand Down
13 changes: 4 additions & 9 deletions e2e/global_helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package e2e

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -28,8 +27,7 @@ func TestGlobalHelpers(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[map[string]any](t, w.Body)
assert.Equal(t, float64(10), result["input_doubled"]) // double(5) = 10
})

Expand All @@ -40,8 +38,7 @@ func TestGlobalHelpers(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[map[string]any](t, w.Body)
assert.Equal(t, float64(20), result["value"]) // double(10) = 20
})

Expand All @@ -52,8 +49,7 @@ func TestGlobalHelpers(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[map[string]any](t, w.Body)
assert.Equal(t, "John Doe", result["fullName"])
})

Expand All @@ -64,8 +60,7 @@ func TestGlobalHelpers(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result []map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[[]map[string]any](t, w.Body)
assert.Len(t, result, 2)
assert.Equal(t, float64(10), result[0]["doubled"]) // double(5) = 10
assert.Equal(t, float64(20), result[1]["doubled"]) // double(10) = 20
Expand Down
5 changes: 1 addition & 4 deletions e2e/mock_dsn_null_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package e2e

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMockDsnNull(t *testing.T) {
Expand All @@ -20,8 +18,7 @@ func TestMockDsnNull(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[map[string]any](t, w.Body)
assert.Equal(t, float64(1), result["id"])
assert.Equal(t, "Mock User", result["name"])
}
8 changes: 2 additions & 6 deletions e2e/mock_error_test.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package e2e

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestMockError(t *testing.T) {
Expand All @@ -21,8 +19,7 @@ func TestMockError(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[map[string]any](t, w.Body)
assert.Equal(t, float64(1), result["id"])
})

Expand All @@ -33,8 +30,7 @@ func TestMockError(t *testing.T) {

assert.Equal(t, http.StatusBadRequest, w.Code)

var result map[string]any
require.NoError(t, json.NewDecoder(w.Body).Decode(&result))
result := decodeJSON[map[string]any](t, w.Body)
assert.Equal(t, "id is required", result["message"])
})
}
Loading
Loading