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