Files
FPandA-Engine/tests/internal/testutil/testutil.go
2026-03-21 08:14:36 +01:00

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())
}
}