Jelajahi Sumber

test repository without sqlmock

Alexander Belanger 5 tahun lalu
induk
melakukan
4c246f59da

+ 3 - 3
.air.toml

@@ -7,11 +7,11 @@ tmp_dir = "tmp"
 
 [build]
 # Just plain old shell command. You could use `make` as well.
-cmd = "go build -o ./tmp/app ./cmd/app"
+cmd = "go build -o ./tmp/migrate ./cmd/migrate; go build -o ./tmp/app ./cmd/app"
 # Binary file yields from `cmd`.
-bin = "tmp/app"
+bin = "tmp/migrate; tmp/app"
 # Customize binary.
-full_bin = "tmp/app"
+full_bin = "tmp/migrate; tmp/app"
 # Watch these filename extensions.
 include_ext = ["go", "mod", "sum", "html"]
 # Ignore these filename extensions or directories.

+ 5 - 1
cmd/migrate/main.go

@@ -22,8 +22,12 @@ func main() {
 		return
 	}
 
-	db.AutoMigrate(
+	err = db.AutoMigrate(
 		&models.User{},
 		&models.ClusterConfig{},
 	)
+
+	if err != nil {
+		panic(err)
+	}
 }

+ 19 - 1
docker-compose.yaml

@@ -29,5 +29,23 @@ services:
     volumes:
       - db:/var/lib/postgresql/data
 
+  metabase:
+    image: metabase/metabase
+    restart: always
+    ports: 
+      - 3000:3000
+    volumes: 
+      - metabase:/metabase-data
+    environment:
+      MB_DB_TYPE: postgres
+      MB_DB_DBNAME: porter
+      MB_DB_PORT: 5432
+      MB_DB_USER: porter
+      MB_DB_PASS: porter
+      MB_DB_HOST: postgres
+    depends_on:
+      - postgres
+
 volumes:
-  db:
+  db:
+  metabase:

+ 2 - 3
go.mod

@@ -11,7 +11,6 @@ require (
 	github.com/go-playground/universal-translator v0.17.0
 	github.com/go-playground/validator/v10 v10.3.0
 	github.com/imdario/mergo v0.3.11 // indirect
-	github.com/jinzhu/gorm v1.9.16
 	github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd
 	github.com/leodido/go-urn v1.2.0 // indirect
 	github.com/mattn/go-colorable v0.1.7 // indirect
@@ -24,8 +23,8 @@ require (
 	golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
 	gopkg.in/go-playground/validator.v9 v9.31.0
 	gopkg.in/yaml.v2 v2.3.0
-	gorm.io/driver/postgres v1.0.1
-	gorm.io/gorm v1.20.1
+	gorm.io/driver/postgres v1.0.2
+	gorm.io/gorm v1.20.2
 	k8s.io/apimachinery v0.19.2
 	k8s.io/client-go v0.0.0-20200917000235-cba7285b7f29
 	k8s.io/klog v1.0.0 // indirect

+ 15 - 2
go.sum

@@ -186,6 +186,8 @@ github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr
 github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
 github.com/jackc/pgconn v1.6.4 h1:S7T6cx5o2OqmxdHaXLH1ZeD1SbI8jBznyYE9Ec0RCQ8=
 github.com/jackc/pgconn v1.6.4/go.mod h1:w2pne1C2tZgP+TvjqLpOigGzNqjBgQW9dUw/4Chex78=
+github.com/jackc/pgconn v1.7.0 h1:pwjzcYyfmz/HQOQlENvG1OcDqauTGaqlVahq934F0/U=
+github.com/jackc/pgconn v1.7.0/go.mod h1:sF/lPpNEMEOp+IYhyQGdAvrG20gWf6A1tKlr0v7JMeA=
 github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
 github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
 github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
@@ -200,6 +202,8 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:
 github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
 github.com/jackc/pgproto3/v2 v2.0.2 h1:q1Hsy66zh4vuNsajBUF2PNqfAMMfxU5mk594lPE9vjY=
 github.com/jackc/pgproto3/v2 v2.0.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.0.5 h1:NUbEWPmCQZbMmYlTjVoNPhc0CfnYyz2bfUAh6A5ZVJM=
+github.com/jackc/pgproto3/v2 v2.0.5/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
 github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
 github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
@@ -211,6 +215,8 @@ github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkAL
 github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ=
 github.com/jackc/pgtype v1.4.2 h1:t+6LWm5eWPLX1H5Se702JSBcirq6uWa4jiG4wV1rAWY=
 github.com/jackc/pgtype v1.4.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
+github.com/jackc/pgtype v1.5.0 h1:jzBqRk2HFG2CV4AIwgCI2PwTgm6UUoCAK2ofHHRirtc=
+github.com/jackc/pgtype v1.5.0/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig=
 github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
 github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
@@ -219,12 +225,13 @@ github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6
 github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg=
 github.com/jackc/pgx/v4 v4.8.1 h1:SUbCLP2pXvf/Sr/25KsuI4aTxiFYIvpfk4l6aTSdyCw=
 github.com/jackc/pgx/v4 v4.8.1/go.mod h1:4HOLxrl8wToZJReD04/yB20GDwf4KBYETvlHciCnwW0=
+github.com/jackc/pgx/v4 v4.9.0 h1:6STjDqppM2ROy5p1wNDcsC7zJTjSHeuCsguZmXyzx7c=
+github.com/jackc/pgx/v4 v4.9.0/go.mod h1:MNGWmViCgqbZck9ujOOBN63gK9XVGILXWCvKLGKmnms=
 github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
-github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
-github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
+github.com/jackc/puddle v1.1.2/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
@@ -619,8 +626,14 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gorm.io/driver/postgres v1.0.1 h1:jRfDNUxpxNrea/97kbcscAQGmiks4UCKAYXsvh4rhOQ=
 gorm.io/driver/postgres v1.0.1/go.mod h1:pv4dVhHvEVrP7k/UYqdBIllbdbpB5VTz89X1O0uOrCA=
+gorm.io/driver/postgres v1.0.2 h1:mB5JjD4QglbCTdMT1aZDxQzHr87XDK1qh0MKIU3P96g=
+gorm.io/driver/postgres v1.0.2/go.mod h1:FvRSYfBI9jEp6ZSjlpS9qNcSjxwYxFc03UOTrHdvvYA=
+gorm.io/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o=
+gorm.io/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs=
 gorm.io/gorm v1.20.1 h1:+hOwlHDqvqmBIMflemMVPLJH7tZYK4RxFDBHEfJTup0=
 gorm.io/gorm v1.20.1/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
+gorm.io/gorm v1.20.2 h1:bZzSEnq7NDGsrd+n3evOOedDrY5oLM5QPlCjZJUK2ro=
+gorm.io/gorm v1.20.2/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 1 - 1
internal/forms/user.go

@@ -1,7 +1,7 @@
 package forms
 
 import (
-	"github.com/jinzhu/gorm"
+	"gorm.io/gorm"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/models"
 	"gopkg.in/yaml.v2"

+ 1 - 1
internal/models/cluster_configs.go

@@ -1,6 +1,6 @@
 package models
 
-import "github.com/jinzhu/gorm"
+import "gorm.io/gorm"
 
 // ClusterConfig that extends gorm.Model
 //

+ 1 - 1
internal/models/cluster_configs_test.go

@@ -3,7 +3,7 @@ package models_test
 import (
 	"testing"
 
-	"github.com/jinzhu/gorm"
+	"gorm.io/gorm"
 	"github.com/porter-dev/porter/internal/models"
 )
 

+ 3 - 2
internal/models/user.go

@@ -1,14 +1,15 @@
 package models
 
 import (
-	"github.com/jinzhu/gorm"
+	"gorm.io/gorm"
 )
 
 // User type that extends gorm.Model
 type User struct {
 	gorm.Model
 	// Unique email for each user
-	Email,
+	// Email string `gorm:"unique"`
+	Email string
 	// Hashed password
 	Password string
 	// The clusters that this user has linked

+ 1 - 1
internal/models/user_test.go

@@ -3,7 +3,7 @@ package models_test
 import (
 	"testing"
 
-	"github.com/jinzhu/gorm"
+	"gorm.io/gorm"
 	"github.com/porter-dev/porter/internal/models"
 )
 

+ 19 - 1
internal/repository/test/test_repository.go → internal/repository/test/user.go

@@ -1,7 +1,9 @@
 package test
 
 import (
-	"github.com/jinzhu/gorm"
+	"errors"
+
+	"gorm.io/gorm"
 	"github.com/porter-dev/porter/internal/models"
 )
 
@@ -20,6 +22,10 @@ func NewUserRepository(canQuery bool) *UserRepository {
 
 // CreateUser adds a new User row to the Users table in array memory
 func (repo UserRepository) CreateUser(user *models.User) (*models.User, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
 	users := repo.users
 	users = append(users, user)
 	user.ID = uint(len(users))
@@ -28,6 +34,10 @@ func (repo UserRepository) CreateUser(user *models.User) (*models.User, error) {
 
 // ReadUser finds a single user based on their unique id
 func (repo UserRepository) ReadUser(id uint) (*models.User, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
 	if int(id-1) >= len(repo.users) || repo.users[id] == nil {
 		return nil, gorm.ErrRecordNotFound
 	}
@@ -38,6 +48,10 @@ func (repo UserRepository) ReadUser(id uint) (*models.User, error) {
 
 // UpdateUser modifies an existing User in the database
 func (repo UserRepository) UpdateUser(user *models.User) (*models.User, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
 	if int(user.ID-1) >= len(repo.users) || repo.users[user.ID] == nil {
 		return nil, gorm.ErrRecordNotFound
 	}
@@ -50,6 +64,10 @@ func (repo UserRepository) UpdateUser(user *models.User) (*models.User, error) {
 
 // DeleteUser deletes a single user using their unique id
 func (repo UserRepository) DeleteUser(user *models.User) (*models.User, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
 	if int(user.ID-1) >= len(repo.users) || repo.users[user.ID] == nil {
 		return nil, gorm.ErrRecordNotFound
 	}

+ 16 - 36
server/api/errors.go

@@ -2,7 +2,6 @@ package api
 
 import (
 	"encoding/json"
-	"fmt"
 	"net/http"
 
 	"github.com/go-playground/validator/v10"
@@ -33,11 +32,16 @@ type ErrorCode int64
 // client.
 //
 // It then logs it via the app.logger and sends a formatted error to the client.
-func (app *App) sendExternalError(err error, errExt HTTPError, w http.ResponseWriter) (intErr error) {
+func (app *App) sendExternalError(
+	err error,
+	code int,
+	errExt HTTPError,
+	w http.ResponseWriter,
+) (intErr error) {
 	respBytes, newErr := json.Marshal(errExt)
 
 	if newErr != nil {
-		app.handleErrorInternalError(newErr, w)
+		app.handleErrorInternal(newErr, w)
 		return newErr
 	}
 
@@ -47,7 +51,8 @@ func (app *App) sendExternalError(err error, errExt HTTPError, w http.ResponseWr
 		Str("errExt", respBody).
 		Msg("")
 
-	fmt.Fprintf(w, respBody)
+	w.WriteHeader(code)
+	w.Write(respBytes)
 
 	return nil
 }
@@ -61,12 +66,7 @@ func (app *App) handleErrorFormDecoding(err error, code ErrorCode, w http.Respon
 		Errors: []string{appErrFormDecoding},
 	}
 
-	intErr := app.sendExternalError(err, errExt, w)
-
-	if intErr == nil {
-		app.logger.Warn().Err(err).Msg("")
-		w.WriteHeader(http.StatusUnprocessableEntity)
-	}
+	app.sendExternalError(err, http.StatusBadRequest, errExt, w)
 }
 
 // handleErrorFormValidation handles an error in the validation of form fields, and
@@ -87,12 +87,7 @@ func (app *App) handleErrorFormValidation(err error, code ErrorCode, w http.Resp
 		Errors: res,
 	}
 
-	intErr := app.sendExternalError(err, errExt, w)
-
-	if intErr == nil {
-		app.logger.Warn().Err(err).Msg("")
-		w.WriteHeader(http.StatusUnprocessableEntity)
-	}
+	app.sendExternalError(err, http.StatusUnprocessableEntity, errExt, w)
 }
 
 // handleErrorRead handles an error in reading a record from the DB. If the record is
@@ -107,12 +102,7 @@ func (app *App) handleErrorRead(err error, code ErrorCode, w http.ResponseWriter
 			Errors: []string{appErrReadNotFound},
 		}
 
-		intErr := app.sendExternalError(err, errExt, w)
-
-		if intErr == nil {
-			app.logger.Warn().Err(err).Msg("")
-			w.WriteHeader(http.StatusNotFound)
-		}
+		app.sendExternalError(err, http.StatusNotFound, errExt, w)
 
 		return
 	}
@@ -128,12 +118,7 @@ func (app *App) handleErrorDataWrite(err error, code ErrorCode, w http.ResponseW
 		Errors: []string{appErrDataWrite},
 	}
 
-	intErr := app.sendExternalError(err, errExt, w)
-
-	if intErr == nil {
-		app.logger.Warn().Err(err).Msg("")
-		w.WriteHeader(http.StatusUnprocessableEntity)
-	}
+	app.sendExternalError(err, http.StatusInternalServerError, errExt, w)
 }
 
 // handleErrorDataRead handles a database read error due to an internal error, such as
@@ -144,18 +129,13 @@ func (app *App) handleErrorDataRead(err error, code ErrorCode, w http.ResponseWr
 		Errors: []string{appErrDataRead},
 	}
 
-	intErr := app.sendExternalError(err, errExt, w)
-
-	if intErr == nil {
-		app.logger.Warn().Err(err).Msg("")
-		w.WriteHeader(http.StatusInternalServerError)
-	}
+	app.sendExternalError(err, http.StatusInternalServerError, errExt, w)
 }
 
 // handleErrorInternalError is a catch-all for internal errors that occur during the
 // processing of a request
-func (app *App) handleErrorInternalError(err error, w http.ResponseWriter) {
+func (app *App) handleErrorInternal(err error, w http.ResponseWriter) {
 	app.logger.Warn().Err(err).Msg("")
 	w.WriteHeader(http.StatusInternalServerError)
-	fmt.Fprintf(w, `{"error": "Internal server error"}`)
+	w.Write([]byte(`{"error": "Internal server error"}`))
 }

+ 112 - 15
server/api/user_handler_test.go

@@ -8,14 +8,17 @@ import (
 	"time"
 
 	"github.com/porter-dev/porter/internal/config"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
 	"github.com/porter-dev/porter/internal/repository/test"
 	"github.com/porter-dev/porter/server/api"
+	"github.com/porter-dev/porter/server/requestlog"
 
 	lr "github.com/porter-dev/porter/internal/logger"
 	vr "github.com/porter-dev/porter/internal/validator"
 )
 
-func initApi() *api.App {
+func initApi(canQuery bool) (*api.App, *repository.Repository) {
 	appConf := config.Conf{
 		Debug: true,
 		Server: config.ServerConf{
@@ -31,27 +34,121 @@ func initApi() *api.App {
 	logger := lr.NewConsole(appConf.Debug)
 	validator := vr.New()
 
-	repo := test.NewRepository(true)
+	repo := test.NewRepository(canQuery)
 
-	return api.New(logger, repo, validator)
+	return api.New(logger, repo, validator), repo
+}
+
+type userTest struct {
+	init func(repo *repository.Repository)
+	msg,
+	method,
+	endpoint,
+	body string
+	expStatus int
+	expBody   string
+	canQuery  bool
+}
+
+var createUserTests = []userTest{
+	userTest{
+		msg:      "Create user",
+		method:   "POST",
+		endpoint: "/api/users",
+		body: `{
+			"email": "belanger@getporter.dev",
+			"password": "hello"
+		}`,
+		expStatus: http.StatusCreated,
+		expBody:   "",
+		canQuery:  true,
+	},
+	userTest{
+		msg:      "Create user invalid email",
+		method:   "POST",
+		endpoint: "/api/users",
+		body: `{
+			"email": "notanemail",
+			"password": "hello"
+		}`,
+		expStatus: http.StatusUnprocessableEntity,
+		expBody:   `{"code":1,"errors":["email validation failed"]}`,
+		canQuery:  true,
+	},
+	userTest{
+		msg:      "Create user missing field",
+		method:   "POST",
+		endpoint: "/api/users",
+		body: `{
+			"password": "hello"
+		}`,
+		expStatus: http.StatusUnprocessableEntity,
+		expBody:   `{"code":1,"errors":["required validation failed"]}`,
+		canQuery:  true,
+	},
+	userTest{
+		msg:      "Create user cannot write to db",
+		method:   "POST",
+		endpoint: "/api/users",
+		body: `{
+			"email": "belanger@getporter.dev",
+			"password": "hello"
+		}`,
+		expStatus: http.StatusInternalServerError,
+		expBody:   `{"code":2,"errors":["data write error"]}`,
+		canQuery:  false,
+	},
+	userTest{
+		init: func(repo *repository.Repository) {
+			repo.User.CreateUser(&models.User{})
+		},
+		msg:      "Create user same email",
+		method:   "POST",
+		endpoint: "/api/users",
+		body: `{
+			"email": "belanger@getporter.dev",
+			"password": "hello"
+		}`,
+		expStatus: http.StatusCreated,
+		expBody:   "",
+		canQuery:  true,
+	},
 }
 
 func TestHandleCreateUser(t *testing.T) {
-	// create a mock API
-	api := initApi()
+	for _, c := range createUserTests {
+		// create a mock API
+		api, repo := initApi(c.canQuery)
 
-	req, err := http.NewRequest("POST", "/api/users", strings.NewReader("{\"email\":\"belanger@getporter.dev\",\"password\":\"hello\"}"))
-	if err != nil {
-		t.Fatal(err)
-	}
+		if c.init != nil {
+			c.init(repo)
+		}
+
+		req, err := http.NewRequest(
+			c.method,
+			c.endpoint,
+			strings.NewReader(c.body),
+		)
+
+		if err != nil {
+			t.Fatal(err)
+		}
 
-	rr := httptest.NewRecorder()
-	handler := http.HandlerFunc(api.HandleCreateUser)
+		rr := httptest.NewRecorder()
+		handler := requestlog.NewHandler(api.HandleCreateUser, api.Logger())
 
-	handler.ServeHTTP(rr, req)
+		handler.ServeHTTP(rr, req)
 
-	if status := rr.Code; status != http.StatusCreated {
-		t.Errorf("handler returned wrong status code: got %v want %v",
-			status, http.StatusCreated)
+		if status := rr.Code; status != c.expStatus {
+			t.Errorf("%s, handler returned wrong status code: got %v want %v",
+				c.msg, status, c.expStatus)
+		}
+
+		if body := rr.Body.String(); body != c.expBody {
+			t.Errorf("%s, handler returned wrong body: got %v want %v",
+				c.msg, body, c.expBody)
+		}
 	}
 }
+
+// var readUserTests = []userTest