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  }