Home > Software engineering >  Go Fiber not able to parse body in unit test
Go Fiber not able to parse body in unit test

Time:10-12

I am officially crying uncle to the benevolent samaritans of Stack Overflow.

I am trying to unit test my GORM (Postgres) Fiber API using a mock DB. I have a Card model and a CreateCardReqBody model for the POST request body. To setup the test, I create a random CreateCardReqBody instance, marshal it into JSON, then pass it into an *httptest.Request. The handler uses Fiber's (*fiber.Ctx).BodyParser function to "unmarshal" the request body into an empty Card struct. However, when I run the test that is supposed to pass, Fiber throws an "Unprocessable Entity" error.

Below are the relevant parts of my code; the test file is a combination of this tutorial and Fiber's documentation on the (*App).Test method. (I realize the code could be cleaned up; I'm just trying to get a proof of life then focus on revising :)

I've done a few things to debug this: I've made a Postman POST request with the same values as the test and it works. Within the test itself, I marshal then unmarshal the CreateCardReqBody struct and that works. I've triple checked the spelling of the JSON fields match, that the struct fields are exported, etc. I've also run the VSCode debugger and the body field within Fiber.Ctx's also looks correct to me.

I'm starting to wonder if it's something with how Fiber parses the body from a test request vs. a real request. I would greatly appreciate any insight one could share on this!

Model Definition

type Card struct {
gorm.Model

// Implicit Gorm foreign key to fleet ID
FleetID uint `gorm:"index"  json:"fleet_id" validate:"required,min=1"`

// Card provider's account number
ProviderAccountNumber string `json:"provider_account_number"`

// Card provider's external card identifier
CardIdentifier string `gorm:"index" json:"card_identifier" validate:"min=1"`

// Implicit Gorm foreign key to driver ID. Driver association is optional.
DriverID uint `json:"associated_driver_id" validate:"min=1"`

// Implicit Gorm foreign key to vehicle ID.
VehicleID uint `json:"associated_vehicle_id" validate:"required,min=1"`

// User-inputted start date, formatted "2020-01-26T22:38:25.000Z" in UTC
StartDate pq.NullTime

}

Test file

// Adapted from tutorial
type testCase struct {
    name          string
    body          CreateCardReqBody
    setupAuth     func(t *testing.T, request *http.Request)
    buildStubs    func(db *mockDB.MockDBInterface)
    checkResponse func(response *http.Response, outputErr error)
}

type CreateCardReqBody struct {
    FleetID               int    `json:"fleet_id"`
    ProviderAccountNumber string `json:"provider_account_number"`
    CardIdentifier        string `json:"card_identifier"`
    StartDate             string `json:"start_date"`
    AssociatedDriverID    int    `json:"associated_driver_id"`
    AssociatedVehicleID   int    `json:"associated_vehicle_id"`
}

func TestCreateCard(t *testing.T) {
    user := randomUser(t)
    vehicle := randomVehicle()
    driver := randomDriver(vehicle.FleetID)
    okReqCard := randomCard(vehicle.FleetID)

    finalOutputCard := okReqCard
    finalOutputCard.ID = 1

    testCases := []testCase{
        {
            name: "Ok",
            body: CreateCardReqBody{
                FleetID:               int(okReqCard.FleetID),
                ProviderAccountNumber: okReqCard.ProviderAccountNumber,
                CardIdentifier:        okReqCard.CardIdentifier,
                StartDate:             okReqCard.StartDate.Time.Format("2006-01-02T15:04:05.999Z"),
                AssociatedDriverID:    int(okReqCard.DriverID),
                AssociatedVehicleID:   int(okReqCard.VehicleID),
            },
            setupAuth: func(t *testing.T, request *http.Request) {
                addAuthorization(t, request, user)
            },
            // Tell mock database what calls to expect and what values to return
            buildStubs: func(db *mockDB.MockDBInterface) {
                db.EXPECT().
                    UserExist(gomock.Eq(fmt.Sprint(vehicle.FleetID))).
                    Times(1).Return(user, true, user.ID)

                db.EXPECT().
                    SearchTSP(gomock.Eq(fmt.Sprint(vehicle.FleetID))).
                    Times(1)

                db.EXPECT().
                    SearchVehicle(gomock.Eq(fmt.Sprint(okReqCard.VehicleID))).
                    Times(1).
                    Return(vehicle, nil)

                db.EXPECT().
                    SearchDriver(gomock.Eq(fmt.Sprint(driver.ID))).
                    Times(1).
                    Return(driver, nil)

                db.EXPECT().
                    CardCreate(gomock.Eq(okReqCard)).
                    Times(1).
                    Return(finalOutputCard, nil)

            },
            checkResponse: func(res *http.Response, outputErr error) {
                require.NoError(t, outputErr)
                // Internal helper func, excluded for brevity
                requireBodyMatchCard(t, finalOutputCard, res.Body)
            },
        },
    }

    for _, test := range testCases {
        t.Run(test.name, func(t *testing.T) {
            ctrl := gomock.NewController(t)
            defer ctrl.Finish()

            mockDB := mockDB.NewMockDBInterface(ctrl)
            test.buildStubs(mockDB)

            jsonBytes, err := json.Marshal(test.body)
            require.NoError(t, err)
            jsonBody := bytes.NewReader(jsonBytes)

            // Debug check: am I able to unmarshal it back? YES.
            errUnmarsh := json.Unmarshal(jsonBytes, &CreateCardReqBody{})
            require.NoError(t, errUnmarsh)

            endpoint := "/v1/transactions/card"
            request := httptest.NewRequest("POST", endpoint, jsonBody)
            // setupAuth is helper function (not shown in this post) that adds authorization to httptest request
            test.setupAuth(t, request)
            
            app := Init("test", mockDB)
            res, err := app.Test(request)

            test.checkResponse(res, err)

        })
    }
}

Route handler being tested

func (server *Server) CreateCard(c *fiber.Ctx) error {
    var card models.Card

    var err error
    // 1) Parse POST data
    if err = c.BodyParser(&card); err != nil {
        return c.Status(http.StatusUnprocessableEntity).SendString(err.Error())
    }
    ...
}

Debugger Output

Json body when defined in test

Body inside Fiber context

CodePudding user response:

facepalm

I forgot to request.Header.Set("Content-Type", "application/json")! Posting this in case it's helpful for anyone else :)

  • Related