Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 2 additions & 24 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,11 @@ import (
"cruder/internal/middleware"
"cruder/internal/repository"
"cruder/internal/service"
"fmt"
"log/slog"
"os"
"strings"

"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"github.com/spf13/viper"
)

func main() {
Expand All @@ -48,7 +45,7 @@ func main() {
os.Exit(1)
}

cfg, err := loadConfig()
cfg, err := config.LoadConfig()
if err != nil {
logger.Error("failed to load config", slog.Any("err", err))
os.Exit(1)
Expand Down Expand Up @@ -79,27 +76,8 @@ func main() {
r.Use(apiKeyMiddleware.Handler())
_ = r.SetTrustedProxies(nil)

handler.New(r, controllers.Users)
handler.New(r, controllers.Users, controllers.Health)
if err := r.Run(); err != nil {
logger.Error("failed to run server", slog.Any("err", err))
}
}

func loadConfig() (*config.Config, error) {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.AddConfigPath("config")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")

if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("error reading config file: %w", err)
}

var cfg config.Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unable to decode config into struct: %w", err)
}
return &cfg, nil
}
2 changes: 1 addition & 1 deletion docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/users": {
"/users/": {
"get": {
"security": [
{
Expand Down
2 changes: 1 addition & 1 deletion docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"basePath": "/api/v1",
"paths": {
"/users": {
"/users/": {
"get": {
"security": [
{
Expand Down
2 changes: 1 addition & 1 deletion docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ info:
title: Users API
version: "1.0"
paths:
/users:
/users/:
get:
produces:
- application/json
Expand Down
23 changes: 23 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package config

import (
"errors"
"fmt"
"os"
"strings"

"github.com/spf13/viper"
)

type Config struct {
Expand All @@ -29,3 +33,22 @@ func (c *Config) GetDSN() (string, error) {

return dsn, nil
}

func LoadConfig() (*Config, error) {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
viper.AddConfigPath("config")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(".")

if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("error reading config file: %w", err)
}

var cfg Config
if err := viper.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("unable to decode config into struct: %w", err)
}
return &cfg, nil
}
6 changes: 4 additions & 2 deletions internal/controller/controllers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package controller
import "cruder/internal/service"

type Controller struct {
Users *UserController
Users *UserController
Health *HealthController
}

func NewController(services *service.Service) *Controller {
return &Controller{
Users: NewUserController(services.Users),
Users: NewUserController(services.Users),
Health: NewHealthController(),
}
}
13 changes: 13 additions & 0 deletions internal/controller/health.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package controller

import "github.com/gin-gonic/gin"

type HealthController struct{}

func NewHealthController() *HealthController {
return &HealthController{}
}

func (c *HealthController) HealthCheck(ctx *gin.Context) {
ctx.JSON(200, gin.H{"status": "ok"})
}
29 changes: 29 additions & 0 deletions internal/controller/health_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package controller

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)

func setupHealthRouter(c *HealthController) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.Default()
r.GET("/healthz", c.HealthCheck)
return r
}

func TestHealthCheck_Success(t *testing.T) {
controller := NewHealthController()
router := setupHealthRouter(controller)

req, err := http.NewRequest("GET", "/healthz", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)

assert.NoError(t, err)
assert.Equal(t, http.StatusOK, w.Code)
}
19 changes: 9 additions & 10 deletions internal/controller/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"strconv"

"cruder/internal/model"
"cruder/internal/repository"
"cruder/internal/service"

"github.com/gin-gonic/gin"
Expand All @@ -26,7 +25,7 @@ func NewUserController(service service.UserService) *UserController {
// @Success 200 {array} model.User
// @Failure 500 {object} model.ErrorResponse "internal server error"
// @Security ApiKeyAuth
// @Router /users [get]
// @Router /users/ [get]
func (c *UserController) GetAllUsers(ctx *gin.Context) {
users, err := c.service.GetAll()
if handleError(ctx, err) {
Expand Down Expand Up @@ -96,7 +95,7 @@ func (c *UserController) GetUserByID(ctx *gin.Context) {
// @Failure 409 {object} model.ErrorResponse "user with username/email already exists"
// @Failure 500 {object} model.ErrorResponse "internal server error"
// @Security ApiKeyAuth
// @Router /users [post]
// @Router /users/ [post]
func (c *UserController) CreateUser(ctx *gin.Context) {
var user model.User

Expand Down Expand Up @@ -191,11 +190,11 @@ func handleError(ctx *gin.Context, err error) bool {
}

var errToStatus = map[error]int{
repository.ErrUserNotFound: http.StatusNotFound,
repository.ErrUserAlreadyExists: http.StatusConflict,
repository.ErrUsernameAlreadyExists: http.StatusConflict,
repository.ErrEmailAlreadyExists: http.StatusConflict,
service.ErrInvalidEmail: http.StatusBadRequest,
service.ErrInvalidUsername: http.StatusBadRequest,
service.ErrInvalidFullName: http.StatusBadRequest,
service.ErrUserNotFound: http.StatusNotFound,
service.ErrUserAlreadyExists: http.StatusConflict,
service.ErrUsernameAlreadyExists: http.StatusConflict,
service.ErrEmailAlreadyExists: http.StatusConflict,
service.ErrInvalidEmail: http.StatusBadRequest,
service.ErrInvalidUsername: http.StatusBadRequest,
service.ErrInvalidFullName: http.StatusBadRequest,
}
35 changes: 17 additions & 18 deletions internal/controller/users_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
mock_service "cruder/internal/mocks/service"
"cruder/internal/model"
"cruder/internal/repository"
"cruder/internal/service"
"encoding/json"
"net/http"
Expand All @@ -16,7 +15,7 @@ import (
"go.uber.org/mock/gomock"
)

func setupRouter(c *UserController) *gin.Engine {
func setupUserRouter(c *UserController) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.Default()
r.GET("/users", c.GetAllUsers)
Expand All @@ -37,7 +36,7 @@ func TestGetAllUsers_Success(t *testing.T) {
mockSvc.EXPECT().GetAll().Return(users, nil)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

// When: GET /users is called
req, _ := http.NewRequest("GET", "/users", nil)
Expand All @@ -61,7 +60,7 @@ func TestGetUserByUsername_Success(t *testing.T) {
mockSvc.EXPECT().GetByUsername("john_doe").Return(expected, nil)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

// When: GET /users/username/john_doe is called
req, _ := http.NewRequest("GET", "/users/username/john_doe", nil)
Expand All @@ -81,10 +80,10 @@ func TestGetUserByUsername_NotFound(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSvc := mock_service.NewMockUserService(ctrl)
mockSvc.EXPECT().GetByUsername("missing").Return(nil, repository.ErrUserNotFound)
mockSvc.EXPECT().GetByUsername("missing").Return(nil, service.ErrUserNotFound)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

// When: GET /users/username/missing is called
req, _ := http.NewRequest("GET", "/users/username/missing", nil)
Expand All @@ -107,7 +106,7 @@ func TestGetUserByID_Success(t *testing.T) {
mockSvc.EXPECT().GetByID(int64(1)).Return(expected, nil)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

// When: GET /users/id/1 is called
req, _ := http.NewRequest("GET", "/users/id/1", nil)
Expand All @@ -129,7 +128,7 @@ func TestGetUserByID_InvalidID(t *testing.T) {
mockSvc := mock_service.NewMockUserService(ctrl)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

// When: GET /users/id/abc is called
req, _ := http.NewRequest("GET", "/users/id/abc", nil)
Expand All @@ -153,7 +152,7 @@ func TestCreateUser_Success(t *testing.T) {
mockSvc.EXPECT().Create(input).Return(created, nil)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

body, _ := json.Marshal(input)

Expand All @@ -177,7 +176,7 @@ func TestCreateUser_InvalidBody(t *testing.T) {
mockSvc := mock_service.NewMockUserService(ctrl)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

// When: POST /users with invalid JSON
req, _ := http.NewRequest("POST", "/users", bytes.NewBuffer([]byte("{bad json")))
Expand All @@ -197,10 +196,10 @@ func TestDeleteUser_NotFound(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSvc := mock_service.NewMockUserService(ctrl)
mockSvc.EXPECT().Delete(int64(99)).Return(repository.ErrUserNotFound)
mockSvc.EXPECT().Delete(int64(99)).Return(service.ErrUserNotFound)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

// When: DELETE /users/99 is called
req, _ := http.NewRequest("DELETE", "/users/99", nil)
Expand All @@ -221,7 +220,7 @@ func TestUpdateUser_IDMismatch(t *testing.T) {
mockSvc := mock_service.NewMockUserService(ctrl)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

user := model.User{ID: 2, Username: "john", Email: "john@doe.ee", FullName: "John Doe"}
body, _ := json.Marshal(user)
Expand Down Expand Up @@ -249,7 +248,7 @@ func TestUpdateUser_Success(t *testing.T) {
mockSvc.EXPECT().Update(input).Return(updated, nil)

controller := NewUserController(mockSvc)
router := setupRouter(controller)
router := setupUserRouter(controller)

body, _ := json.Marshal(input)

Expand All @@ -272,7 +271,7 @@ func TestHandleError_UserNotFound(t *testing.T) {
c, _ := gin.CreateTestContext(w)

// When: calling handleError with ErrUserNotFound
result := handleError(c, repository.ErrUserNotFound)
result := handleError(c, service.ErrUserNotFound)

// Then: should return true and response with 404 + correct error message
assert.True(t, result)
Expand All @@ -289,7 +288,7 @@ func TestHandleError_UserAlreadyExists(t *testing.T) {
c, _ := gin.CreateTestContext(w)

// When: calling handleError with ErrUserAlreadyExists
result := handleError(c, repository.ErrUserAlreadyExists)
result := handleError(c, service.ErrUserAlreadyExists)

// Then: should return true and response with 409 + correct error message
assert.True(t, result)
Expand All @@ -306,7 +305,7 @@ func TestHandleError_UsernameAlreadyExists(t *testing.T) {
c, _ := gin.CreateTestContext(w)

// When: calling handleError with ErrUsernameAlreadyExists
result := handleError(c, repository.ErrUsernameAlreadyExists)
result := handleError(c, service.ErrUsernameAlreadyExists)

// Then: should return true and response with 409 + correct error message
assert.True(t, result)
Expand All @@ -323,7 +322,7 @@ func TestHandleError_EmailAlreadyExists(t *testing.T) {
c, _ := gin.CreateTestContext(w)

// When: calling handleError with ErrEmailAlreadyExists
result := handleError(c, repository.ErrEmailAlreadyExists)
result := handleError(c, service.ErrEmailAlreadyExists)

// Then: should return true and response with 409 + correct error message
assert.True(t, result)
Expand Down
6 changes: 2 additions & 4 deletions internal/handler/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import (
ginSwagger "github.com/swaggo/gin-swagger"
)

func New(router *gin.Engine, userController *controller.UserController) *gin.Engine {
router.GET("/healthz", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
func New(router *gin.Engine, userController *controller.UserController, healthController *controller.HealthController) *gin.Engine {
router.GET("/healthz", healthController.HealthCheck)
v1 := router.Group("/api/v1")
{
userGroup := v1.Group("/users")
Expand Down
Loading