feat: 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-08 01:15:07 +00:00
commit 961bdd81d4
146 files changed
+10444

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
package api
import (
"os"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/gin-gonic/gin"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/stretchr/testify/require"
)
func newTestServer(t *testing.T, store db.Store, td worker.TaskDistributor, userService service.UserService, authService service.AuthService) *Server {
config := config.Config{
AuthTokenExp: time.Minute,
AllowedOrigins: []string{
"*",
},
}
tokenMaker, err := auth.NewPasetoMaker(config)
require.NoError(t, err)
server, err := NewServer(config, store, td, tokenMaker, userService, authService)
require.NoError(t, err)
return server
}
func TestMain(m *testing.M) {
gin.SetMode(gin.TestMode)
os.Exit(m.Run())
}
+80
View File
@@ -0,0 +1,80 @@
package api
import (
"errors"
"fmt"
"net/http"
"strings"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/handlers/http/response"
"github.com/gin-gonic/gin"
)
const (
authHeaderKey = "authorization"
authTypeBearer = "bearer"
authorizationPayloadKey = "authorization_payload"
)
func authMiddleware(tokenMaker auth.Maker, accessibleRoles []string) gin.HandlerFunc {
return func(ctx *gin.Context) {
authorizationHeader := ctx.GetHeader(authHeaderKey)
if len(authorizationHeader) == 0 {
err := errors.New("authorization header is not provided")
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
return
}
// Split
fields := strings.Fields(authorizationHeader)
if len(fields) < 2 {
err := errors.New("invalid authorization header format")
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
}
authType := strings.ToLower(fields[0])
if authType != authTypeBearer {
err := fmt.Errorf("unsupported authorization type %s", authType)
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
return
}
accessToken := fields[1]
payload, err := tokenMaker.VerifyToken(accessToken)
if err != nil {
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusUnauthorized, resp)
return
}
if !role.HasPermission(payload.Role, accessibleRoles) {
err := fmt.Errorf("permission denied")
resp := response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.AbortWithStatusJSON(http.StatusForbidden, resp)
return
}
ctx.Set(authorizationPayloadKey, payload)
ctx.Next()
}
}
+114
View File
@@ -0,0 +1,114 @@
package api
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
mocksvc "github.com/FernandoJVideira/simple_bank/internal/service/mock"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func addAuth(t *testing.T, request *http.Request, tokenMaker auth.Maker, authType string, username string, role string, duration time.Duration) {
token, payload, err := tokenMaker.CreateToken(username, role, duration)
require.NoError(t, err)
require.NotEmpty(t, payload)
authorizationHeader := fmt.Sprintf("%s %s", authType, token)
request.Header.Set(authHeaderKey, authorizationHeader)
}
func TestAuthMiddleware(t *testing.T) {
username := testutil.RandomOwner()
testRole := role.MemberRole
testCases := []struct {
name string
setupAuth func(t *testing.T, request *http.Request, tokenMaker auth.Maker)
checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, authTypeBearer, username, testRole, time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusOK, recorder.Code)
},
},
{
name: "NoAuthorization",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
// Do not add any authorization header
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "UnsupportedAuthType",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, "UnsupportedAuthType", username, testRole, time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "InvalidAuthFormat",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, "", username, testRole, time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
{
name: "ExpiredAtToken",
setupAuth: func(t *testing.T, request *http.Request, tokenMaker auth.Maker) {
addAuth(t, request, tokenMaker, authTypeBearer, username, testRole, -time.Minute)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusUnauthorized, recorder.Code)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
authCtrl := gomock.NewController(t)
defer authCtrl.Finish()
authService := mocksvc.NewMockAuthService(authCtrl)
s := newTestServer(t, nil, nil, nil, authService)
authPath := "/auth"
s.router.GET(
authPath,
authMiddleware(s.tokenMaker, []string{role.AdminRole, role.MemberRole}),
func(ctx *gin.Context) {
ctx.JSON(200, gin.H{})
},
)
recorder := httptest.NewRecorder()
request, err := http.NewRequest(http.MethodGet, authPath, nil)
require.NoError(t, err)
tc.setupAuth(t, request, s.tokenMaker)
s.router.ServeHTTP(recorder, request)
tc.checkResponse(t, recorder)
})
}
}
+8
View File
@@ -0,0 +1,8 @@
package response
type JSONResponse struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
+130
View File
@@ -0,0 +1,130 @@
// Package api provides the HTTP server and routing for the banking service.
package api
import (
"context"
"net/http"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/rs/zerolog/log"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
// Server serves HTTP requests for our banking service.
type Server struct {
config config.Config
store db.Store
tokenMaker auth.Maker
router *gin.Engine
taskDisributor worker.TaskDistributor
httpServer *http.Server
userService service.UserService
authService service.AuthService
}
func NewServer(
config config.Config,
store db.Store,
taskDistributor worker.TaskDistributor,
tokenMaker auth.Maker,
userService service.UserService,
authService service.AuthService,
) (*Server, error) {
s := &Server{
config: config,
store: store,
tokenMaker: tokenMaker,
taskDisributor: taskDistributor,
userService: userService,
authService: authService,
}
// if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// err := v.RegisterValidation("currency", validCurrency)
// if err != nil {
// return nil, err
// }
// }
s.setupRouter(config)
return s, nil
}
func (s *Server) setupRouter(config config.Config) {
router := gin.Default()
// Configure CORS middleware
router.Use(cors.New(cors.Config{
AllowOrigins: config.AllowedOrigins,
AllowMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowHeaders: []string{
"Accept",
"Authorization",
"Content-Type",
"X-CSRF-Token",
},
AllowCredentials: true,
}))
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
// Add routes
v1 := router.Group("/api/v1")
{
auth := v1.Group("/auth")
{
auth.POST("/login", s.loginUser)
auth.POST("/register", s.createUser)
auth.GET("/verify", s.verifyEmail)
auth.POST("/refresh", s.renewAccessToken)
}
user := v1.Group("/users")
{
me := user.Group("/me").Use(authMiddleware(s.tokenMaker, []string{role.AdminRole, role.MemberRole}))
{
me.GET("/", s.getCurrentUser)
me.PUT("/", s.updateUser)
me.PUT("/password", s.changePassword)
}
user.GET("/:username", s.getUser)
}
}
s.router = router
}
// Start runs the HTTP server on a specific address.
func (s *Server) Start(address string) error {
s.httpServer = &http.Server{
Addr: address,
Handler: s.router,
}
return s.httpServer.ListenAndServe()
}
func (s *Server) Shutdown(ctx context.Context) error {
log.Info().Msg("shutting down Gin HTTP server")
if s.httpServer != nil {
return s.httpServer.Shutdown(ctx)
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package api
import (
"errors"
"net/http"
"strings"
"time"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/handlers/http/response"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/gin-gonic/gin"
)
type renewAccessTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
type renewAccessTokenResponse struct {
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
}
func (s *Server) renewAccessToken(ctx *gin.Context) {
var req renewAccessTokenRequest
var resp response.JSONResponse
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
// Call auth service - all business logic moved there
result, err := s.authService.RenewAccessToken(ctx, service.TokenRenewalRequest{
RefreshToken: req.RefreshToken,
})
if err != nil {
statusCode := http.StatusInternalServerError
errMsg := err.Error()
// Map service errors to HTTP codes
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(errMsg, "session not found") {
statusCode = http.StatusNotFound
} else if strings.Contains(errMsg, "invalid") ||
strings.Contains(errMsg, "blocked") ||
strings.Contains(errMsg, "expired") ||
strings.Contains(errMsg, "mismatched") {
statusCode = http.StatusUnauthorized
}
resp = response.JSONResponse{
Success: false,
Error: errMsg,
}
ctx.JSON(statusCode, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Access token renewed successfully",
Data: renewAccessTokenResponse{
AccessToken: result.AccessToken,
AccessTokenExpiresAt: result.AccessTokenExpiresAt,
},
}
ctx.JSON(http.StatusOK, resp)
}
+429
View File
@@ -0,0 +1,429 @@
package api
import (
"errors"
"net/http"
"strings"
"time"
_ "github.com/FernandoJVideira/simple_bank/docs"
"github.com/FernandoJVideira/simple_bank/internal/auth"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/handlers/http/response"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type createUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=6"`
FullName string `json:"full_name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
type userResponse struct {
Username string `json:"username"`
FullName string `json:"full_name"`
Email string `json:"email"`
PasswordChangedAt time.Time `json:"password_changed_at"`
CreatedAt time.Time `json:"created_at"`
}
func newUserResponse(user db.User) userResponse {
return userResponse{
Username: user.Username,
FullName: user.FullName,
Email: user.Email,
PasswordChangedAt: user.PasswordChangedAt,
CreatedAt: user.CreatedAt,
}
}
// CreateUser godoc
// @Summary Create a new user
// @Description Register a new user account
// @Tags auth
// @Accept json
// @Produce json
// @Param request body createUserRequest true "User registration details"
// @Success 201 {object} response.JSONResponse{data=userResponse}
// @Failure 400 {object} response.JSONResponse
// @Failure 500 {object} response.JSONResponse
// @Router /v1/auth/register [post]
func (s *Server) createUser(ctx *gin.Context) {
var req createUserRequest
var resp response.JSONResponse
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
result, err := s.userService.CreateUser(ctx, service.RegisterUserRequest{
Username: req.Username,
Password: req.Password,
FullName: req.FullName,
Email: req.Email,
})
if err != nil {
errMessage := err.Error()
if db.ErrorCode(err) == db.UniqueViolation {
resp = response.JSONResponse{
Success: false,
Error: errMessage,
}
ctx.JSON(http.StatusForbidden, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User created successfully",
Data: newUserResponse(result.User),
}
ctx.JSON(http.StatusCreated, resp)
}
type getUserRequest struct {
Username string `uri:"username" binding:"required,alphanum"`
}
func (s *Server) getUser(ctx *gin.Context) {
var req getUserRequest
var resp response.JSONResponse
if err := ctx.ShouldBindUri(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
user, err := s.userService.GetUser(ctx, req.Username)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User retrieved successfully",
Data: newUserResponse(*user),
}
ctx.JSON(http.StatusOK, resp)
}
func (s *Server) getCurrentUser(ctx *gin.Context) {
var resp response.JSONResponse
authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload)
user, err := s.userService.GetUser(ctx, authPayload.Username)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Current user retrieved successfully",
Data: newUserResponse(*user),
}
ctx.JSON(http.StatusOK, resp)
}
type UpdateUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
FullName string `json:"full_name" binding:"omitempty,min=3"`
Email string `json:"email" binding:"omitempty,email"`
}
func (s *Server) updateUser(ctx *gin.Context) {
var req UpdateUserRequest
var resp response.JSONResponse
authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload)
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
if authPayload.Username != req.Username {
resp = response.JSONResponse{
Success: false,
Error: "Cannot update other user's information",
}
ctx.JSON(http.StatusForbidden, resp)
return
}
params := service.UserUpdateRequest{
Username: strings.TrimSpace(req.Username),
}
if strings.TrimSpace(req.FullName) != "" {
params.FullName = &req.FullName
}
if strings.TrimSpace(req.Email) != "" {
params.Email = &req.Email
}
user, err := s.userService.UpdateUser(ctx, params)
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User updated successfully",
Data: newUserResponse(*user),
}
ctx.JSON(http.StatusOK, resp)
}
type changePasswordRequest struct {
CurrentPassword string `json:"current_password" binding:"required,min=6"`
NewPassword string `json:"new_password" binding:"required,min=6"`
ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=NewPassword"`
}
func (s *Server) changePassword(ctx *gin.Context) {
var req changePasswordRequest
var resp response.JSONResponse
authPayload := ctx.MustGet(authorizationPayloadKey).(*auth.Payload)
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
updUser, err := s.userService.UpdatePassword(ctx, service.PasswordUpdateRequest{
Username: authPayload.Username,
OldPassword: req.CurrentPassword,
NewPassword: req.NewPassword,
})
if err != nil {
if errors.Is(err, db.ErrRecordNotFound) {
resp = response.JSONResponse{
Success: false,
Error: "User not found",
}
ctx.JSON(http.StatusNotFound, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Password changed successfully",
Data: newUserResponse(*updUser),
}
ctx.JSON(http.StatusOK, resp)
}
type loginUserRequest struct {
Username string `json:"username" binding:"required,alphanum"`
Password string `json:"password" binding:"required,min=6"`
}
type loginUserResponse struct {
SessionID uuid.UUID `json:"session_id"`
AccessToken string `json:"access_token"`
AccessTokenExpiresAt time.Time `json:"access_token_expires_at"`
RefreshToken string `json:"refresh_token"`
RefreshTokenExpiresAt time.Time `json:"refresh_token_expires_at"`
User userResponse `json:"user"`
}
func (s *Server) loginUser(ctx *gin.Context) {
var req loginUserRequest
var resp response.JSONResponse
if err := ctx.ShouldBindJSON(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
loginRes, err := s.authService.Login(ctx, service.LoginCredentials{
Username: req.Username,
Password: req.Password,
UserAgent: ctx.Request.UserAgent(),
ClientIP: ctx.ClientIP(),
})
if err != nil {
statusCode := http.StatusInternalServerError
errMsg := err.Error()
// Map service errors to HTTP codes
if errors.Is(err, db.ErrRecordNotFound) || strings.Contains(errMsg, "user not found") {
statusCode = http.StatusNotFound
} else if strings.Contains(errMsg, "incorrect username or password") {
statusCode = http.StatusUnauthorized
}
resp = response.JSONResponse{
Success: false,
Error: errMsg,
}
ctx.JSON(statusCode, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "User logged in successfully",
Data: loginUserResponse{
SessionID: loginRes.SessionID,
AccessToken: loginRes.AccessToken,
AccessTokenExpiresAt: loginRes.AccessTokenExpiresAt,
RefreshToken: loginRes.RefreshToken,
RefreshTokenExpiresAt: loginRes.RefreshTokenExpiresAt,
User: newUserResponse(loginRes.User),
},
}
ctx.JSON(http.StatusOK, resp)
}
type verifyEmailRequest struct {
ID int64 `form:"id" binding:"required,min=1"`
Code string `form:"code" binding:"required,min=32,max=128"`
}
type verifyEmailResponse struct {
IsVerified bool `json:"is_verified"`
}
func (s *Server) verifyEmail(ctx *gin.Context) {
var req verifyEmailRequest
var resp response.JSONResponse
if err := ctx.ShouldBindQuery(&req); err != nil {
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
result, err := s.userService.VerifyEmail(ctx, service.EmailVerificationRequest{
EmailID: req.ID,
SecretCode: req.Code,
})
if err != nil {
if errors.Is(err, db.ErrForeignKeyViolation) {
resp = response.JSONResponse{
Success: false,
Error: "Email verification record not found",
}
ctx.JSON(http.StatusForbidden, resp)
return
}
if errors.Is(err, db.ErrCheckViolation) {
resp = response.JSONResponse{
Success: false,
Error: "Invalid verification code",
}
ctx.JSON(http.StatusBadRequest, resp)
return
}
resp = response.JSONResponse{
Success: false,
Error: err.Error(),
}
ctx.JSON(http.StatusInternalServerError, resp)
return
}
resp = response.JSONResponse{
Success: true,
Message: "Email verified successfully",
Data: verifyEmailResponse{
IsVerified: result.User.IsEmailVerified,
},
}
ctx.JSON(http.StatusOK, resp)
}
+131
View File
@@ -0,0 +1,131 @@
package api
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
"github.com/FernandoJVideira/simple_bank/internal/domain/role"
"github.com/FernandoJVideira/simple_bank/internal/handlers/http/response"
"github.com/FernandoJVideira/simple_bank/internal/security"
"github.com/FernandoJVideira/simple_bank/internal/service"
mocksvc "github.com/FernandoJVideira/simple_bank/internal/service/mock"
"github.com/FernandoJVideira/simple_bank/pkg/testutil"
)
func randomUser(t *testing.T) (user db.User, password string) {
password = testutil.RandomString(6)
hashedPassword, err := security.HashPassword(password)
if err != nil {
t.Fatal(err)
}
user = db.User{
Username: testutil.RandomOwner(),
HashedPassword: hashedPassword,
Role: role.MemberRole,
FullName: testutil.RandomOwner(),
Email: testutil.RandomEmail(),
}
return
}
func TestCreateUserAPI(t *testing.T) {
user, password := randomUser(t)
testCases := []struct {
name string
body gin.H
buildStubs func(userService *mocksvc.MockUserService)
checkResponse func(t *testing.T, recorder *httptest.ResponseRecorder)
}{
{
name: "OK",
body: gin.H{
"username": user.Username,
"password": password,
"full_name": user.FullName,
"email": user.Email,
},
buildStubs: func(userService *mocksvc.MockUserService) {
servcArg := service.RegisterUserRequest{
Username: user.Username,
Password: password,
FullName: user.FullName,
Email: user.Email,
}
userService.EXPECT().
CreateUser(gomock.Any(), servcArg).
Times(1).
Return(&service.RegisterUserResponse{
User: user,
}, nil)
},
checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) {
require.Equal(t, http.StatusCreated, recorder.Code)
requireBodyMatchUser(t, recorder.Body, user)
},
},
}
for i := range testCases {
tc := testCases[i]
t.Run(tc.name, func(t *testing.T) {
storeCtrl := gomock.NewController(t)
defer storeCtrl.Finish()
userCtrl := gomock.NewController(t)
defer userCtrl.Finish()
userService := mocksvc.NewMockUserService(userCtrl)
tc.buildStubs(userService)
// Start test server and send request
s := newTestServer(t, nil, nil, userService, nil)
recorder := httptest.NewRecorder()
data, err := json.Marshal(tc.body)
require.NoError(t, err)
url := "/api/v1/auth/register"
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
require.NoError(t, err)
s.router.ServeHTTP(recorder, req)
tc.checkResponse(t, recorder)
})
}
}
func requireBodyMatchUser(t *testing.T, body *bytes.Buffer, user db.User) {
data, err := io.ReadAll(body)
require.NoError(t, err)
var gotUser db.User
var resp response.JSONResponse
err = json.Unmarshal(data, &resp)
require.NoError(t, err)
data, err = json.Marshal(resp.Data)
require.NoError(t, err)
err = json.Unmarshal(data, &gotUser)
require.NoError(t, err)
require.Equal(t, user.Username, gotUser.Username)
require.Equal(t, user.FullName, gotUser.FullName)
require.Equal(t, user.Email, gotUser.Email)
}
+13
View File
@@ -0,0 +1,13 @@
package api
// (import (
// "github.com/FernandoJVideira/simple_bank/util"
// "github.com/go-playground/validator/v10"
// )
// var validCurrency validator.Func = func(fieldlevel validator.FieldLevel) bool {
// if currency, ok := fieldlevel.Field().Interface().(string); ok {
// return util.IsSupportedCurrency(currency)
// }
// return false
// })