go-htmx-gsd #1
2 changed files with 183 additions and 0 deletions
33
backend/internal/db/pool_test.go
Normal file
33
backend/internal/db/pool_test.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
//go:build red_gate
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestPool_Connects is an integration test that requires a live Postgres
|
||||
// reachable via DATABASE_URL. Skipped in unit-test runs without the env var
|
||||
// set. DSN value itself is never logged (info-disclosure T-01-06).
|
||||
func TestPool_Connects(t *testing.T) {
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("DATABASE_URL not set — integration test skipped")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pool, err := NewPool(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatalf("NewPool: unexpected error: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
t.Fatalf("pool.Ping: unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
150
backend/internal/web/handlers_test.go
Normal file
150
backend/internal/web/handlers_test.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
//go:build red_gate
|
||||
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// stubPinger satisfies the web.Pinger interface that Plan 01-03 will declare.
|
||||
// Local to this test file — never reachable from production code.
|
||||
type stubPinger struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubPinger) Ping(ctx context.Context) error { return s.err }
|
||||
|
||||
func TestHealthz_OK(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
|
||||
HealthzHandler(stubPinger{err: nil}).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d; want 200", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "application/json") {
|
||||
t.Errorf("Content-Type = %q; want application/json", ct)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, `"status":"ok"`) {
|
||||
t.Errorf("body missing status:ok; got: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"db":"ok"`) {
|
||||
t.Errorf("body missing db:ok; got: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthz_Down(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
|
||||
HealthzHandler(stubPinger{err: errors.New("conn refused")}).ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status = %d; want 503", rec.Code)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, `"status":"degraded"`) {
|
||||
t.Errorf("body missing status:degraded; got: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"db":"down"`) {
|
||||
t.Errorf("body missing db:down; got: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndex_RendersHxGet(t *testing.T) {
|
||||
router := NewRouter(stubPinger{}, "./static")
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d; want 200", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||
t.Errorf("Content-Type = %q; want text/html", ct)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
for _, want := range []string{
|
||||
`hx-get="/demo/time"`,
|
||||
`hx-target="#demo-out"`,
|
||||
`ui-button-solid-default-md`,
|
||||
`Fetch server time`,
|
||||
} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("body missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDemoTime_Fragment(t *testing.T) {
|
||||
router := NewRouter(stubPinger{}, "./static")
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/demo/time", nil)
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d; want 200", rec.Code)
|
||||
}
|
||||
if ct := rec.Header().Get("Content-Type"); !strings.Contains(ct, "text/html") {
|
||||
t.Errorf("Content-Type = %q; want text/html", ct)
|
||||
}
|
||||
body := rec.Body.String()
|
||||
if strings.Contains(body, "<html") {
|
||||
t.Errorf("body looks like full page (contains <html); want fragment\nbody: %s", body)
|
||||
}
|
||||
iso := regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z`)
|
||||
if !iso.MatchString(body) {
|
||||
t.Errorf("body missing ISO-8601 UTC timestamp; got: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestID_HeaderSet(t *testing.T) {
|
||||
router := NewRouter(stubPinger{}, "./static")
|
||||
rec := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
rid := rec.Header().Get("X-Request-ID")
|
||||
if rid == "" {
|
||||
t.Fatal("X-Request-ID header is empty")
|
||||
}
|
||||
uuidv4 := regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`)
|
||||
if !uuidv4.MatchString(rid) {
|
||||
t.Errorf("X-Request-ID %q does not match UUIDv4 regex", rid)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSlog_HandlerSwitch(t *testing.T) {
|
||||
var prodBuf bytes.Buffer
|
||||
prodHandler := NewSlogHandler("production", &prodBuf)
|
||||
slog.New(prodHandler).Info("x")
|
||||
var prodLine map[string]any
|
||||
if err := json.Unmarshal(bytes.TrimSpace(prodBuf.Bytes()), &prodLine); err != nil {
|
||||
t.Fatalf("production handler should emit JSON; parse error: %v\nbuf: %s", err, prodBuf.String())
|
||||
}
|
||||
if prodLine["msg"] != "x" {
|
||||
t.Errorf("production log msg = %v; want \"x\"", prodLine["msg"])
|
||||
}
|
||||
|
||||
var devBuf bytes.Buffer
|
||||
devHandler := NewSlogHandler("development", &devBuf)
|
||||
slog.New(devHandler).Info("x")
|
||||
var devLine map[string]any
|
||||
if err := json.Unmarshal(bytes.TrimSpace(devBuf.Bytes()), &devLine); err == nil {
|
||||
t.Errorf("development handler should NOT emit JSON; got parseable JSON: %s", devBuf.String())
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue