134 lines
4.1 KiB
Go
134 lines
4.1 KiB
Go
package testutil
|
|
|
|
// Package testutil provides test helpers for handler and integration tests.
|
|
// It wires up an in-memory SQLite database, a lightweight HTTP request helper,
|
|
// and common assertion utilities.
|
|
|
|
import (
|
|
"Engine/internal/database"
|
|
"bytes"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
_ "modernc.org/sqlite" // pure-Go SQLite driver, no CGO required
|
|
)
|
|
|
|
// ── Database ──────────────────────────────────────────────────────────────────
|
|
|
|
// NewTestDB opens a fresh in-memory SQLite database, runs your schema
|
|
// migrations, and registers a t.Cleanup to close it when the test ends.
|
|
//
|
|
// Each call gets its own isolated database — tests never share state.
|
|
func NewTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
|
|
// "file::memory:?cache=shared" would share across connections;
|
|
// the plain ":memory:" gives a fully isolated DB per open call.
|
|
db, err := sql.Open("sqlite", ":memory:")
|
|
if err != nil {
|
|
t.Fatalf("testutil.NewTestDB: open: %v", err)
|
|
}
|
|
|
|
// SQLite in-memory works best with a single connection.
|
|
db.SetMaxOpenConns(1)
|
|
|
|
if err := database.Migrate(db); err != nil {
|
|
db.Close()
|
|
t.Fatalf("testutil.NewTestDB: migrate: %v", err)
|
|
}
|
|
|
|
t.Cleanup(func() { db.Close() })
|
|
return db
|
|
}
|
|
|
|
// ── HTTP helpers ──────────────────────────────────────────────────────────────
|
|
|
|
// Do fires an HTTP request directly at handler h and returns the recorded
|
|
// response. body is marshalled to JSON; pass nil to send no body.
|
|
//
|
|
// Usage:
|
|
//
|
|
// w := testutil.Do(t, http.HandlerFunc(h.Ingest), http.MethodPost, "/", payload)
|
|
func Do(t *testing.T, h http.Handler, method, path string, body any) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
var r io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
t.Fatalf("testutil.Do: marshal body: %v", err)
|
|
}
|
|
r = bytes.NewReader(b)
|
|
}
|
|
|
|
req := httptest.NewRequest(method, path, r)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
h.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
// MustJSON marshals v to JSON and returns it as an io.Reader.
|
|
// Intended for use with http.Client.Post in full-server integration tests.
|
|
//
|
|
// Usage:
|
|
//
|
|
// resp, _ := client.Post(srv.URL+"/api/v1/budgets", "application/json", testutil.MustJSON(t, payload))
|
|
func MustJSON(t *testing.T, v any) io.Reader {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("testutil.MustJSON: %v", err)
|
|
}
|
|
return bytes.NewReader(b)
|
|
}
|
|
|
|
// ── Assertions ────────────────────────────────────────────────────────────────
|
|
|
|
// AssertStatus fails the test if the recorder's status code does not match want.
|
|
func AssertStatus(t *testing.T, w *httptest.ResponseRecorder, want int) {
|
|
t.Helper()
|
|
if w.Code != want {
|
|
t.Errorf("status: got %d, want %d\n\tbody: %s", w.Code, want, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// AssertJSONField decodes the recorder's body as a JSON object and checks that
|
|
// the top-level key equals the expected string value. Useful for quick smoke
|
|
// checks without defining a full response struct.
|
|
//
|
|
// Usage:
|
|
//
|
|
// testutil.AssertJSONField(t, w, "status", "ok")
|
|
func AssertJSONField(t *testing.T, w *httptest.ResponseRecorder, key, want string) {
|
|
t.Helper()
|
|
var m map[string]any
|
|
if err := json.NewDecoder(w.Body).Decode(&m); err != nil {
|
|
t.Fatalf("AssertJSONField: decode body: %v", err)
|
|
}
|
|
got, ok := m[key]
|
|
if !ok {
|
|
t.Errorf("AssertJSONField: key %q not found in response", key)
|
|
return
|
|
}
|
|
if got != want {
|
|
t.Errorf("AssertJSONField: %q = %q, want %q", key, got, want)
|
|
}
|
|
}
|
|
|
|
// DecodeJSON decodes the recorder's body into dst.
|
|
// Fails the test immediately if decoding errors.
|
|
func DecodeJSON(t *testing.T, w *httptest.ResponseRecorder, dst any) {
|
|
t.Helper()
|
|
if err := json.NewDecoder(w.Body).Decode(dst); err != nil {
|
|
t.Fatalf("DecodeJSON: %v\n\tbody: %s", err, w.Body.String())
|
|
}
|
|
}
|