Testing Go http.Request.FormFile? - go

How do I set the Request.FormFile when trying to test an endpoint?
Partial code:
func (a *EP) Endpoint(w http.ResponseWriter, r *http.Request) {
...
x, err := strconv.Atoi(r.FormValue("x"))
if err != nil {
a.ren.Text(w, http.StatusInternalServerError, err.Error())
return
}
f, fh, err := r.FormFile("y")
if err != nil {
a.ren.Text(w, http.StatusInternalServerError, err.Error())
return
}
defer f.Close()
...
}
How do I use the httptest lib to generate a post request that has value that I can get in FormFile?

You don't need to mock the complete FormFile struct as suggested by the other answer. The mime/multipart package implements a Writer type that lets you create a FormFile. From the docs
CreateFormFile is a convenience wrapper around CreatePart. It creates
a new form-data header with the provided field name and file name.
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error)
Then, you can pass this io.Writer to httptest.NewRequest, which accepts a reader as an argument.
request := httptest.NewRequest("POST", "/", myReader)
To do this, you can either write the FormFile to an io.ReaderWriter buffer or use an io.Pipe. Here is a complete example that makes use of pipes:
func TestUploadImage(t *testing.T) {
// Set up a pipe to avoid buffering
pr, pw := io.Pipe()
// This writer is going to transform
// what we pass to it to multipart form data
// and write it to our io.Pipe
writer := multipart.NewWriter(pw)
go func() {
defer writer.Close()
// We create the form data field 'fileupload'
// which returns another writer to write the actual file
part, err := writer.CreateFormFile("fileupload", "someimg.png")
if err != nil {
t.Error(err)
}
// https://yourbasic.org/golang/create-image/
img := createImage()
// Encode() takes an io.Writer.
// We pass the multipart field
// 'fileupload' that we defined
// earlier which, in turn, writes
// to our io.Pipe
err = png.Encode(part, img)
if err != nil {
t.Error(err)
}
}()
// We read from the pipe which receives data
// from the multipart writer, which, in turn,
// receives data from png.Encode().
// We have 3 chained writers!
request := httptest.NewRequest("POST", "/", pr)
request.Header.Add("Content-Type", writer.FormDataContentType())
response := httptest.NewRecorder()
handler := UploadFileHandler()
handler.ServeHTTP(response, request)
t.Log("It should respond with an HTTP status code of 200")
if response.Code != 200 {
t.Errorf("Expected %s, received %d", 200, response.Code)
}
t.Log("It should create a file named 'someimg.png' in uploads folder")
if _, err := os.Stat("./uploads/someimg.png"); os.IsNotExist(err) {
t.Error("Expected file ./uploads/someimg.png' to exist")
}
}
This function makes use of the image package to generate a file dynamically taking advantage of the fact that you can pass an io.Writer to png.Encode. In the same vein, you could pass your multipart Writer to generate the bytes in a CSV format (NewWriter in package "encoding/csv"), generating a file on the fly, without needing to read anything from your filesystem.

If you have a look at the implementation of the FormFile function you'll see that it reads the exposed MultipartForm field.
https://golang.org/src/net/http/request.go?s=39022:39107#L1249
// FormFile returns the first file for the provided form key.
1258 // FormFile calls ParseMultipartForm and ParseForm if necessary.
1259 func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
1260 if r.MultipartForm == multipartByReader {
1261 return nil, nil, errors.New("http: multipart handled by MultipartReader")
1262 }
1263 if r.MultipartForm == nil {
1264 err := r.ParseMultipartForm(defaultMaxMemory)
1265 if err != nil {
1266 return nil, nil, err
1267 }
1268 }
1269 if r.MultipartForm != nil && r.MultipartForm.File != nil {
1270 if fhs := r.MultipartForm.File[key]; len(fhs) > 0 {
1271 f, err := fhs[0].Open()
1272 return f, fhs[0], err
1273 }
1274 }
1275 return nil, nil, ErrMissingFile
1276 }
In your test you should be able to create a test instance of multipart.Form and assign it to your request object - https://golang.org/pkg/mime/multipart/#Form
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
Of course this will require that you use a real filepath which isn't great from a testing perspective. To get around this you could define an interface to read FormFile from a request object and pass a mock implementation into your EP struct.
Here is a good post with a few examples on how to do this: https://husobee.github.io/golang/testing/unit-test/2015/06/08/golang-unit-testing.html

I combined these and other answers into an Echo example without pipes or goroutines:
func Test_submitFile(t *testing.T) {
path := "testfile.txt"
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("object", path)
assert.NoError(t, err)
sample, err := os.Open(path)
assert.NoError(t, err)
_, err = io.Copy(part, sample)
assert.NoError(t, err)
assert.NoError(t, writer.Close())
e := echo.New()
req := httptest.NewRequest(http.MethodPost, "/", body)
req.Header.Set(echo.HeaderContentType, writer.FormDataContentType())
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)
c.SetPath("/submit")
if assert.NoError(t, submitFile(c)) {
assert.Equal(t, 200, rec.Code)
assert.Contains(t, rec.Body.String(), path)
fi, err := os.Stat(expectedPath)
if os.IsNotExist(err) {
t.Fatal("Upload file does not exist", expectedPath)
}
assert.Equal(t, wantSize, fi.Size())
}
}

By combining the previous answers, this worked for me:
filePath := "file.jpg"
fieldName := "file"
body := new(bytes.Buffer)
mw := multipart.NewWriter(body)
file, err := os.Open(filePath)
if err != nil {
t.Fatal(err)
}
w, err := mw.CreateFormFile(fieldName, filePath)
if err != nil {
t.Fatal(err)
}
if _, err := io.Copy(w, file); err != nil {
t.Fatal(err)
}
// close the writer before making the request
mw.Close()
req := httptest.NewRequest(http.MethodPost, "/upload", body)
req.Header.Add("Content-Type", mw.FormDataContentType())
res := httptest.NewRecorder()
// router is of type http.Handler
router.ServeHTTP(res, req)

Related

Send zip file as form-data in POST request

I am trying to send a zip file in POST request body as form-data in GO. So, it is supposed to be in a key-value format. I am able to achieve the same while sending an unzipped file. Here is the snippet,
buf := new(bytes.Buffer)
multiPartWriter := multipart.NewWriter(buf)
part, err := multiPartWriter.CreateFormFile("File", fileName)
However, when it comes to zipping it, I am able to find this,
zipWriter := zip.NewWriter(buf)
zipFile, err := zipWriter.Create(fileName)
Unlike multipart.Writer, in case of zip.Writer I can't find any option to create form file in a key-value fashion.
How can I achieve this for zip as well?
Code snippet[updated],
func SendPostRequest(url string, content []byte, fileName string,
doZip bool)(*http.Response, error) {
buf := new(bytes.Buffer)
multiPartWriter := multipart.NewWriter(buf)
part, _ := multiPartWriter.CreateFormFile("File", fileName)
if doZip {
zipWriter := zip.NewWriter(part)
zipFile, _ := zipWriter.Create(fileName)
zipFile.Write(content)
zipWriter.Close()
} else {
part.Write(content)
multiPartWriter.Close()
}
request, _ := http.NewRequest(http.MethodPost, url, buf)
request.Header.Add(constants.Header_content_type_key, multiPartWriter.FormDataContentType())
client := &http.Client{}
response, err := client.Do(request)
return client.Do(request)
}
Ordinarily, I think you'd want to use multiPartWriter.CreatePart(header), where header is of type MIMEHeader. CreatePart() will return an io.Writer - pass that to zip.NewWriter():
// Assume you've created your header already
w, err := multiPartWriter.CreatePart(header)
if err != nil {
// do whatever
}
zipWriter := zip.NewWriter(w)
// Add stuff to zipWriter
You might need to play around, YMMV.
As a solution to this one - I am doing a zip of the content byte slice only, when it comes to zipping. Rest are common for both zipped/non-zipped approach. Here are the code snippets,
func sendPostRequest(url string, content []byte, projectId string, fileName string, fileType string, doZip bool) (*http.Response, error) {
buf := new(bytes.Buffer)
writer := multipart.NewWriter(buf)
part, err := writer.CreateFormFile("File", fileName)
if err != nil {
return nil, errors.New("Invocation of *multipart.Writer.CreateFormFile() failed: " + err.Error())
}
content, err = ZipBytes(fileName, content, doZip)
if err != nil {
return nil, err
}
_, err = part.Write(content)
if err != nil {
return nil, errors.New("Invocation of io.Writer.Write() failed: " + err.Error())
}
if err = writer.Close(); err != nil {
return nil, errors.New("Failed to close *multipart.Writer: " + err.Error())
}
request, err := http.NewRequest(http.MethodPost, url, buf)
if err != nil {
return nil, errors.New("Failed to create request for Validation API multipart file uploading: " + err.Error())
}
request.Header.Add(constants.Header_content_type_key, writer.FormDataContentType())
request.Header.Add(constants.Project_Id_header_str, projectId)
request.Header.Add(constants.File_type_header_str, fileType)
if doZip {
request.Header.Add(constants.IsZipped_header_str, "true")
} else {
request.Header.Add(constants.IsZipped_header_str, "false")
}
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return nil, errors.New("Failed to invoke Validation API: " + err.Error())
} else if !strings.Contains(strconv.Itoa(response.StatusCode), "20") {
respMap, _ := processValidationResponse(response)
message := fmt.Sprintf("Failed to invoke Validation API with HTTP %s : %s", strconv.Itoa(response.StatusCode), respMap["message"])
return nil, errors.New(message)
}
return response, nil
}
func ZipBytes(fileName string, content []byte, doZip bool) ([]byte, error) {
if doZip {
log.Println("Zipped file will be sent for validation")
buf := new(bytes.Buffer)
zipWriter := zip.NewWriter(buf)
zipFile, err := zipWriter.Create(fileName)
if err != nil {
return nil, errors.New("Zip file creation failed: " + err.Error())
}
_, err = zipFile.Write(content)
if err != nil {
return nil, errors.New("Writing to zip file failed: " + err.Error())
}
if err = zipWriter.Close(); err != nil {
return nil, errors.New("Failed to close zip writer: " + err.Error())
}
return buf.Bytes(), nil
} else {
log.Println("Normal file will be sent for validation")
return content, nil
}
}

How to show progress during upload asynchronously with WASM

I am currently using Go WASM to upload a file to a server. During the upload it shall emit a call to update the upload progress in the UI.
I am currently using the following struct to have an indication of the progress:
type progressReporter struct {
r io.Reader
fileSizeEncrypted int64
sent int64
file js.Value
}
func (pr *progressReporter) Read(p []byte) (int, error) {
n, err := pr.r.Read(p)
pr.sent = pr.sent + int64(n)
pr.report()
return n, err
}
func (pr *progressReporter) report() {
go js.Global().Get("dropzoneObject").Call("emit", "uploadprogress", pr.file, pr.sent*100/pr.fileSizeEncrypted, pr.sent)
}
The upload happens in a promise:
func UploadChunk(this js.Value, args []js.Value) interface{} {
[...]
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
resolve := args[0]
reject := args[1]
go func() {
[...]
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", "encrypted.file")
if err != nil {
return err
}
_, err = part.Write(*data)
if err != nil {
return err
}
err = writer.Close()
if err != nil {
return err
}
pReporter := progressReporter{
r: body,
fileSizeEncrypted: fileSize,
sent: offset,
file: jsFile,
}
r, err := http.NewRequest("POST", "./uploadChunk", &pReporter)
if err != nil {
return err
}
r.Header.Set("Content-Type", writer.FormDataContentType())
client := &http.Client{}
resp, err := client.Do(r)
if err != nil {
return err
}
[...]
}
}
Although the code works fine, all emit calls to update the UI are sent after the POST request is finished. Is there any way to have this call asynchronously?
The full source code can be found here

Zip a Directory and not Have the Result Saved in File System

I am able to zip a file using logic similar to the zip writer seen here.
This results in an array of bytes ([]byte) being created within the bytes.Buffer object that is returned. I would just like to know if there is there any way I can upload this 'zipped' array of bytes to an API endpoint that expects a 'multipart/form-data' request body (without having to save it locally).
Supplementary information:
I have code that utilizes this when compressing a folder. I am able to successfully execute an HTTP POST request with the zip file to the endpoint with this logic.
However, this unfortunately saves zipped files in a user's local file system. I would like to try to avoid this :)
You can create multipart writer and write []byte zipped data into field with field name you like and file name like below.
func addZipFileToReq(zipped []byte) (*http.Request, error){
body := bytes.NewBuffer(nil)
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile(`fileField`, `filename`)
if err != nil {
return nil, err
}
_, err = part.Write(zipped)
if err != nil {
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
r, err := http.NewRequest(http.MethodPost, "https://example.com", body)
if err != nil {
return nil, err
}
r.Header.Set("Content-Type", writer.FormDataContentType())
return r, nil
}
If you want to stream-upload the zip, you should be able to do so with io.Pipe. The following is an incomplete and untested example to demonstrate the general idea. To make it work you'll need to modify it and potentially fix whatever bugs you encounter.
func UploadReader(r io.Reader) error {
req, err := http.NewRequest("POST", "<UPLOAD_URL>", r)
if err != nil {
return err
}
// TODO set necessary headers (content type, auth, etc)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
} else if res.StatusCode != 200 {
return errors.New("not ok")
}
return nil
}
func ZipDir(dir string, w io.Writer) error {
zw := zip.NewWriter(w)
defer zw.Close()
return filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
header, err := zip.FileInfoHeader(fi)
if err != nil {
return err
}
header.Name = path
header.Method = zip.Deflate
w, err := zw.CreateHeader(header)
if err != nil {
return err
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(w, f); err != nil {
return err
}
return nil
})
}
func UploadDir(dir string) error {
r, w := io.Pipe()
ch := make(chan error)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
defer w.Close()
if err := ZipDir(dir, w); err != nil {
ch <- err
}
}()
wg.Add(1)
go func() {
defer wg.Done()
defer r.Close()
if err := UploadReader(r); err != nil {
ch <- err
}
}()
go func() {
wg.Wait()
close(ch)
}()
return <-ch
}

Go - Mock http.Response body with a file

I'm trying to test a Go function which performs a call to an external service. Here's the function:
func (gs *EuGameService) retrieveGames(client model.HTTPClient) (model.EuGamesResponse, error) {
req, err := http.NewRequest(http.MethodGet, gs.getGamesEndpoint, nil)
if err != nil {
log.Fatal("Error while creating request ", err)
return nil, err
}
resp, err := client.Do(req)
if err != nil {
log.Fatal("Error while retrieving EU games", err)
return nil, err
}
var euGames model.EuGamesResponse
decoder := json.NewDecoder(resp.Body)
decoder.Decode(&euGames)
return euGames, nil
}
to properly test it, I'm trying to inject a mock client.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type mockClient struct{}
func (mc *mockClient) Do(req *http.Request) (*http.Response, error) {
mock, _ := os.Open("../stubs/eugames.json")
defer mock.Close()
r := ioutil.NopCloser(bufio.NewReader(mock))
return &http.Response{
Status: string(http.StatusOK),
StatusCode: http.StatusOK,
Body: r,
}, nil
}
the file eugames.json contains a couple of games. But for some reason, the body is always empty! What am I missing here? I tried to use a constant with the file content and it works, games are decoded correctly. So I'm assuming there's a problem with my use of the file.

Go lang RPC return EOF error

I'm using http to call RPC with code below
func (c *CallClient) Wallet(method string, req, rep interface{}) error {
client := &http.Client{}
data, _ := EncodeClientRequest(method, req)
reqest, _ := http.NewRequest("POST", c.endpoint, bytes.NewBuffer(data))
resp, err := client.Do(reqest)
if err != nil {
return err
}
defer resp.Body.Close()
io.Copy(ioutil.Discard, resp.Body)
return DecodeClientResponse(resp.Body, rep)
}
with EncodeClientRquest && DecodeClientResponse
// EncodeClientRequest encodes parameters for a JSON-RPC client request.
func EncodeClientRequest(method string, args interface{}) ([]byte, error) {
c := &clientRequest{
Version: "2.0",
Method: method,
Params: [1]interface{}{args},
Id: uint64(rand.Int63()),
}
return json.Marshal(c)
}
// DecodeClientResponse decodes the response body of a client request into
// the interface reply.
func DecodeClientResponse(r io.Reader, reply interface{}) error {
var c clientResponse
if err := json.NewDecoder(r).Decode(&c); err != nil {
return err
}
if c.Error != nil {
return fmt.Errorf("%v", c.Error)
}
if c.Result == nil {
return errors.New("result is null")
}
return json.Unmarshal(*c.Result, reply)
}
And I got error EOF.
This line:
io.Copy(ioutil.Discard, resp.Body)
reads the whole resp.Body, leaving the reader with no more bytes to be read. Therefore any successive calls to resp.Body.Read will return EOF and the json.Decoder.Decode method does use the io.Reader.Read method when decoding the given reader's content, so...
And since resp.Body is an io.ReadCloser, which is an interface that does not support "rewinding", and you want to read the body content more than once (ioutil.Discard and json.Decode), you'll have to read the body into a variable that you can re-read afterwards. It's up to you how you do that, a slice of bytes, or bytes.Reader, or something else.
Example using bytes.Reader:
func (c *CallClient) Wallet(method string, req, rep interface{}) error {
client := &http.Client{}
data, err := EncodeClientRequest(method, req)
if err != nil {
return err
}
reqest, err := http.NewRequest("POST", c.endpoint, bytes.NewBuffer(data))
if err != nil {
return err
}
resp, err := client.Do(reqest)
if err != nil {
return err
}
defer resp.Body.Close()
// get a reader that can be "rewound"
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, resp.Body); err != nil {
return err
}
br := bytes.NewReader(buf.Bytes())
if _, err := io.Copy(ioutil.Discard, br); err != nil {
return err
}
// rewind
if _, err := br.Seek(0, 0); err != nil {
return err
}
return DecodeClientResponse(br, rep)
}

Resources