Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions router-tests/graphql_over_get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (

"github.com/stretchr/testify/require"
"github.com/wundergraph/cosmo/router-tests/testenv"
"github.com/wundergraph/cosmo/router/core"
"golang.org/x/net/html"
)

func TestOperationsOverGET(t *testing.T) {
Expand Down Expand Up @@ -54,6 +56,129 @@ func TestOperationsOverGET(t *testing.T) {
require.Equal(t, `{"errors":[{"message":"Mutations can only be sent over HTTP POST"}]}`, res.Body)
})
})

t.Run("Query should be successful with custom path", func(t *testing.T) {
t.Parallel()

testenv.Run(t, &testenv.Config{
OverrideGraphQLPath: "/custom-graphql",
}, func(t *testing.T, xEnv *testenv.Environment) {
res, err := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{
OperationName: []byte(`Employees`),
Query: `query Employees { employees { id } }`,
})
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.Response.StatusCode)
require.Equal(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":10},{"id":11},{"id":12}]}}`, res.Body)
})
})

t.Run("Mutation should not be allowed with custom path", func(t *testing.T) {
t.Parallel()

testenv.Run(t, &testenv.Config{
OverrideGraphQLPath: "/custom-graphql",
}, func(t *testing.T, xEnv *testenv.Environment) {
res, err := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{
OperationName: []byte(`updateEmployeeTag`),
Query: "mutation updateEmployeeTag {\n updateEmployeeTag(id: 10, tag: \"dd\") {\n id\n }\n}",
})
require.NoError(t, err)
require.Equal(t, http.StatusMethodNotAllowed, res.Response.StatusCode)
require.Equal(t, `{"errors":[{"message":"Mutations can only be sent over HTTP POST"}]}`, res.Body)
})
})

t.Run("Should return 404 for unknown path", func(t *testing.T) {
t.Parallel()

testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithGraphQLPath("/custom-graphql"),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
// Default path for creating requests is /graphql if not updated with `OverrideGraphQLPath`
res, err := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{
OperationName: []byte(`Employees`),
Query: `query Employees { employees { id } }`,
})

require.NoError(t, err)
require.Equal(t, http.StatusNotFound, res.Response.StatusCode)
})
})

t.Run("Should not create wildcard for root path", func(t *testing.T) {
Comment thread
StarpTech marked this conversation as resolved.
t.Parallel()

testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithGraphQLPath("/"),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
// Default path for creating requests is /graphql if not updated with `OverrideGraphQLPath`
res, err := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{
OperationName: []byte(`Employees`),
Query: `query Employees { employees { id } }`,
})

require.NoError(t, err)
require.Equal(t, http.StatusNotFound, res.Response.StatusCode)
})
})

t.Run("Should allow to create wildcard path", func(t *testing.T) {
t.Parallel()

testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithGraphQLPath("/*"),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
// Default path for creating requests is /graphql if not updated with `OverrideGraphQLPath`
res, err := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{
OperationName: []byte(`Employees`),
Query: `query Employees { employees { id } }`,
})

require.NoError(t, err)
require.Equal(t, http.StatusOK, res.Response.StatusCode)
require.Equal(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":10},{"id":11},{"id":12}]}}`, res.Body)

})
})

t.Run("Should serve both graphql and playground on the same path", func(t *testing.T) {
t.Parallel()

testenv.Run(t, &testenv.Config{
OverrideGraphQLPath: "/", // Default playground handler path
}, func(t *testing.T, xEnv *testenv.Environment) {
// We could see that successful graphql queries have been made in the previous tests
res, err := xEnv.MakeGraphQLRequestOverGET(testenv.GraphQLRequest{
OperationName: []byte(`Employees`),
Query: `query Employees { employees { id } }`,
})

require.NoError(t, err)
require.Equal(t, http.StatusOK, res.Response.StatusCode)
require.Equal(t, `{"data":{"employees":[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":7},{"id":8},{"id":10},{"id":11},{"id":12}]}}`, res.Body)

// If the graphql path and the playground path is the same, the playground will be mounted as a middleware and served based on the Accept header
// The accept header must be text/html to get the playground
header := http.Header{
"Accept": {"text/html; charset=utf-8"}, // simulate simplified browser request
}

httpRes, err := xEnv.MakeRequest(http.MethodGet, "/", header, nil)
require.NoError(t, err)
require.Equal(t, http.StatusOK, httpRes.StatusCode)

defer func() { _ = httpRes.Body.Close() }()
_, err = html.Parse(httpRes.Body)
require.NoError(t, err)
})
})
}

func TestSubscriptionOverGET(t *testing.T) {
Expand Down Expand Up @@ -143,4 +268,48 @@ func TestSubscriptionOverGET(t *testing.T) {
wg.Wait()
})
})

t.Run("should create subscription for custom graphql path", func(t *testing.T) {
t.Parallel()

type currentTimePayload struct {
Data struct {
CurrentTime struct {
UnixTime float64 `json:"unixTime"`
Timestamp string `json:"timestamp"`
} `json:"currentTime"`
} `json:"data"`
}

testenv.Run(t, &testenv.Config{
OverrideGraphQLPath: "/custom-graphql",
}, func(t *testing.T, xEnv *testenv.Environment) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

var wg sync.WaitGroup
wg.Add(2)

go xEnv.GraphQLSubscriptionOverSSEWithQueryParam(ctx, testenv.GraphQLRequest{
OperationName: []byte(`CurrentTime`),
Query: `subscription CurrentTime { currentTime { unixTime timeStamp }}`,
Header: map[string][]string{
"Content-Type": {"application/json"},
"Connection": {"keep-alive"},
"Cache-Control": {"no-cache"},
},
}, func(data string) {
defer wg.Done()

var payload currentTimePayload
err := json.Unmarshal([]byte(data), &payload)
require.NoError(t, err)

require.NotZero(t, payload.Data.CurrentTime.UnixTime)
require.NotEmpty(t, payload.Data.CurrentTime.Timestamp)
})

wg.Wait()
})
})
}
79 changes: 79 additions & 0 deletions router-tests/graphql_over_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,4 +239,83 @@ func TestGraphQLOverHTTPCompatibility(t *testing.T) {
require.Equal(t, http.StatusOK, res.StatusCode)
})
})

t.Run("requests with custom Path", func(t *testing.T) {
t.Parallel()

testenv.Run(t, &testenv.Config{
OverrideGraphQLPath: "/custom-graphql",
}, func(t *testing.T, xEnv *testenv.Environment) {
t.Run("valid request should return 200 with custom path", func(t *testing.T) {
t.Parallel()
header := http.Header{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
}
body := []byte(`{"query":"query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}","variables":{"criteria":{"nationality":"GERMAN"}}}`)
res, err := xEnv.MakeRequest("POST", "/custom-graphql", header, bytes.NewReader(body))
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode)
data, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, `{"data":{"findEmployees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}}]}}`, string(data))
})

t.Run("valid request should return 404 with custom path", func(t *testing.T) {
t.Parallel()
header := http.Header{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
}
body := []byte(`{"query":"query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}","variables":{"criteria":{"nationality":"GERMAN"}}}`)
res, err := xEnv.MakeRequest("POST", "/graphql", header, bytes.NewReader(body))
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, res.StatusCode)
})

})

testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithGraphQLPath("/*"),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
t.Run("valid request should return status 200 when wildcard was defined for path", func(t *testing.T) {
t.Parallel()

header := http.Header{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
}

body := []byte(`{"query":"query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}","variables":{"criteria":{"nationality":"GERMAN"}}}`)
res, err := xEnv.MakeRequest("POST", "/graphql", header, bytes.NewReader(body))
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode)
data, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.Equal(t, `{"data":{"findEmployees":[{"id":1,"details":{"forename":"Jens","surname":"Neuse"}},{"id":2,"details":{"forename":"Dustin","surname":"Deus"}},{"id":4,"details":{"forename":"Björn","surname":"Schwenzer"}},{"id":11,"details":{"forename":"Alexandra","surname":"Neuse"}}]}}`, string(data))
})
})

testenv.Run(t, &testenv.Config{
RouterOptions: []core.Option{
core.WithGraphQLPath("/"),
},
}, func(t *testing.T, xEnv *testenv.Environment) {
t.Run("valid request should return status 404 when no wildcard was defined on root path", func(t *testing.T) {
t.Parallel()

header := http.Header{
"Content-Type": []string{"application/json"},
"Accept": []string{"application/json"},
}

body := []byte(`{"query":"query Find($criteria: SearchInput!) {findEmployees(criteria: $criteria){id details {forename surname}}}","variables":{"criteria":{"nationality":"GERMAN"}}}`)
res, err := xEnv.MakeRequest("POST", "/graphql", header, bytes.NewReader(body))
require.NoError(t, err)
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
})
})
}
8 changes: 4 additions & 4 deletions router/core/graph_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,11 @@ func newGraphServer(ctx context.Context, r *Router, routerConfig *nodev1.RouterC
})

// Mount the feature flag handler. It calls the base mux if no feature flag is set.
cr.Mount(r.graphqlPath, multiGraphHandler)
cr.Handle(r.graphqlPath, multiGraphHandler)

if r.webSocketConfiguration != nil && r.webSocketConfiguration.Enabled && r.webSocketConfiguration.AbsintheProtocol.Enabled {
// Mount the Absinthe protocol handler for WebSockets
httpRouter.Mount(r.webSocketConfiguration.AbsintheProtocol.HandlerPath, multiGraphHandler)
httpRouter.Handle(r.webSocketConfiguration.AbsintheProtocol.HandlerPath, multiGraphHandler)
}
})

Expand Down Expand Up @@ -1081,9 +1081,9 @@ func (s *graphServer) buildGraphMux(ctx context.Context,
httpRouter.Use(s.routerMiddlewares...)

// GraphQL over POST
httpRouter.Post("/", graphqlHandler.ServeHTTP)
httpRouter.Post(s.graphqlPath, graphqlHandler.ServeHTTP)
// GraphQL over GET
httpRouter.Get("/", graphqlHandler.ServeHTTP)
httpRouter.Get(s.graphqlPath, graphqlHandler.ServeHTTP)

gm.mux = httpRouter

Expand Down