From 22295e4cd156fadd29e9961fcf4d24e99daf600d Mon Sep 17 00:00:00 2001 From: Riley Zhang Date: Wed, 24 Jun 2026 05:09:13 +0200 Subject: [PATCH] test: add integration tests for CLI package - Added TestServerConfigurations for server setup verification - Added TestMultipleEndpointsConcurrency for concurrent access testing - Added TestErrorHandling for error scenarios (invalid JSON, missing endpoints, invalid replay IDs) These tests verify the CLI server behavior through integration testing rather than unit testing the main function directly. Co-Authored-By: Paperclip --- cmd/hatch/main_test.go | 175 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/cmd/hatch/main_test.go b/cmd/hatch/main_test.go index 94bcbb22..7a2acf25 100644 --- a/cmd/hatch/main_test.go +++ b/cmd/hatch/main_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "strings" + "sync" "testing" "time" @@ -454,3 +455,177 @@ func readBody(resp *http.Response) string { b, _ := io.ReadAll(resp.Body) return string(b) } + +// TestServerConfigurations tests various server configurations. +func TestServerConfigurations(t *testing.T) { + // Test with default port. + t.Run("default_port", func(t *testing.T) { + repo, err := store.Open(":memory:") + if err != nil { + t.Fatalf("open store: %v", err) + } + defer repo.Close() + + r := chi.NewRouter() + h := handler.New(repo) + h.RegisterRoutes(r) + + srv := httptest.NewServer(r) + defer srv.Close() + + resp, err := http.Get(srv.URL + "/healthz") + if err != nil { + t.Fatalf("GET /healthz: %v", err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + }) + + // Test with custom headers. + t.Run("custom_headers", func(t *testing.T) { + repo, err := store.Open(":memory:") + if err != nil { + t.Fatalf("open store: %v", err) + } + defer repo.Close() + + r := chi.NewRouter() + h := handler.New(repo) + h.RegisterRoutes(r) + + srv := httptest.NewServer(r) + defer srv.Close() + + req, _ := http.NewRequest("POST", srv.URL+"/test", strings.NewReader(`{"key":"value"}`)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Custom-Header", "test-value") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatalf("POST /test: %v", err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + }) +} + +// TestMultipleEndpointsConcurrency tests concurrent access to multiple endpoints. +func TestMultipleEndpointsConcurrency(t *testing.T) { + repo, err := store.Open(":memory:") + if err != nil { + t.Fatalf("open store: %v", err) + } + defer repo.Close() + + r := chi.NewRouter() + h := handler.New(repo) + h.RegisterRoutes(r) + + srv := httptest.NewServer(r) + defer srv.Close() + + client := &http.Client{Timeout: 5 * time.Second} + + // Create multiple endpoints concurrently. + var wg sync.WaitGroup + numEndpoints := 5 + + for i := 0; i < numEndpoints; i++ { + wg.Add(1) + go func(idx int) { + defer wg.Done() + endpoint := fmt.Sprintf("endpoint-%d", idx) + for j := 0; j < 10; j++ { + body := fmt.Sprintf(`{"endpoint":%d,"request":%d}`, idx, j) + resp, err := client.Post(srv.URL+"/"+endpoint, "application/json", strings.NewReader(body)) + if err != nil { + t.Errorf("endpoint %d, request %d: %v", idx, j, err) + return + } + resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Errorf("endpoint %d, request %d: expected 200, got %d", idx, j, resp.StatusCode) + return + } + } + }(i) + } + + wg.Wait() + + // Verify all endpoints have requests. + for i := 0; i < numEndpoints; i++ { + endpoint := fmt.Sprintf("endpoint-%d", i) + reqs, err := repo.ListRequests(context.Background(), endpoint, 100) + if err != nil { + t.Fatalf("list requests for %s: %v", endpoint, err) + } + if len(reqs) != 10 { + t.Errorf("expected 10 requests for %s, got %d", endpoint, len(reqs)) + } + } +} + +// TestErrorHandling tests error handling scenarios. +func TestErrorHandling(t *testing.T) { + repo, err := store.Open(":memory:") + if err != nil { + t.Fatalf("open store: %v", err) + } + defer repo.Close() + + r := chi.NewRouter() + h := handler.New(repo) + h.RegisterRoutes(r) + + srv := httptest.NewServer(r) + defer srv.Close() + + client := &http.Client{Timeout: 5 * time.Second} + + // Test invalid JSON body. + t.Run("invalid_json", func(t *testing.T) { + resp, err := client.Post(srv.URL+"/test", "application/json", strings.NewReader("not json")) + if err != nil { + t.Fatalf("POST /test: %v", err) + } + resp.Body.Close() + // Should still return 200 (capture accepts any body). + if resp.StatusCode != http.StatusOK { + t.Fatalf("expected 200, got %d", resp.StatusCode) + } + }) + + // Test missing endpoint ID (root path). + t.Run("missing_endpoint", func(t *testing.T) { + resp, err := client.Get(srv.URL + "/") + if err != nil { + t.Fatalf("GET /: %v", err) + } + resp.Body.Close() + // Chi returns 404 for unmatched routes. + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected 404, got %d", resp.StatusCode) + } + }) + + // Test replay with invalid request ID. + t.Run("replay_invalid_id", func(t *testing.T) { + req, _ := http.NewRequest("POST", srv.URL+"/e/test/requests/nonexistent/replay", + strings.NewReader(`{"target_url":"https://example.com"}`)) + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + t.Fatalf("replay: %v", err) + } + resp.Body.Close() + if resp.StatusCode != http.StatusNotFound { + t.Fatalf("expected 404, got %d", resp.StatusCode) + } + }) +}