Skip to content

Commit

Permalink
Add WithFiles client option for fileupload GQLgen client tests
Browse files Browse the repository at this point in the history
Add a `WithFiles` GQLgen client option to support the fileupload input
within tests, using the core Golang `os` package and File type, which
converts `os.File`s to their appropriate multipart form data within a
request.

- If there are no files this should just simply convert a
  `application/json` Content-Type to supported `multipart/form-data`
  • Loading branch information
Sonna committed Dec 12, 2020
1 parent 101842f commit 08ef942
Show file tree
Hide file tree
Showing 2 changed files with 200 additions and 0 deletions.
92 changes: 92 additions & 0 deletions client/withfilesoption.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package client

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"mime/multipart"
"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.([]*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"] }`
mapData := ""
filesData := findFiles("variables", bd.Variables)
if len(filesData) > 0 {
mapDataFiles := []string{}
for i, fileData := range filesData {
mapDataFiles = append(
mapDataFiles,
fmt.Sprintf(`"%d":["%s"]`, i, fileData.mapKey),
)
}
mapData = `{` + strings.Join(mapDataFiles, ",") + `}`
}
bodyWriter.WriteField("map", mapData)

// --b7955bd2e1d17b67ac157b9e9ddb6238888caefc6f3541920a1debad284d
// Content-Disposition: form-data; name="0"; filename="tempFile"
// Content-Type: application/octet-stream
//
for i, fileData := range filesData {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%d"; filename="%s"`, i, fileData.file.Name()))
h.Set("Content-Type", "application/octet-stream")
ff, _ := bodyWriter.CreatePart(h)
b, _ := ioutil.ReadFile(fileData.file.Name())
ff.Write(b)
}
bodyWriter.Close()

bd.HTTP.Body = ioutil.NopCloser(bodyBuf)
bd.HTTP.Header.Set("Content-Type", bodyWriter.FormDataContentType())
}
}
108 changes: 108 additions & 0 deletions client/withfilesoption_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package client_test

import (
"io/ioutil"
"net/http"
"os"
"testing"

"github.com/99designs/gqlgen/client"
"github.com/stretchr/testify/require"
)

func TestWithFiles(t *testing.T) {
tempFile1, _ := ioutil.TempFile(os.TempDir(), "tempFile")
tempFile2, _ := ioutil.TempFile(os.TempDir(), "tempFile")
tempFile3, _ := ioutil.TempFile(os.TempDir(), "tempFile")
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) {
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":"{ id }","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=`)
require.Contains(t, string(bodyBytes), `Content-Type: application/octet-stream`)
require.Contains(t, string(bodyBytes), `The quick brown fox jumps over the lazy dog`)

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) {
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":"{ id }","variables":{"input":{"files":[{},{}]}}}`)
require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="map"`)
require.Contains(t, string(bodyBytes), `{"0":["variables.input.files.0"],"1":["variables.input.files.1"]}`)
require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="0"; filename=`)
require.Contains(t, string(bodyBytes), `Content-Type: application/octet-stream`)
require.Contains(t, string(bodyBytes), `The quick brown fox jumps over the lazy dog`)
require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="1"; filename=`)
require.Contains(t, string(bodyBytes), `hello world`)

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) {
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":"{ id }","variables":{"req":{"files":[{},{}],"foo":{"bar":{}}}}}`)
require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="map"`)
require.Contains(t, string(bodyBytes), `{"0":["variables.req.files.0"],"1":["variables.req.files.1"],"2":["variables.req.foo.bar"]}`)
require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="0"; filename=`)
require.Contains(t, string(bodyBytes), `Content-Type: application/octet-stream`)
require.Contains(t, string(bodyBytes), `The quick brown fox jumps over the lazy dog`)
require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="1"; filename=`)
require.Contains(t, string(bodyBytes), `hello world`)
require.Contains(t, string(bodyBytes), `Content-Disposition: form-data; name="2"; filename=`)
require.Contains(t, string(bodyBytes), `La-Li-Lu-Le-Lo`)

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(),
)
})
}

0 comments on commit 08ef942

Please sign in to comment.