From 8f61dbb798463e6c0e961fd40203442c1ec4a9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 12:04:19 -0300 Subject: [PATCH 1/9] feat: thumb processor API resources --- cmd/http/main.go | 19 +- go.mod | 17 +- go.sum | 33 +++- internal/adapters/db/thumb.go | 72 ++++++-- .../adapters/rest/handler/thumb_handler.go | 166 ++++++++++++++++++ internal/adapters/sqs/queue_adapter_sqs.go | 43 +++++ internal/config/config.go | 45 +++++ .../thumb_queue_adapter_interface.go | 7 + ...thumb_repository_adapter_interface copy.go | 9 + internal/core/domain/entity/thumb_process.go | 40 +++++ internal/core/ports/thumb_ports.go | 14 ++ internal/core/thumb/thumb.go | 13 -- internal/core/thumb/thumb_service.go | 52 +++++- 13 files changed, 481 insertions(+), 49 deletions(-) create mode 100644 internal/adapters/rest/handler/thumb_handler.go create mode 100644 internal/adapters/sqs/queue_adapter_sqs.go create mode 100644 internal/config/config.go create mode 100644 internal/core/domain/contracts/thumb_queue_adapter_interface.go create mode 100644 internal/core/domain/contracts/thumb_repository_adapter_interface copy.go create mode 100644 internal/core/domain/entity/thumb_process.go create mode 100644 internal/core/ports/thumb_ports.go delete mode 100644 internal/core/thumb/thumb.go diff --git a/cmd/http/main.go b/cmd/http/main.go index 4cd75fb..ee530f7 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -1,14 +1,12 @@ package main import ( - "fmt" "log" - "os" - "github.com/joho/godotenv" _ "github.com/pangolin-do-golang/thumb-processor-api/docs" dbAdapter "github.com/pangolin-do-golang/thumb-processor-api/internal/adapters/db" "github.com/pangolin-do-golang/thumb-processor-api/internal/adapters/rest/server" + "github.com/pangolin-do-golang/thumb-processor-api/internal/config" "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -41,15 +39,12 @@ func main() { } func initDb() (*gorm.DB, error) { - _ = godotenv.Load() - dsn := fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=disable TimeZone=America/Sao_Paulo", - os.Getenv("DB_USERNAME"), - os.Getenv("DB_PASSWORD"), - os.Getenv("DB_HOST"), - os.Getenv("DB_PORT"), - os.Getenv("DB_NAME"), - ) - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + cfg, err := config.Load() + if err != nil { + log.Fatalln(err) + } + + db, err := gorm.Open(postgres.Open(cfg.DB.GetDNS()), &gorm.Config{}) if err != nil { log.Panic(err) } diff --git a/go.mod b/go.mod index 07c6156..00f984d 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,12 @@ module github.com/pangolin-do-golang/thumb-processor-api go 1.23.3 require ( + github.com/aws/aws-sdk-go-v2 v1.36.1 + github.com/aws/aws-sdk-go-v2/config v1.29.6 + github.com/aws/aws-sdk-go-v2/service/sqs v1.37.14 + github.com/caarlos0/env/v11 v11.3.1 github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 - github.com/joho/godotenv v1.5.1 github.com/stretchr/testify v1.10.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 @@ -16,6 +19,17 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.59 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/bytedance/sonic v1.12.8 // indirect github.com/bytedance/sonic/loader v0.2.3 // indirect github.com/cloudwego/base64x v0.1.5 // indirect @@ -47,6 +61,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect golang.org/x/arch v0.13.0 // indirect diff --git a/go.sum b/go.sum index 40e99c0..2bbcf51 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,40 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/aws/aws-sdk-go-v2 v1.36.1 h1:iTDl5U6oAhkNPba0e1t1hrwAo02ZMqbrGq4k5JBWM5E= +github.com/aws/aws-sdk-go-v2 v1.36.1/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= +github.com/aws/aws-sdk-go-v2/config v1.29.6 h1:fqgqEKK5HaZVWLQoLiC9Q+xDlSp+1LYidp6ybGE2OGg= +github.com/aws/aws-sdk-go-v2/config v1.29.6/go.mod h1:Ft+WLODzDQmCTHDvqAH1JfC2xxbZ0MxpZAcJqmE1LTQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.59 h1:9btwmrt//Q6JcSdgJOLI98sdr5p7tssS9yAsGe8aKP4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.59/go.mod h1:NM8fM6ovI3zak23UISdWidyZuI1ghNe2xjzUZAyT+08= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28 h1:KwsodFKVQTlI5EyhRSugALzsV6mG/SGrdjlMXSZSdso= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.28/go.mod h1:EY3APf9MzygVhKuPXAc5H+MkGb8k/DOSQjWS0LgkKqI= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32 h1:BjUcr3X3K0wZPGFg2bxOWW3VPN8rkE3/61zhP+IHviA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.32/go.mod h1:80+OGC/bgzzFFTUmcuwD0lb4YutwQeKLFpmt6hoWapU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32 h1:m1GeXHVMJsRsUAqG6HjZWx9dj7F5TR+cF1bjyfYyBd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.32/go.mod h1:IitoQxGfaKdVLNg0hD8/DXmAqNy0H4K2H2Sf91ti8sI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13 h1:SYVGSFQHlchIcy6e7x12bsrxClCXSP5et8cqVhL8cuw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.13/go.mod h1:kizuDaLX37bG5WZaoxGPQR/LNFXpxp0vsUnqfkWXfNE= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.14 h1:KSVbQW2umLp7i4Lo6mvBUz5PqV+Ze/IL6LCTasxQWEk= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.14/go.mod h1:jiaEkIw2Bb6IsoY9PDAZqVXJjNaKSxQGGj10CiloDWU= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.15 h1:/eE3DogBjYlvlbhd2ssWyeuovWunHLxfgw3s/OJa4GQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.15/go.mod h1:2PCJYpi7EKeA5SkStAmZlF6fi0uUABuhtF8ILHjGc3Y= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14 h1:M/zwXiL2iXUrHputuXgmO94TVNmcenPHxgLXLutodKE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.14/go.mod h1:RVwIw3y/IqxC2YEXSIkAzRDdEU1iRabDPaYjpGCbCGQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.14 h1:TzeR06UCMUq+KA3bDkujxK1GVGy+G8qQN/QVYzGLkQE= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.14/go.mod h1:dspXf/oYWGWo6DEvj98wpaTeqt5+DMidZD0A9BYTizc= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/bytedance/sonic v1.12.8 h1:4xYRVRlXIgvSZ4e8iVTlMF5szgpXd4AfvuWgA8I8lgs= github.com/bytedance/sonic v1.12.8/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0= github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= @@ -54,8 +84,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -88,6 +116,7 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/adapters/db/thumb.go b/internal/adapters/db/thumb.go index ccdbe58..ea872f1 100644 --- a/internal/adapters/db/thumb.go +++ b/internal/adapters/db/thumb.go @@ -1,40 +1,82 @@ package db import ( + "errors" + "github.com/google/uuid" - "github.com/pangolin-do-golang/thumb-processor-api/internal/core/thumb" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" ) -type PostgresThumbRepository struct { - db IDB -} - type ThumbPostgres struct { BaseModel + VideoPath string + Status string + Error string + ThumbnailPath string } func (op ThumbPostgres) TableName() string { return "thumb" } -func NewPostgresThumbRepository(db IDB) *PostgresThumbRepository { - return &PostgresThumbRepository{db: db} +type PostgresThumbRepository struct { + db IDB } -func (r *PostgresThumbRepository) Update(_ thumb.Thumb) error { +func (r *PostgresThumbRepository) Create(process *entity.ThumbProcess) error { + record := &ThumbPostgres{ + VideoPath: process.Video.Path, + ThumbnailPath: process.Thumbnail.Path, + Status: process.Status, + Error: process.Error, + } + + result := r.db.Create(record) - return nil + return result.Error } -func (r *PostgresThumbRepository) Create(_ *thumb.Thumb) (*thumb.Thumb, error) { +func (r *PostgresThumbRepository) List() []entity.ThumbProcess { + processes := []entity.ThumbProcess{} + records := []ThumbPostgres{} + + r.db.Find(&records) + + for _, record := range records { + processes = append(processes, entity.ThumbProcess{ + ID: record.ID, + Status: record.Status, + Error: record.Error, + Video: entity.ThumbProcessVideo{ + Path: record.VideoPath, + }, + Thumbnail: entity.ThumbProcessThumb{ + Path: record.ThumbnailPath, + }, + }) + } - return nil, nil + return processes } -func (r *PostgresThumbRepository) Get(_ uuid.UUID) (*thumb.Thumb, error) { - return nil, nil +func NewPostgresThumbRepository(db IDB) *PostgresThumbRepository { + return &PostgresThumbRepository{db: db} } -func (r *PostgresThumbRepository) GetAll() ([]thumb.Thumb, error) { - return nil, nil +func (r *PostgresThumbRepository) Update(process *entity.ThumbProcess) error { + if process.ID == uuid.Nil { + return errors.New("process id is required") + } + + result := r.db.Save(&ThumbPostgres{ + BaseModel: BaseModel{ + ID: process.ID, + }, + VideoPath: process.Video.Path, + ThumbnailPath: process.Thumbnail.Path, + Status: process.Status, + Error: process.Error, + }) + + return result.Error } diff --git a/internal/adapters/rest/handler/thumb_handler.go b/internal/adapters/rest/handler/thumb_handler.go new file mode 100644 index 0000000..7868047 --- /dev/null +++ b/internal/adapters/rest/handler/thumb_handler.go @@ -0,0 +1,166 @@ +package handler + +import ( + "net/http" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/ports" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/thumb" +) + +type ThumbHandler struct { + thumbService thumb.IThumbService +} + +func NewThumbHandler(service thumb.IThumbService) *ThumbHandler { + return &ThumbHandler{ + thumbService: service, + } +} + +func (h *ThumbHandler) RegisterRoutes(router *gin.Engine) { + thumbGroup := router.Group("/thumbs") + thumbGroup.POST("", h.CreateProcess) + thumbGroup.PUT("/:id", h.UpdateProcess) + thumbGroup.GET("", h.ListProcesses) +} + +// @Summary Create a new thumbnail process +// @Description Start a new asynchronous thumbnail generation process +// @Tags thumbs +// @Accept json +// @Produce json +// @Param request body CreateProcessRequest true "Video URL" +// @Success 202 {string} string "Process started" +// @Failure 400 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /thumbs [post] +func (h *ThumbHandler) CreateProcess(c *gin.Context) { + var request CreateProcessRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Invalid request format", + }) + return + } + + if request.URL == "" { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "URL is required", + }) + return + } + + err := h.thumbService.CreateProcessAsync(&ports.CreateProcessRequest{ + Url: request.URL, + }) + if err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: err.Error(), + }) + return + } + + c.JSON(http.StatusAccepted, gin.H{ + "message": "Process started successfully", + }) +} + +// @Summary Update a thumbnail process +// @Description Update the status of an existing thumbnail process +// @Tags thumbs +// @Accept json +// @Produce json +// @Param id path string true "Process ID" +// @Param request body UpdateProcessRequest true "Process update information" +// @Success 200 {object} ThumbProcessResponse +// @Failure 400 {object} ErrorResponse +// @Failure 404 {object} ErrorResponse +// @Failure 500 {object} ErrorResponse +// @Router /thumbs/{id} [put] +func (h *ThumbHandler) UpdateProcess(c *gin.Context) { + var request UpdateProcessRequest + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Invalid request format", + }) + return + } + + id, err := uuid.Parse(c.Param("id")) + if err != nil { + c.JSON(http.StatusBadRequest, ErrorResponse{ + Error: "Process ID is required", + }) + return + } + + updated, err := h.thumbService.UpdateProcess(&ports.UpdateProcessRequest{ + ID: id, + Status: request.Status, + Error: request.Error, + ThumbnailPath: request.ThumbnailPath, + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, ErrorResponse{ + Error: err.Error(), + }) + return + } + + c.JSON(http.StatusOK, ThumbProcessResponse{ + ID: updated.ID.String(), + Status: updated.Status, + Error: updated.Error, + ThumbnailPath: updated.Thumbnail.Path, + }) +} + +// @Summary List all thumbnail processes +// @Description Get a list of all thumbnail processes +// @Tags thumbs +// @Produce json +// @Success 200 {array} ThumbProcessResponse +// @Failure 500 {object} ErrorResponse +// @Router /thumbs [get] +func (h *ThumbHandler) ListProcesses(c *gin.Context) { + processes := h.thumbService.ListProcess() + + response := make([]ThumbProcessResponse, len(*processes)) + for i, process := range *processes { + response[i] = ThumbProcessResponse{ + ID: process.ID.String(), + Status: process.Status, + Error: process.Error, + ThumbnailPath: process.Thumbnail.Path, + } + } + + c.JSON(http.StatusOK, response) +} + +type CreateProcessRequest struct { + URL string `json:"url" binding:"required"` +} + +type UpdateProcessRequest struct { + Status string `json:"status" binding:"required"` + Error string `json:"error,omitempty"` + ThumbnailPath string `json:"thumbnail_path,omitempty"` +} + +type ThumbProcessResponse struct { + ID string `json:"id"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + ThumbnailPath string `json:"thumbnail_path,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ErrorResponse struct { + Error string `json:"error"` +} diff --git a/internal/adapters/sqs/queue_adapter_sqs.go b/internal/adapters/sqs/queue_adapter_sqs.go new file mode 100644 index 0000000..52a9837 --- /dev/null +++ b/internal/adapters/sqs/queue_adapter_sqs.go @@ -0,0 +1,43 @@ +package sqs + +import ( + "context" + "encoding/json" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/sqs" + cfg "github.com/pangolin-do-golang/thumb-processor-api/internal/config" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" +) + +type SQSThumbQueue struct { + client *sqs.Client + queueURL string +} + +func NewSQSThumbQueue(c *cfg.Config) (*SQSThumbQueue, error) { + sdkConfig, err := config.LoadDefaultConfig(context.TODO()) + if err != nil { + return nil, err + } + + return &SQSThumbQueue{ + client: sqs.NewFromConfig(sdkConfig), + queueURL: c.SQS.QueueURL, + }, nil +} + +func (q *SQSThumbQueue) SendEvent(process *entity.ThumbProcess) error { + messageBody, err := json.Marshal(process) + if err != nil { + return err + } + + _, err = q.client.SendMessage(context.Background(), &sqs.SendMessageInput{ + QueueUrl: &q.queueURL, + MessageBody: aws.String(string(messageBody)), + }) + + return err +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..82c1bf2 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + + "github.com/caarlos0/env/v11" +) + +type S3 struct { + Bucket string `env:"S3_BUCKET"` +} + +type SQS struct { + QueueURL string `env:"SQS_QUEUE_URL"` +} + +type Config struct { + S3 S3 + SQS SQS + DB Database +} + +func Load() (*Config, error) { + cfg := Config{} + err := env.Parse(&cfg) + return &cfg, err +} + +type Database struct { + User string `env:"DB_USERNAME"` + Password string `env:"DB_PASSWORD"` + Host string `env:"DB_HOST"` + Port string `env:"DB_PORT"` + Name string `env:"DB_NAME"` +} + +func (db *Database) GetDNS() string { + return fmt.Sprintf("user=%s password=%s host=%s port=%s dbname=%s sslmode=disable TimeZone=America/Sao_Paulo", + db.User, + db.Password, + db.Host, + db.Port, + db.Name, + ) +} diff --git a/internal/core/domain/contracts/thumb_queue_adapter_interface.go b/internal/core/domain/contracts/thumb_queue_adapter_interface.go new file mode 100644 index 0000000..6195fd3 --- /dev/null +++ b/internal/core/domain/contracts/thumb_queue_adapter_interface.go @@ -0,0 +1,7 @@ +package contracts + +import "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + +type IThumbQueueAdapter interface { + SendEvent(process *entity.ThumbProcess) error +} diff --git a/internal/core/domain/contracts/thumb_repository_adapter_interface copy.go b/internal/core/domain/contracts/thumb_repository_adapter_interface copy.go new file mode 100644 index 0000000..dffd6ef --- /dev/null +++ b/internal/core/domain/contracts/thumb_repository_adapter_interface copy.go @@ -0,0 +1,9 @@ +package contracts + +import "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + +type IThumbRepositoryAdapter interface { + Create(process *entity.ThumbProcess) error + Update(process *entity.ThumbProcess) (*entity.ThumbProcess, error) + List() *[]entity.ThumbProcess +} diff --git a/internal/core/domain/entity/thumb_process.go b/internal/core/domain/entity/thumb_process.go new file mode 100644 index 0000000..0a08635 --- /dev/null +++ b/internal/core/domain/entity/thumb_process.go @@ -0,0 +1,40 @@ +package entity + +import "github.com/google/uuid" + +const ( + ThumbProcessStatusPending string = "pending" + ThumbProcessStatusComplete string = "complete" + ThumbProcessStatusFailed string = "failed" +) + +var AllowedProcessStatus = map[string]bool{ + ThumbProcessStatusPending: true, + ThumbProcessStatusComplete: true, + ThumbProcessStatusFailed: true, +} + +type ThumbProcess struct { + ID uuid.UUID `json:"id"` + Video ThumbProcessVideo `json:"video"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + Thumbnail ThumbProcessThumb `json:"thumbnail"` +} + +type ThumbProcessVideo struct { + Path string `json:"path"` +} + +type ThumbProcessThumb struct { + Path string `json:"url"` +} + +func NewThumbProcess(url string) *ThumbProcess { + return &ThumbProcess{ + Video: ThumbProcessVideo{ + Path: url, + }, + Status: ThumbProcessStatusPending, + } +} diff --git a/internal/core/ports/thumb_ports.go b/internal/core/ports/thumb_ports.go new file mode 100644 index 0000000..2647831 --- /dev/null +++ b/internal/core/ports/thumb_ports.go @@ -0,0 +1,14 @@ +package ports + +import "github.com/google/uuid" + +type CreateProcessRequest struct { + Url string `json:"url"` +} + +type UpdateProcessRequest struct { + ID uuid.UUID `json:"id"` + Status string `json:"status"` + Error string `json:"error,omitempty"` + ThumbnailPath string `json:"thumbnailPath"` +} diff --git a/internal/core/thumb/thumb.go b/internal/core/thumb/thumb.go deleted file mode 100644 index f0e4725..0000000 --- a/internal/core/thumb/thumb.go +++ /dev/null @@ -1,13 +0,0 @@ -package thumb - -import ( - "github.com/google/uuid" -) - -type Thumb struct { - ID uuid.UUID `json:"id"` -} - -type IThumbService interface { - GetThumb(id uuid.UUID) (*Thumb, error) -} diff --git a/internal/core/thumb/thumb_service.go b/internal/core/thumb/thumb_service.go index 7f251c1..1da98ef 100644 --- a/internal/core/thumb/thumb_service.go +++ b/internal/core/thumb/thumb_service.go @@ -1,18 +1,58 @@ package thumb import ( - "github.com/google/uuid" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/contracts" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/ports" ) +type IThumbService interface { + CreateProcessAsync(request *ports.CreateProcessRequest) error + UpdateProcess(request *ports.UpdateProcessRequest) (*entity.ThumbProcess, error) + ListProcess() *[]entity.ThumbProcess +} + type Service struct { + processRepository contracts.IThumbRepositoryAdapter + queueAdapter contracts.IThumbQueueAdapter +} + +func NewThumbService(repo contracts.IThumbRepositoryAdapter, q contracts.IThumbQueueAdapter) *Service { + return &Service{processRepository: repo, queueAdapter: q} } -func NewThumbService() *Service { - return &Service{} +func (s *Service) CreateProcessAsync(request *ports.CreateProcessRequest) error { + thumbProcess := entity.NewThumbProcess(request.Url) + + if err := s.processRepository.Create(thumbProcess); err != nil { + return err + } + + if err := s.queueAdapter.SendEvent(thumbProcess); err != nil { + return err + } + + return nil +} + +func (s *Service) UpdateProcess(request *ports.UpdateProcessRequest) (*entity.ThumbProcess, error) { + thumbProcess := &entity.ThumbProcess{ + ID: request.ID, + Status: request.Status, + Error: request.Error, + Thumbnail: entity.ThumbProcessThumb{Path: request.ThumbnailPath}, + } + + updated, err := s.processRepository.Update(thumbProcess) + if err != nil { + return nil, err + } + + return updated, nil } -func (s *Service) GetThumb(_ uuid.UUID) (*Thumb, error) { - var thumb *Thumb +func (s *Service) ListProcess() *[]entity.ThumbProcess { + processList := s.processRepository.List() - return thumb, nil + return processList } From 85954b5c80c62dae5ac6461dc2bf5f5f740d28a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 12:04:32 -0300 Subject: [PATCH 2/9] feat: resources unit tests --- .../rest/handler/thumb_handler_test.go | 117 ++++++++++++++++++ internal/config/config_test.go | 111 +++++++++++++++++ internal/core/thumb/thumb_service_test.go | 98 +++++++++++++-- .../mocks/adaptermocks/IThumbQueueAdapter.go | 45 +++++++ .../adaptermocks/IThumbRepositoryAdapter.go | 95 ++++++++++++++ internal/mocks/servicemocks/IThumbService.go | 97 +++++++++++++++ 6 files changed, 555 insertions(+), 8 deletions(-) create mode 100644 internal/adapters/rest/handler/thumb_handler_test.go create mode 100644 internal/config/config_test.go create mode 100644 internal/mocks/adaptermocks/IThumbQueueAdapter.go create mode 100644 internal/mocks/adaptermocks/IThumbRepositoryAdapter.go create mode 100644 internal/mocks/servicemocks/IThumbService.go diff --git a/internal/adapters/rest/handler/thumb_handler_test.go b/internal/adapters/rest/handler/thumb_handler_test.go new file mode 100644 index 0000000..d807e20 --- /dev/null +++ b/internal/adapters/rest/handler/thumb_handler_test.go @@ -0,0 +1,117 @@ +package handler + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + "github.com/pangolin-do-golang/thumb-processor-api/internal/mocks/servicemocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func setupTest() (*gin.Engine, *servicemocks.IThumbService) { + gin.SetMode(gin.TestMode) + router := gin.New() + mockService := new(servicemocks.IThumbService) + handler := NewThumbHandler(mockService) + handler.RegisterRoutes(router) + return router, mockService +} + +func TestCreateProcess(t *testing.T) { + router, mockService := setupTest() + + t.Run("successful creation", func(t *testing.T) { + mockService.On("CreateProcessAsync", mock.AnythingOfType("*ports.CreateProcessRequest")).Return(nil).Once() + + body := CreateProcessRequest{URL: "https://example.com/video.mp4"} + jsonBody, _ := json.Marshal(body) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/thumbs", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusAccepted, w.Code) + }) + + t.Run("invalid request", func(t *testing.T) { + body := CreateProcessRequest{URL: ""} // Empty URL + jsonBody, _ := json.Marshal(body) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/thumbs", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + }) +} + +func TestUpdateProcess(t *testing.T) { + router, mockService := setupTest() + mockedUUID, _ := uuid.NewV7() + + t.Run("successful update", func(t *testing.T) { + updatedProcess := &entity.ThumbProcess{ + ID: mockedUUID, + Status: "completed", + Thumbnail: entity.ThumbProcessThumb{ + Path: "path/to/thumbnail.jpg", + }, + } + + mockService.On("UpdateProcess", mock.AnythingOfType("*ports.UpdateProcessRequest")).Return(updatedProcess, nil).Once() + + body := UpdateProcessRequest{ + Status: "completed", + ThumbnailPath: "path/to/thumbnail.jpg", + } + jsonBody, _ := json.Marshal(body) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/thumbs/"+mockedUUID.String(), bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + }) +} + +func TestListProcesses(t *testing.T) { + router, mockService := setupTest() + mockedUUIDComplete, _ := uuid.NewV7() + mockedUUIDProcessing, _ := uuid.NewV7() + + processes := &[]entity.ThumbProcess{ + { + ID: mockedUUIDComplete, + Video: entity.ThumbProcessVideo{Path: "https://example.com/video1.mp4"}, + Status: "completed", + }, + { + ID: mockedUUIDProcessing, + Video: entity.ThumbProcessVideo{Path: "https://example.com/video2.mp4"}, + Status: "processing", + }, + } + + mockService.On("ListProcess").Return(processes).Once() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/thumbs", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var response []ThumbProcessResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Len(t, response, 2) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..7828065 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,111 @@ +package config + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoad(t *testing.T) { + tests := []struct { + name string + envVars map[string]string + want *Config + wantErr bool + wantErrMsg string // Add a field for specific error message check + }{ + { + name: "Success", + envVars: map[string]string{ + "S3_BUCKET": "test-bucket", + "SQS_QUEUE_URL": "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue", + "DB_USERNAME": "test_db_username", + "DB_PASSWORD": "test_db_password", + "DB_HOST": "test_db_host", + "DB_PORT": "test_db_port", + "DB_NAME": "test_db_name", + }, + want: &Config{ + S3: S3{Bucket: "test-bucket"}, + SQS: SQS{QueueURL: "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue"}, + DB: Database{ + User: "test_db_username", + Password: "test_db_password", + Host: "test_db_host", + Port: "test_db_port", + Name: "test_db_name", + }, + }, + wantErr: false, + }, + { + name: "MissingS3Bucket", + envVars: map[string]string{ + "SQS_QUEUE_URL": "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue", + }, + want: &Config{ + SQS: SQS{QueueURL: "https://sqs.us-east-1.amazonaws.com/123456789012/test-queue"}, // S3 should be empty + }, + wantErr: false, // env package doesn't return error if some vars are missing + }, + { + name: "MissingSQSQueueURL", + envVars: map[string]string{ + "S3_BUCKET": "test-bucket", + }, + want: &Config{ + S3: S3{Bucket: "test-bucket"}, // SQS should be empty + }, + wantErr: false, // env package doesn't return error if some vars are missing + }, + { + name: "EmptyEnvVars", + envVars: map[string]string{}, + want: &Config{}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set environment variables for the test + for k, v := range tt.envVars { + os.Setenv(k, v) + defer os.Unsetenv(k) // Ensure cleanup after the test + } + + got, err := Load() + + if tt.wantErr { + assert.Error(t, err) + if tt.wantErrMsg != "" { + assert.Contains(t, err.Error(), tt.wantErrMsg) // check specific error message + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +// Example of a test for a function that uses the config +func TestSomethingThatUsesConfig(t *testing.T) { + // Set necessary environment variables + os.Setenv("S3_BUCKET", "test-bucket") + os.Setenv("SQS_QUEUE_URL", "test-queue") + defer func() { + os.Unsetenv("S3_BUCKET") + os.Unsetenv("SQS_QUEUE_URL") + }() + + cfg, err := Load() + assert.NoError(t, err) + + // Now you can use cfg in your assertions or test logic + assert.Equal(t, "test-bucket", cfg.S3.Bucket) + assert.Equal(t, "test-queue", cfg.SQS.QueueURL) + + // ... your test logic here ... +} diff --git a/internal/core/thumb/thumb_service_test.go b/internal/core/thumb/thumb_service_test.go index cda68be..0537b9e 100644 --- a/internal/core/thumb/thumb_service_test.go +++ b/internal/core/thumb/thumb_service_test.go @@ -1,19 +1,101 @@ package thumb import ( - "github.com/google/uuid" + "errors" "testing" + + "github.com/google/uuid" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/ports" + "github.com/pangolin-do-golang/thumb-processor-api/internal/mocks/adaptermocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) -func TestGetThumb(t *testing.T) { - service := NewThumbService() +func TestCreateProcessAsync(t *testing.T) { + testAppRequest := &ports.CreateProcessRequest{Url: "https://example.com/video.mp4"} + + mockRepo := new(adaptermocks.IThumbRepositoryAdapter) + mockQueue := new(adaptermocks.IThumbQueueAdapter) + service := NewThumbService(mockRepo, mockQueue) + + t.Run("successful creation", func(t *testing.T) { + mockRepo.On("Create", mock.AnythingOfType("*entity.ThumbProcess")).Return(nil).Once() + mockQueue.On("SendEvent", mock.AnythingOfType("*entity.ThumbProcess")).Return(nil).Once() + + err := service.CreateProcessAsync(testAppRequest) + + assert.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockRepo.On("Create", mock.AnythingOfType("*entity.ThumbProcess")).Return(errors.New("db error")).Once() + + err := service.CreateProcessAsync(testAppRequest) + + assert.Error(t, err) + }) + + t.Run("queue error", func(t *testing.T) { + mockRepo.On("Create", mock.AnythingOfType("*entity.ThumbProcess")).Return(nil) + mockQueue.On("SendEvent", mock.AnythingOfType("*entity.ThumbProcess")).Return(errors.New("queue error")).Once() + + err := service.CreateProcessAsync(testAppRequest) + + assert.Error(t, err) + }) +} + +func TestListProcess(t *testing.T) { + mockRepo := new(adaptermocks.IThumbRepositoryAdapter) + mockQueue := new(adaptermocks.IThumbQueueAdapter) + service := NewThumbService(mockRepo, mockQueue) - expectedID := uuid.New() + expectedList := &[]entity.ThumbProcess{ + *entity.NewThumbProcess("https://example.com/video1.mp4"), + *entity.NewThumbProcess("https://example.com/video2.mp4"), + } - _, err := service.GetThumb(expectedID) + mockRepo.On("List").Return(expectedList) + result := service.ListProcess() - if err != nil { - t.Errorf("GetThumb returned an error: %v", err) - return + assert.Equal(t, expectedList, result) +} + +func TestUpdateProcess(t *testing.T) { + mockRepo := new(adaptermocks.IThumbRepositoryAdapter) + mockQueue := new(adaptermocks.IThumbQueueAdapter) + service := NewThumbService(mockRepo, mockQueue) + mockedID, _ := uuid.NewV7() + + request := &ports.UpdateProcessRequest{ + ID: mockedID, + Status: entity.ThumbProcessStatusComplete, + Error: "", + ThumbnailPath: "https://example.com/video-thumbs.zip", } + + thumbProcess := &entity.ThumbProcess{ + ID: request.ID, + Status: request.Status, + Error: request.Error, + Thumbnail: entity.ThumbProcessThumb{Path: request.ThumbnailPath}, + } + + t.Run("successful creation", func(t *testing.T) { + mockRepo.On("Update", thumbProcess).Return(thumbProcess, nil).Once() + + updated, err := service.UpdateProcess(request) + + assert.NotNil(t, updated) + assert.NoError(t, err) + }) + + t.Run("repository error", func(t *testing.T) { + mockRepo.On("Update", mock.AnythingOfType("*entity.ThumbProcess")).Return(nil, errors.New("db error")).Once() + + updated, err := service.UpdateProcess(request) + assert.Nil(t, updated) + assert.Error(t, err) + }) } diff --git a/internal/mocks/adaptermocks/IThumbQueueAdapter.go b/internal/mocks/adaptermocks/IThumbQueueAdapter.go new file mode 100644 index 0000000..3a86d19 --- /dev/null +++ b/internal/mocks/adaptermocks/IThumbQueueAdapter.go @@ -0,0 +1,45 @@ +// Code generated by mockery v2.51.1. DO NOT EDIT. + +package adaptermocks + +import ( + entity "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + mock "github.com/stretchr/testify/mock" +) + +// IThumbQueueAdapter is an autogenerated mock type for the IThumbQueueAdapter type +type IThumbQueueAdapter struct { + mock.Mock +} + +// SendEvent provides a mock function with given fields: process +func (_m *IThumbQueueAdapter) SendEvent(process *entity.ThumbProcess) error { + ret := _m.Called(process) + + if len(ret) == 0 { + panic("no return value specified for SendEvent") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*entity.ThumbProcess) error); ok { + r0 = rf(process) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewIThumbQueueAdapter creates a new instance of IThumbQueueAdapter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIThumbQueueAdapter(t interface { + mock.TestingT + Cleanup(func()) +}) *IThumbQueueAdapter { + mock := &IThumbQueueAdapter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/mocks/adaptermocks/IThumbRepositoryAdapter.go b/internal/mocks/adaptermocks/IThumbRepositoryAdapter.go new file mode 100644 index 0000000..295cd6b --- /dev/null +++ b/internal/mocks/adaptermocks/IThumbRepositoryAdapter.go @@ -0,0 +1,95 @@ +// Code generated by mockery v2.51.1. DO NOT EDIT. + +package adaptermocks + +import ( + entity "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + mock "github.com/stretchr/testify/mock" +) + +// IThumbRepositoryAdapter is an autogenerated mock type for the IThumbRepositoryAdapter type +type IThumbRepositoryAdapter struct { + mock.Mock +} + +// Create provides a mock function with given fields: process +func (_m *IThumbRepositoryAdapter) Create(process *entity.ThumbProcess) error { + ret := _m.Called(process) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*entity.ThumbProcess) error); ok { + r0 = rf(process) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// List provides a mock function with no fields +func (_m *IThumbRepositoryAdapter) List() *[]entity.ThumbProcess { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for List") + } + + var r0 *[]entity.ThumbProcess + if rf, ok := ret.Get(0).(func() *[]entity.ThumbProcess); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*[]entity.ThumbProcess) + } + } + + return r0 +} + +// Update provides a mock function with given fields: process +func (_m *IThumbRepositoryAdapter) Update(process *entity.ThumbProcess) (*entity.ThumbProcess, error) { + ret := _m.Called(process) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 *entity.ThumbProcess + var r1 error + if rf, ok := ret.Get(0).(func(*entity.ThumbProcess) (*entity.ThumbProcess, error)); ok { + return rf(process) + } + if rf, ok := ret.Get(0).(func(*entity.ThumbProcess) *entity.ThumbProcess); ok { + r0 = rf(process) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*entity.ThumbProcess) + } + } + + if rf, ok := ret.Get(1).(func(*entity.ThumbProcess) error); ok { + r1 = rf(process) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewIThumbRepositoryAdapter creates a new instance of IThumbRepositoryAdapter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIThumbRepositoryAdapter(t interface { + mock.TestingT + Cleanup(func()) +}) *IThumbRepositoryAdapter { + mock := &IThumbRepositoryAdapter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/mocks/servicemocks/IThumbService.go b/internal/mocks/servicemocks/IThumbService.go new file mode 100644 index 0000000..613a24b --- /dev/null +++ b/internal/mocks/servicemocks/IThumbService.go @@ -0,0 +1,97 @@ +// Code generated by mockery v2.51.1. DO NOT EDIT. + +package servicemocks + +import ( + entity "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + mock "github.com/stretchr/testify/mock" + + ports "github.com/pangolin-do-golang/thumb-processor-api/internal/core/ports" +) + +// IThumbService is an autogenerated mock type for the IThumbService type +type IThumbService struct { + mock.Mock +} + +// CreateProcessAsync provides a mock function with given fields: request +func (_m *IThumbService) CreateProcessAsync(request *ports.CreateProcessRequest) error { + ret := _m.Called(request) + + if len(ret) == 0 { + panic("no return value specified for CreateProcessAsync") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*ports.CreateProcessRequest) error); ok { + r0 = rf(request) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ListProcess provides a mock function with no fields +func (_m *IThumbService) ListProcess() *[]entity.ThumbProcess { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ListProcess") + } + + var r0 *[]entity.ThumbProcess + if rf, ok := ret.Get(0).(func() *[]entity.ThumbProcess); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*[]entity.ThumbProcess) + } + } + + return r0 +} + +// UpdateProcess provides a mock function with given fields: request +func (_m *IThumbService) UpdateProcess(request *ports.UpdateProcessRequest) (*entity.ThumbProcess, error) { + ret := _m.Called(request) + + if len(ret) == 0 { + panic("no return value specified for UpdateProcess") + } + + var r0 *entity.ThumbProcess + var r1 error + if rf, ok := ret.Get(0).(func(*ports.UpdateProcessRequest) (*entity.ThumbProcess, error)); ok { + return rf(request) + } + if rf, ok := ret.Get(0).(func(*ports.UpdateProcessRequest) *entity.ThumbProcess); ok { + r0 = rf(request) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*entity.ThumbProcess) + } + } + + if rf, ok := ret.Get(1).(func(*ports.UpdateProcessRequest) error); ok { + r1 = rf(request) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewIThumbService creates a new instance of IThumbService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIThumbService(t interface { + mock.TestingT + Cleanup(func()) +}) *IThumbService { + mock := &IThumbService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 40993abbc505d7fa5964a1d7f9499ce328d3461d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 12:31:00 -0300 Subject: [PATCH 3/9] feat: thumb repository unit tests --- .../db/{thumb.go => thumb_repository.go} | 0 internal/adapters/db/thumb_repository_test.go | 194 +++++++ internal/mocks/adaptermocks/IDB.go | 484 ++++++++++++++++++ 3 files changed, 678 insertions(+) rename internal/adapters/db/{thumb.go => thumb_repository.go} (100%) create mode 100644 internal/adapters/db/thumb_repository_test.go create mode 100644 internal/mocks/adaptermocks/IDB.go diff --git a/internal/adapters/db/thumb.go b/internal/adapters/db/thumb_repository.go similarity index 100% rename from internal/adapters/db/thumb.go rename to internal/adapters/db/thumb_repository.go diff --git a/internal/adapters/db/thumb_repository_test.go b/internal/adapters/db/thumb_repository_test.go new file mode 100644 index 0000000..b7d5b4f --- /dev/null +++ b/internal/adapters/db/thumb_repository_test.go @@ -0,0 +1,194 @@ +// internal/adapters/db/thumb_test.go +package db + +import ( + "errors" + "testing" + + "github.com/google/uuid" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/domain/entity" + "github.com/pangolin-do-golang/thumb-processor-api/internal/mocks/adaptermocks" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "gorm.io/gorm" +) + +func TestPostgresThumbRepository_Create(t *testing.T) { + mockDB := new(adaptermocks.IDB) + repo := NewPostgresThumbRepository(mockDB) + + t.Run("successful creation", func(t *testing.T) { + process := &entity.ThumbProcess{ + Video: entity.ThumbProcessVideo{ + Path: "test-video.mp4", + }, + Thumbnail: entity.ThumbProcessThumb{ + Path: "test-thumb.jpg", + }, + Status: "pending", + Error: "", + } + + mockDB.On("Create", mock.AnythingOfType("*db.ThumbPostgres")).Return(&gorm.DB{}).Once() + + err := repo.Create(process) + + assert.NoError(t, err) + }) + + t.Run("database error", func(t *testing.T) { + process := &entity.ThumbProcess{ + Video: entity.ThumbProcessVideo{ + Path: "test-video.mp4", + }, + Thumbnail: entity.ThumbProcessThumb{ + Path: "test-thumb.jpg", + }, + Status: "pending", + Error: "", + } + + expectedError := errors.New("database error") + mockDB.On("Create", mock.AnythingOfType("*db.ThumbPostgres")).Return(&gorm.DB{Error: expectedError}).Once() + + err := repo.Create(process) + + assert.Error(t, err) + assert.Equal(t, expectedError, err) + }) +} + +func TestPostgresThumbRepository_List(t *testing.T) { + mockDB := new(adaptermocks.IDB) + repo := NewPostgresThumbRepository(mockDB) + + t.Run("successful list retrieval", func(t *testing.T) { + mockDB.On("Find", mock.AnythingOfType("*[]db.ThumbPostgres"), mock.Anything). + Run(func(args mock.Arguments) { + arg := args.Get(0).(*[]ThumbPostgres) + *arg = []ThumbPostgres{ + { + BaseModel: BaseModel{ + ID: uuid.New(), + }, + VideoPath: "video1.mp4", + ThumbnailPath: "thumb1.jpg", + Status: "completed", + Error: "", + }, + { + BaseModel: BaseModel{ + ID: uuid.New(), + }, + VideoPath: "video2.mp4", + ThumbnailPath: "thumb2.jpg", + Status: "pending", + Error: "", + }, + } + }).Return(&gorm.DB{}).Once() + + processes := repo.List() + + assert.Len(t, processes, 2) + assert.Equal(t, "video1.mp4", processes[0].Video.Path) + assert.Equal(t, "thumb1.jpg", processes[0].Thumbnail.Path) + assert.Equal(t, "completed", processes[0].Status) + assert.Equal(t, "video2.mp4", processes[1].Video.Path) + assert.Equal(t, "thumb2.jpg", processes[1].Thumbnail.Path) + assert.Equal(t, "pending", processes[1].Status) + }) + + t.Run("empty list", func(t *testing.T) { + mockDB.On("Find", mock.AnythingOfType("*[]db.ThumbPostgres"), mock.Anything). + Run(func(args mock.Arguments) { + arg := args.Get(0).(*[]ThumbPostgres) + *arg = []ThumbPostgres{} + }).Return(&gorm.DB{}).Once() + + processes := repo.List() + + assert.Empty(t, processes) + }) +} + +func TestPostgresThumbRepository_Update(t *testing.T) { + mockDB := new(adaptermocks.IDB) + repo := NewPostgresThumbRepository(mockDB) + + t.Run("successful update", func(t *testing.T) { + processID := uuid.New() + process := &entity.ThumbProcess{ + ID: processID, + Video: entity.ThumbProcessVideo{ + Path: "updated-video.mp4", + }, + Thumbnail: entity.ThumbProcessThumb{ + Path: "updated-thumb.jpg", + }, + Status: "completed", + Error: "", + } + + mockDB.On("Save", mock.AnythingOfType("*db.ThumbPostgres")).Return(&gorm.DB{}).Once() + + err := repo.Update(process) + + assert.NoError(t, err) + }) + + t.Run("update with missing ID", func(t *testing.T) { + process := &entity.ThumbProcess{ + Video: entity.ThumbProcessVideo{ + Path: "updated-video.mp4", + }, + Thumbnail: entity.ThumbProcessThumb{ + Path: "updated-thumb.jpg", + }, + Status: "completed", + Error: "", + } + + err := repo.Update(process) + + assert.Error(t, err) + assert.Equal(t, "process id is required", err.Error()) + mockDB.AssertNotCalled(t, "Save") + }) + + t.Run("database error during update", func(t *testing.T) { + processID := uuid.New() + process := &entity.ThumbProcess{ + ID: processID, + Video: entity.ThumbProcessVideo{ + Path: "updated-video.mp4", + }, + Thumbnail: entity.ThumbProcessThumb{ + Path: "updated-thumb.jpg", + }, + Status: "completed", + Error: "", + } + + expectedError := errors.New("database error") + mockDB.On("Save", mock.AnythingOfType("*db.ThumbPostgres")).Return(&gorm.DB{Error: expectedError}).Once() + + err := repo.Update(process) + + assert.Error(t, err) + assert.Equal(t, expectedError, err) + }) +} + +func TestNewPostgresThumbRepository(t *testing.T) { + mockDB := new(adaptermocks.IDB) + repo := NewPostgresThumbRepository(mockDB) + + assert.NotNil(t, repo) + assert.Equal(t, mockDB, repo.db) +} + +func TestThumbPostgres_TableName(t *testing.T) { + thumb := ThumbPostgres{} + assert.Equal(t, "thumb", thumb.TableName()) +} diff --git a/internal/mocks/adaptermocks/IDB.go b/internal/mocks/adaptermocks/IDB.go new file mode 100644 index 0000000..a8bedbb --- /dev/null +++ b/internal/mocks/adaptermocks/IDB.go @@ -0,0 +1,484 @@ +// Code generated by mockery v2.51.1. DO NOT EDIT. + +package adaptermocks + +import ( + sql "database/sql" + + mock "github.com/stretchr/testify/mock" + gorm "gorm.io/gorm" +) + +// IDB is an autogenerated mock type for the IDB type +type IDB struct { + mock.Mock +} + +// Count provides a mock function with given fields: count +func (_m *IDB) Count(count *int64) *gorm.DB { + ret := _m.Called(count) + + if len(ret) == 0 { + panic("no return value specified for Count") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(*int64) *gorm.DB); ok { + r0 = rf(count) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Create provides a mock function with given fields: value +func (_m *IDB) Create(value interface{}) *gorm.DB { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}) *gorm.DB); ok { + r0 = rf(value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// CreateInBatches provides a mock function with given fields: value, batchSize +func (_m *IDB) CreateInBatches(value interface{}, batchSize int) *gorm.DB { + ret := _m.Called(value, batchSize) + + if len(ret) == 0 { + panic("no return value specified for CreateInBatches") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, int) *gorm.DB); ok { + r0 = rf(value, batchSize) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Delete provides a mock function with given fields: value, conds +func (_m *IDB) Delete(value interface{}, conds ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, value) + _ca = append(_ca, conds...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *gorm.DB); ok { + r0 = rf(value, conds...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Exec provides a mock function with given fields: _a0, values +func (_m *IDB) Exec(_a0 string, values ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, _a0) + _ca = append(_ca, values...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Exec") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(string, ...interface{}) *gorm.DB); ok { + r0 = rf(_a0, values...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Find provides a mock function with given fields: dest, conds +func (_m *IDB) Find(dest interface{}, conds ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, dest) + _ca = append(_ca, conds...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Find") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *gorm.DB); ok { + r0 = rf(dest, conds...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// First provides a mock function with given fields: dest, conds +func (_m *IDB) First(dest interface{}, conds ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, dest) + _ca = append(_ca, conds...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for First") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *gorm.DB); ok { + r0 = rf(dest, conds...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Last provides a mock function with given fields: dest, conds +func (_m *IDB) Last(dest interface{}, conds ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, dest) + _ca = append(_ca, conds...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Last") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *gorm.DB); ok { + r0 = rf(dest, conds...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Limit provides a mock function with given fields: limit +func (_m *IDB) Limit(limit int) *gorm.DB { + ret := _m.Called(limit) + + if len(ret) == 0 { + panic("no return value specified for Limit") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(int) *gorm.DB); ok { + r0 = rf(limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Model provides a mock function with given fields: value +func (_m *IDB) Model(value interface{}) *gorm.DB { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for Model") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}) *gorm.DB); ok { + r0 = rf(value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Order provides a mock function with given fields: value +func (_m *IDB) Order(value interface{}) *gorm.DB { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for Order") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}) *gorm.DB); ok { + r0 = rf(value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Raw provides a mock function with given fields: _a0, values +func (_m *IDB) Raw(_a0 string, values ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, _a0) + _ca = append(_ca, values...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Raw") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(string, ...interface{}) *gorm.DB); ok { + r0 = rf(_a0, values...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Row provides a mock function with no fields +func (_m *IDB) Row() *sql.Row { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Row") + } + + var r0 *sql.Row + if rf, ok := ret.Get(0).(func() *sql.Row); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Row) + } + } + + return r0 +} + +// Rows provides a mock function with no fields +func (_m *IDB) Rows() (*sql.Rows, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Rows") + } + + var r0 *sql.Rows + var r1 error + if rf, ok := ret.Get(0).(func() (*sql.Rows, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *sql.Rows); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*sql.Rows) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Save provides a mock function with given fields: value +func (_m *IDB) Save(value interface{}) *gorm.DB { + ret := _m.Called(value) + + if len(ret) == 0 { + panic("no return value specified for Save") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}) *gorm.DB); ok { + r0 = rf(value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Scan provides a mock function with given fields: dest +func (_m *IDB) Scan(dest interface{}) *gorm.DB { + ret := _m.Called(dest) + + if len(ret) == 0 { + panic("no return value specified for Scan") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}) *gorm.DB); ok { + r0 = rf(dest) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// ScanRows provides a mock function with given fields: rows, dest +func (_m *IDB) ScanRows(rows *sql.Rows, dest interface{}) error { + ret := _m.Called(rows, dest) + + if len(ret) == 0 { + panic("no return value specified for ScanRows") + } + + var r0 error + if rf, ok := ret.Get(0).(func(*sql.Rows, interface{}) error); ok { + r0 = rf(rows, dest) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Select provides a mock function with given fields: query, args +func (_m *IDB) Select(query interface{}, args ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Select") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *gorm.DB); ok { + r0 = rf(query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Take provides a mock function with given fields: dest, conds +func (_m *IDB) Take(dest interface{}, conds ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, dest) + _ca = append(_ca, conds...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Take") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *gorm.DB); ok { + r0 = rf(dest, conds...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Update provides a mock function with given fields: column, value +func (_m *IDB) Update(column string, value interface{}) *gorm.DB { + ret := _m.Called(column, value) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(string, interface{}) *gorm.DB); ok { + r0 = rf(column, value) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// Where provides a mock function with given fields: query, args +func (_m *IDB) Where(query interface{}, args ...interface{}) *gorm.DB { + var _ca []interface{} + _ca = append(_ca, query) + _ca = append(_ca, args...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Where") + } + + var r0 *gorm.DB + if rf, ok := ret.Get(0).(func(interface{}, ...interface{}) *gorm.DB); ok { + r0 = rf(query, args...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*gorm.DB) + } + } + + return r0 +} + +// NewIDB creates a new instance of IDB. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIDB(t interface { + mock.TestingT + Cleanup(func()) +}) *IDB { + mock := &IDB{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 90cd65f3a603b540a36f13fcf7a3967177df8991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 12:46:52 -0300 Subject: [PATCH 4/9] feat: thumb components cmd --- cmd/http/main.go | 34 +++++++++++++------------ internal/adapters/rest/server/server.go | 9 +++++-- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/cmd/http/main.go b/cmd/http/main.go index ee530f7..2b026d0 100644 --- a/cmd/http/main.go +++ b/cmd/http/main.go @@ -6,7 +6,9 @@ import ( _ "github.com/pangolin-do-golang/thumb-processor-api/docs" dbAdapter "github.com/pangolin-do-golang/thumb-processor-api/internal/adapters/db" "github.com/pangolin-do-golang/thumb-processor-api/internal/adapters/rest/server" + "github.com/pangolin-do-golang/thumb-processor-api/internal/adapters/sqs" "github.com/pangolin-do-golang/thumb-processor-api/internal/config" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/thumb" "gorm.io/driver/postgres" "gorm.io/gorm" ) @@ -18,32 +20,32 @@ import ( // @host localhost:8080 // @BasePath / func main() { - _, err := initDb() + cfg, err := config.Load() if err != nil { - panic(err) + log.Fatalln(err) } - /** - - _ := dbAdapter.NewPostgresThumbRepository(db) - - _ := thumb.NewThumbService() + databaseAdapter, err := newDatabaseConnection(cfg) + if err != nil { + log.Fatalln(err) + } - _ := dbAdapter.NewPostgresThumbRepository(db) + queueAdapter, err := sqs.NewSQSThumbQueue(cfg) + if err != nil { + log.Fatalln(err) + } - **/ + thumbRepository := dbAdapter.NewPostgresThumbRepository(databaseAdapter) + thumbService := thumb.NewThumbService(thumbRepository, queueAdapter) - restServer := server.NewRestServer(&server.RestServerOptions{}) + restServer := server.NewRestServer(&server.RestServerOptions{ + ThumService: thumbService, + }) restServer.Serve() } -func initDb() (*gorm.DB, error) { - cfg, err := config.Load() - if err != nil { - log.Fatalln(err) - } - +func newDatabaseConnection(cfg *config.Config) (*gorm.DB, error) { db, err := gorm.Open(postgres.Open(cfg.DB.GetDNS()), &gorm.Config{}) if err != nil { log.Panic(err) diff --git a/internal/adapters/rest/server/server.go b/internal/adapters/rest/server/server.go index 26e9433..0cb8cb1 100644 --- a/internal/adapters/rest/server/server.go +++ b/internal/adapters/rest/server/server.go @@ -4,17 +4,22 @@ import ( "github.com/gin-gonic/gin" "github.com/pangolin-do-golang/thumb-processor-api/internal/adapters/rest/handler" "github.com/pangolin-do-golang/thumb-processor-api/internal/adapters/rest/middleware" + "github.com/pangolin-do-golang/thumb-processor-api/internal/core/thumb" "github.com/pangolin-do-golang/thumb-processor-api/internal/core/users" ) type RestServer struct { + thumbService thumb.IThumbService } type RestServerOptions struct { + ThumService thumb.IThumbService } -func NewRestServer(_ *RestServerOptions) *RestServer { - return &RestServer{} +func NewRestServer(opts *RestServerOptions) *RestServer { + return &RestServer{ + thumbService: opts.ThumService, + } } func (rs RestServer) Serve() { From 60434e1e870da29825af8e6cc3b25570bfd71cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 12:47:07 -0300 Subject: [PATCH 5/9] refactor: thumb repository --- internal/adapters/db/thumb_repository.go | 24 +++++++++++++++---- internal/adapters/db/thumb_repository_test.go | 11 +++++---- ... => thumb_repository_adapter_interface.go} | 0 3 files changed, 26 insertions(+), 9 deletions(-) rename internal/core/domain/contracts/{thumb_repository_adapter_interface copy.go => thumb_repository_adapter_interface.go} (100%) diff --git a/internal/adapters/db/thumb_repository.go b/internal/adapters/db/thumb_repository.go index ea872f1..bc31470 100644 --- a/internal/adapters/db/thumb_repository.go +++ b/internal/adapters/db/thumb_repository.go @@ -36,7 +36,7 @@ func (r *PostgresThumbRepository) Create(process *entity.ThumbProcess) error { return result.Error } -func (r *PostgresThumbRepository) List() []entity.ThumbProcess { +func (r *PostgresThumbRepository) List() *[]entity.ThumbProcess { processes := []entity.ThumbProcess{} records := []ThumbPostgres{} @@ -56,16 +56,16 @@ func (r *PostgresThumbRepository) List() []entity.ThumbProcess { }) } - return processes + return &processes } func NewPostgresThumbRepository(db IDB) *PostgresThumbRepository { return &PostgresThumbRepository{db: db} } -func (r *PostgresThumbRepository) Update(process *entity.ThumbProcess) error { +func (r *PostgresThumbRepository) Update(process *entity.ThumbProcess) (*entity.ThumbProcess, error) { if process.ID == uuid.Nil { - return errors.New("process id is required") + return nil, errors.New("process id is required") } result := r.db.Save(&ThumbPostgres{ @@ -78,5 +78,19 @@ func (r *PostgresThumbRepository) Update(process *entity.ThumbProcess) error { Error: process.Error, }) - return result.Error + if result.Error != nil { + return nil, result.Error + } + + return &entity.ThumbProcess{ + ID: process.ID, + Status: process.Status, + Error: process.Error, + Video: entity.ThumbProcessVideo{ + Path: process.Video.Path, + }, + Thumbnail: entity.ThumbProcessThumb{ + Path: process.Thumbnail.Path, + }, + }, nil } diff --git a/internal/adapters/db/thumb_repository_test.go b/internal/adapters/db/thumb_repository_test.go index b7d5b4f..c2bab3c 100644 --- a/internal/adapters/db/thumb_repository_test.go +++ b/internal/adapters/db/thumb_repository_test.go @@ -88,7 +88,7 @@ func TestPostgresThumbRepository_List(t *testing.T) { } }).Return(&gorm.DB{}).Once() - processes := repo.List() + processes := *repo.List() assert.Len(t, processes, 2) assert.Equal(t, "video1.mp4", processes[0].Video.Path) @@ -132,9 +132,10 @@ func TestPostgresThumbRepository_Update(t *testing.T) { mockDB.On("Save", mock.AnythingOfType("*db.ThumbPostgres")).Return(&gorm.DB{}).Once() - err := repo.Update(process) + updated, err := repo.Update(process) assert.NoError(t, err) + assert.NotNil(t, updated) }) t.Run("update with missing ID", func(t *testing.T) { @@ -149,8 +150,9 @@ func TestPostgresThumbRepository_Update(t *testing.T) { Error: "", } - err := repo.Update(process) + updated, err := repo.Update(process) + assert.Nil(t, updated) assert.Error(t, err) assert.Equal(t, "process id is required", err.Error()) mockDB.AssertNotCalled(t, "Save") @@ -173,8 +175,9 @@ func TestPostgresThumbRepository_Update(t *testing.T) { expectedError := errors.New("database error") mockDB.On("Save", mock.AnythingOfType("*db.ThumbPostgres")).Return(&gorm.DB{Error: expectedError}).Once() - err := repo.Update(process) + updated, err := repo.Update(process) + assert.Nil(t, updated) assert.Error(t, err) assert.Equal(t, expectedError, err) }) diff --git a/internal/core/domain/contracts/thumb_repository_adapter_interface copy.go b/internal/core/domain/contracts/thumb_repository_adapter_interface.go similarity index 100% rename from internal/core/domain/contracts/thumb_repository_adapter_interface copy.go rename to internal/core/domain/contracts/thumb_repository_adapter_interface.go From eb907ba3e9f0b924fcb6f341c134135e8142fa05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 13:47:15 -0300 Subject: [PATCH 6/9] tests: add thumb handler subtests --- .../rest/handler/thumb_handler_test.go | 97 +++++++++++++++++++ internal/config/config_test.go | 20 ---- 2 files changed, 97 insertions(+), 20 deletions(-) diff --git a/internal/adapters/rest/handler/thumb_handler_test.go b/internal/adapters/rest/handler/thumb_handler_test.go index d807e20..7cd5e0d 100644 --- a/internal/adapters/rest/handler/thumb_handler_test.go +++ b/internal/adapters/rest/handler/thumb_handler_test.go @@ -3,6 +3,7 @@ package handler import ( "bytes" "encoding/json" + "errors" "net/http" "net/http/httptest" "testing" @@ -52,6 +53,42 @@ func TestCreateProcess(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) }) + + t.Run("service error", func(t *testing.T) { + mockService.On("CreateProcessAsync", mock.AnythingOfType("*ports.CreateProcessRequest")). + Return(errors.New("service error")).Once() + + body := CreateProcessRequest{URL: "https://example.com/video.mp4"} + jsonBody, _ := json.Marshal(body) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/thumbs", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + + var response ErrorResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "service error", response.Error) + }) + + t.Run("api bind json error", func(t *testing.T) { + invalidJSON := []byte(`{"url": invalid-json}`) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/thumbs", bytes.NewBuffer(invalidJSON)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + + var response ErrorResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Invalid request format", response.Error) + }) } func TestUpdateProcess(t *testing.T) { @@ -82,6 +119,66 @@ func TestUpdateProcess(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code) }) + + t.Run("api bind ID param error", func(t *testing.T) { + body := UpdateProcessRequest{ + Status: "completed", + ThumbnailPath: "path/to/thumbnail.jpg", + } + jsonBody, _ := json.Marshal(body) + + w := httptest.NewRecorder() + + req, _ := http.NewRequest("PUT", "/thumbs/invalid-uuid", bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + + var response ErrorResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Process ID is required", response.Error) + }) + + t.Run("api bind json error", func(t *testing.T) { + invalidJSON := []byte(`{"status": invalid-json}`) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/thumbs/"+mockedUUID.String(), bytes.NewBuffer(invalidJSON)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusBadRequest, w.Code) + + var response ErrorResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "Invalid request format", response.Error) + }) + + t.Run("service error", func(t *testing.T) { + mockService.On("UpdateProcess", mock.AnythingOfType("*ports.UpdateProcessRequest")). + Return(nil, errors.New("service error")).Once() + + body := UpdateProcessRequest{ + Status: "completed", + ThumbnailPath: "path/to/thumbnail.jpg", + } + jsonBody, _ := json.Marshal(body) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/thumbs/"+mockedUUID.String(), bytes.NewBuffer(jsonBody)) + req.Header.Set("Content-Type", "application/json") + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + + var response ErrorResponse + err := json.Unmarshal(w.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "service error", response.Error) + }) } func TestListProcesses(t *testing.T) { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7828065..b676f5f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -89,23 +89,3 @@ func TestLoad(t *testing.T) { }) } } - -// Example of a test for a function that uses the config -func TestSomethingThatUsesConfig(t *testing.T) { - // Set necessary environment variables - os.Setenv("S3_BUCKET", "test-bucket") - os.Setenv("SQS_QUEUE_URL", "test-queue") - defer func() { - os.Unsetenv("S3_BUCKET") - os.Unsetenv("SQS_QUEUE_URL") - }() - - cfg, err := Load() - assert.NoError(t, err) - - // Now you can use cfg in your assertions or test logic - assert.Equal(t, "test-bucket", cfg.S3.Bucket) - assert.Equal(t, "test-queue", cfg.SQS.QueueURL) - - // ... your test logic here ... -} From 1266d130523a2157651ececde5559a5c5629260e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 14:00:14 -0300 Subject: [PATCH 7/9] chore: update API docs --- docs/docs.go | 212 ++++++++++++++++++ docs/swagger.json | 212 ++++++++++++++++++ docs/swagger.yaml | 140 ++++++++++++ internal/adapters/rest/handler/healthcheck.go | 4 +- .../adapters/rest/handler/thumb_handler.go | 8 +- internal/adapters/rest/server/server.go | 2 + 6 files changed, 573 insertions(+), 5 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index 4301abc..08b5c47 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -15,6 +15,27 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { + "/health": { + "get": { + "description": "Checks the health status of the application.", + "produces": [ + "application/json" + ], + "tags": [ + "Health Check" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/login": { "get": { "description": "Authenticates a user using Basic Authentication and returns user information.", @@ -49,6 +70,138 @@ const docTemplate = `{ } } }, + "/thumbs": { + "get": { + "description": "Get a list of all thumbnail processes", + "produces": [ + "application/json" + ], + "tags": [ + "Video Thumbs Processor" + ], + "summary": "List all thumbnail processes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.ThumbProcessResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + }, + "post": { + "description": "Start a new asynchronous thumbnail generation process from S3 video URL", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Video Thumbs Processor" + ], + "summary": "Create a new thumbnail process", + "parameters": [ + { + "description": "Video URL", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CreateProcessRequest" + } + } + ], + "responses": { + "202": { + "description": "Process started", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/thumbs/{id}": { + "put": { + "description": "Update the status of an existing thumbnail process", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Video Thumbs Processor" + ], + "summary": "Update a thumbnail process", + "parameters": [ + { + "type": "string", + "description": "Process ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Process update information", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateProcessRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ThumbProcessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, "/user": { "post": { "description": "Creates a new user with the provided nickname and password.", @@ -87,6 +240,17 @@ const docTemplate = `{ } }, "definitions": { + "handler.CreateProcessRequest": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + }, "handler.CreateUserRequest": { "type": "object", "required": [ @@ -101,6 +265,54 @@ const docTemplate = `{ "type": "string" } } + }, + "handler.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "handler.ThumbProcessResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "error": { + "type": "string" + }, + "id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "thumbnail_path": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.UpdateProcessRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "error": { + "type": "string" + }, + "status": { + "type": "string" + }, + "thumbnail_path": { + "type": "string" + } + } } } }` diff --git a/docs/swagger.json b/docs/swagger.json index bc416ac..69af02c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -9,6 +9,27 @@ "host": "localhost:8080", "basePath": "/", "paths": { + "/health": { + "get": { + "description": "Checks the health status of the application.", + "produces": [ + "application/json" + ], + "tags": [ + "Health Check" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, "/login": { "get": { "description": "Authenticates a user using Basic Authentication and returns user information.", @@ -43,6 +64,138 @@ } } }, + "/thumbs": { + "get": { + "description": "Get a list of all thumbnail processes", + "produces": [ + "application/json" + ], + "tags": [ + "Video Thumbs Processor" + ], + "summary": "List all thumbnail processes", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.ThumbProcessResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + }, + "post": { + "description": "Start a new asynchronous thumbnail generation process from S3 video URL", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Video Thumbs Processor" + ], + "summary": "Create a new thumbnail process", + "parameters": [ + { + "description": "Video URL", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CreateProcessRequest" + } + } + ], + "responses": { + "202": { + "description": "Process started", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, + "/thumbs/{id}": { + "put": { + "description": "Update the status of an existing thumbnail process", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Video Thumbs Processor" + ], + "summary": "Update a thumbnail process", + "parameters": [ + { + "type": "string", + "description": "Process ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Process update information", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateProcessRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ThumbProcessResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ErrorResponse" + } + } + } + } + }, "/user": { "post": { "description": "Creates a new user with the provided nickname and password.", @@ -81,6 +234,17 @@ } }, "definitions": { + "handler.CreateProcessRequest": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string" + } + } + }, "handler.CreateUserRequest": { "type": "object", "required": [ @@ -95,6 +259,54 @@ "type": "string" } } + }, + "handler.ErrorResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + } + }, + "handler.ThumbProcessResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "error": { + "type": "string" + }, + "id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "thumbnail_path": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.UpdateProcessRequest": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "error": { + "type": "string" + }, + "status": { + "type": "string" + }, + "thumbnail_path": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2a834e4..2839c39 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,5 +1,12 @@ basePath: / definitions: + handler.CreateProcessRequest: + properties: + url: + type: string + required: + - url + type: object handler.CreateUserRequest: properties: nickname: @@ -10,6 +17,37 @@ definitions: - nickname - password type: object + handler.ErrorResponse: + properties: + error: + type: string + type: object + handler.ThumbProcessResponse: + properties: + created_at: + type: string + error: + type: string + id: + type: string + status: + type: string + thumbnail_path: + type: string + updated_at: + type: string + type: object + handler.UpdateProcessRequest: + properties: + error: + type: string + status: + type: string + thumbnail_path: + type: string + required: + - status + type: object host: localhost:8080 info: contact: {} @@ -17,6 +55,20 @@ info: title: Thumb processor worker version: 0.1.0 paths: + /health: + get: + description: Checks the health status of the application. + produces: + - application/json + responses: + "200": + description: OK + schema: + additionalProperties: true + type: object + summary: Health Check + tags: + - Health Check /login: get: consumes: @@ -41,6 +93,94 @@ paths: summary: User Login tags: - Auth + /thumbs: + get: + description: Get a list of all thumbnail processes + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/handler.ThumbProcessResponse' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.ErrorResponse' + summary: List all thumbnail processes + tags: + - Video Thumbs Processor + post: + consumes: + - application/json + description: Start a new asynchronous thumbnail generation process from S3 video + URL + parameters: + - description: Video URL + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.CreateProcessRequest' + produces: + - application/json + responses: + "202": + description: Process started + schema: + type: string + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.ErrorResponse' + summary: Create a new thumbnail process + tags: + - Video Thumbs Processor + /thumbs/{id}: + put: + consumes: + - application/json + description: Update the status of an existing thumbnail process + parameters: + - description: Process ID + in: path + name: id + required: true + type: string + - description: Process update information + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdateProcessRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ThumbProcessResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/handler.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.ErrorResponse' + summary: Update a thumbnail process + tags: + - Video Thumbs Processor /user: post: consumes: diff --git a/internal/adapters/rest/handler/healthcheck.go b/internal/adapters/rest/handler/healthcheck.go index eb709f0..f836154 100644 --- a/internal/adapters/rest/handler/healthcheck.go +++ b/internal/adapters/rest/handler/healthcheck.go @@ -1,12 +1,14 @@ package handler import ( - "github.com/gin-gonic/gin" "net/http" + + "github.com/gin-gonic/gin" ) // RegisterHealthCheck registers the health check endpoint with the given Gin engine. // +// @Tags Health Check // @Summary Health Check // @Description Checks the health status of the application. // @Produce json diff --git a/internal/adapters/rest/handler/thumb_handler.go b/internal/adapters/rest/handler/thumb_handler.go index 7868047..c3d2af3 100644 --- a/internal/adapters/rest/handler/thumb_handler.go +++ b/internal/adapters/rest/handler/thumb_handler.go @@ -28,8 +28,8 @@ func (h *ThumbHandler) RegisterRoutes(router *gin.Engine) { } // @Summary Create a new thumbnail process -// @Description Start a new asynchronous thumbnail generation process -// @Tags thumbs +// @Description Start a new asynchronous thumbnail generation process from S3 video URL +// @Tags Video Thumbs Processor // @Accept json // @Produce json // @Param request body CreateProcessRequest true "Video URL" @@ -70,7 +70,7 @@ func (h *ThumbHandler) CreateProcess(c *gin.Context) { // @Summary Update a thumbnail process // @Description Update the status of an existing thumbnail process -// @Tags thumbs +// @Tags Video Thumbs Processor // @Accept json // @Produce json // @Param id path string true "Process ID" @@ -121,7 +121,7 @@ func (h *ThumbHandler) UpdateProcess(c *gin.Context) { // @Summary List all thumbnail processes // @Description Get a list of all thumbnail processes -// @Tags thumbs +// @Tags Video Thumbs Processor // @Produce json // @Success 200 {array} ThumbProcessResponse // @Failure 500 {object} ErrorResponse diff --git a/internal/adapters/rest/server/server.go b/internal/adapters/rest/server/server.go index 0cb8cb1..a98c353 100644 --- a/internal/adapters/rest/server/server.go +++ b/internal/adapters/rest/server/server.go @@ -37,6 +37,8 @@ func (rs RestServer) Serve() { handler.RegisterLoginHandlers(authorizedGroup) + handler.NewThumbHandler(rs.thumbService).RegisterRoutes(r) + err := r.Run("0.0.0.0:8080") if err != nil { panic(err) From 49110956590a2b1eb87f04e1605884a679e0ad37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=ADrio=20Neto?= Date: Sat, 8 Feb 2025 14:02:25 -0300 Subject: [PATCH 8/9] fix: apply authorizedGroup on thumb resources --- internal/adapters/rest/handler/thumb_handler.go | 2 +- internal/adapters/rest/server/server.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/adapters/rest/handler/thumb_handler.go b/internal/adapters/rest/handler/thumb_handler.go index c3d2af3..1869fdd 100644 --- a/internal/adapters/rest/handler/thumb_handler.go +++ b/internal/adapters/rest/handler/thumb_handler.go @@ -20,7 +20,7 @@ func NewThumbHandler(service thumb.IThumbService) *ThumbHandler { } } -func (h *ThumbHandler) RegisterRoutes(router *gin.Engine) { +func (h *ThumbHandler) RegisterRoutes(router *gin.RouterGroup) { thumbGroup := router.Group("/thumbs") thumbGroup.POST("", h.CreateProcess) thumbGroup.PUT("/:id", h.UpdateProcess) diff --git a/internal/adapters/rest/server/server.go b/internal/adapters/rest/server/server.go index a98c353..739c363 100644 --- a/internal/adapters/rest/server/server.go +++ b/internal/adapters/rest/server/server.go @@ -37,7 +37,7 @@ func (rs RestServer) Serve() { handler.RegisterLoginHandlers(authorizedGroup) - handler.NewThumbHandler(rs.thumbService).RegisterRoutes(r) + handler.NewThumbHandler(rs.thumbService).RegisterRoutes(authorizedGroup) err := r.Run("0.0.0.0:8080") if err != nil { From ce51f069d8f2a46a2867773fe402eb973a6388d9 Mon Sep 17 00:00:00 2001 From: Kevin Martins Date: Sat, 8 Feb 2025 20:38:03 +0000 Subject: [PATCH 9/9] Fix testy --- internal/adapters/rest/handler/thumb_handler_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/adapters/rest/handler/thumb_handler_test.go b/internal/adapters/rest/handler/thumb_handler_test.go index 7cd5e0d..d50bf84 100644 --- a/internal/adapters/rest/handler/thumb_handler_test.go +++ b/internal/adapters/rest/handler/thumb_handler_test.go @@ -21,7 +21,8 @@ func setupTest() (*gin.Engine, *servicemocks.IThumbService) { router := gin.New() mockService := new(servicemocks.IThumbService) handler := NewThumbHandler(mockService) - handler.RegisterRoutes(router) + group := router.Group("/") + handler.RegisterRoutes(group) return router, mockService }