github.com/fortexxx/gqlgen@v0.10.3-0.20191216030626-ca5ea8b21ead/graphql/handler/transport/http_form_test.go (about)

     1  package transport_test
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"io/ioutil"
     7  	"mime/multipart"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"testing"
    11  
    12  	"github.com/99designs/gqlgen/graphql"
    13  	"github.com/99designs/gqlgen/graphql/handler"
    14  	"github.com/99designs/gqlgen/graphql/handler/transport"
    15  	"github.com/stretchr/testify/require"
    16  	"github.com/vektah/gqlparser"
    17  	"github.com/vektah/gqlparser/ast"
    18  )
    19  
    20  func TestFileUpload(t *testing.T) {
    21  	es := &graphql.ExecutableSchemaMock{
    22  		ExecFunc: func(ctx context.Context) graphql.ResponseHandler {
    23  			return graphql.OneShot(graphql.ErrorResponse(ctx, "not implemented"))
    24  		},
    25  		SchemaFunc: func() *ast.Schema {
    26  			return gqlparser.MustLoadSchema(&ast.Source{Input: `
    27  				type Mutation {
    28  					singleUpload(file: Upload!): String!
    29  					singleUploadWithPayload(req: UploadFile!): String!
    30  					multipleUpload(files: [Upload!]!): String!
    31  					multipleUploadWithPayload(req: [UploadFile!]!): String!
    32  				}
    33  				scalar Upload
    34  				scalar UploadFile
    35  			`})
    36  		},
    37  	}
    38  
    39  	h := handler.New(es)
    40  	multipartForm := transport.MultipartForm{}
    41  	h.AddTransport(&multipartForm)
    42  
    43  	t.Run("valid single file upload", func(t *testing.T) {
    44  		es.ExecFunc = func(ctx context.Context) graphql.ResponseHandler {
    45  			op := graphql.GetOperationContext(ctx).Operation
    46  			require.Equal(t, len(op.VariableDefinitions), 1)
    47  			require.Equal(t, op.VariableDefinitions[0].Variable, "file")
    48  			return graphql.OneShot(&graphql.Response{Data: []byte(`{"singleUpload":"test"}`)})
    49  		}
    50  
    51  		operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) }", "variables": { "file": null } }`
    52  		mapData := `{ "0": ["variables.file"] }`
    53  		files := []file{
    54  			{
    55  				mapKey:  "0",
    56  				name:    "a.txt",
    57  				content: "test1",
    58  			},
    59  		}
    60  		req := createUploadRequest(t, operations, mapData, files)
    61  
    62  		resp := httptest.NewRecorder()
    63  		h.ServeHTTP(resp, req)
    64  		require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
    65  		require.Equal(t, `{"data":{"singleUpload":"test"}}`, resp.Body.String())
    66  	})
    67  
    68  	t.Run("valid single file upload with payload", func(t *testing.T) {
    69  		es.ExecFunc = func(ctx context.Context) graphql.ResponseHandler {
    70  			op := graphql.GetOperationContext(ctx).Operation
    71  			require.Equal(t, len(op.VariableDefinitions), 1)
    72  			require.Equal(t, op.VariableDefinitions[0].Variable, "req")
    73  			return graphql.OneShot(&graphql.Response{Data: []byte(`{"singleUploadWithPayload":"test"}`)})
    74  		}
    75  
    76  		operations := `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) }", "variables": { "req": {"file": null, "id": 1 } } }`
    77  		mapData := `{ "0": ["variables.req.file"] }`
    78  		files := []file{
    79  			{
    80  				mapKey:  "0",
    81  				name:    "a.txt",
    82  				content: "test1",
    83  			},
    84  		}
    85  		req := createUploadRequest(t, operations, mapData, files)
    86  
    87  		resp := httptest.NewRecorder()
    88  		h.ServeHTTP(resp, req)
    89  		require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
    90  		require.Equal(t, `{"data":{"singleUploadWithPayload":"test"}}`, resp.Body.String())
    91  	})
    92  
    93  	t.Run("valid file list upload", func(t *testing.T) {
    94  		es.ExecFunc = func(ctx context.Context) graphql.ResponseHandler {
    95  			op := graphql.GetOperationContext(ctx).Operation
    96  			require.Equal(t, len(op.VariableDefinitions), 1)
    97  			require.Equal(t, op.VariableDefinitions[0].Variable, "files")
    98  			return graphql.OneShot(&graphql.Response{Data: []byte(`{"multipleUpload":[{"id":1},{"id":2}]}`)})
    99  		}
   100  
   101  		operations := `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) }", "variables": { "files": [null, null] } }`
   102  		mapData := `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }`
   103  		files := []file{
   104  			{
   105  				mapKey:  "0",
   106  				name:    "a.txt",
   107  				content: "test1",
   108  			},
   109  			{
   110  				mapKey:  "1",
   111  				name:    "b.txt",
   112  				content: "test2",
   113  			},
   114  		}
   115  		req := createUploadRequest(t, operations, mapData, files)
   116  
   117  		resp := httptest.NewRecorder()
   118  		h.ServeHTTP(resp, req)
   119  		require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
   120  		require.Equal(t, `{"data":{"multipleUpload":[{"id":1},{"id":2}]}}`, resp.Body.String())
   121  	})
   122  
   123  	t.Run("valid file list upload with payload", func(t *testing.T) {
   124  		es.ExecFunc = func(ctx context.Context) graphql.ResponseHandler {
   125  			op := graphql.GetOperationContext(ctx).Operation
   126  			require.Equal(t, len(op.VariableDefinitions), 1)
   127  			require.Equal(t, op.VariableDefinitions[0].Variable, "req")
   128  			return graphql.OneShot(&graphql.Response{Data: []byte(`{"multipleUploadWithPayload":[{"id":1},{"id":2}]}`)})
   129  		}
   130  
   131  		operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`
   132  		mapData := `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }`
   133  		files := []file{
   134  			{
   135  				mapKey:  "0",
   136  				name:    "a.txt",
   137  				content: "test1",
   138  			},
   139  			{
   140  				mapKey:  "1",
   141  				name:    "b.txt",
   142  				content: "test2",
   143  			},
   144  		}
   145  		req := createUploadRequest(t, operations, mapData, files)
   146  
   147  		resp := httptest.NewRecorder()
   148  		h.ServeHTTP(resp, req)
   149  		require.Equal(t, http.StatusOK, resp.Code)
   150  		require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String())
   151  	})
   152  
   153  	t.Run("valid file list upload with payload and file reuse", func(t *testing.T) {
   154  		test := func(uploadMaxMemory int64) {
   155  			es.ExecFunc = func(ctx context.Context) graphql.ResponseHandler {
   156  				op := graphql.GetOperationContext(ctx).Operation
   157  				require.Equal(t, len(op.VariableDefinitions), 1)
   158  				require.Equal(t, op.VariableDefinitions[0].Variable, "req")
   159  				return graphql.OneShot(&graphql.Response{Data: []byte(`{"multipleUploadWithPayload":[{"id":1},{"id":2}]}`)})
   160  			}
   161  			multipartForm.MaxMemory = uploadMaxMemory
   162  
   163  			operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }`
   164  			mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }`
   165  			files := []file{
   166  				{
   167  					mapKey:  "0",
   168  					name:    "a.txt",
   169  					content: "test1",
   170  				},
   171  			}
   172  			req := createUploadRequest(t, operations, mapData, files)
   173  
   174  			resp := httptest.NewRecorder()
   175  			h.ServeHTTP(resp, req)
   176  			require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
   177  			require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1},{"id":2}]}}`, resp.Body.String())
   178  		}
   179  
   180  		t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T) {
   181  			test(5000)
   182  		})
   183  
   184  		t.Run("payload bigger than UploadMaxMemory, persisted to disk", func(t *testing.T) {
   185  			test(2)
   186  		})
   187  	})
   188  
   189  	validOperations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) }", "variables": { "file": null } }`
   190  	validMap := `{ "0": ["variables.file"] }`
   191  	validFiles := []file{
   192  		{
   193  			mapKey:  "0",
   194  			name:    "a.txt",
   195  			content: "test1",
   196  		},
   197  	}
   198  
   199  	t.Run("failed to parse multipart", func(t *testing.T) {
   200  		req := &http.Request{
   201  			Method: "POST",
   202  			Header: http.Header{"Content-Type": {`multipart/form-data; boundary="foo123"`}},
   203  			Body:   ioutil.NopCloser(new(bytes.Buffer)),
   204  		}
   205  		resp := httptest.NewRecorder()
   206  		h.ServeHTTP(resp, req)
   207  		require.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
   208  		require.Equal(t, `{"errors":[{"message":"failed to parse multipart form"}],"data":null}`, resp.Body.String())
   209  	})
   210  
   211  	t.Run("fail parse operation", func(t *testing.T) {
   212  		operations := `invalid operation`
   213  		req := createUploadRequest(t, operations, validMap, validFiles)
   214  
   215  		resp := httptest.NewRecorder()
   216  		h.ServeHTTP(resp, req)
   217  		require.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
   218  		require.Equal(t, `{"errors":[{"message":"operations form field could not be decoded"}],"data":null}`, resp.Body.String())
   219  	})
   220  
   221  	t.Run("fail parse map", func(t *testing.T) {
   222  		mapData := `invalid map`
   223  		req := createUploadRequest(t, validOperations, mapData, validFiles)
   224  
   225  		resp := httptest.NewRecorder()
   226  		h.ServeHTTP(resp, req)
   227  		require.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
   228  		require.Equal(t, `{"errors":[{"message":"map form field could not be decoded"}],"data":null}`, resp.Body.String())
   229  	})
   230  
   231  	t.Run("fail missing file", func(t *testing.T) {
   232  		var files []file
   233  		req := createUploadRequest(t, validOperations, validMap, files)
   234  
   235  		resp := httptest.NewRecorder()
   236  		h.ServeHTTP(resp, req)
   237  		require.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
   238  		require.Equal(t, `{"errors":[{"message":"failed to get key 0 from form"}],"data":null}`, resp.Body.String())
   239  	})
   240  
   241  	t.Run("fail map entry with invalid operations paths prefix", func(t *testing.T) {
   242  		mapData := `{ "0": ["var.file"] }`
   243  		req := createUploadRequest(t, validOperations, mapData, validFiles)
   244  
   245  		resp := httptest.NewRecorder()
   246  		h.ServeHTTP(resp, req)
   247  		require.Equal(t, http.StatusUnprocessableEntity, resp.Code, resp.Body.String())
   248  		require.Equal(t, `{"errors":[{"message":"invalid operations paths for key 0"}],"data":null}`, resp.Body.String())
   249  	})
   250  
   251  	t.Run("fail parse request big body", func(t *testing.T) {
   252  		multipartForm.MaxUploadSize = 2
   253  		req := createUploadRequest(t, validOperations, validMap, validFiles)
   254  
   255  		resp := httptest.NewRecorder()
   256  		h.ServeHTTP(resp, req)
   257  		require.Equal(t, http.StatusOK, resp.Code, resp.Body.String())
   258  		require.Equal(t, `{"errors":[{"message":"failed to parse multipart form, request body too large"}],"data":null}`, resp.Body.String())
   259  	})
   260  }
   261  
   262  type file struct {
   263  	mapKey  string
   264  	name    string
   265  	content string
   266  }
   267  
   268  func createUploadRequest(t *testing.T, operations, mapData string, files []file) *http.Request {
   269  	bodyBuf := &bytes.Buffer{}
   270  	bodyWriter := multipart.NewWriter(bodyBuf)
   271  
   272  	err := bodyWriter.WriteField("operations", operations)
   273  	require.NoError(t, err)
   274  
   275  	err = bodyWriter.WriteField("map", mapData)
   276  	require.NoError(t, err)
   277  
   278  	for i := range files {
   279  		ff, err := bodyWriter.CreateFormFile(files[i].mapKey, files[i].name)
   280  		require.NoError(t, err)
   281  		_, err = ff.Write([]byte(files[i].content))
   282  		require.NoError(t, err)
   283  	}
   284  	err = bodyWriter.Close()
   285  	require.NoError(t, err)
   286  
   287  	req, err := http.NewRequest("POST", "/graphql", bodyBuf)
   288  	require.NoError(t, err)
   289  
   290  	req.Header.Set("Content-Type", bodyWriter.FormDataContentType())
   291  	return req
   292  }