hatch-surf/internal/store/sqlite_repo_test.go
CTO 90c3f6285d feat: mock endpoint — configure response per endpoint
- Add mock_status, mock_headers, mock_body columns to endpoints schema
- Add MockConfig struct and UpsertMock to Repository interface
- Implement UpsertMock in sqliteRepo (INSERT ON CONFLICT UPDATE)
- Update GetEndpoint to return mock configuration when present
- Add PUT /e/{endpoint_id}/mock handler with JSON body {status, headers, body}
- Add catch-all /{endpoint_id} handler returning mock or default 200
- Header sanitization: only allow Content-Type, Cache-Control, X-*
- Wire handler into cmd/hatch main with SQLite store
- Fix Dockerfile: COPY go.sum for reproducible builds
- Tests: store mock CRUD, handler mock config + header sanitization

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-22 14:32:53 +02:00

108 lines
4.1 KiB
Go

package store
import (
"context"
"database/sql"
"testing"
"time"
)
func openTestRepo(t *testing.T) Repository {
t.Helper()
db, err := sql.Open("sqlite", ":memory:?_journal_mode=WAL&_foreign_keys=on")
if err != nil { t.Fatalf("open in-memory sqlite: %v", err) }
db.SetMaxOpenConns(1)
if err := migrate(db); err != nil { db.Close(); t.Fatalf("migrate: %v", err) }
repo, err := NewSQLiteRepo(db)
if err != nil { db.Close(); t.Fatalf("new sqlite repo: %v", err) }
t.Cleanup(func() { repo.Close() })
return repo
}
func TestCreateAndGetEndpoint(t *testing.T) {
repo := openTestRepo(t)
e, err := repo.CreateEndpoint(context.Background(), "test-one")
if err != nil { t.Fatalf("CreateEndpoint: %v", err) }
if e.ID == "" { t.Error("expected non-empty ID") }
if e.URL != "test-one" { t.Errorf("url: got %q", e.URL) }
if e.CreatedAt == "" || e.UpdatedAt == "" { t.Error("expected timestamps") }
got, err := repo.GetEndpoint(context.Background(), e.ID)
if err != nil { t.Fatalf("GetEndpoint: %v", err) }
if got.ID != e.ID || got.URL != e.URL { t.Errorf("got %+v", got) }
}
func TestGetEndpointNotFound(t *testing.T) {
repo := openTestRepo(t)
_, err := repo.GetEndpoint(context.Background(), "nonexistent")
if err == nil { t.Fatal("expected error") }
}
func TestCreateEndpointDuplicateURL(t *testing.T) {
repo := openTestRepo(t)
ctx := context.Background()
if _, err := repo.CreateEndpoint(ctx, "dup"); err != nil { t.Fatal(err) }
if _, err := repo.CreateEndpoint(ctx, "dup"); err == nil { t.Fatal("expected UNIQUE error") }
}
func TestAppendAndListRequests(t *testing.T) {
repo := openTestRepo(t)
ctx := context.Background()
e, err := repo.CreateEndpoint(ctx, "reqs-test")
if err != nil { t.Fatal(err) }
r1 := &Request{Method: "POST", Path: "/webhook", Headers: `{"Content-Type":"application/json"}`, Query: "foo=bar", Body: []byte(`{"hello":"world"}`)}
if err := repo.AppendRequest(ctx, e.ID, r1); err != nil { t.Fatal(err) }
time.Sleep(time.Millisecond)
r2 := &Request{Method: "GET", Path: "/webhook", Headers: `{}`}
if err := repo.AppendRequest(ctx, e.ID, r2); err != nil { t.Fatal(err) }
reqs, err := repo.ListRequests(ctx, e.ID, 10)
if err != nil { t.Fatal(err) }
if len(reqs) != 2 { t.Fatalf("got %d requests", len(reqs)) }
if reqs[0].Method != "GET" { t.Errorf("first: got %s", reqs[0].Method) }
if reqs[1].Method != "POST" { t.Errorf("second: got %s", reqs[1].Method) }
}
func TestListRequestsLimit(t *testing.T) {
repo := openTestRepo(t)
ctx := context.Background()
e, _ := repo.CreateEndpoint(ctx, "limit-test")
for i := 0; i < 5; i++ { repo.AppendRequest(ctx, e.ID, &Request{Method: "GET", Path: "/"}) }
reqs, err := repo.ListRequests(ctx, e.ID, 3)
if err != nil { t.Fatal(err) }
if len(reqs) != 3 { t.Fatalf("got %d", len(reqs)) }
}
func TestListRequestsEmpty(t *testing.T) {
repo := openTestRepo(t)
ctx := context.Background()
e, _ := repo.CreateEndpoint(ctx, "empty-test")
reqs, _ := repo.ListRequests(ctx, e.ID, 10)
if len(reqs) != 0 { t.Errorf("got %d", len(reqs)) }
}
func TestAppendRequestBodyBinary(t *testing.T) {
repo := openTestRepo(t)
ctx := context.Background()
e, _ := repo.CreateEndpoint(ctx, "binary-test")
body := []byte{0x00, 0x01, 0x02, 0xFF}
if err := repo.AppendRequest(ctx, e.ID, &Request{Method: "POST", Path: "/binary", Body: body}); err != nil { t.Fatal(err) }
reqs, _ := repo.ListRequests(ctx, e.ID, 1)
if len(reqs) != 1 { t.Fatal("expected 1 request") }
if len(reqs[0].Body) != 4 { t.Errorf("body len: %d", len(reqs[0].Body)) }
}
func TestMigrateIdempotent(t *testing.T) {
db, err := sql.Open("sqlite", ":memory:?_journal_mode=WAL&_foreign_keys=on")
if err != nil { t.Fatal(err) }
defer db.Close()
db.SetMaxOpenConns(1)
if err := migrate(db); err != nil { t.Fatal(err) }
if err := migrate(db); err != nil { t.Fatal(err) }
var name string
if err := db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='endpoints'").Scan(&name); err != nil {
t.Fatalf("endpoints table: %v", err)
}
if err := db.QueryRow("SELECT name FROM sqlite_master WHERE type='table' AND name='requests'").Scan(&name); err != nil {
t.Fatalf("requests table: %v", err)
}
}