hatch-surf/internal/handler/api_v1_test.go
Riley Zhang 6f03ff2465 Add linting, formatting, and test infrastructure for Phase 1
- Add .golangci.yml with sensible linting rules
- Add Makefile with test, lint, fmt, vet, build targets
- Enhance CI pipeline with Go formatting check and golangci-lint job
- Create shared testutil.FakeRepository for handler tests
- Refactor handler tests to use shared FakeRepository
- Add data/ and coverage.out to .gitignore

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-24 04:59:52 +02:00

183 lines
5.8 KiB
Go

package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/elfoundation/hatch/internal/store"
"github.com/elfoundation/hatch/internal/testutil"
)
func TestV1CaptureRequest(t *testing.T) {
repo := testutil.NewFakeRepository()
r := testRouter(repo)
body := `{"method":"POST","path":"/test","headers":{"X-Custom":"value"},"query":"foo=bar","body":"hello"}`
req := httptest.NewRequest(http.MethodPost, "/v1/endpoints/test-ep/requests", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusCreated {
t.Fatalf("expected 201 Created, got %d: %s", w.Code, w.Body.String())
}
// Verify response JSON contains the request.
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("invalid JSON response: %v", err)
}
if resp["method"] != "POST" {
t.Errorf("expected method POST, got %v", resp["method"])
}
if resp["path"] != "/test" {
t.Errorf("expected path /test, got %v", resp["path"])
}
// Verify request stored in repo.
if len(repo.Requests) != 1 {
t.Fatalf("expected 1 request stored, got %d", len(repo.Requests))
}
if repo.Requests[0].Headers != `{"X-Custom":"value"}` {
t.Errorf("expected headers JSON, got %q", repo.Requests[0].Headers)
}
}
func TestV1ListRequests(t *testing.T) {
repo := testutil.NewFakeRepository()
repo.CreateEndpoint(nil, "list-ep")
// Add a few requests.
repo.AppendRequest(nil, "list-ep", &store.Request{Method: "GET", Path: "/a"})
repo.AppendRequest(nil, "list-ep", &store.Request{Method: "POST", Path: "/b"})
repo.AppendRequest(nil, "list-ep", &store.Request{Method: "PUT", Path: "/c"})
r := testRouter(repo)
req := httptest.NewRequest(http.MethodGet, "/v1/endpoints/list-ep/requests", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
}
var list []map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
t.Fatalf("invalid JSON array: %v", err)
}
if len(list) != 3 {
t.Errorf("expected 3 requests, got %d", len(list))
}
// Verify all methods present.
methods := make(map[string]bool)
for _, r := range list {
methods[r["method"].(string)] = true
}
if !methods["GET"] || !methods["POST"] || !methods["PUT"] {
t.Errorf("expected GET, POST, PUT, got %v", methods)
}
}
func TestV1SearchRequests(t *testing.T) {
repo := testutil.NewFakeRepository()
repo.CreateEndpoint(nil, "search-ep")
repo.AppendRequest(nil, "search-ep", &store.Request{Method: "GET", Path: "/users", Headers: `{"Accept":"json"}`})
repo.AppendRequest(nil, "search-ep", &store.Request{Method: "POST", Path: "/orders", Body: []byte(`{"item":"apple"}`)})
repo.AppendRequest(nil, "search-ep", &store.Request{Method: "DELETE", Path: "/users/123"})
r := testRouter(repo)
// Search for "users".
req := httptest.NewRequest(http.MethodGet, "/v1/endpoints/search-ep/requests?q=users", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
}
var list []map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
t.Fatalf("invalid JSON array: %v", err)
}
if len(list) != 2 {
t.Errorf("expected 2 requests matching 'users', got %d", len(list))
}
// Verify the matched methods.
methods := make(map[string]bool)
for _, r := range list {
methods[r["method"].(string)] = true
}
if !methods["GET"] || !methods["DELETE"] {
t.Errorf("expected GET and DELETE, got %v", methods)
}
}
func TestV1MockSet(t *testing.T) {
repo := testutil.NewFakeRepository()
repo.CreateEndpoint(nil, "mock-ep")
r := testRouter(repo)
mockBody := `{"status":201,"headers":{"X-Mock":"true"},"body":"bW9ja2Vk"}`
req := httptest.NewRequest(http.MethodPut, "/v1/endpoints/mock-ep/mock", bytes.NewBufferString(mockBody))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
}
// Verify mock stored.
mock, err := repo.GetMock(nil, "mock-ep")
if err != nil {
t.Fatalf("mock not stored: %v", err)
}
if mock.Status != 201 {
t.Errorf("expected status 201, got %d", mock.Status)
}
if mock.Headers["X-Mock"] != "true" {
t.Errorf("expected X-Mock header, got %v", mock.Headers)
}
}
func TestV1OpenAPI(t *testing.T) {
repo := testutil.NewFakeRepository()
r := testRouter(repo)
req := httptest.NewRequest(http.MethodGet, "/v1/endpoints/test/openapi.json", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d: %s", w.Code, w.Body.String())
}
ct := w.Header().Get("Content-Type")
if ct != "application/json" {
t.Errorf("expected application/json, got %q", ct)
}
var spec map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &spec); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if spec["openapi"] != "3.1.0" {
t.Errorf("expected openapi 3.1.0, got %v", spec["openapi"])
}
if spec["info"].(map[string]interface{})["title"] != "Hatch API" {
t.Errorf("expected title 'Hatch API', got %v", spec["info"])
}
// Verify paths exist.
paths, ok := spec["paths"].(map[string]interface{})
if !ok {
t.Fatal("missing paths")
}
if _, ok := paths["/v1/endpoints/{endpointID}/requests"]; !ok {
t.Error("missing capture/list path")
}
if _, ok := paths["/v1/endpoints/{endpointID}/requests/{requestID}/replay"]; !ok {
t.Error("missing replay path")
}
if _, ok := paths["/v1/endpoints/{endpointID}/mock"]; !ok {
t.Error("missing mock path")
}
}
// Import store package for the fakeRepo usage (already imported via handler_test.go)
// We just need to ensure the test compiles.
var _ = store.Request{}