Added swaggo and testify #8
+18
-1
@@ -19,6 +19,23 @@ import (
|
|||||||
"github.com/robindittmar/dttmr-api/internal/telemetry"
|
"github.com/robindittmar/dttmr-api/internal/telemetry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// @title dttmr-api
|
||||||
|
// @version 0.1.0
|
||||||
|
// @description API documentation for dttmr-api service.
|
||||||
|
// @termsOfService http://swagger.io/terms/
|
||||||
|
|
||||||
|
// @contact.name Robin Dittmar
|
||||||
|
// @contact.email robindittmar@gmail.com
|
||||||
|
|
||||||
|
// @license.name MIT
|
||||||
|
// @license.url https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// @host localhost:8080
|
||||||
|
// @BasePath /api/v1
|
||||||
|
|
||||||
|
// @securityDefinitions.apikey Bearer Token
|
||||||
|
// @in Header
|
||||||
|
// @name Authorization
|
||||||
func main() {
|
func main() {
|
||||||
serviceName := "dttmr-api"
|
serviceName := "dttmr-api"
|
||||||
serviceVersion := "0.1.0"
|
serviceVersion := "0.1.0"
|
||||||
@@ -80,7 +97,7 @@ func run(serviceName string, serviceVersion string) error {
|
|||||||
go func() {
|
go func() {
|
||||||
slog.Info("starting http server", "addr", srv.Addr)
|
slog.Info("starting http server", "addr", srv.Addr)
|
||||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
slog.Error("failed to start http server", err)
|
slog.Error("failed to start http server", slog.Any("error", err))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ module github.com/robindittmar/dttmr-api
|
|||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/DATA-DOG/go-sqlmock v1.5.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/jackc/pgx/v5 v5.10.0
|
github.com/jackc/pgx/v5 v5.10.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/stretchr/testify v1.11.1
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0
|
||||||
go.opentelemetry.io/otel v1.44.0
|
go.opentelemetry.io/otel v1.44.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
|
||||||
@@ -20,6 +22,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/felixge/httpsnoop v1.1.0 // indirect
|
github.com/felixge/httpsnoop v1.1.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.4 // indirect
|
github.com/go-logr/logr v1.4.4 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
@@ -29,6 +32,8 @@ require (
|
|||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
github.com/lib/pq v1.12.3 // indirect
|
github.com/lib/pq v1.12.3 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
go.opentelemetry.io/otel/metric v1.44.0 // indirect
|
||||||
@@ -41,4 +46,5 @@ require (
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720171339-e059f2f05d78 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720171339-e059f2f05d78 // indirect
|
||||||
google.golang.org/grpc v1.82.1 // indirect
|
google.golang.org/grpc v1.82.1 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ func NewAuthHandler(authService *domain.AuthService) *AuthHandler {
|
|||||||
return &AuthHandler{AuthService: authService}
|
return &AuthHandler{AuthService: authService}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login handles the login of a user
|
||||||
|
//
|
||||||
|
// @Summary Login route
|
||||||
|
// @Description User authorization and token issuing
|
||||||
|
// @Tags Authorization
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body request.LoginPayload true "Login payload"
|
||||||
|
// @Success 200 {object} domain.AuthToken
|
||||||
|
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||||
|
// @Error 500 {object} response.ErrorResponse "failed to login"
|
||||||
|
// @Router /api/v1/login [post]
|
||||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,15 @@ type apiResponse struct {
|
|||||||
Form map[string]string `json:"form"`
|
Form map[string]string `json:"form"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultHandler handles the default route
|
||||||
|
//
|
||||||
|
// @Summary Default route handler
|
||||||
|
// @Description Default route handler
|
||||||
|
// @Tags
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} apiResponse
|
||||||
|
// @Router /api/v1/ [get]
|
||||||
func DefaultHandler(w http.ResponseWriter, r *http.Request) {
|
func DefaultHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ type healthResponse struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthHandler handles the health check route
|
||||||
|
//
|
||||||
|
// @Summary Health check
|
||||||
|
// @Description Health check reports the status of the API
|
||||||
|
// @Tags Health
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} healthResponse
|
||||||
|
// @Router /health [get]
|
||||||
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/robindittmar/dttmr-api/internal/api/handler"
|
"github.com/robindittmar/dttmr-api/internal/api/handler"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,12 +16,6 @@ func TestHealthHandler_Success(t *testing.T) {
|
|||||||
|
|
||||||
handler.HealthHandler(rr, req)
|
handler.HealthHandler(rr, req)
|
||||||
|
|
||||||
if rr.Code != http.StatusOK {
|
assert.Equal(t, http.StatusOK, rr.Code)
|
||||||
t.Errorf("HealthHandler returned wrong status code: got %v want %v", rr.Code, http.StatusOK)
|
assert.Equal(t, `{"status":"ok"}`, rr.Body.String())
|
||||||
}
|
|
||||||
|
|
||||||
expected := `{"status":"ok"}`
|
|
||||||
if rr.Body.String() != expected {
|
|
||||||
t.Errorf("HealthHandler returned unexpected body: got %v want %v", rr.Body.String(), expected)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ func NewListHandler(listService *domain.ListService) *ListHandler {
|
|||||||
return &ListHandler{ListService: listService}
|
return &ListHandler{ListService: listService}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateList handles the creation of a list
|
||||||
|
//
|
||||||
|
// @Summary Create list route
|
||||||
|
// @Description Create a list and associate user(s) to it
|
||||||
|
// @Tags List
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body request.CreateListPayload true "Create list payload"
|
||||||
|
// @Success 201 {object} domain.List
|
||||||
|
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||||
|
// @Error 500 {object} response.ErrorResponse "failed to create list"
|
||||||
|
// @Router /api/v1/list [post]
|
||||||
func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) {
|
func (h *ListHandler) CreateList(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ func NewUserHandler(userService *domain.UserService) *UserHandler {
|
|||||||
return &UserHandler{UserService: userService}
|
return &UserHandler{UserService: userService}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUser handles the creation of a user
|
||||||
|
//
|
||||||
|
// @Summary Create user route
|
||||||
|
// @Description Create a user
|
||||||
|
// @Tags User
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param payload body request.CreateUserPayload true "Create user payload"
|
||||||
|
// @Success 201 {object} domain.User
|
||||||
|
// @Error 400 {object} response.ErrorResponse "failed to decode request body"
|
||||||
|
// @Error 500 {object} response.ErrorResponse "failed to create user"
|
||||||
|
// @Router /api/v1/user [post]
|
||||||
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
|
|||||||
@@ -30,14 +30,17 @@ func NewMux(cfg Config) http.Handler {
|
|||||||
|
|
||||||
protected := middleware.WithJWT(authService)
|
protected := middleware.WithJWT(authService)
|
||||||
|
|
||||||
|
apiMux := http.NewServeMux()
|
||||||
|
apiMux.HandleFunc("/", handler.DefaultHandler)
|
||||||
|
apiMux.HandleFunc("POST /login", authHandler.Login)
|
||||||
|
|
||||||
|
apiMux.Handle("POST /users", protected(userHandler.CreateUser))
|
||||||
|
|
||||||
|
apiMux.Handle("POST /lists", protected(listHandler.CreateList))
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/", handler.DefaultHandler)
|
|
||||||
mux.HandleFunc("GET /health", handler.HealthHandler)
|
mux.HandleFunc("GET /health", handler.HealthHandler)
|
||||||
mux.HandleFunc("POST /login", authHandler.Login)
|
mux.Handle("/api/v1/", http.StripPrefix("/api/v1", apiMux))
|
||||||
|
|
||||||
mux.Handle("POST /users", protected(userHandler.CreateUser))
|
|
||||||
|
|
||||||
mux.Handle("POST /lists", protected(listHandler.CreateList))
|
|
||||||
|
|
||||||
var httpHandler http.Handler = mux
|
var httpHandler http.Handler = mux
|
||||||
httpHandler = middleware.WithMaxBytes(1024 * 64)(httpHandler)
|
httpHandler = middleware.WithMaxBytes(1024 * 64)(httpHandler)
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package domain_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/robindittmar/dttmr-api/internal/domain"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockListRepo struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockListRepo) CreateList(ctx context.Context, name string, userIDs []string) (*domain.List, error) {
|
||||||
|
args := m.Called(ctx, name, userIDs)
|
||||||
|
var list *domain.List
|
||||||
|
if l := args.Get(0); l != nil {
|
||||||
|
list = l.(*domain.List)
|
||||||
|
}
|
||||||
|
return list, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListService_Create_Success(t *testing.T) {
|
||||||
|
expectedList := &domain.List{
|
||||||
|
ID: "1",
|
||||||
|
Name: "My List",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
ModifiedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := new(mockListRepo)
|
||||||
|
repo.On("CreateList", mock.Anything, "My List", []string{"user1", "user2"}).Return(expectedList, nil)
|
||||||
|
|
||||||
|
service := domain.NewListService(repo)
|
||||||
|
list, err := service.Create(context.Background(), "My List", []string{"user1", "user2"})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedList, list)
|
||||||
|
repo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListService_Create_EmptyName(t *testing.T) {
|
||||||
|
repo := new(mockListRepo)
|
||||||
|
service := domain.NewListService(repo)
|
||||||
|
|
||||||
|
list, err := service.Create(context.Background(), "", []string{"user1"})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "list name must not be empty")
|
||||||
|
assert.Nil(t, list)
|
||||||
|
repo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListService_Create_EmptyUsers(t *testing.T) {
|
||||||
|
repo := new(mockListRepo)
|
||||||
|
service := domain.NewListService(repo)
|
||||||
|
|
||||||
|
list, err := service.Create(context.Background(), "My List", []string{})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.EqualError(t, err, "users must have at least one associated user")
|
||||||
|
assert.Nil(t, list)
|
||||||
|
repo.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListService_Create_RepoError(t *testing.T) {
|
||||||
|
expectedErr := errors.New("database error")
|
||||||
|
repo := new(mockListRepo)
|
||||||
|
repo.On("CreateList", mock.Anything, "My List", []string{"user1"}).Return(nil, expectedErr)
|
||||||
|
|
||||||
|
service := domain.NewListService(repo)
|
||||||
|
|
||||||
|
list, err := service.Create(context.Background(), "My List", []string{"user1"})
|
||||||
|
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.ErrorIs(t, err, expectedErr)
|
||||||
|
assert.Nil(t, list)
|
||||||
|
repo.AssertExpectations(t)
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
|
"github.com/robindittmar/dttmr-api/internal/domain"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
user, err := repo.CreateUser(ctx, email, name, passwordHash)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expectedUser, user)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("begin_tx_error", func(t *testing.T) {
|
||||||
|
mock.ExpectBegin().WillReturnError(fmt.Errorf("tx error"))
|
||||||
|
|
||||||
|
user, err := repo.CreateUser(ctx, email, name, passwordHash)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "begin transaction")
|
||||||
|
assert.Nil(t, user)
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("insert_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).
|
||||||
|
WillReturnError(fmt.Errorf("insert error"))
|
||||||
|
mock.ExpectRollback()
|
||||||
|
|
||||||
|
user, err := repo.CreateUser(ctx, email, name, passwordHash)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "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) {
|
||||||
|
email := "test@example.com"
|
||||||
|
name := "Test User"
|
||||||
|
passwordHash := "hashedpassword123"
|
||||||
|
now := time.Now()
|
||||||
|
expectedID := "42"
|
||||||
|
|
||||||
|
insertQuery := regexp.QuoteMeta(
|
||||||
|
"INSERT INTO users (email, name, password_hash) VALUES ($1, $2, $3) RETURNING id, created_at",
|
||||||
|
)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
setupMock func(mock sqlmock.Sqlmock)
|
||||||
|
expectedError string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Success: User created perfectly",
|
||||||
|
setupMock: func(mock sqlmock.Sqlmock) {
|
||||||
|
mock.ExpectBegin()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at"}).
|
||||||
|
AddRow(expectedID, now)
|
||||||
|
|
||||||
|
mock.ExpectQuery(insertQuery).
|
||||||
|
WithArgs(email, name, passwordHash).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
mock.ExpectCommit()
|
||||||
|
},
|
||||||
|
expectedError: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failure: Database connection fails on BeginTx",
|
||||||
|
setupMock: func(mock sqlmock.Sqlmock) {
|
||||||
|
mock.ExpectBegin().WillReturnError(errors.New("db connection failed"))
|
||||||
|
},
|
||||||
|
expectedError: "begin transaction: db connection failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failure: Query fails (e.g., duplicate email)",
|
||||||
|
setupMock: func(mock sqlmock.Sqlmock) {
|
||||||
|
mock.ExpectBegin()
|
||||||
|
|
||||||
|
mock.ExpectQuery(insertQuery).
|
||||||
|
WithArgs(email, name, passwordHash).
|
||||||
|
WillReturnError(errors.New("unique constraint violation"))
|
||||||
|
|
||||||
|
mock.ExpectRollback()
|
||||||
|
},
|
||||||
|
expectedError: "failed to insert user: unique constraint violation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Failure: Commit fails (e.g., network timeout)",
|
||||||
|
setupMock: func(mock sqlmock.Sqlmock) {
|
||||||
|
mock.ExpectBegin()
|
||||||
|
|
||||||
|
rows := sqlmock.NewRows([]string{"id", "created_at"}).
|
||||||
|
AddRow(expectedID, now)
|
||||||
|
|
||||||
|
mock.ExpectQuery(insertQuery).
|
||||||
|
WithArgs(email, name, passwordHash).
|
||||||
|
WillReturnRows(rows)
|
||||||
|
|
||||||
|
mock.ExpectCommit().WillReturnError(errors.New("commit timeout"))
|
||||||
|
},
|
||||||
|
expectedError: "commit transaction: commit timeout",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
db, mock, err := sqlmock.New()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
tc.setupMock(mock)
|
||||||
|
|
||||||
|
repo := NewUserRepo(db)
|
||||||
|
|
||||||
|
user, err := repo.CreateUser(context.Background(), email, name, passwordHash)
|
||||||
|
|
||||||
|
if tc.expectedError != "" {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tc.expectedError)
|
||||||
|
assert.Nil(t, user)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, user)
|
||||||
|
assert.Equal(t, expectedID, user.ID)
|
||||||
|
assert.Equal(t, email, user.Email)
|
||||||
|
assert.Equal(t, name, user.Name)
|
||||||
|
assert.Equal(t, now, user.CreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NoError(t, mock.ExpectationsWereMet())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user