在Go语言中测试一个HTTP端点,该端点处理多部分/表单数据的表单提交

vuktfyat  于 2023-02-06  发布在  Go
关注(0)|答案(1)|浏览(132)

I am creating an API endpoint to handle form submissions.
The form takes the following:

  1. Name
  2. Email
  3. Phone
  4. Photo files (up to 5)
    Then basically, sends an email to some email address with the photos as attachments.
    I want to write tests for my handler to make sure everything is working well, however, I am struggling.

CODE:

Below is my HTTP handler (will run in AWS lambda, not that it matters).

package aj

import (
    "fmt"
    "mime"
    "net/http"

    "go.uber.org/zap"
)

const expectedContentType string = "multipart/form-data"

func FormSubmissionHandler(logger *zap.Logger, emailSender EmailSender) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // enforce a multipart/form-data content-type
        contentType := r.Header.Get("content-type")

        mediatype, _, err := mime.ParseMediaType(contentType)
        if err != nil {
            logger.Error("error when parsing the mime type", zap.Error(err))
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }

        if mediatype != expectedContentType {
            logger.Error("unsupported content-type", zap.Error(err))
            http.Error(w, fmt.Sprintf("api expects %v content-type", expectedContentType), http.StatusUnsupportedMediaType)
            return
        }

        err = r.ParseMultipartForm(32 << 20)
        if err != nil {
            logger.Error("error parsing form data", zap.Error(err))
            http.Error(w, "error parsing form data", http.StatusBadRequest)
            return
        }

        name := r.PostForm.Get("name")
        if name == "" {
            fmt.Println("inside if statement for name")
            logger.Error("name not set", zap.Error(err))
            http.Error(w, "api expects name to be set", http.StatusBadRequest)
            return
        }

        email := r.PostForm.Get("email")
        if email == "" {
            logger.Error("email not set", zap.Error(err))
            http.Error(w, "api expects email to be set", http.StatusBadRequest)
            return
        }

        phone := r.PostForm.Get("phone")
        if phone == "" {
            logger.Error("phone not set", zap.Error(err))
            http.Error(w, "api expects phone to be set", http.StatusBadRequest)
            return
        }

        emailService := NewEmailService()

        m := NewMessage("Test", "Body message.")

        err = emailService.SendEmail(logger, r.Context(), m)
        if err != nil {
            logger.Error("an error occurred sending the email", zap.Error(err))
            http.Error(w, "error sending email", http.StatusBadRequest)
            return
        }

        w.WriteHeader(http.StatusOK)
    })
}

The now updated test giving me trouble is:

package aj

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "image/jpeg"
    "io"
    "mime/multipart"
    "net/http"
    "net/http/httptest"
    "os"
    "strings"
    "testing"

    "go.uber.org/zap"
)

type StubEmailService struct {
    sendEmail func(logger *zap.Logger, ctx context.Context, email *Message) error
}

func (s *StubEmailService) SendEmail(logger *zap.Logger, ctx context.Context, email *Message) error {
    return s.sendEmail(logger, ctx, email)
}

func TestFormSubmissionHandler(t *testing.T) {
    // create the logger
    logger, _ := zap.NewProduction()

    t.Run("returns 400 (bad request) when name is not set in the body", func(t *testing.T) {
        // set up a pipe avoid buffering
        pipeReader, pipeWriter := io.Pipe()

        // this writer is going to transform what we pass to it to multipart form data
        // and write it to our io.Pipe
        multipartWriter := multipart.NewWriter(pipeWriter)

        go func() {
            // close it when it has done its job
            defer multipartWriter.Close()

            // create a form field writer for name
            nameField, err := multipartWriter.CreateFormField("name")
            if err != nil {
                t.Error(err)
            }

            // write string to the form field writer for name
            nameField.Write([]byte("John Doe"))

            // we create the form data field 'photo' which returns another writer to write the actual file
            fileField, err := multipartWriter.CreateFormFile("photo", "test.png")
            if err != nil {
                t.Error(err)
            }

            // read image file as array of bytes
            fileBytes, err := os.ReadFile("../../00-test-image.jpg")

            // create an io.Reader
            reader := bytes.NewReader(fileBytes)

            // convert the bytes to a jpeg image
            image, err := jpeg.Decode(reader)
            if err != nil {
                t.Error(err)
            }

            // Encode() takes an io.Writer. We pass the multipart field 'photo' that we defined
            // earlier which, in turn, writes to our io.Pipe
            err = jpeg.Encode(fileField, image, &jpeg.Options{Quality: 75})
            if err != nil {
                t.Error(err)
            }
        }()

        formData := HandleFormRequest{Name: "John Doe", Email: "john.doe@example.com", Phone: "07542147833"}

        // create the stub patient store
        emailService := StubEmailService{
            sendEmail: func(_ *zap.Logger, _ context.Context, email *Message) error {
                if !strings.Contains(email.Body, formData.Name) {
                    t.Errorf("expected email.Body to contain %s", formData.Name)
                }
                return nil
            },
        }

        // create a request to pass to our handler
        req := httptest.NewRequest(http.MethodPost, "/handler", pipeReader)

        // set the content type
        req.Header.Set("content-type", "multipart/form-data")

        // create a response recorder
        res := httptest.NewRecorder()

        // get the handler
        handler := FormSubmissionHandler(logger, &emailService)

        // our handler satisfies http.handler, so we can call its serve http method
        // directly and pass in our request and response recorder
        handler.ServeHTTP(res, req)

        // assert status code is what we expect
        assertStatusCode(t, res.Code, http.StatusBadRequest)
    })
}

func assertStatusCode(t testing.TB, got, want int) {
    t.Helper()

    if got != want {
        t.Errorf("handler returned wrong status code: got %v want %v", got, want)
    }
}

As mentioned in the test name, I want to make sure a Name property is coming through with the request.
When I run go test ./... -v I get:
=== RUN TestFormSubmissionHandler/returns_400_(bad_request)_when_name_is_not_set_in_the_body {"level":"error","ts":1675459283.4969518,"caller":"aj/handler.go:33","msg":"error parsing form data","error":"no multipart boundary param in Content-Type","stacktrace":"github.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj.FormSubmissionHandler.func1\n\t/home/j/code/go/src/github.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj/handler.go:33\nnet/http.HandlerFunc.ServeHTTP\n\t/usr/local/go/src/net/http/server.go:2109\ngithub.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj.TestFormSubmissionHandler.func3\n\t/home/j/code/go/src/github.com/jwankhalaf-dh/ajgenerators.co.uk__form-handler/api/aj/handler_test.go:132\ntesting.tRunner\n\t/usr/local/go/src/testing/testing.go:1446"}
I understand the error , but I am not sure how to overcome it.
My next test would be to test the same thing but for email, and then phone, then finally, I'd like to test file data, but I'm not sure how.

yvgpqqbh

yvgpqqbh1#

多亏了Adrian和Cerise,我才能正确地构造请求中的multipart/form-data(更新的代码在问题中)。
然而,它仍然不起作用,原因是,我正在做:

// set the content type
req.Header.Set("content-type", "multipart/form-data")

而不是:

// set the content type
req.Header.Add("content-type", multipartWriter.FormDataContentType())

相关问题