// SPDX-License-Identifier: EUPL-1.2 package api_test import ( "context" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/99designs/gqlgen/graphql" "github.com/gin-gonic/gin" "github.com/vektah/gqlparser/v2" "github.com/vektah/gqlparser/v2/ast" api "forge.lthn.ai/core/go-api" ) // newTestSchema creates a minimal ExecutableSchema that responds to { name } // with {"name":"test"}. This avoids importing gqlgen's internal testserver // while providing a realistic schema for handler tests. func newTestSchema() graphql.ExecutableSchema { schema := gqlparser.MustLoadSchema(&ast.Source{Input: ` type Query { name: String! } `}) return &graphql.ExecutableSchemaMock{ SchemaFunc: func() *ast.Schema { return schema }, ExecFunc: func(ctx context.Context) graphql.ResponseHandler { ran := false return func(ctx context.Context) *graphql.Response { if ran { return nil } ran = true return &graphql.Response{Data: []byte(`{"name":"test"}`)} } }, ComplexityFunc: func(_ context.Context, _, _ string, childComplexity int, _ map[string]any) (int, bool) { return childComplexity, true }, } } // ── GraphQL endpoint ────────────────────────────────────────────────── func TestWithGraphQL_Good_EndpointResponds(t *testing.T) { gin.SetMode(gin.TestMode) e, err := api.New(api.WithGraphQL(newTestSchema())) if err != nil { t.Fatalf("unexpected error: %v", err) } srv := httptest.NewServer(e.Handler()) defer srv.Close() body := `{"query":"{ name }"}` resp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } respBody, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read body: %v", err) } if !strings.Contains(string(respBody), `"name":"test"`) { t.Fatalf("expected response containing name:test, got %q", string(respBody)) } } func TestWithGraphQL_Good_PlaygroundServesHTML(t *testing.T) { gin.SetMode(gin.TestMode) e, err := api.New(api.WithGraphQL(newTestSchema(), api.WithPlayground())) if err != nil { t.Fatalf("unexpected error: %v", err) } srv := httptest.NewServer(e.Handler()) defer srv.Close() resp, err := http.Get(srv.URL + "/graphql/playground") if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } ct := resp.Header.Get("Content-Type") if !strings.Contains(ct, "text/html") { t.Fatalf("expected Content-Type containing text/html, got %q", ct) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read body: %v", err) } if !strings.Contains(string(body), "GraphQL") { t.Fatalf("expected playground HTML containing 'GraphQL', got %q", string(body)[:200]) } } func TestWithGraphQL_Good_NoPlaygroundByDefault(t *testing.T) { gin.SetMode(gin.TestMode) // Without WithPlayground(), /graphql/playground should return 404. e, err := api.New(api.WithGraphQL(newTestSchema())) if err != nil { t.Fatalf("unexpected error: %v", err) } h := e.Handler() w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/graphql/playground", nil) h.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Fatalf("expected 404 for /graphql/playground without WithPlayground, got %d", w.Code) } } func TestWithGraphQL_Good_CustomPath(t *testing.T) { gin.SetMode(gin.TestMode) e, err := api.New(api.WithGraphQL(newTestSchema(), api.WithGraphQLPath("/gql"), api.WithPlayground())) if err != nil { t.Fatalf("unexpected error: %v", err) } srv := httptest.NewServer(e.Handler()) defer srv.Close() // Query endpoint should be at /gql. body := `{"query":"{ name }"}` resp, err := http.Post(srv.URL+"/gql", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200 at /gql, got %d", resp.StatusCode) } respBody, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read body: %v", err) } if !strings.Contains(string(respBody), `"name":"test"`) { t.Fatalf("expected response containing name:test, got %q", string(respBody)) } // Playground should be at /gql/playground. pgResp, err := http.Get(srv.URL + "/gql/playground") if err != nil { t.Fatalf("playground request failed: %v", err) } defer pgResp.Body.Close() if pgResp.StatusCode != http.StatusOK { t.Fatalf("expected 200 at /gql/playground, got %d", pgResp.StatusCode) } // The default path should not exist. defaultResp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("default path request failed: %v", err) } defer defaultResp.Body.Close() if defaultResp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404 at /graphql when custom path is /gql, got %d", defaultResp.StatusCode) } } func TestWithGraphQL_Good_CombinesWithOtherMiddleware(t *testing.T) { gin.SetMode(gin.TestMode) e, err := api.New( api.WithRequestID(), api.WithGraphQL(newTestSchema()), ) if err != nil { t.Fatalf("unexpected error: %v", err) } srv := httptest.NewServer(e.Handler()) defer srv.Close() body := `{"query":"{ name }"}` resp, err := http.Post(srv.URL+"/graphql", "application/json", strings.NewReader(body)) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } // RequestID middleware should have injected the header. reqID := resp.Header.Get("X-Request-ID") if reqID == "" { t.Fatal("expected X-Request-ID header from RequestID middleware") } respBody, err := io.ReadAll(resp.Body) if err != nil { t.Fatalf("failed to read body: %v", err) } if !strings.Contains(string(respBody), `"name":"test"`) { t.Fatalf("expected response containing name:test, got %q", string(respBody)) } }