I am creating an API endpoint to handle form submissions.
The form takes the following:
- Name
- Phone
- 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.
1条答案
按热度按时间yvgpqqbh1#
多亏了Adrian和Cerise,我才能正确地构造请求中的
multipart/form-data
(更新的代码在问题中)。然而,它仍然不起作用,原因是,我正在做:
而不是: