test: re-worked user repo tests

This commit is contained in:
2026-09-09 16:25:48 +02:00
parent f81eb3be96
commit 366386c8bf
2 changed files with 188 additions and 165 deletions
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"git.dittmar.dev/robin/dttmr-api/internal/api/response" "git.dittmar.dev/robin/dttmr-api/internal/api/response"
) )
type VersionResponse struct { type versionResponse struct {
Version string `json:"version"` Version string `json:"version"`
Commit string `json:"commit"` Commit string `json:"commit"`
BuildTime string `json:"buildTime"` BuildTime string `json:"buildTime"`
@@ -19,11 +19,11 @@ type VersionResponse struct {
// @Tags Version // @Tags Version
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {object} VersionResponse // @Success 200 {object} versionResponse
// @Router /version [get] // @Router /version [get]
func VersionHandler(version string, commit string, buildTime string) func(http.ResponseWriter, *http.Request) { func VersionHandler(version string, commit string, buildTime string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
response.JSON(r.Context(), w, http.StatusOK, VersionResponse{ response.JSON(r.Context(), w, http.StatusOK, versionResponse{
Version: version, Version: version,
Commit: commit, Commit: commit,
BuildTime: buildTime, BuildTime: buildTime,
+185 -162
View File
@@ -1,185 +1,208 @@
package repository package repository
import ( import (
"fmt" "context"
"database/sql"
"errors"
"regexp"
"testing" "testing"
"time" "time"
"git.dittmar.dev/robin/dttmr-api/internal/domain"
"github.com/DATA-DOG/go-sqlmock" "github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"git.dittmar.dev/robin/dttmr-api/internal/domain"
)
func newUserRepo(t *testing.T) (*UserRepo, sqlmock.Sqlmock) {
t.Helper()
db, mock, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, mock.ExpectationsWereMet())
_ = db.Close()
})
return &UserRepo{Repo: NewRepo(NewTransactor(db))}, mock
}
const (
insertUserQuery = `INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id, created_at`
deleteUserQuery = `DELETE FROM users WHERE id = $1`
updatePassQuery = `UPDATE users SET password_hash = $1 WHERE id = $2`
selectUserQuery = `SELECT id, email, name FROM users WHERE email = $1`
) )
func TestUserRepo_CreateUser(t *testing.T) { func TestUserRepo_CreateUser(t *testing.T) {
db, mock, err := sqlmock.New()
assert.NoError(t, err)
defer db.Close()
//repo := NewUserRepo(db)
//ctx := context.Background()
email := "test@example.com"
name := "Test User"
passwordHash := "hashedpassword123"
now := time.Now()
expectedUser := &domain.User{
ID: "1",
Email: email,
Name: name,
CreatedAt: now,
}
t.Run("success", func(t *testing.T) { t.Run("success", func(t *testing.T) {
mock.ExpectBegin() repo, mock := newUserRepo(t)
mock.ExpectQuery(`^INSERT INTO users \(email, name, password_hash\) VALUES \(\$1, \$2, \$3\) RETURNING id, created_at$`). createdAt := time.Date(2026, 9, 9, 10, 0, 0, 0, time.UTC)
WithArgs(email, name, passwordHash). mock.ExpectQuery(regexp.QuoteMeta(insertUserQuery)).
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at"}).AddRow(expectedUser.ID, expectedUser.CreatedAt)) WithArgs("robin@dittmar.dev", "Robin", "$2a$10$hash").
WillReturnRows(
sqlmock.NewRows([]string{"id", "created_at"}).
AddRow("2f1c...", createdAt),
)
mock.ExpectCommit() user, err := repo.CreateUser(context.Background(), "robin@dittmar.dev", "Robin", "$2a$10$hash")
//user, err := repo.CreateUser(ctx, email, name, passwordHash) require.NoError(t, err)
//assert.NoError(t, err) require.NotNil(t, user)
//assert.Equal(t, expectedUser, user) assert.Equal(t, &domain.User{
//assert.NoError(t, mock.ExpectationsWereMet()) ID: "2f1c...",
Email: "robin@dittmar.dev",
Name: "Robin",
CreatedAt: createdAt,
}, user)
}) })
t.Run("begin_tx_error", func(t *testing.T) { t.Run("db error is wrapped", func(t *testing.T) {
mock.ExpectBegin().WillReturnError(fmt.Errorf("tx error")) repo, mock := newUserRepo(t)
//user, err := repo.CreateUser(ctx, email, name, passwordHash) dbErr := errors.New("duplicate key value violates unique constraint")
//assert.Error(t, err) mock.ExpectQuery(regexp.QuoteMeta(insertUserQuery)).
//assert.Contains(t, err.Error(), "begin transaction") WithArgs("robin@dittmar.dev", "Robin", "$2a$10$hash").
//assert.Nil(t, user) WillReturnError(dbErr)
//assert.NoError(t, mock.ExpectationsWereMet())
})
t.Run("insert_error", func(t *testing.T) { user, err := repo.CreateUser(context.Background(), "robin@dittmar.dev", "Robin", "$2a$10$hash")
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO users \(email, name, password_hash\) VALUES \(\$1, \$2, \$3\) RETURNING id, created_at$`).
WithArgs(email, name, passwordHash).
WillReturnError(fmt.Errorf("insert error"))
mock.ExpectRollback()
//user, err := repo.CreateUser(ctx, email, name, passwordHash) assert.Nil(t, user)
//assert.Error(t, err) assert.ErrorIs(t, err, dbErr)
//assert.Contains(t, err.Error(), "failed to insert user") assert.ErrorContains(t, err, "failed to insert user")
//assert.Nil(t, user)
//assert.NoError(t, mock.ExpectationsWereMet())
})
t.Run("commit_error", func(t *testing.T) {
mock.ExpectBegin()
mock.ExpectQuery(`^INSERT INTO users \(email, name, password_hash\) VALUES \(\$1, \$2, \$3\) RETURNING id, created_at$`).
WithArgs(email, name, passwordHash).
WillReturnRows(sqlmock.NewRows([]string{"id", "created_at"}).AddRow(expectedUser.ID, expectedUser.CreatedAt))
mock.ExpectCommit().WillReturnError(fmt.Errorf("commit error"))
//user, err := repo.CreateUser(ctx, email, name, passwordHash)
//assert.Error(t, err)
//assert.Contains(t, err.Error(), "commit transaction")
//assert.Nil(t, user)
//assert.NoError(t, mock.ExpectationsWereMet())
}) })
} }
//func TestUserRepo_CreateUser2(t *testing.T) { func TestUserRepo_DeleteUser(t *testing.T) {
// email := "test@example.com" t.Run("success", func(t *testing.T) {
// name := "Test User" repo, mock := newUserRepo(t)
// passwordHash := "hashedpassword123"
// now := time.Now() mock.ExpectExec(regexp.QuoteMeta(deleteUserQuery)).
// expectedID := "42" WithArgs("user-1").
// WillReturnResult(sqlmock.NewResult(0, 1))
// insertQuery := regexp.QuoteMeta(
// "INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id, created_at", assert.NoError(t, repo.DeleteUser(context.Background(), "user-1"))
// ) })
//
// testCases := []struct { t.Run("unknown id is not reported", func(t *testing.T) {
// name string repo, mock := newUserRepo(t)
// setupMock func(mock sqlmock.Sqlmock)
// expectedError string mock.ExpectExec(regexp.QuoteMeta(deleteUserQuery)).
// }{ WithArgs("does-not-exist").
// { WillReturnResult(sqlmock.NewResult(0, 0))
// name: "Success: User created perfectly",
// setupMock: func(mock sqlmock.Sqlmock) { assert.NoError(t, repo.DeleteUser(context.Background(), "does-not-exist"))
// mock.ExpectBegin() })
//
// rows := sqlmock.NewRows([]string{"id", "created_at"}). t.Run("db error is wrapped", func(t *testing.T) {
// AddRow(expectedID, now) repo, mock := newUserRepo(t)
//
// mock.ExpectQuery(insertQuery). dbErr := errors.New("connection reset")
// WithArgs(email, name, passwordHash). mock.ExpectExec(regexp.QuoteMeta(deleteUserQuery)).
// WillReturnRows(rows) WithArgs("user-1").
// WillReturnError(dbErr)
// mock.ExpectCommit()
// }, err := repo.DeleteUser(context.Background(), "user-1")
// expectedError: "",
// }, assert.ErrorIs(t, err, dbErr)
// { assert.ErrorContains(t, err, "failed to delete user")
// name: "Failure: Database connection fails on BeginTx", })
// setupMock: func(mock sqlmock.Sqlmock) { }
// mock.ExpectBegin().WillReturnError(errors.New("db connection failed"))
// }, func TestUserRepo_ChangePassword(t *testing.T) {
// expectedError: "begin transaction: db connection failed", t.Run("success", func(t *testing.T) {
// }, repo, mock := newUserRepo(t)
// {
// name: "Failure: Query fails (e.g., duplicate email)", mock.ExpectExec(regexp.QuoteMeta(updatePassQuery)).
// setupMock: func(mock sqlmock.Sqlmock) { WithArgs("$2a$10$newhash", "user-1").
// mock.ExpectBegin() WillReturnResult(sqlmock.NewResult(0, 1))
//
// mock.ExpectQuery(insertQuery). assert.NoError(t, repo.ChangePassword(context.Background(), "user-1", "$2a$10$newhash"))
// WithArgs(email, name, passwordHash). })
// WillReturnError(errors.New("unique constraint violation"))
// t.Run("db error is wrapped", func(t *testing.T) {
// mock.ExpectRollback() repo, mock := newUserRepo(t)
// },
// expectedError: "failed to insert user: unique constraint violation", dbErr := errors.New("deadlock detected")
// }, mock.ExpectExec(regexp.QuoteMeta(updatePassQuery)).
// { WithArgs("$2a$10$newhash", "user-1").
// name: "Failure: Commit fails (e.g., network timeout)", WillReturnError(dbErr)
// setupMock: func(mock sqlmock.Sqlmock) {
// mock.ExpectBegin() err := repo.ChangePassword(context.Background(), "user-1", "$2a$10$newhash")
//
// rows := sqlmock.NewRows([]string{"id", "created_at"}). assert.ErrorIs(t, err, dbErr)
// AddRow(expectedID, now) assert.ErrorContains(t, err, "failed to update user")
// })
// mock.ExpectQuery(insertQuery). }
// WithArgs(email, name, passwordHash).
// WillReturnRows(rows) func TestUserRepo_UsesTransactionFromContext(t *testing.T) {
// repo, mock := newUserRepo(t)
// mock.ExpectCommit().WillReturnError(errors.New("commit timeout"))
// }, mock.ExpectBegin()
// expectedError: "commit transaction: commit timeout", mock.ExpectExec(regexp.QuoteMeta(deleteUserQuery)).
// }, WithArgs("user-1").
// } WillReturnResult(sqlmock.NewResult(0, 1))
// mock.ExpectCommit()
// for _, tc := range testCases {
// t.Run(tc.name, func(t *testing.T) { tx, err := repo.db.BeginTx(context.Background(), nil)
// db, mock, err := sqlmock.New() require.NoError(t, err)
// require.NoError(t, err)
// defer db.Close() ctx := context.WithValue(context.Background(), txKey{}, tx)
// require.NoError(t, repo.DeleteUser(ctx, "user-1"))
// tc.setupMock(mock) require.NoError(t, tx.Commit())
// }
// //repo := NewUserRepo(db)
// // func TestUserRepo_GetUserByEmail(t *testing.T) {
// //user, err := repo.CreateUser(context.Background(), email, name, passwordHash) t.Run("success", func(t *testing.T) {
// repo, mock := newUserRepo(t)
// if tc.expectedError != "" {
// require.Error(t, err) mock.ExpectQuery(regexp.QuoteMeta(selectUserQuery)).
// assert.Contains(t, err.Error(), tc.expectedError) WithArgs("robin@dittmar.dev").
// //assert.Nil(t, user) WillReturnRows(
// } else { sqlmock.NewRows([]string{"id", "email", "name"}).
// require.NoError(t, err) AddRow("user-1", "robin@dittmar.dev", "Robin"),
// //require.NotNil(t, user) )
// //assert.Equal(t, expectedID, user.ID)
// //assert.Equal(t, email, user.Email) user, err := repo.GetUserByEmail(context.Background(), "robin@dittmar.dev")
// //assert.Equal(t, name, user.Name)
// //assert.Equal(t, now, user.CreatedAt) require.NoError(t, err)
// } require.NotNil(t, user)
// assert.Equal(t, "user-1", user.ID)
// assert.NoError(t, mock.ExpectationsWereMet()) assert.Equal(t, "robin@dittmar.dev", user.Email)
// }) assert.Equal(t, "Robin", user.Name)
// } assert.Zero(t, user.CreatedAt) // not selected by this query
//} })
t.Run("not found stays matchable via errors.Is", func(t *testing.T) {
repo, mock := newUserRepo(t)
mock.ExpectQuery(regexp.QuoteMeta(selectUserQuery)).
WithArgs("nobody@dittmar.dev").
WillReturnError(sql.ErrNoRows)
user, err := repo.GetUserByEmail(context.Background(), "nobody@dittmar.dev")
assert.Nil(t, user)
assert.ErrorIs(t, err, sql.ErrNoRows)
assert.ErrorContains(t, err, "failed to get user")
})
t.Run("scan error on type mismatch", func(t *testing.T) {
repo, mock := newUserRepo(t)
mock.ExpectQuery(regexp.QuoteMeta(selectUserQuery)).
WithArgs("robin@dittmar.dev").
WillReturnRows(
sqlmock.NewRows([]string{"id", "email", "name"}).
AddRow(nil, "robin@dittmar.dev", "Robin"),
)
user, err := repo.GetUserByEmail(context.Background(), "robin@dittmar.dev")
assert.Nil(t, user)
assert.Error(t, err)
})
}