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 }