1

I'm trying to write a test for a handler which receives a file. As part of such I'm trying to configure my context so the handler can use it.

My intention is to create a file and use multipart.FileHeader to open it.

f, err := os.CreateTemp("", "upload-test")
require.NoError(t, err)
_, err = f.Write([]byte("1234"))
require.NoError(t, err)
err = f.Close()
require.NoError(t, err)

fileHeader := &multipart.FileHeader{
    Filename: f.Name(),
    Size:     4,
}
open, err := fileHeader.Open()
require.NoError(t, err)

However the Open method returns: "open : no such file or directory"

UPDATE:

My endpoint receives the below struct

type UploadDatabaseRequest struct {
    Group    string                `form:"group" binding:"required"`
    Database *multipart.FileHeader `form:"database" binding:"required"`
}

Based on Ivan 's answer I tried to read the request using multipart.NewReader so I could get a *multipart.FileHeader. However the below code results in "unexpected EOF" from the call to multipartReader.ReadForm(100000000).

contentType := multipartWriter.FormDataContentType()
_, params, err := mime.ParseMediaType(contentType)
assert.NoError(t, err)

multipartReader := multipart.NewReader(bytes.NewReader(buf.Bytes()), params["boundary"])
form, err := multipartReader.ReadForm(100000000)
require.NoError(t, err)
fileHeader := form.File["file"][0]
uploadRequest := &UploadDatabaseRequest{
    Group:    groupName,
    Database: fileHeader,
}
user672009
  • 4,379
  • 8
  • 44
  • 77

1 Answers1

1

I created an example to demonstrate how it should work. First, let me present the code, then, I'll walk you through all of the relevant parts.

Upload

The upload is done in the handlers_test.go file. I wrote two tests to show off how to create a valid HTTP Request with a multipart body (I assumed that the communication was based on HTTP). Here you can find the code:

package multipart

import (
    "bytes"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestHandleFile(t *testing.T) {
    t.Run("MultipartRequest", func(t *testing.T) {
        // instantiate multipart request
        var buf bytes.Buffer
        multipartWriter := multipart.NewWriter(&buf)
        defer multipartWriter.Close()

        // add form field
        filePart, _ := multipartWriter.CreateFormFile("file", "file.txt")
        filePart.Write([]byte("Hello, World!"))

        r := httptest.NewRequest(http.MethodPost, "/file", &buf)
        w := httptest.NewRecorder()

        r.Header.Set("Content-Type", multipartWriter.FormDataContentType())

        HandleFile(w, r)

        data, _ := io.ReadAll(w.Result().Body)

        assert.Equal(t, http.StatusOK, w.Result().StatusCode)
        assert.Equal(t, []byte("Hello, World!"), data)
    })

    t.Run("PlainRequest", func(t *testing.T) {
        r := httptest.NewRequest(http.MethodPost, "/file", nil)
        w := httptest.NewRecorder()

        HandleFile(w, r)

        assert.Equal(t, http.StatusBadRequest, w.Result().StatusCode)
    })
}

We can focus on the subtest MultipartRequest. First, it instantiates a multipart body which will be used later as the request payload of the HTTP request we're going to send. Then, we create a file part and write dummy content to it. Before sending out the request, we've to see the Content-Type header that will be used for parsing stuff. The rest of the test should be pretty straightforward.

Read

The read (or parsing) is done by the HTTP server. The file involved is the handlers.go file:

package multipart

import (
    "io"
    "mime"
    "mime/multipart"
    "net/http"
    "strings"
)

func HandleFile(w http.ResponseWriter, r *http.Request) {
    mediaType, params, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
    if strings.HasPrefix(mediaType, "multipart/") {
        multipartReader := multipart.NewReader(r.Body, params["boundary"])
        filePart, _ := multipartReader.NextPart()
        // pay attention here when you read large file
        data, _ := io.ReadAll(filePart)
        w.Write(data)
        return
    }
    w.WriteHeader(http.StatusBadRequest)
    w.Write([]byte("request is not multipart"))
}

Here, the relevant steps can be summarized in the following list:

  1. We retrieve and parse the Content-Type header from the HTTP Request
  2. We check if the above value starts with the string multipart/
  3. If so, we read the next (and only) part of the body and we write its content to the response stream
  4. If not, we return a BadRequest error to the HTTP client

In the code I put some comments to explain some delicate sections that deserve attention. Furthermore, I simplified the codebase by not handling any error that might happen.
Hope to help in solving your issue, let me know!

ossan
  • 1,665
  • 4
  • 10
  • 1
    That's an impressive answer! Very much appreciated. My reason for using multipart.FileHeader is that my endpoint receives more than just the file. I've updated the question to include the struct it receives. Sorry for not including it to begin with! – user672009 Jan 21 '23 at 05:18
  • Hi @user672009, can you update the question with all of the code for the update and parsing sections? In this way, I can help you better and I'm sure to provide you exactly what you need, thanks! – ossan Jan 23 '23 at 08:22
  • Please also consider that the struct `UploadDatabaseRequest` should not have a field of type `*multipart.FileHeader`. The latter is retrieved when you invoke the method `ParseMultipartForm` and then you retrieve the `Form` value. Your request should have fields, files, or both of them. Let me know! – ossan Jan 23 '23 at 09:16