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>
This commit is contained in:
parent
57a9eb6835
commit
6f03ff2465
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
@ -17,6 +17,14 @@ jobs:
|
|||||||
go-version: '1.25'
|
go-version: '1.25'
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: |
|
||||||
|
if [ -n "$(gofmt -l .)" ]; then
|
||||||
|
echo "Files not formatted:"
|
||||||
|
gofmt -l .
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Vet
|
- name: Vet
|
||||||
run: go vet ./...
|
run: go vet ./...
|
||||||
|
|
||||||
@ -41,6 +49,21 @@ jobs:
|
|||||||
curl -fsS http://localhost:8080/healthz | grep -q 'ok'
|
curl -fsS http://localhost:8080/healthz | grep -q 'ok'
|
||||||
docker stop hatch-ci
|
docker stop hatch-ci
|
||||||
|
|
||||||
|
lint-go:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.25'
|
||||||
|
cache: true
|
||||||
|
|
||||||
|
- name: Run golangci-lint
|
||||||
|
uses: golangci/golangci-lint-action@v6
|
||||||
|
with:
|
||||||
|
version: latest
|
||||||
|
|
||||||
lint-docs:
|
lint-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
5
.gitignore
vendored
5
.gitignore
vendored
@ -12,6 +12,11 @@ go.work
|
|||||||
/bin
|
/bin
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
data/
|
||||||
|
*.db
|
||||||
|
coverage.out
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
57
.golangci.yml
Normal file
57
.golangci.yml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- gosimple
|
||||||
|
- govet
|
||||||
|
- ineffassign
|
||||||
|
- staticcheck
|
||||||
|
- unused
|
||||||
|
- gofmt
|
||||||
|
- misspell
|
||||||
|
- unconvert
|
||||||
|
- unparam
|
||||||
|
- prealloc
|
||||||
|
- gocritic
|
||||||
|
- revive
|
||||||
|
|
||||||
|
linters-settings:
|
||||||
|
errcheck:
|
||||||
|
check-type-assertions: true
|
||||||
|
check-blank: true
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
revive:
|
||||||
|
rules:
|
||||||
|
- name: blank-imports
|
||||||
|
- name: context-as-argument
|
||||||
|
- name: dot-imports
|
||||||
|
- name: error-return
|
||||||
|
- name: error-strings
|
||||||
|
- name: error-naming
|
||||||
|
- name: exported
|
||||||
|
- name: if-return
|
||||||
|
- name: increment-decrement
|
||||||
|
- name: var-naming
|
||||||
|
- name: var-declaration
|
||||||
|
- name: range
|
||||||
|
- name: receiver-naming
|
||||||
|
- name: indent-error-flow
|
||||||
|
- name: superfluous-else
|
||||||
|
- name: unreachable-code
|
||||||
|
- name: redefines-builtin-id
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-rules:
|
||||||
|
# Exclude some linters from running on test files
|
||||||
|
- path: _test\.go
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
- gocritic
|
||||||
|
- unparam
|
||||||
|
# Exclude known issues in test files
|
||||||
|
- path: _test\.go
|
||||||
|
text: "func name"
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
modules-download-mode: readonly
|
||||||
35
Makefile
Normal file
35
Makefile
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
.PHONY: all build test lint fmt vet clean help
|
||||||
|
|
||||||
|
all: lint test build
|
||||||
|
|
||||||
|
## build: Build the hatch binary
|
||||||
|
build:
|
||||||
|
CGO_ENABLED=0 go build -o hatch ./cmd/hatch
|
||||||
|
|
||||||
|
## test: Run all tests with race detection and coverage
|
||||||
|
test:
|
||||||
|
go test ./... -v -race -coverprofile=coverage.out
|
||||||
|
|
||||||
|
## lint: Run golangci-lint
|
||||||
|
lint:
|
||||||
|
golangci-lint run ./...
|
||||||
|
|
||||||
|
## fmt: Format code with gofmt and goimports
|
||||||
|
fmt:
|
||||||
|
gofmt -s -w .
|
||||||
|
goimports -w .
|
||||||
|
|
||||||
|
## vet: Run go vet
|
||||||
|
vet:
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
## clean: Remove build artifacts
|
||||||
|
clean:
|
||||||
|
rm -f hatch coverage.out
|
||||||
|
|
||||||
|
## help: Show this help message
|
||||||
|
help:
|
||||||
|
@echo "Usage: make [target]"
|
||||||
|
@echo ""
|
||||||
|
@echo "Targets:"
|
||||||
|
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/## / /'
|
||||||
@ -8,10 +8,11 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/elfoundation/hatch/internal/store"
|
"github.com/elfoundation/hatch/internal/store"
|
||||||
|
"github.com/elfoundation/hatch/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestV1CaptureRequest(t *testing.T) {
|
func TestV1CaptureRequest(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
|
|
||||||
body := `{"method":"POST","path":"/test","headers":{"X-Custom":"value"},"query":"foo=bar","body":"hello"}`
|
body := `{"method":"POST","path":"/test","headers":{"X-Custom":"value"},"query":"foo=bar","body":"hello"}`
|
||||||
@ -35,16 +36,16 @@ func TestV1CaptureRequest(t *testing.T) {
|
|||||||
t.Errorf("expected path /test, got %v", resp["path"])
|
t.Errorf("expected path /test, got %v", resp["path"])
|
||||||
}
|
}
|
||||||
// Verify request stored in repo.
|
// Verify request stored in repo.
|
||||||
if len(repo.requests) != 1 {
|
if len(repo.Requests) != 1 {
|
||||||
t.Fatalf("expected 1 request stored, got %d", len(repo.requests))
|
t.Fatalf("expected 1 request stored, got %d", len(repo.Requests))
|
||||||
}
|
}
|
||||||
if repo.requests[0].Headers != `{"X-Custom":"value"}` {
|
if repo.Requests[0].Headers != `{"X-Custom":"value"}` {
|
||||||
t.Errorf("expected headers JSON, got %q", repo.requests[0].Headers)
|
t.Errorf("expected headers JSON, got %q", repo.Requests[0].Headers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV1ListRequests(t *testing.T) {
|
func TestV1ListRequests(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "list-ep")
|
repo.CreateEndpoint(nil, "list-ep")
|
||||||
// Add a few requests.
|
// Add a few requests.
|
||||||
repo.AppendRequest(nil, "list-ep", &store.Request{Method: "GET", Path: "/a"})
|
repo.AppendRequest(nil, "list-ep", &store.Request{Method: "GET", Path: "/a"})
|
||||||
@ -77,7 +78,7 @@ func TestV1ListRequests(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestV1SearchRequests(t *testing.T) {
|
func TestV1SearchRequests(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "search-ep")
|
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: "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: "POST", Path: "/orders", Body: []byte(`{"item":"apple"}`)})
|
||||||
@ -111,7 +112,7 @@ func TestV1SearchRequests(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestV1MockSet(t *testing.T) {
|
func TestV1MockSet(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "mock-ep")
|
repo.CreateEndpoint(nil, "mock-ep")
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
|
|
||||||
@ -138,7 +139,7 @@ func TestV1MockSet(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestV1OpenAPI(t *testing.T) {
|
func TestV1OpenAPI(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
req := httptest.NewRequest(http.MethodGet, "/v1/endpoints/test/openapi.json", nil)
|
req := httptest.NewRequest(http.MethodGet, "/v1/endpoints/test/openapi.json", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|||||||
@ -11,89 +11,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/elfoundation/hatch/internal/store"
|
"github.com/elfoundation/hatch/internal/store"
|
||||||
|
"github.com/elfoundation/hatch/internal/testutil"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fakeRepo implements store.Repository for tests.
|
|
||||||
type fakeRepo struct {
|
|
||||||
endpoints map[string]*store.Endpoint
|
|
||||||
requests []*store.Request
|
|
||||||
mocks map[string]*store.MockConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func newFakeRepo() *fakeRepo {
|
|
||||||
return &fakeRepo{
|
|
||||||
endpoints: map[string]*store.Endpoint{},
|
|
||||||
mocks: map[string]*store.MockConfig{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *fakeRepo) CreateEndpoint(_ context.Context, u string) (*store.Endpoint, error) {
|
|
||||||
e := &store.Endpoint{ID: u, URL: u, CreatedAt: "t", UpdatedAt: "t"}
|
|
||||||
f.endpoints[u] = e
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) GetEndpoint(_ context.Context, id string) (*store.Endpoint, error) {
|
|
||||||
e, ok := f.endpoints[id]
|
|
||||||
if !ok {
|
|
||||||
return nil, errNotFound
|
|
||||||
}
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) AppendRequest(_ context.Context, eid string, r *store.Request) error {
|
|
||||||
r.ID = "req-" + string(rune(len(f.requests)+'0'))
|
|
||||||
r.EndpointID = eid
|
|
||||||
f.requests = append(f.requests, r)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) GetRequest(_ context.Context, id string) (*store.Request, error) {
|
|
||||||
for _, r := range f.requests {
|
|
||||||
if r.ID == id {
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errNotFound
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) ListRequests(_ context.Context, _ string, _ int) ([]*store.Request, error) {
|
|
||||||
return f.requests, nil
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) SearchRequests(_ context.Context, _ string, query string, limit int) ([]*store.Request, error) {
|
|
||||||
if query == "" {
|
|
||||||
return f.requests, nil
|
|
||||||
}
|
|
||||||
var matched []*store.Request
|
|
||||||
for _, r := range f.requests {
|
|
||||||
if strings.Contains(r.Method, query) ||
|
|
||||||
strings.Contains(r.Path, query) ||
|
|
||||||
strings.Contains(r.Headers, query) ||
|
|
||||||
strings.Contains(r.Query, query) ||
|
|
||||||
strings.Contains(string(r.Body), query) {
|
|
||||||
matched = append(matched, r)
|
|
||||||
if limit > 0 && len(matched) >= limit {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matched, nil
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) GetMock(_ context.Context, endpointID string) (*store.MockConfig, error) {
|
|
||||||
m, ok := f.mocks[endpointID]
|
|
||||||
if !ok {
|
|
||||||
return nil, errNotFound
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) SetMock(_ context.Context, mock *store.MockConfig) error {
|
|
||||||
f.mocks[mock.EndpointID] = mock
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (f *fakeRepo) Close() error { return nil }
|
|
||||||
|
|
||||||
var errNotFound = &se{"nf"}
|
|
||||||
|
|
||||||
type se struct{ m string }
|
|
||||||
|
|
||||||
func (e *se) Error() string { return e.m }
|
|
||||||
|
|
||||||
// testRouter creates a chi router with all routes registered using a fake repo.
|
// testRouter creates a chi router with all routes registered using a fake repo.
|
||||||
func testRouter(repo store.Repository) chi.Router {
|
func testRouter(repo store.Repository) chi.Router {
|
||||||
@ -104,7 +25,7 @@ func testRouter(repo store.Repository) chi.Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHealthz(t *testing.T) {
|
func TestHealthz(t *testing.T) {
|
||||||
r := testRouter(newFakeRepo())
|
r := testRouter(testutil.NewFakeRepository())
|
||||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
@ -121,7 +42,7 @@ func TestHealthz(t *testing.T) {
|
|||||||
func TestCaptureRecordsAllVerbs(t *testing.T) {
|
func TestCaptureRecordsAllVerbs(t *testing.T) {
|
||||||
methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"}
|
methods := []string{"GET", "POST", "PUT", "PATCH", "DELETE"}
|
||||||
for _, m := range methods {
|
for _, m := range methods {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
|
|
||||||
body := ""
|
body := ""
|
||||||
@ -138,11 +59,11 @@ func TestCaptureRecordsAllVerbs(t *testing.T) {
|
|||||||
if w.Code != 200 {
|
if w.Code != 200 {
|
||||||
t.Errorf("%s: expected 200, got %d", m, w.Code)
|
t.Errorf("%s: expected 200, got %d", m, w.Code)
|
||||||
}
|
}
|
||||||
if len(repo.requests) != 1 {
|
if len(repo.Requests) != 1 {
|
||||||
t.Errorf("%s: expected 1 request, got %d", m, len(repo.requests))
|
t.Errorf("%s: expected 1 request, got %d", m, len(repo.Requests))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reqCaptured := repo.requests[0]
|
reqCaptured := repo.Requests[0]
|
||||||
if reqCaptured.Method != m {
|
if reqCaptured.Method != m {
|
||||||
t.Errorf("%s: wrong method %s", m, reqCaptured.Method)
|
t.Errorf("%s: wrong method %s", m, reqCaptured.Method)
|
||||||
}
|
}
|
||||||
@ -150,7 +71,7 @@ func TestCaptureRecordsAllVerbs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSSEStreamReceivesEventOnCapture(t *testing.T) {
|
func TestSSEStreamReceivesEventOnCapture(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
srv := httptest.NewServer(testRouter(repo))
|
srv := httptest.NewServer(testRouter(repo))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
@ -224,7 +145,7 @@ func TestSSEStreamReceivesEventOnCapture(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInspectReturnsHTML(t *testing.T) {
|
func TestInspectReturnsHTML(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "ep")
|
repo.CreateEndpoint(nil, "ep")
|
||||||
repo.AppendRequest(nil, "ep", &store.Request{
|
repo.AppendRequest(nil, "ep", &store.Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@ -290,7 +211,7 @@ func TestInspectReturnsHTML(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInspectEmptyState(t *testing.T) {
|
func TestInspectEmptyState(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "new-ep")
|
repo.CreateEndpoint(nil, "new-ep")
|
||||||
|
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
@ -320,7 +241,7 @@ func TestInspectEmptyState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestInspectAutoCreatesEndpoint(t *testing.T) {
|
func TestInspectAutoCreatesEndpoint(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
|
|
||||||
// The endpoint doesn't exist yet.
|
// The endpoint doesn't exist yet.
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
@ -350,7 +271,7 @@ func TestInspectAutoCreatesEndpoint(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCaptureReturnsJSON200(t *testing.T) {
|
func TestCaptureReturnsJSON200(t *testing.T) {
|
||||||
r := testRouter(newFakeRepo())
|
r := testRouter(testutil.NewFakeRepository())
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r.ServeHTTP(w, httptest.NewRequest("GET", "/t", nil))
|
r.ServeHTTP(w, httptest.NewRequest("GET", "/t", nil))
|
||||||
|
|
||||||
@ -369,7 +290,7 @@ func TestCaptureReturnsJSON200(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMockSetsAndConfiguresResponse(t *testing.T) {
|
func TestMockSetsAndConfiguresResponse(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "mock-ep")
|
repo.CreateEndpoint(nil, "mock-ep")
|
||||||
|
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
@ -399,7 +320,7 @@ func TestMockSetsAndConfiguresResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMockReturnsConfiguredResponseOnCapture(t *testing.T) {
|
func TestMockReturnsConfiguredResponseOnCapture(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "mock-cap")
|
repo.CreateEndpoint(nil, "mock-cap")
|
||||||
// Pre-set a mock.
|
// Pre-set a mock.
|
||||||
repo.SetMock(nil, &store.MockConfig{
|
repo.SetMock(nil, &store.MockConfig{
|
||||||
@ -427,16 +348,16 @@ func TestMockReturnsConfiguredResponseOnCapture(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Request should still be captured even when mock responds.
|
// Request should still be captured even when mock responds.
|
||||||
if len(repo.requests) != 1 {
|
if len(repo.Requests) != 1 {
|
||||||
t.Fatalf("expected 1 captured request, got %d", len(repo.requests))
|
t.Fatalf("expected 1 captured request, got %d", len(repo.Requests))
|
||||||
}
|
}
|
||||||
if repo.requests[0].Method != "POST" {
|
if repo.Requests[0].Method != "POST" {
|
||||||
t.Errorf("captured method: %s", repo.requests[0].Method)
|
t.Errorf("captured method: %s", repo.Requests[0].Method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMockAutoCreatesEndpointOnSet(t *testing.T) {
|
func TestMockAutoCreatesEndpointOnSet(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
|
|
||||||
// Set a mock on an endpoint that doesn't exist yet.
|
// Set a mock on an endpoint that doesn't exist yet.
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/elfoundation/hatch/internal/store"
|
"github.com/elfoundation/hatch/internal/store"
|
||||||
|
"github.com/elfoundation/hatch/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReplayMissingTargetURL(t *testing.T) {
|
func TestReplayMissingTargetURL(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "ep")
|
repo.CreateEndpoint(nil, "ep")
|
||||||
repo.AppendRequest(nil, "ep", &store.Request{Method: "POST", Path: "/webhook", Headers: "{}"})
|
repo.AppendRequest(nil, "ep", &store.Request{Method: "POST", Path: "/webhook", Headers: "{}"})
|
||||||
reqID := repo.requests[0].ID
|
reqID := repo.Requests[0].ID
|
||||||
|
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
body := `{}`
|
body := `{}`
|
||||||
@ -34,10 +35,10 @@ func TestReplayMissingTargetURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReplayInvalidScheme(t *testing.T) {
|
func TestReplayInvalidScheme(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "ep")
|
repo.CreateEndpoint(nil, "ep")
|
||||||
repo.AppendRequest(nil, "ep", &store.Request{Method: "GET", Path: "/", Headers: "{}"})
|
repo.AppendRequest(nil, "ep", &store.Request{Method: "GET", Path: "/", Headers: "{}"})
|
||||||
reqID := repo.requests[0].ID
|
reqID := repo.Requests[0].ID
|
||||||
|
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
body := `{"target_url": "ftp://bad.scheme/"}`
|
body := `{"target_url": "ftp://bad.scheme/"}`
|
||||||
@ -52,10 +53,10 @@ func TestReplayInvalidScheme(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaySSRFBlocksLocalhost(t *testing.T) {
|
func TestReplaySSRFBlocksLocalhost(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "ep")
|
repo.CreateEndpoint(nil, "ep")
|
||||||
repo.AppendRequest(nil, "ep", &store.Request{Method: "GET", Path: "/", Headers: "{}"})
|
repo.AppendRequest(nil, "ep", &store.Request{Method: "GET", Path: "/", Headers: "{}"})
|
||||||
reqID := repo.requests[0].ID
|
reqID := repo.Requests[0].ID
|
||||||
|
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
body := `{"target_url": "http://localhost:8080/"}`
|
body := `{"target_url": "http://localhost:8080/"}`
|
||||||
@ -70,10 +71,10 @@ func TestReplaySSRFBlocksLocalhost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReplaySSRFBlocksPrivate(t *testing.T) {
|
func TestReplaySSRFBlocksPrivate(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "ep")
|
repo.CreateEndpoint(nil, "ep")
|
||||||
repo.AppendRequest(nil, "ep", &store.Request{Method: "GET", Path: "/", Headers: "{}"})
|
repo.AppendRequest(nil, "ep", &store.Request{Method: "GET", Path: "/", Headers: "{}"})
|
||||||
reqID := repo.requests[0].ID
|
reqID := repo.Requests[0].ID
|
||||||
|
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
for _, addr := range []string{
|
for _, addr := range []string{
|
||||||
@ -96,7 +97,7 @@ func TestReplaySSRFBlocksPrivate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReplayNotFound(t *testing.T) {
|
func TestReplayNotFound(t *testing.T) {
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
r := testRouter(repo)
|
r := testRouter(repo)
|
||||||
body := `{"target_url": "https://example.com/"}`
|
body := `{"target_url": "https://example.com/"}`
|
||||||
req := httptest.NewRequest(http.MethodPost, "/e/ep/requests/nonexistent/replay", strings.NewReader(body))
|
req := httptest.NewRequest(http.MethodPost, "/e/ep/requests/nonexistent/replay", strings.NewReader(body))
|
||||||
@ -118,7 +119,7 @@ func TestReplaySuccess(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer sink.Close()
|
defer sink.Close()
|
||||||
|
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
repo.CreateEndpoint(nil, "ep")
|
repo.CreateEndpoint(nil, "ep")
|
||||||
repo.AppendRequest(nil, "ep", &store.Request{
|
repo.AppendRequest(nil, "ep", &store.Request{
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@ -127,7 +128,7 @@ func TestReplaySuccess(t *testing.T) {
|
|||||||
Query: "foo=bar",
|
Query: "foo=bar",
|
||||||
Body: []byte(`{"msg":"hello"}`),
|
Body: []byte(`{"msg":"hello"}`),
|
||||||
})
|
})
|
||||||
reqID := repo.requests[0].ID
|
reqID := repo.Requests[0].ID
|
||||||
|
|
||||||
// Set env to allow private replay since httptest server is on loopback.
|
// Set env to allow private replay since httptest server is on loopback.
|
||||||
t.Setenv("HATCH_ALLOW_PRIVATE_REPLAY", "true")
|
t.Setenv("HATCH_ALLOW_PRIVATE_REPLAY", "true")
|
||||||
@ -175,7 +176,7 @@ func TestReplayE2ECaptureThenReplay(t *testing.T) {
|
|||||||
defer sink.Close()
|
defer sink.Close()
|
||||||
|
|
||||||
// Step 2: Capture a request.
|
// Step 2: Capture a request.
|
||||||
repo := newFakeRepo()
|
repo := testutil.NewFakeRepository()
|
||||||
rt := testRouter(repo)
|
rt := testRouter(repo)
|
||||||
|
|
||||||
captureReq := httptest.NewRequest("POST", "/e2e-test", strings.NewReader(`{"order":"1"}`))
|
captureReq := httptest.NewRequest("POST", "/e2e-test", strings.NewReader(`{"order":"1"}`))
|
||||||
@ -183,10 +184,10 @@ func TestReplayE2ECaptureThenReplay(t *testing.T) {
|
|||||||
captureW := httptest.NewRecorder()
|
captureW := httptest.NewRecorder()
|
||||||
rt.ServeHTTP(captureW, captureReq)
|
rt.ServeHTTP(captureW, captureReq)
|
||||||
|
|
||||||
if len(repo.requests) != 1 {
|
if len(repo.Requests) != 1 {
|
||||||
t.Fatalf("expected 1 captured request, got %d", len(repo.requests))
|
t.Fatalf("expected 1 captured request, got %d", len(repo.Requests))
|
||||||
}
|
}
|
||||||
captured := repo.requests[0]
|
captured := repo.Requests[0]
|
||||||
if captured.Method != "POST" {
|
if captured.Method != "POST" {
|
||||||
t.Errorf("captured method: %s", captured.Method)
|
t.Errorf("captured method: %s", captured.Method)
|
||||||
}
|
}
|
||||||
|
|||||||
98
internal/testutil/fake_repo.go
Normal file
98
internal/testutil/fake_repo.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package testutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/elfoundation/hatch/internal/store"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FakeRepository implements store.Repository for tests.
|
||||||
|
type FakeRepository struct {
|
||||||
|
Endpoints map[string]*store.Endpoint
|
||||||
|
Requests []*store.Request
|
||||||
|
Mocks map[string]*store.MockConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFakeRepository creates a new FakeRepository.
|
||||||
|
func NewFakeRepository() *FakeRepository {
|
||||||
|
return &FakeRepository{
|
||||||
|
Endpoints: map[string]*store.Endpoint{},
|
||||||
|
Mocks: map[string]*store.MockConfig{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) CreateEndpoint(_ context.Context, url string) (*store.Endpoint, error) {
|
||||||
|
e := &store.Endpoint{ID: url, URL: url, CreatedAt: "t", UpdatedAt: "t"}
|
||||||
|
f.Endpoints[url] = e
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) GetEndpoint(_ context.Context, id string) (*store.Endpoint, error) {
|
||||||
|
e, ok := f.Endpoints[id]
|
||||||
|
if !ok {
|
||||||
|
return nil, errNotFound
|
||||||
|
}
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) AppendRequest(_ context.Context, eid string, r *store.Request) error {
|
||||||
|
r.ID = "req-" + string(rune(len(f.Requests)+'0'))
|
||||||
|
r.EndpointID = eid
|
||||||
|
f.Requests = append(f.Requests, r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) GetRequest(_ context.Context, id string) (*store.Request, error) {
|
||||||
|
for _, r := range f.Requests {
|
||||||
|
if r.ID == id {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) ListRequests(_ context.Context, _ string, _ int) ([]*store.Request, error) {
|
||||||
|
return f.Requests, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) SearchRequests(_ context.Context, _ string, query string, limit int) ([]*store.Request, error) {
|
||||||
|
if query == "" {
|
||||||
|
return f.Requests, nil
|
||||||
|
}
|
||||||
|
var matched []*store.Request
|
||||||
|
for _, r := range f.Requests {
|
||||||
|
if strings.Contains(r.Method, query) ||
|
||||||
|
strings.Contains(r.Path, query) ||
|
||||||
|
strings.Contains(r.Headers, query) ||
|
||||||
|
strings.Contains(r.Query, query) ||
|
||||||
|
strings.Contains(string(r.Body), query) {
|
||||||
|
matched = append(matched, r)
|
||||||
|
if limit > 0 && len(matched) >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matched, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) GetMock(_ context.Context, endpointID string) (*store.MockConfig, error) {
|
||||||
|
m, ok := f.Mocks[endpointID]
|
||||||
|
if !ok {
|
||||||
|
return nil, errNotFound
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) SetMock(_ context.Context, mock *store.MockConfig) error {
|
||||||
|
f.Mocks[mock.EndpointID] = mock
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FakeRepository) Close() error { return nil }
|
||||||
|
|
||||||
|
type notFoundError struct{}
|
||||||
|
|
||||||
|
func (e *notFoundError) Error() string { return "not found" }
|
||||||
|
|
||||||
|
var errNotFound = ¬FoundError{}
|
||||||
Loading…
Reference in New Issue
Block a user