From 8e387abf9a84003b56c0c15793963ea86e09dfee Mon Sep 17 00:00:00 2001 From: Steve Coffman Date: Wed, 13 Oct 2021 20:11:12 -0400 Subject: [PATCH] Revert "Update GQLgen test client to work with multipart form data (#1418)" This reverts commit 1318f12792e86c76a2cdff9132ebac5b3e30e148. --- client/client.go | 10 +- client/client_test.go | 41 ---- client/withfilesoption.go | 133 ------------- client/withfilesoption_test.go | 225 --------------------- example/fileupload/fileupload_test.go | 268 +++++++++++++++----------- 5 files changed, 157 insertions(+), 520 deletions(-) delete mode 100644 client/withfilesoption.go delete mode 100644 client/withfilesoption_test.go diff --git a/client/client.go b/client/client.go index e29c5fe5b3d..7a2825c952b 100644 --- a/client/client.go +++ b/client/client.go @@ -9,7 +9,6 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "regexp" "github.com/mitchellh/mapstructure" ) @@ -121,18 +120,15 @@ func (p *Client) newRequest(query string, options ...Option) (*http.Request, err option(bd) } - contentType := bd.HTTP.Header.Get("Content-Type") - switch { - case regexp.MustCompile(`multipart/form-data; ?boundary=.*`).MatchString(contentType): - break - case "application/json" == contentType: + switch bd.HTTP.Header.Get("Content-Type") { + case "application/json": requestBody, err := json.Marshal(bd) if err != nil { return nil, fmt.Errorf("encode: %w", err) } bd.HTTP.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody)) default: - panic("unsupported encoding " + bd.HTTP.Header.Get("Content-Type")) + panic("unsupported encoding" + bd.HTTP.Header.Get("Content-Type")) } return bd.HTTP, nil diff --git a/client/client_test.go b/client/client_test.go index 176c48d12ae..569151cd8a2 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -1,12 +1,9 @@ package client_test import ( - "bytes" "encoding/json" "io/ioutil" - "mime/multipart" "net/http" - "net/textproto" "testing" "github.com/99designs/gqlgen/client" @@ -42,44 +39,6 @@ func TestClient(t *testing.T) { require.Equal(t, "bob", resp.Name) } -func TestClientMultipartFormData(t *testing.T) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - bodyBytes, err := ioutil.ReadAll(r.Body) - require.NoError(t, err) - require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="operations"`) - require.Contains(t, string(bodyBytes), `{"query":"mutation ($input: Input!) {}","variables":{"file":{}}`) - require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="map"`) - require.Contains(t, string(bodyBytes), `{"0":["variables.file"]}`) - require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="0"; filename="example.txt"`) - require.Contains(t, string(bodyBytes), `Content-Type: text/plain`) - require.Contains(t, string(bodyBytes), `Hello World`) - - w.Write([]byte(`{}`)) - }) - - c := client.New(h) - - var resp struct{} - c.MustPost("{ id }", &resp, - func(bd *client.Request) { - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - bodyWriter.WriteField("operations", `{"query":"mutation ($input: Input!) {}","variables":{"file":{}}`) - bodyWriter.WriteField("map", `{"0":["variables.file"]}`) - - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", `form-data; name="0"; filename="example.txt"`) - h.Set("Content-Type", "text/plain") - ff, _ := bodyWriter.CreatePart(h) - ff.Write([]byte("Hello World")) - bodyWriter.Close() - - bd.HTTP.Body = ioutil.NopCloser(bodyBuf) - bd.HTTP.Header.Set("Content-Type", bodyWriter.FormDataContentType()) - }, - ) -} - func TestAddHeader(t *testing.T) { h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "ASDF", r.Header.Get("Test-Key")) diff --git a/client/withfilesoption.go b/client/withfilesoption.go deleted file mode 100644 index eff0d1c25f3..00000000000 --- a/client/withfilesoption.go +++ /dev/null @@ -1,133 +0,0 @@ -package client - -import ( - "bytes" - "encoding/json" - "fmt" - "io/ioutil" - "mime/multipart" - "net/http" - "net/textproto" - "os" - "strings" -) - -type fileFormDataMap struct { - mapKey string - file *os.File -} - -func findFiles(parentMapKey string, variables map[string]interface{}) []*fileFormDataMap { - files := []*fileFormDataMap{} - for key, value := range variables { - if v, ok := value.(map[string]interface{}); ok { - files = append(files, findFiles(parentMapKey+"."+key, v)...) - } else if v, ok := value.([]map[string]interface{}); ok { - for i, arr := range v { - files = append(files, findFiles(fmt.Sprintf(`%s.%s.%d`, parentMapKey, key, i), arr)...) - } - } else if v, ok := value.([]*os.File); ok { - for i, file := range v { - files = append(files, &fileFormDataMap{ - mapKey: fmt.Sprintf(`%s.%s.%d`, parentMapKey, key, i), - file: file, - }) - } - } else if v, ok := value.(*os.File); ok { - files = append(files, &fileFormDataMap{ - mapKey: parentMapKey + "." + key, - file: v, - }) - } - } - - return files -} - -// WithFiles encodes the outgoing request body as multipart form data for file variables -func WithFiles() Option { - return func(bd *Request) { - bodyBuf := &bytes.Buffer{} - bodyWriter := multipart.NewWriter(bodyBuf) - - //-b7955bd2e1d17b67ac157b9e9ddb6238888caefc6f3541920a1debad284d - // Content-Disposition: form-data; name="operations" - // - // {"query":"mutation ($input: Input!) {}","variables":{"input":{"file":{}}} - requestBody, _ := json.Marshal(bd) - bodyWriter.WriteField("operations", string(requestBody)) - - // --b7955bd2e1d17b67ac157b9e9ddb6238888caefc6f3541920a1debad284d - // Content-Disposition: form-data; name="map" - // - // `{ "0":["variables.input.file"] }` - // or - // `{ "0":["variables.input.files.0"], "1":["variables.input.files.1"] }` - // or - // `{ "0": ["variables.input.0.file"], "1": ["variables.input.1.file"] }` - // or - // `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` - mapData := "" - filesData := findFiles("variables", bd.Variables) - filesGroup := [][]*fileFormDataMap{} - for _, fd := range filesData { - foundDuplicate := false - for j, fg := range filesGroup { - f1, _ := fd.file.Stat() - f2, _ := fg[0].file.Stat() - if os.SameFile(f1, f2) { - foundDuplicate = true - filesGroup[j] = append(filesGroup[j], fd) - } - } - - if !foundDuplicate { - filesGroup = append(filesGroup, []*fileFormDataMap{fd}) - } - } - if len(filesGroup) > 0 { - mapDataFiles := []string{} - - for i, fileData := range filesGroup { - mapDataFiles = append( - mapDataFiles, - fmt.Sprintf(`"%d":[%s]`, i, strings.Join(collect(fileData, wrapMapKeyInQuotes), ",")), - ) - } - - mapData = `{` + strings.Join(mapDataFiles, ",") + `}` - } - bodyWriter.WriteField("map", mapData) - - // --b7955bd2e1d17b67ac157b9e9ddb6238888caefc6f3541920a1debad284d - // Content-Disposition: form-data; name="0"; filename="tempFile" - // Content-Type: text/plain; charset=utf-8 - // or - // Content-Type: application/octet-stream - // - for i, fileData := range filesGroup { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%d"; filename="%s"`, i, fileData[0].file.Name())) - b, _ := ioutil.ReadFile(fileData[0].file.Name()) - h.Set("Content-Type", http.DetectContentType(b)) - ff, _ := bodyWriter.CreatePart(h) - ff.Write(b) - } - bodyWriter.Close() - - bd.HTTP.Body = ioutil.NopCloser(bodyBuf) - bd.HTTP.Header.Set("Content-Type", bodyWriter.FormDataContentType()) - } -} - -func collect(strArr []*fileFormDataMap, f func(s *fileFormDataMap) string) []string { - result := make([]string, len(strArr)) - for i, str := range strArr { - result[i] = f(str) - } - return result -} - -func wrapMapKeyInQuotes(s *fileFormDataMap) string { - return fmt.Sprintf("\"%s\"", s.mapKey) -} diff --git a/client/withfilesoption_test.go b/client/withfilesoption_test.go deleted file mode 100644 index f3528936ff4..00000000000 --- a/client/withfilesoption_test.go +++ /dev/null @@ -1,225 +0,0 @@ -package client_test - -import ( - "fmt" - "io" - "io/ioutil" - "mime" - "mime/multipart" - "net/http" - "os" - "regexp" - "strings" - "testing" - - "github.com/99designs/gqlgen/client" - "github.com/stretchr/testify/require" -) - -func TestWithFiles(t *testing.T) { - tempFile1, _ := ioutil.TempFile(os.TempDir(), "tempFile1") - tempFile2, _ := ioutil.TempFile(os.TempDir(), "tempFile2") - tempFile3, _ := ioutil.TempFile(os.TempDir(), "tempFile3") - defer os.Remove(tempFile1.Name()) - defer os.Remove(tempFile2.Name()) - defer os.Remove(tempFile3.Name()) - tempFile1.WriteString(`The quick brown fox jumps over the lazy dog`) - tempFile2.WriteString(`hello world`) - tempFile3.WriteString(`La-Li-Lu-Le-Lo`) - - t.Run("with one file", func(t *testing.T) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - require.NoError(t, err) - require.True(t, strings.HasPrefix(mediaType, "multipart/")) - - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } - require.NoError(t, err) - - slurp, err := ioutil.ReadAll(p) - require.NoError(t, err) - - contentDisposition := p.Header.Get("Content-Disposition") - fmt.Printf("Part %q: %q\n", contentDisposition, slurp) - - if contentDisposition == `form-data; name="operations"` { - require.Equal(t, `{"query":"{ id }","variables":{"file":{}}}`, string(slurp)) - } - if contentDisposition == `form-data; name="map"` { - require.Equal(t, `{"0":["variables.file"]}`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="0"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `The quick brown fox jumps over the lazy dog`, string(slurp)) - } - } - w.Write([]byte(`{}`)) - }) - - c := client.New(h) - - var resp struct{} - c.MustPost("{ id }", &resp, - client.Var("file", tempFile1), - client.WithFiles(), - ) - }) - - t.Run("with multiple files", func(t *testing.T) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - require.NoError(t, err) - require.True(t, strings.HasPrefix(mediaType, "multipart/")) - - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } - require.NoError(t, err) - - slurp, err := ioutil.ReadAll(p) - require.NoError(t, err) - - contentDisposition := p.Header.Get("Content-Disposition") - fmt.Printf("Part %q: %q\n", contentDisposition, slurp) - - if contentDisposition == `form-data; name="operations"` { - require.Equal(t, `{"query":"{ id }","variables":{"input":{"files":[{},{}]}}}`, string(slurp)) - } - if contentDisposition == `form-data; name="map"` { - require.Equal(t, `{"0":["variables.input.files.0"],"1":["variables.input.files.1"]}`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="0"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `The quick brown fox jumps over the lazy dog`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="1"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `hello world`, string(slurp)) - } - } - w.Write([]byte(`{}`)) - }) - - c := client.New(h) - - var resp struct{} - c.MustPost("{ id }", &resp, - client.Var("input", map[string]interface{}{ - "files": []*os.File{tempFile1, tempFile2}, - }), - client.WithFiles(), - ) - }) - - t.Run("with multiple files across multiple variables", func(t *testing.T) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - require.NoError(t, err) - require.True(t, strings.HasPrefix(mediaType, "multipart/")) - - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } - require.NoError(t, err) - - slurp, err := ioutil.ReadAll(p) - require.NoError(t, err) - - contentDisposition := p.Header.Get("Content-Disposition") - fmt.Printf("Part %q: %q\n", contentDisposition, slurp) - - if contentDisposition == `form-data; name="operations"` { - require.Equal(t, `{"query":"{ id }","variables":{"req":{"files":[{},{}],"foo":{"bar":{}}}}}`, string(slurp)) - } - if contentDisposition == `form-data; name="map"` { - require.Equal(t, `{"0":["variables.req.files.0"],"1":["variables.req.files.1"],"2":["variables.req.foo.bar"]}`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="0"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `The quick brown fox jumps over the lazy dog`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="1"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `hello world`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="2"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `La-Li-Lu-Le-Lo`, string(slurp)) - } - } - w.Write([]byte(`{}`)) - }) - - c := client.New(h) - - var resp struct{} - c.MustPost("{ id }", &resp, - client.Var("req", map[string]interface{}{ - "files": []*os.File{tempFile1, tempFile2}, - "foo": map[string]interface{}{ - "bar": tempFile3, - }, - }), - client.WithFiles(), - ) - }) - - t.Run("with multiple files and file reuse", func(t *testing.T) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - require.NoError(t, err) - require.True(t, strings.HasPrefix(mediaType, "multipart/")) - - mr := multipart.NewReader(r.Body, params["boundary"]) - for { - p, err := mr.NextPart() - if err == io.EOF { - break - } - require.NoError(t, err) - - slurp, err := ioutil.ReadAll(p) - require.NoError(t, err) - - contentDisposition := p.Header.Get("Content-Disposition") - fmt.Printf("Part %q: %q\n", contentDisposition, slurp) - - if contentDisposition == `form-data; name="operations"` { - require.Equal(t, `{"query":"{ id }","variables":{"files":[{},{},{}]}}`, string(slurp)) - } - if contentDisposition == `form-data; name="map"` { - require.Equal(t, `{"0":["variables.files.0","variables.files.2"],"1":["variables.files.1"]}`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="0"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `The quick brown fox jumps over the lazy dog`, string(slurp)) - } - if regexp.MustCompile(`form-data; name="1"; filename=.*`).MatchString(contentDisposition) { - require.Equal(t, `text/plain; charset=utf-8`, p.Header.Get("Content-Type")) - require.Equal(t, `hello world`, string(slurp)) - } - require.False(t, regexp.MustCompile(`form-data; name="2"; filename=.*`).MatchString(contentDisposition)) - } - w.Write([]byte(`{}`)) - }) - - c := client.New(h) - - var resp struct{} - c.MustPost("{ id }", &resp, - client.Var("files", []*os.File{tempFile1, tempFile2, tempFile1}), - client.WithFiles(), - ) - }) -} diff --git a/example/fileupload/fileupload_test.go b/example/fileupload/fileupload_test.go index 158825edfbf..8d68252e009 100644 --- a/example/fileupload/fileupload_test.go +++ b/example/fileupload/fileupload_test.go @@ -2,14 +2,17 @@ package fileupload import ( + "bytes" "context" + "fmt" "io" "io/ioutil" + "mime/multipart" + "net/http" "net/http/httptest" - "os" + "net/textproto" "testing" - gqlclient "github.com/99designs/gqlgen/client" "github.com/99designs/gqlgen/example/fileupload/model" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql/handler" @@ -18,23 +21,10 @@ import ( ) func TestFileUpload(t *testing.T) { - resolver := &Stub{} - srv := httptest.NewServer(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))) - defer srv.Close() - gql := gqlclient.New(srv.Config.Handler, gqlclient.Path("/graphql")) - - aTxtFile, _ := ioutil.TempFile(os.TempDir(), "a.txt") - defer os.Remove(aTxtFile.Name()) - aTxtFile.WriteString(`test`) - - a1TxtFile, _ := ioutil.TempFile(os.TempDir(), "a.txt") - b1TxtFile, _ := ioutil.TempFile(os.TempDir(), "b.txt") - defer os.Remove(a1TxtFile.Name()) - defer os.Remove(b1TxtFile.Name()) - a1TxtFile.WriteString(`test1`) - b1TxtFile.WriteString(`test2`) + client := http.Client{} t.Run("valid single file upload", func(t *testing.T) { + resolver := &Stub{} resolver.MutationResolver.SingleUpload = func(ctx context.Context, file graphql.Upload) (*model.File, error) { require.NotNil(t, file) require.NotNil(t, file.File) @@ -49,28 +39,34 @@ func TestFileUpload(t *testing.T) { ContentType: file.ContentType, }, nil } + srv := httptest.NewServer(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() - mutation := `mutation ($file: Upload!) { - singleUpload(file: $file) { - id - name - content - contentType - } - }` - var result struct { - SingleUpload *model.File + operations := `{ "query": "mutation ($file: Upload!) { singleUpload(file: $file) { id, name, content, contentType } }", "variables": { "file": null } }` + mapData := `{ "0": ["variables.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test", + contentType: "text/plain", + }, } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - err := gql.Post(mutation, &result, gqlclient.Var("file", aTxtFile), gqlclient.WithFiles()) + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + responseString := string(responseBody) + require.Equal(t, `{"data":{"singleUpload":{"id":1,"name":"a.txt","content":"test","contentType":"text/plain"}}}`, responseString) + err = resp.Body.Close() require.Nil(t, err) - require.Equal(t, 1, result.SingleUpload.ID) - require.Contains(t, result.SingleUpload.Name, "a.txt") - require.Equal(t, "test", result.SingleUpload.Content) - require.Equal(t, "text/plain; charset=utf-8", result.SingleUpload.ContentType) }) t.Run("valid single file upload with payload", func(t *testing.T) { + resolver := &Stub{} resolver.MutationResolver.SingleUploadWithPayload = func(ctx context.Context, req model.UploadFile) (*model.File, error) { require.Equal(t, req.ID, 1) require.NotNil(t, req.File) @@ -86,28 +82,33 @@ func TestFileUpload(t *testing.T) { ContentType: req.File.ContentType, }, nil } + srv := httptest.NewServer(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() - mutation := `mutation ($req: UploadFile!) { - singleUploadWithPayload(req: $req) { - id - name - content - contentType - } - }` - var result struct { - SingleUploadWithPayload *model.File + operations := `{ "query": "mutation ($req: UploadFile!) { singleUploadWithPayload(req: $req) { id, name, content, contentType } }", "variables": { "req": {"file": null, "id": 1 } } }` + mapData := `{ "0": ["variables.req.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test", + contentType: "text/plain", + }, } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - err := gql.Post(mutation, &result, gqlclient.Var("req", map[string]interface{}{"id": 1, "file": aTxtFile}), gqlclient.WithFiles()) + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"singleUploadWithPayload":{"id":1,"name":"a.txt","content":"test","contentType":"text/plain"}}}`, string(responseBody)) + err = resp.Body.Close() require.Nil(t, err) - require.Equal(t, 1, result.SingleUploadWithPayload.ID) - require.Contains(t, result.SingleUploadWithPayload.Name, "a.txt") - require.Equal(t, "test", result.SingleUploadWithPayload.Content) - require.Equal(t, "text/plain; charset=utf-8", result.SingleUploadWithPayload.ContentType) }) t.Run("valid file list upload", func(t *testing.T) { + resolver := &Stub{} resolver.MutationResolver.MultipleUpload = func(ctx context.Context, files []*graphql.Upload) ([]*model.File, error) { require.Len(t, files, 2) var contents []string @@ -127,32 +128,39 @@ func TestFileUpload(t *testing.T) { require.ElementsMatch(t, []string{"test1", "test2"}, contents) return resp, nil } + srv := httptest.NewServer(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() - mutation := `mutation($files: [Upload!]!) { - multipleUpload(files: $files) { - id - name - content - contentType - } - }` - var result struct { - MultipleUpload []*model.File + operations := `{ "query": "mutation($files: [Upload!]!) { multipleUpload(files: $files) { id, name, content, contentType } }", "variables": { "files": [null, null] } }` + mapData := `{ "0": ["variables.files.0"], "1": ["variables.files.1"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + contentType: "text/plain", + }, + { + mapKey: "1", + name: "b.txt", + content: "test2", + contentType: "text/plain", + }, } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - err := gql.Post(mutation, &result, gqlclient.Var("files", []*os.File{a1TxtFile, b1TxtFile}), gqlclient.WithFiles()) + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"multipleUpload":[{"id":1,"name":"a.txt","content":"test1","contentType":"text/plain"},{"id":2,"name":"b.txt","content":"test2","contentType":"text/plain"}]}}`, string(responseBody)) + err = resp.Body.Close() require.Nil(t, err) - require.Equal(t, 1, result.MultipleUpload[0].ID) - require.Contains(t, result.MultipleUpload[0].Name, "a.txt") - require.Equal(t, "test1", result.MultipleUpload[0].Content) - require.Equal(t, "text/plain; charset=utf-8", result.MultipleUpload[0].ContentType) - require.Equal(t, 2, result.MultipleUpload[1].ID) - require.Contains(t, result.MultipleUpload[1].Name, "b.txt") - require.Equal(t, "test2", result.MultipleUpload[1].Content) - require.Equal(t, "text/plain; charset=utf-8", result.MultipleUpload[1].ContentType) }) t.Run("valid file list upload with payload", func(t *testing.T) { + resolver := &Stub{} resolver.MutationResolver.MultipleUploadWithPayload = func(ctx context.Context, req []*model.UploadFile) ([]*model.File, error) { require.Len(t, req, 2) var ids []int @@ -176,32 +184,35 @@ func TestFileUpload(t *testing.T) { require.ElementsMatch(t, []string{"test1", "test2"}, contents) return resp, nil } + srv := httptest.NewServer(handler.NewDefaultServer(NewExecutableSchema(Config{Resolvers: resolver}))) + defer srv.Close() - mutation := `mutation($req: [UploadFile!]!) { - multipleUploadWithPayload(req: $req) { - id - name - content - contentType - } - }` - var result struct { - MultipleUploadWithPayload []*model.File + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content, contentType } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file"], "1": ["variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + contentType: "text/plain", + }, + { + mapKey: "1", + name: "b.txt", + content: "test2", + contentType: "text/plain", + }, } + req := createUploadRequest(t, srv.URL, operations, mapData, files) - err := gql.Post(mutation, &result, gqlclient.Var("req", []map[string]interface{}{ - {"id": 1, "file": a1TxtFile}, - {"id": 2, "file": b1TxtFile}, - }), gqlclient.WithFiles()) + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1","contentType":"text/plain"},{"id":2,"name":"b.txt","content":"test2","contentType":"text/plain"}]}}`, string(responseBody)) + err = resp.Body.Close() require.Nil(t, err) - require.Equal(t, 1, result.MultipleUploadWithPayload[0].ID) - require.Contains(t, result.MultipleUploadWithPayload[0].Name, "a.txt") - require.Equal(t, "test1", result.MultipleUploadWithPayload[0].Content) - require.Equal(t, "text/plain; charset=utf-8", result.MultipleUploadWithPayload[0].ContentType) - require.Equal(t, 2, result.MultipleUploadWithPayload[1].ID) - require.Contains(t, result.MultipleUploadWithPayload[1].Name, "b.txt") - require.Equal(t, "test2", result.MultipleUploadWithPayload[1].Content) - require.Equal(t, "text/plain; charset=utf-8", result.MultipleUploadWithPayload[1].ContentType) }) t.Run("valid file list upload with payload and file reuse", func(t *testing.T) { @@ -241,39 +252,32 @@ func TestFileUpload(t *testing.T) { return resp, nil } + operations := `{ "query": "mutation($req: [UploadFile!]!) { multipleUploadWithPayload(req: $req) { id, name, content, contentType } }", "variables": { "req": [ { "id": 1, "file": null }, { "id": 2, "file": null } ] } }` + mapData := `{ "0": ["variables.req.0.file", "variables.req.1.file"] }` + files := []file{ + { + mapKey: "0", + name: "a.txt", + content: "test1", + contentType: "text/plain", + }, + } + test := func(uploadMaxMemory int64) { hndlr := handler.New(NewExecutableSchema(Config{Resolvers: resolver})) hndlr.AddTransport(transport.MultipartForm{MaxMemory: uploadMaxMemory}) srv := httptest.NewServer(hndlr) defer srv.Close() - gql := gqlclient.New(srv.Config.Handler, gqlclient.Path("/graphql")) - - mutation := `mutation($req: [UploadFile!]!) { - multipleUploadWithPayload(req: $req) { - id - name - content - contentType - } - }` - var result struct { - MultipleUploadWithPayload []*model.File - } - - err := gql.Post(mutation, &result, gqlclient.Var("req", []map[string]interface{}{ - {"id": 1, "file": a1TxtFile}, - {"id": 2, "file": a1TxtFile}, - }), gqlclient.WithFiles()) + req := createUploadRequest(t, srv.URL, operations, mapData, files) + resp, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, http.StatusOK, resp.StatusCode) + responseBody, err := ioutil.ReadAll(resp.Body) + require.Nil(t, err) + require.Equal(t, `{"data":{"multipleUploadWithPayload":[{"id":1,"name":"a.txt","content":"test1","contentType":"text/plain"},{"id":2,"name":"a.txt","content":"test1","contentType":"text/plain"}]}}`, string(responseBody)) + err = resp.Body.Close() require.Nil(t, err) - require.Equal(t, 1, result.MultipleUploadWithPayload[0].ID) - require.Contains(t, result.MultipleUploadWithPayload[0].Name, "a.txt") - require.Equal(t, "test1", result.MultipleUploadWithPayload[0].Content) - require.Equal(t, "text/plain; charset=utf-8", result.MultipleUploadWithPayload[0].ContentType) - require.Equal(t, 2, result.MultipleUploadWithPayload[1].ID) - require.Contains(t, result.MultipleUploadWithPayload[1].Name, "a.txt") - require.Equal(t, "test1", result.MultipleUploadWithPayload[1].Content) - require.Equal(t, "text/plain; charset=utf-8", result.MultipleUploadWithPayload[1].ContentType) } t.Run("payload smaller than UploadMaxMemory, stored in memory", func(t *testing.T) { @@ -285,3 +289,39 @@ func TestFileUpload(t *testing.T) { }) }) } + +type file struct { + mapKey string + name string + content string + contentType string +} + +func createUploadRequest(t *testing.T, url, operations, mapData string, files []file) *http.Request { + bodyBuf := &bytes.Buffer{} + bodyWriter := multipart.NewWriter(bodyBuf) + + err := bodyWriter.WriteField("operations", operations) + require.NoError(t, err) + + err = bodyWriter.WriteField("map", mapData) + require.NoError(t, err) + + for i := range files { + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, files[i].mapKey, files[i].name)) + h.Set("Content-Type", files[i].contentType) + ff, err := bodyWriter.CreatePart(h) + require.NoError(t, err) + _, err = ff.Write([]byte(files[i].content)) + require.NoError(t, err) + } + err = bodyWriter.Close() + require.NoError(t, err) + + req, err := http.NewRequest("POST", fmt.Sprintf("%s/graphql", url), bodyBuf) + require.NoError(t, err) + + req.Header.Set("Content-Type", bodyWriter.FormDataContentType()) + return req +}