Repository: jensneuse/graphql-go-tools
Pull request #196 (2020-03-01 14:34:21)

merge Tyk technologies master


Patch:
diff --git a/go.mod b/go.mod
index fb2c4e9d..b5249cd1 100644
--- a/go.mod
+++ b/go.mod
@@ -24,6 +24,7 @@ require (
 	github.com/sebdah/goldie v0.0.0-20180424091453-8784dd1ab561
 	github.com/spf13/cobra v0.0.5
 	github.com/spf13/viper v1.3.2
+	github.com/stretchr/testify v1.4.0
 	github.com/tidwall/gjson v1.3.5
 	github.com/tidwall/sjson v1.0.4
 	github.com/valyala/fasttemplate v1.1.0
diff --git a/pkg/http/handler.go b/pkg/http/handler.go
index 6ab152fd..83a4b00d 100644
--- a/pkg/http/handler.go
+++ b/pkg/http/handler.go
@@ -9,6 +9,10 @@ import (
 	"net/http"
 )
 
+const (
+	httpHeaderUpgrade string = "Upgrade"
+)
+
 func NewGraphqlHTTPHandlerFunc(executionHandler *execution.Handler, logger log.Logger, upgrader *ws.HTTPUpgrader) http.Handler {
 	return &GraphQLHTTPRequestHandler{
 		log:              logger,
@@ -48,7 +52,7 @@ func (g *GraphQLHTTPRequestHandler) upgradeWithNewGoroutine(w http.ResponseWrite
 }
 
 func (g *GraphQLHTTPRequestHandler) isWebsocketUpgrade(r *http.Request) bool {
-	for _, header := range r.Header["Upgrade"] {
+	for _, header := range r.Header[httpHeaderUpgrade] {
 		if header == "websocket" {
 			return true
 		}
@@ -63,7 +67,7 @@ func (g *GraphQLHTTPRequestHandler) extraVariables(r *http.Request, out io.Write
 	}
 
 	cookies := map[string]string{}
-	for _,cookie := range r.Cookies() {
+	for _, cookie := range r.Cookies() {
 		cookies[cookie.Name] = cookie.Value
 	}
 
diff --git a/pkg/http/http.go b/pkg/http/http.go
index 8897cced..28427f16 100644
--- a/pkg/http/http.go
+++ b/pkg/http/http.go
@@ -8,6 +8,12 @@ import (
 	"net/http"
 )
 
+const (
+	httpHeaderContentType string = "Content-Type"
+
+	httpContentTypeApplicationJson string = "application/json"
+)
+
 func (g *GraphQLHTTPRequestHandler) handleHTTP(w http.ResponseWriter, r *http.Request) {
 	data, err := ioutil.ReadAll(r.Body)
 	if err != nil {
@@ -46,7 +52,8 @@ func (g *GraphQLHTTPRequestHandler) handleHTTP(w http.ResponseWriter, r *http.Re
 		w.WriteHeader(http.StatusInternalServerError)
 		return
 	}
-	w.WriteHeader(http.StatusOK)
+
 	w.Header().Add("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
 	_, _ = buf.WriteTo(w)
 }
diff --git a/pkg/http/ws.go b/pkg/http/ws.go
index fcdad190..e0caa80a 100644
--- a/pkg/http/ws.go
+++ b/pkg/http/ws.go
@@ -4,12 +4,14 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"net"
+	"net/http"
+
 	"github.com/gobwas/ws"
 	"github.com/gobwas/ws/wsutil"
 	log "github.com/jensneuse/abstractlogger"
+
 	"github.com/jensneuse/graphql-go-tools/pkg/execution"
-	"net"
-	"net/http"
 )
 
 const (
diff --git a/pkg/playground/fixtures/handlers.golden b/pkg/playground/fixtures/handlers.golden
index 600ed35e..47aba055 100644
--- a/pkg/playground/fixtures/handlers.golden
+++ b/pkg/playground/fixtures/handlers.golden
@@ -4,19 +4,19 @@
   Handler: (http.HandlerFunc) <nil>
  },
  (playground.HandlerConfig) {
-  Path: (string) (len=14) "playground.css",
+  Path: (string) (len=15) "/playground.css",
   Handler: (http.HandlerFunc) <nil>
  },
  (playground.HandlerConfig) {
-  Path: (string) (len=13) "playground.js",
+  Path: (string) (len=14) "/playground.js",
   Handler: (http.HandlerFunc) <nil>
  },
  (playground.HandlerConfig) {
-  Path: (string) (len=11) "favicon.png",
+  Path: (string) (len=12) "/favicon.png",
   Handler: (http.HandlerFunc) <nil>
  },
  (playground.HandlerConfig) {
-  Path: (string) (len=8) "logo.png",
+  Path: (string) (len=9) "/logo.png",
   Handler: (http.HandlerFunc) <nil>
  }
 }
diff --git a/pkg/playground/playground.go b/pkg/playground/playground.go
index a9cbf2e6..eff80399 100644
--- a/pkg/playground/playground.go
+++ b/pkg/playground/playground.go
@@ -84,10 +84,10 @@ type Playground struct {
 func New(config Config) *Playground {
 
 	data := playgroundTemplateData{
-		CssURL:                  path.Join(config.PathPrefix, cssFile),
-		JsURL:                   path.Join(config.PathPrefix, jsFile),
-		FavIconURL:              path.Join(config.PathPrefix, faviconFile),
-		LogoURL:                 path.Join(config.PathPrefix, logoFile),
+		CssURL:                  path.Join("/", config.PathPrefix, cssFile),
+		JsURL:                   path.Join("/", config.PathPrefix, jsFile),
+		FavIconURL:              path.Join("/", config.PathPrefix, faviconFile),
+		LogoURL:                 path.Join("/", config.PathPrefix, logoFile),
 		EndpointURL:             config.GraphqlEndpointPath,
 		SubscriptionEndpointURL: config.GraphQLSubscriptionEndpointPath,
 	}


Test patch:
diff --git a/pkg/http/handler_test.go b/pkg/http/handler_test.go
new file mode 100644
index 00000000..2561e9ce
--- /dev/null
+++ b/pkg/http/handler_test.go
@@ -0,0 +1,225 @@
+package http
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"github.com/gobwas/ws"
+	"github.com/gobwas/ws/wsutil"
+	"github.com/jensneuse/abstractlogger"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestGraphQLHTTPRequestHandler_ServeHTTP(t *testing.T) {
+	handler := NewGraphqlHTTPHandlerFunc(newStarWarsExecutionHandler(t), abstractlogger.NoopLogger, &ws.DefaultHTTPUpgrader)
+	server := httptest.NewServer(handler)
+	defer server.Close()
+
+	addr := server.Listener.Addr().String()
+	httpAddr := fmt.Sprintf("http://%s", addr)
+	wsAddr := fmt.Sprintf("ws://%s", addr)
+
+	t.Run("http", func(t *testing.T) {
+		t.Run("should return 400 Bad Request when query does not fit to schema", func(t *testing.T) {
+			requestBodyBytes := invalidQueryRequestBody(t)
+			req, err := http.NewRequest(http.MethodPost, httpAddr, bytes.NewBuffer(requestBodyBytes))
+			require.NoError(t, err)
+
+			client := http.Client{}
+			resp, err := client.Do(req)
+			require.NoError(t, err)
+
+			assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
+		})
+
+		t.Run("should successfully handle query and return 200 OK", func(t *testing.T) {
+			starWarsCases := []starWarsTestCase{
+				{
+					name:        "simple hero query",
+					requestBody: starWarsLoadQuery(t, fileSimpleHeroQuery, nil),
+				},
+				{
+					name:        "droid query with argument and variable",
+					requestBody: starWarsLoadQuery(t, fileDroidWithArgAndVarQuery, queryVariables{"droidID": "2000"}),
+				},
+				{
+					name:        "hero with aliases query",
+					requestBody: starWarsLoadQuery(t, fileHeroWithAliasesQuery, nil),
+				},
+				{
+					name:        "fragments query",
+					requestBody: starWarsLoadQuery(t, fileFragmentsQuery, queryVariables{"droidID": "2000"}),
+				},
+				{
+					name:        "hero with operation name query",
+					requestBody: starWarsLoadQuery(t, fileHeroWithOperationNameQuery, nil),
+				},
+				{
+					name:        "directives include query",
+					requestBody: starWarsLoadQuery(t, fileDirectivesIncludeQuery, queryVariables{"withFriends": true}),
+				},
+				{
+					name:        "directives skip query",
+					requestBody: starWarsLoadQuery(t, fileDirectivesSkipQuery, queryVariables{"skipFriends": true}),
+				},
+				{
+					name:        "create review mutation",
+					requestBody: starWarsLoadQuery(t, fileCreateReviewMutation, queryVariables{"ep": "JEDI", "review": starWarsReviewInput()}),
+				},
+				{
+					name:        "inline fragments query",
+					requestBody: starWarsLoadQuery(t, fileInlineFragmentsQuery, nil),
+				},
+				{
+					name:        "union query",
+					requestBody: starWarsLoadQuery(t, fileUnionQuery, queryVariables{"name": "Han Solo"}),
+				},
+			}
+
+			for _, testCase := range starWarsCases {
+				testCase := testCase
+
+				t.Run(testCase.name, func(t *testing.T) {
+					requestBodyBytes := testCase.requestBody
+					req, err := http.NewRequest(http.MethodPost, httpAddr, bytes.NewBuffer(requestBodyBytes))
+					require.NoError(t, err)
+
+					client := http.Client{}
+					resp, err := client.Do(req)
+					require.NoError(t, err)
+
+					responseBodyBytes, err := ioutil.ReadAll(resp.Body)
+					require.NoError(t, err)
+
+					assert.Equal(t, http.StatusOK, resp.StatusCode)
+					assert.Contains(t, resp.Header.Get(httpHeaderContentType), httpContentTypeApplicationJson)
+					assert.Equal(t, `{"data":null}`, string(responseBodyBytes))
+				})
+			}
+
+		})
+	})
+
+	t.Run("websockets", func(t *testing.T) {
+		var clientConn net.Conn
+		defer func() {
+			err := clientConn.Close()
+			require.NoError(t, err)
+		}()
+
+		ctx, cancelFunc := context.WithCancel(context.Background())
+
+		t.Run("should upgrade to websocket and establish connection successfully", func(t *testing.T) {
+			var err error
+			clientConn, _, _, err = ws.Dial(ctx, wsAddr)
+			assert.NoError(t, err)
+
+			initialClientMessage := WebsocketMessage{
+				Id:      "",
+				Type:    CONNECTION_INIT,
+				Payload: nil,
+			}
+			sendMessageToServer(t, clientConn, initialClientMessage)
+
+			serverMessage := readMessageFromServer(t, clientConn)
+			assert.Equal(t, `{"id":"","type":"connection_ack","payload":null}`, string(serverMessage))
+		})
+
+		t.Run("should successfully start a subscription", func(t *testing.T) {
+			startSubscriptionMessage := WebsocketMessage{
+				Id:      "1",
+				Type:    START,
+				Payload: starWarsLoadQuery(t, fileRemainingJedisSubscription, nil),
+			}
+			sendMessageToServer(t, clientConn, startSubscriptionMessage)
+
+			serverMessage := readMessageFromServer(t, clientConn)
+			assert.Equal(t, `{"id":"1","type":"data","payload":{"data":null}}`, string(serverMessage))
+		})
+
+		cancelFunc()
+	})
+
+}
+
+func TestGraphQLHTTPRequestHandler_IsWebsocketUpgrade(t *testing.T) {
+	handler := NewGraphqlHTTPHandlerFunc(nil, nil, nil).(*GraphQLHTTPRequestHandler)
+
+	t.Run("should return false if upgrade header does not exist", func(t *testing.T) {
+		req, err := http.NewRequest(http.MethodGet, "/", nil)
+		require.NoError(t, err)
+
+		isWebsocketUpgrade := handler.isWebsocketUpgrade(req)
+		assert.False(t, isWebsocketUpgrade)
+	})
+
+	t.Run("should return false if upgrade header does not contain websocket", func(t *testing.T) {
+		req, err := http.NewRequest(http.MethodGet, "/", nil)
+		require.NoError(t, err)
+
+		req.Header = map[string][]string{
+			httpHeaderUpgrade: {"any"},
+		}
+
+		isWebsocketUpgrade := handler.isWebsocketUpgrade(req)
+		assert.False(t, isWebsocketUpgrade)
+	})
+
+	t.Run("should return true if upgrade header contains websocket", func(t *testing.T) {
+		req, err := http.NewRequest(http.MethodGet, "/", nil)
+		require.NoError(t, err)
+
+		req.Header = map[string][]string{
+			httpHeaderUpgrade: {"any", "websocket"},
+		}
+
+		isWebsocketUpgrade := handler.isWebsocketUpgrade(req)
+		assert.True(t, isWebsocketUpgrade)
+	})
+}
+
+func TestGraphQLHTTPRequestHandler_ExtraVariables(t *testing.T) {
+	handler := NewGraphqlHTTPHandlerFunc(nil, nil, nil).(*GraphQLHTTPRequestHandler)
+
+	t.Run("should create extra variables successfully", func(t *testing.T) {
+		req, err := http.NewRequest(http.MethodGet, "http://localhost:8080/path", nil)
+		require.NoError(t, err)
+
+		req.Header = map[string][]string{
+			httpHeaderUpgrade: {"websocket"},
+		}
+
+		req.AddCookie(&http.Cookie{})
+
+		extraVariablesBytes := &bytes.Buffer{}
+		err = handler.extraVariables(req, extraVariablesBytes)
+
+		assert.NoError(t, err)
+
+		expectedJson := fmt.Sprintf("%s\n", `{"request":{"cookies":{},"headers":{"Cookie":"=","Upgrade":"websocket"},"host":"localhost:8080","method":"GET","uri":""}}`)
+		assert.Equal(t, expectedJson, extraVariablesBytes.String())
+	})
+}
+
+func sendMessageToServer(t *testing.T, clientConn net.Conn, message WebsocketMessage) {
+	messageBytes, err := json.Marshal(message)
+	require.NoError(t, err)
+
+	err = wsutil.WriteClientText(clientConn, messageBytes)
+	require.NoError(t, err)
+}
+
+func readMessageFromServer(t *testing.T, clientConn net.Conn) []byte {
+	msgBytes, _, err := wsutil.ReadServerData(clientConn)
+	require.NoError(t, err)
+
+	return msgBytes
+}
diff --git a/pkg/http/star_wars_test.go b/pkg/http/star_wars_test.go
new file mode 100644
index 00000000..51c1e77a
--- /dev/null
+++ b/pkg/http/star_wars_test.go
@@ -0,0 +1,84 @@
+package http
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"testing"
+
+	"github.com/jensneuse/abstractlogger"
+	"github.com/stretchr/testify/require"
+
+	"github.com/jensneuse/graphql-go-tools/pkg/execution"
+)
+
+type queryVariables map[string]interface{}
+
+const (
+	fileSimpleHeroQuery            = "./testdata/queries/simple_hero.query"
+	fileHeroWithOperationNameQuery = "./testdata/queries/hero_with_operation_name.query"
+	fileHeroWithAliasesQuery       = "./testdata/queries/hero_with_aliases.query"
+	fileDroidWithArgAndVarQuery    = "./testdata/queries/droid_with_arg_and_var.query"
+	fileFragmentsQuery             = "./testdata/queries/fragments.query"
+	fileDirectivesIncludeQuery     = "./testdata/queries/directives_include.query"
+	fileDirectivesSkipQuery        = "./testdata/queries/directives_skip.query"
+	fileCreateReviewMutation       = "./testdata/mutations/create_review.mutation"
+	fileInlineFragmentsQuery       = "./testdata/queries/inline_fragments.query"
+	fileUnionQuery                 = "./testdata/queries/inline_fragments.query"
+	fileRemainingJedisSubscription = "./testdata/subscriptions/remaining_jedis.subscription"
+)
+
+type starWarsTestCase struct {
+	name        string
+	requestBody []byte
+}
+
+func newStarWarsExecutionHandler(t *testing.T) *execution.Handler {
+	base, err := execution.NewBaseDataSourcePlanner(starWarsSchema(t), execution.PlannerConfiguration{}, abstractlogger.NoopLogger)
+	require.NoError(t, err)
+	executionHandler := execution.NewHandler(base, nil)
+	return executionHandler
+}
+
+func starWarsSchema(t *testing.T) []byte {
+	schema, err := ioutil.ReadFile("./testdata/star_wars.graphql")
+	require.NoError(t, err)
+	return schema
+}
+
+func starWarsLoadQuery(t *testing.T, fileName string, variables queryVariables) []byte {
+	query, err := ioutil.ReadFile(fileName)
+	require.NoError(t, err)
+
+	return starWarsRequestBody(t, string(query), variables)
+}
+
+func invalidQueryRequestBody(t *testing.T) []byte {
+	return starWarsRequestBody(t, "query { trap { meme } }", nil)
+}
+
+func starWarsRequestBody(t *testing.T, query string, variables queryVariables) []byte {
+	var variableJsonBytes []byte
+	if len(variables) > 0 {
+		var err error
+		variableJsonBytes, err = json.Marshal(variables)
+		require.NoError(t, err)
+	}
+
+	body := execution.GraphqlRequest{
+		OperationName: "",
+		Variables:     variableJsonBytes,
+		Query:         query,
+	}
+
+	jsonBytes, err := json.Marshal(body)
+	require.NoError(t, err)
+
+	return jsonBytes
+}
+
+func starWarsReviewInput() map[string]interface{} {
+	return map[string]interface{}{
+		"stars":      5,
+		"commentary": "This is a great movie!",
+	}
+}
diff --git a/pkg/http/testdata/mutations/create_review.mutation b/pkg/http/testdata/mutations/create_review.mutation
new file mode 100644
index 00000000..efd69caa
--- /dev/null
+++ b/pkg/http/testdata/mutations/create_review.mutation
@@ -0,0 +1,7 @@
+mutation CreateReviewForEpisode($ep: Episode!, $review: ReviewInput!) {
+    createReview(episode: $ep, review: $review) {
+        id
+        stars
+        commentary
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/directives_include.query b/pkg/http/testdata/queries/directives_include.query
new file mode 100644
index 00000000..93e60d76
--- /dev/null
+++ b/pkg/http/testdata/queries/directives_include.query
@@ -0,0 +1,8 @@
+query Hero($withFriends: Boolean!) {
+    hero {
+        name
+        friends @include(if: $withFriends) {
+            name
+        }
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/directives_skip.query b/pkg/http/testdata/queries/directives_skip.query
new file mode 100644
index 00000000..6a3c8a2e
--- /dev/null
+++ b/pkg/http/testdata/queries/directives_skip.query
@@ -0,0 +1,8 @@
+query Hero($skipFriends: Boolean!) {
+    hero {
+        name
+        friends @skip(if: $skipFriends) {
+            name
+        }
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/droid_with_arg_and_var.query b/pkg/http/testdata/queries/droid_with_arg_and_var.query
new file mode 100644
index 00000000..562498d9
--- /dev/null
+++ b/pkg/http/testdata/queries/droid_with_arg_and_var.query
@@ -0,0 +1,5 @@
+query Droid($droidID: ID!) {
+    droid(id: $droidID) {
+        name
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/fragments.query b/pkg/http/testdata/queries/fragments.query
new file mode 100644
index 00000000..29b6503f
--- /dev/null
+++ b/pkg/http/testdata/queries/fragments.query
@@ -0,0 +1,12 @@
+query Fragments($droidID: ID!){
+    hero {
+        ...characterFields
+    }
+    droid(id: $droidID) {
+        ...characterFields
+    }
+}
+
+fragment characterFields on Character {
+    name
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/hero_with_aliases.query b/pkg/http/testdata/queries/hero_with_aliases.query
new file mode 100644
index 00000000..3796ac3a
--- /dev/null
+++ b/pkg/http/testdata/queries/hero_with_aliases.query
@@ -0,0 +1,8 @@
+{
+    empireHero: hero {
+        name
+    }
+    jediHero: hero {
+        name
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/hero_with_operation_name.query b/pkg/http/testdata/queries/hero_with_operation_name.query
new file mode 100644
index 00000000..c6e4e7af
--- /dev/null
+++ b/pkg/http/testdata/queries/hero_with_operation_name.query
@@ -0,0 +1,5 @@
+query Hero {
+    hero {
+        name
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/inline_fragments.query b/pkg/http/testdata/queries/inline_fragments.query
new file mode 100644
index 00000000..fe2ac994
--- /dev/null
+++ b/pkg/http/testdata/queries/inline_fragments.query
@@ -0,0 +1,14 @@
+{
+    hero {
+        friends {
+            ...on Droid {
+                name
+                primaryFunction
+            }
+            ...on Human {
+                name
+                height
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/simple_hero.query b/pkg/http/testdata/queries/simple_hero.query
new file mode 100644
index 00000000..add80bdb
--- /dev/null
+++ b/pkg/http/testdata/queries/simple_hero.query
@@ -0,0 +1,5 @@
+{
+    hero {
+        name
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/queries/union.query b/pkg/http/testdata/queries/union.query
new file mode 100644
index 00000000..0bd30898
--- /dev/null
+++ b/pkg/http/testdata/queries/union.query
@@ -0,0 +1,16 @@
+query Search($name: String!) {
+    search(name: $name) {
+        ...on Droid {
+            name
+            primaryFunction
+        }
+        ...on Human {
+            name
+            height
+        }
+        ...on Starship {
+            name
+            length
+        }
+    }
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/star_wars.graphql b/pkg/http/testdata/star_wars.graphql
new file mode 100644
index 00000000..556dec9e
--- /dev/null
+++ b/pkg/http/testdata/star_wars.graphql
@@ -0,0 +1,60 @@
+union SearchResult = Human | Droid | Starship
+
+schema {
+    query: Query
+    mutation: Mutation
+    subscription: Subscription
+}
+
+type Query {
+    hero: Character
+    droid(id: ID!): Droid
+    search(name: String!): SearchResult
+}
+
+type Mutation {
+    createReview(episode: Episode!, review: ReviewInput!): Review
+}
+
+type Subscription {
+    remainingJedis: Int!
+}
+
+input ReviewInput {
+    stars: Int!
+    commentary: String
+}
+
+type Review {
+    id: ID!
+    stars: Int!
+    commentary: String
+}
+
+enum Episode {
+    NEWHOPE
+    EMPIRE
+    JEDI
+}
+
+interface Character {
+    name: String!
+    friends: [Character]
+}
+
+type Human implements Character {
+    name: String!
+    height: String!
+    friends: [Character]
+}
+
+type Droid implements Character {
+    name: String!
+    primaryFunction: String!
+    friends: [Character]
+}
+
+type Startship {
+    name: String!
+    length: Float!
+}
\ No newline at end of file
diff --git a/pkg/http/testdata/subscriptions/remaining_jedis.subscription b/pkg/http/testdata/subscriptions/remaining_jedis.subscription
new file mode 100644
index 00000000..ab9eea03
--- /dev/null
+++ b/pkg/http/testdata/subscriptions/remaining_jedis.subscription
@@ -0,0 +1,3 @@
+subscription {
+    remainingJedis
+}
\ No newline at end of file
diff --git a/pkg/playground/playground_test.go b/pkg/playground/playground_test.go
index 0e046192..7cd9c4a9 100644
--- a/pkg/playground/playground_test.go
+++ b/pkg/playground/playground_test.go
@@ -8,11 +8,30 @@ import (
 	"github.com/davecgh/go-spew/spew"
 	"github.com/jensneuse/diffview"
 	"github.com/sebdah/goldie"
+	"github.com/stretchr/testify/assert"
 )
 
+func TestNew(t *testing.T) {
+	t.Run("should prefix asset paths with slash (/) even when prefix path is empty", func(t *testing.T) {
+		config := Config{
+			PathPrefix:                      "",
+			PlaygroundPath:                  "/playground",
+			GraphqlEndpointPath:             "/graphql",
+			GraphQLSubscriptionEndpointPath: "/graphqlws",
+		}
+
+		playground := New(config)
+
+		assert.Equal(t, playground.data.CssURL, "/playground.css")
+		assert.Equal(t, playground.data.JsURL, "/playground.js")
+		assert.Equal(t, playground.data.FavIconURL, "/favicon.png")
+		assert.Equal(t, playground.data.LogoURL, "/logo.png")
+	})
+}
+
 func TestConfigureHandlers(t *testing.T) {
 	config := Config{
-		PathPrefix:                      "",
+		PathPrefix:                      "/",
 		PlaygroundPath:                  "/playground",
 		GraphqlEndpointPath:             "/graphql",
 		GraphQLSubscriptionEndpointPath: "/graphqlws",