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