From 0c0b692850a512672628ac02517e578898e8eb2d Mon Sep 17 00:00:00 2001 From: Felipe Luz Oliveira <75860661+felipebrsk@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:39:32 -0300 Subject: [PATCH] feat: adding outline for games (#12) --- .github/workflows/ci.yml | 1 + DONE.md | 31 ++ TODO.md | 9 + cmd/server/main.go | 2 + cmd/server/routes/init_handlers.go | 6 +- cmd/server/routes/routes.go | 7 +- di/di.go | 5 +- di/migrations.go | 32 +- di/setup_dependencies.go | 6 +- go.mod | 2 + go.sum | 2 + internal/adapters/api/game_handler.go | 49 +++ internal/adapters/db/game_repository_mysql.go | 69 +++ internal/domain/categoriable.go | 20 + internal/domain/commentable.go | 32 ++ internal/domain/crack.go | 39 ++ internal/domain/cracker.go | 27 ++ internal/domain/critic.go | 27 ++ internal/domain/criticable.go | 32 ++ internal/domain/developer.go | 27 ++ internal/domain/dlc.go | 32 ++ internal/domain/dlc_store.go | 30 ++ internal/domain/galleriable.go | 28 ++ internal/domain/game.go | 63 +++ internal/domain/game_developer.go | 29 ++ internal/domain/game_language.go | 32 ++ internal/domain/game_publisher.go | 29 ++ internal/domain/game_store.go | 30 ++ internal/domain/game_support.go | 30 ++ internal/domain/genreable.go | 20 + internal/domain/heartable.go | 28 ++ internal/domain/language.go | 27 ++ internal/domain/platformable.go | 20 + internal/domain/protection.go | 26 ++ internal/domain/publisher.go | 27 ++ internal/domain/requirement.go | 37 ++ internal/domain/requirement_type.go | 36 ++ internal/domain/reviewable.go | 31 ++ internal/domain/store.go | 28 ++ internal/domain/taggable.go | 20 + internal/domain/torrent.go | 31 ++ internal/domain/torrent_provider.go | 27 ++ internal/domain/user.go | 1 + internal/domain/validations.go | 18 + internal/domain/viewable.go | 29 ++ internal/ports/game_repository.go | 7 + internal/resources/category_resource.go | 25 ++ internal/resources/commentable_resource.go | 34 ++ internal/resources/crack_resource.go | 40 ++ internal/resources/cracker_resource.go | 27 ++ internal/resources/critic_resource.go | 19 + internal/resources/criticable_resource.go | 31 ++ internal/resources/developer_resource.go | 27 ++ internal/resources/dlc_resource.go | 51 +++ internal/resources/dlc_store_resource.go | 19 + internal/resources/galleriable_resource.go | 33 ++ internal/resources/game_language_resource.go | 21 + internal/resources/game_resource.go | 269 ++++++++++++ internal/resources/game_store_resource.go | 19 + internal/resources/genre_resource.go | 25 ++ internal/resources/language_resource.go | 27 ++ internal/resources/platform_resource.go | 25 ++ internal/resources/protection_resource.go | 25 ++ internal/resources/publisher_resource.go | 27 ++ internal/resources/requirement_resource.go | 31 ++ .../resources/requirement_type_resource.go | 27 ++ internal/resources/review_resource.go | 42 ++ internal/resources/store_resource.go | 21 + internal/resources/support_resource.go | 21 + internal/resources/tag_resource.go | 25 ++ .../resources/torrent_provider_resource.go | 17 + internal/resources/torrent_resource.go | 32 ++ internal/resources/user_resource.go | 33 ++ internal/usecases/game_service.go | 18 + pkg/utils/utils.go | 8 + tests/unit/db/game_repository_mysql_test.go | 356 +++++++++++++++ tests/unit/domains/categoriable_test.go | 216 +++++++++ tests/unit/domains/commentable_test.go | 292 ++++++++++++ tests/unit/domains/crack_test.go | 315 +++++++++++++ tests/unit/domains/cracker_test.go | 252 +++++++++++ tests/unit/domains/critic_test.go | 314 +++++++++++++ tests/unit/domains/criticable_test.go | 288 ++++++++++++ tests/unit/domains/developer_test.go | 252 +++++++++++ tests/unit/domains/dlc_store_test.go | 312 +++++++++++++ tests/unit/domains/dlc_test.go | 304 +++++++++++++ tests/unit/domains/galleriable_test.go | 277 ++++++++++++ tests/unit/domains/game_developer_test.go | 287 ++++++++++++ tests/unit/domains/game_language_test.go | 321 ++++++++++++++ tests/unit/domains/game_publisher_test.go | 287 ++++++++++++ tests/unit/domains/game_store_test.go | 309 +++++++++++++ tests/unit/domains/game_support_test.go | 300 +++++++++++++ tests/unit/domains/game_test.go | 414 ++++++++++++++++++ tests/unit/domains/genreable_test.go | 216 +++++++++ tests/unit/domains/heartable_test.go | 282 ++++++++++++ tests/unit/domains/language_test.go | 309 +++++++++++++ tests/unit/domains/platformable_test.go | 216 +++++++++ tests/unit/domains/protection_test.go | 243 ++++++++++ tests/unit/domains/publisher_test.go | 252 +++++++++++ tests/unit/domains/requirement_test.go | 367 ++++++++++++++++ tests/unit/domains/requirement_type_test.go | 282 ++++++++++++ tests/unit/domains/reviewable_test.go | 314 +++++++++++++ tests/unit/domains/store_test.go | 323 ++++++++++++++ tests/unit/domains/taggable_test.go | 216 +++++++++ tests/unit/domains/torrent_provider_test.go | 253 +++++++++++ tests/unit/domains/torrent_test.go | 307 +++++++++++++ tests/unit/domains/viewable_test.go | 261 +++++++++++ tests/unit/ports/game_repository_test.go | 95 ++++ .../unit/resources/category_resource_test.go | 103 +++++ .../resources/commentable_resource_test.go | 174 ++++++++ tests/unit/resources/crack_resource_test.go | 85 ++++ tests/unit/resources/cracker_resource_test.go | 89 ++++ tests/unit/resources/critic_resource_test.go | 49 +++ .../resources/criticable_resource_test.go | 66 +++ .../unit/resources/developer_resource_test.go | 89 ++++ tests/unit/resources/dlc_resource_test.go | 127 ++++++ .../unit/resources/dlc_store_resource_test.go | 72 +++ .../resources/galleriable_resource_test.go | 56 +++ .../resources/game_language_resource_test.go | 74 ++++ tests/unit/resources/game_resource_test.go | 362 +++++++++++++++ .../resources/game_store_resource_test.go | 84 ++++ .../resources/game_support_resource_test.go | 58 +++ tests/unit/resources/genre_resource_test.go | 103 +++++ .../unit/resources/language_resource_test.go | 109 +++++ .../unit/resources/platform_resource_test.go | 103 +++++ .../resources/protection_resource_test.go | 83 ++++ .../unit/resources/publisher_resource_test.go | 89 ++++ .../resources/requirement_resource_test.go | 95 ++++ .../requirement_type_resource_test.go | 98 +++++ tests/unit/resources/review_resource_test.go | 186 ++++++++ tests/unit/resources/store_resource_test.go | 51 +++ tests/unit/resources/tag_resource_test.go | 103 +++++ .../torrent_provider_resource_test.go | 41 ++ tests/unit/resources/torrent_resource_test.go | 142 ++++++ 133 files changed, 13878 insertions(+), 5 deletions(-) create mode 100644 internal/adapters/api/game_handler.go create mode 100644 internal/adapters/db/game_repository_mysql.go create mode 100644 internal/domain/categoriable.go create mode 100644 internal/domain/commentable.go create mode 100644 internal/domain/crack.go create mode 100644 internal/domain/cracker.go create mode 100644 internal/domain/critic.go create mode 100644 internal/domain/criticable.go create mode 100644 internal/domain/developer.go create mode 100644 internal/domain/dlc.go create mode 100644 internal/domain/dlc_store.go create mode 100644 internal/domain/galleriable.go create mode 100644 internal/domain/game.go create mode 100644 internal/domain/game_developer.go create mode 100644 internal/domain/game_language.go create mode 100644 internal/domain/game_publisher.go create mode 100644 internal/domain/game_store.go create mode 100644 internal/domain/game_support.go create mode 100644 internal/domain/genreable.go create mode 100644 internal/domain/heartable.go create mode 100644 internal/domain/language.go create mode 100644 internal/domain/platformable.go create mode 100644 internal/domain/protection.go create mode 100644 internal/domain/publisher.go create mode 100644 internal/domain/requirement.go create mode 100644 internal/domain/requirement_type.go create mode 100644 internal/domain/reviewable.go create mode 100644 internal/domain/store.go create mode 100644 internal/domain/taggable.go create mode 100644 internal/domain/torrent.go create mode 100644 internal/domain/torrent_provider.go create mode 100644 internal/domain/viewable.go create mode 100644 internal/ports/game_repository.go create mode 100644 internal/resources/category_resource.go create mode 100644 internal/resources/commentable_resource.go create mode 100644 internal/resources/crack_resource.go create mode 100644 internal/resources/cracker_resource.go create mode 100644 internal/resources/critic_resource.go create mode 100644 internal/resources/criticable_resource.go create mode 100644 internal/resources/developer_resource.go create mode 100644 internal/resources/dlc_resource.go create mode 100644 internal/resources/dlc_store_resource.go create mode 100644 internal/resources/galleriable_resource.go create mode 100644 internal/resources/game_language_resource.go create mode 100644 internal/resources/game_resource.go create mode 100644 internal/resources/game_store_resource.go create mode 100644 internal/resources/genre_resource.go create mode 100644 internal/resources/language_resource.go create mode 100644 internal/resources/platform_resource.go create mode 100644 internal/resources/protection_resource.go create mode 100644 internal/resources/publisher_resource.go create mode 100644 internal/resources/requirement_resource.go create mode 100644 internal/resources/requirement_type_resource.go create mode 100644 internal/resources/review_resource.go create mode 100644 internal/resources/store_resource.go create mode 100644 internal/resources/support_resource.go create mode 100644 internal/resources/tag_resource.go create mode 100644 internal/resources/torrent_provider_resource.go create mode 100644 internal/resources/torrent_resource.go create mode 100644 internal/usecases/game_service.go create mode 100644 tests/unit/db/game_repository_mysql_test.go create mode 100644 tests/unit/domains/categoriable_test.go create mode 100644 tests/unit/domains/commentable_test.go create mode 100644 tests/unit/domains/crack_test.go create mode 100644 tests/unit/domains/cracker_test.go create mode 100644 tests/unit/domains/critic_test.go create mode 100644 tests/unit/domains/criticable_test.go create mode 100644 tests/unit/domains/developer_test.go create mode 100644 tests/unit/domains/dlc_store_test.go create mode 100644 tests/unit/domains/dlc_test.go create mode 100644 tests/unit/domains/galleriable_test.go create mode 100644 tests/unit/domains/game_developer_test.go create mode 100644 tests/unit/domains/game_language_test.go create mode 100644 tests/unit/domains/game_publisher_test.go create mode 100644 tests/unit/domains/game_store_test.go create mode 100644 tests/unit/domains/game_support_test.go create mode 100644 tests/unit/domains/game_test.go create mode 100644 tests/unit/domains/genreable_test.go create mode 100644 tests/unit/domains/heartable_test.go create mode 100644 tests/unit/domains/language_test.go create mode 100644 tests/unit/domains/platformable_test.go create mode 100644 tests/unit/domains/protection_test.go create mode 100644 tests/unit/domains/publisher_test.go create mode 100644 tests/unit/domains/requirement_test.go create mode 100644 tests/unit/domains/requirement_type_test.go create mode 100644 tests/unit/domains/reviewable_test.go create mode 100644 tests/unit/domains/store_test.go create mode 100644 tests/unit/domains/taggable_test.go create mode 100644 tests/unit/domains/torrent_provider_test.go create mode 100644 tests/unit/domains/torrent_test.go create mode 100644 tests/unit/domains/viewable_test.go create mode 100644 tests/unit/ports/game_repository_test.go create mode 100644 tests/unit/resources/category_resource_test.go create mode 100644 tests/unit/resources/commentable_resource_test.go create mode 100644 tests/unit/resources/crack_resource_test.go create mode 100644 tests/unit/resources/cracker_resource_test.go create mode 100644 tests/unit/resources/critic_resource_test.go create mode 100644 tests/unit/resources/criticable_resource_test.go create mode 100644 tests/unit/resources/developer_resource_test.go create mode 100644 tests/unit/resources/dlc_resource_test.go create mode 100644 tests/unit/resources/dlc_store_resource_test.go create mode 100644 tests/unit/resources/galleriable_resource_test.go create mode 100644 tests/unit/resources/game_language_resource_test.go create mode 100644 tests/unit/resources/game_resource_test.go create mode 100644 tests/unit/resources/game_store_resource_test.go create mode 100644 tests/unit/resources/game_support_resource_test.go create mode 100644 tests/unit/resources/genre_resource_test.go create mode 100644 tests/unit/resources/language_resource_test.go create mode 100644 tests/unit/resources/platform_resource_test.go create mode 100644 tests/unit/resources/protection_resource_test.go create mode 100644 tests/unit/resources/publisher_resource_test.go create mode 100644 tests/unit/resources/requirement_resource_test.go create mode 100644 tests/unit/resources/requirement_type_resource_test.go create mode 100644 tests/unit/resources/review_resource_test.go create mode 100644 tests/unit/resources/store_resource_test.go create mode 100644 tests/unit/resources/tag_resource_test.go create mode 100644 tests/unit/resources/torrent_provider_resource_test.go create mode 100644 tests/unit/resources/torrent_resource_test.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 992f851..6dcfb7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload results to Codecov + if: github.event.pull_request.draft == false uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/DONE.md b/DONE.md index 37924ad..f559963 100644 --- a/DONE.md +++ b/DONE.md @@ -98,5 +98,36 @@ - [x] Create tags table - [x] Create genres table - [x] Create platforms table +- [x] Create languages table +- [x] Adding outline for games + - [x] Associating games with platforms + - [x] Associating games with categories + - [x] Associating games with tags + - [x] Associating games with genres + - [x] Associating games with languages +- [x] Create association for games or DLCs + - [x] Create torrent websites (such as firgitl, skidrow etc) + - [x] Create the kind of protections (such as Denuvo, Steam, GOG etc) + - [x] Create game developers (such as Game Science) + - [x] Create game publishers (such as Game Science) + - [x] Create requirement types + - [x] A requirement type should have a potential column, that should be enum, with minimum, maximum or recommended + - [x] A requirement type should have a type column, that should be enum, with windows, mac or linux + - [x] Create requirements and associate with requirement types +- [x] Create game outline system + - [x] Add game DLCs + - [x] Add game critics + - [x] Add game torrents + - [x] Add game crack + - [x] Add game reviews + - [x] Add game galleries + - [x] Add game publishers + - [x] Add game developers + - [x] Add game requirements + - [x] Add game comments (for torrents section) + - [x] Add game support + - [x] Add game hearts + - [x] Add game views + - [x] Add game stores ### Post MVP diff --git a/TODO.md b/TODO.md index 6f4f80b..615018a 100644 --- a/TODO.md +++ b/TODO.md @@ -20,6 +20,13 @@ - [ ] Review the action keys for missions and titles - [ ] Planning all missions and titles type - [ ] Planning all daily/weekly/monthly missions +- [x] Create viewable structure + - [x] Add game views count + - [ ] Add post views count +- [ ] Create heartable structure + - [x] Add game hearts count + - [ ] Add post hearts count + - [x] Add comment hearts count ### Post-MVP @@ -72,6 +79,7 @@ - [ ] AWS - [ ] SNS - [ ] Lambda +- [ ] Make user friend requests - [ ] Create a chat between users - [ ] User can be able to chat another users - [ ] User can be able to create a group and chat them @@ -79,6 +87,7 @@ - [ ] User can be able to add and remove members (creator or admin) - [ ] User can be able to add admins on groups (owner only) - [ ] Chat should use realtime + - [ ] User can chat only friends - check if possibility will be only to that ones that accepts the friend request - [ ] Create quizz that could reward with some coins and experience, maybe titles - [ ] Award with coins and experience on comment, heart a game, or something else - [ ] Heart a game; diff --git a/cmd/server/main.go b/cmd/server/main.go index 18d7fae..b9f314e 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -24,6 +24,7 @@ func main() { transactionService, notificationService, missionService, + gameService, db := di.InitDependencies() // Setup routes with dependency injection @@ -39,6 +40,7 @@ func main() { transactionService, notificationService, missionService, + gameService, ) c := cron.New() diff --git a/cmd/server/routes/init_handlers.go b/cmd/server/routes/init_handlers.go index 76c5c30..a4dade5 100644 --- a/cmd/server/routes/init_handlers.go +++ b/cmd/server/routes/init_handlers.go @@ -17,6 +17,7 @@ func InitHandlers( transactionService *usecases.TransactionService, notificationService *usecases.NotificationService, missionService *usecases.MissionService, + gameService *usecases.GameService, ) ( authHandler *api.AuthHandler, passwordResetHandler *api.PasswordResetHandler, @@ -27,6 +28,7 @@ func InitHandlers( transactionHandler *api.TransactionHandler, notificationHandler *api.NotificationHandler, missionHandler *api.MissionHandler, + gameHandler *api.GameHandler, ) { userHandler = api.NewUserHandler(userService) authHandler = api.NewAuthHandler(authService, userService) @@ -37,6 +39,7 @@ func InitHandlers( transactionHandler = api.NewTransactionHandler(transactionService, userService) notificationHandler = api.NewNotificationHandler(notificationService, userService) missionHandler = api.NewMissionHandler(missionService, userService) + gameHandler = api.NewGameHandler(gameService, userService) return authHandler, passwordResetHandler, @@ -46,5 +49,6 @@ func InitHandlers( titleHandler, transactionHandler, notificationHandler, - missionHandler + missionHandler, + gameHandler } diff --git a/cmd/server/routes/routes.go b/cmd/server/routes/routes.go index c7b3515..87ae585 100644 --- a/cmd/server/routes/routes.go +++ b/cmd/server/routes/routes.go @@ -23,6 +23,7 @@ func SetupRouter( transactionService *usecases.TransactionService, notificationService *usecases.NotificationService, missionService *usecases.MissionService, + gameService *usecases.GameService, ) *gin.Engine { r := gin.Default() env := config.LoadConfig() @@ -51,7 +52,8 @@ func SetupRouter( titleHandler, transactionHandler, notificationHandler, - missionHandler := InitHandlers( + missionHandler, + gameHandler := InitHandlers( authService, userService, passwordResetService, @@ -63,6 +65,7 @@ func SetupRouter( transactionService, notificationService, missionService, + gameService, ) // Define the middlewares @@ -108,6 +111,8 @@ func SetupRouter( protected.GET("/missions", missionHandler.GetAllForUser) protected.POST("/missions/:id/complete", missionHandler.CompleteMission) + + protected.GET("/games/:slug", gameHandler.FindBySlug) } // Common routes diff --git a/di/di.go b/di/di.go index 06c7615..5253fa5 100644 --- a/di/di.go +++ b/di/di.go @@ -25,6 +25,7 @@ func InitDependencies() ( *usecases.TransactionService, *usecases.NotificationService, *usecases.MissionService, + *usecases.GameService, *gorm.DB, ) { cfg := config.LoadConfig() @@ -50,7 +51,8 @@ func InitDependencies() ( walletService, transactionService, notificationService, - missionService := Setup(dbConn) + missionService, + gameService := Setup(dbConn) // Setup clients for non-test environment if cfg.ENV != "testing" { @@ -84,5 +86,6 @@ func InitDependencies() ( transactionService, notificationService, missionService, + gameService, dbConn } diff --git a/di/migrations.go b/di/migrations.go index 6687cda..ba6d8c6 100644 --- a/di/migrations.go +++ b/di/migrations.go @@ -8,7 +8,7 @@ import ( ) func MigrateModels(dbConn *gorm.DB) { - models := []interface{}{ + models := []any{ &domain.Reward{}, &domain.Level{}, &domain.Wallet{}, @@ -31,6 +31,36 @@ func MigrateModels(dbConn *gorm.DB) { &domain.Tag{}, &domain.Platform{}, &domain.Category{}, + &domain.Categoriable{}, + &domain.Genreable{}, + &domain.Taggable{}, + &domain.Platformable{}, + &domain.Language{}, + &domain.GameLanguage{}, + &domain.RequirementType{}, + &domain.Requirement{}, + &domain.Protection{}, + &domain.Cracker{}, + &domain.Crack{}, + &domain.TorrentProvider{}, + &domain.Torrent{}, + &domain.Publisher{}, + &domain.GamePublisher{}, + &domain.Developer{}, + &domain.GameDeveloper{}, + &domain.GameSupport{}, + &domain.Reviewable{}, + &domain.Viewable{}, + &domain.Heartable{}, + &domain.Critic{}, + &domain.Criticable{}, + &domain.Store{}, + &domain.GameStore{}, + &domain.Commentable{}, + &domain.Galleriable{}, + &domain.DLC{}, + &domain.DLCStore{}, + &domain.Game{}, } for _, model := range models { diff --git a/di/setup_dependencies.go b/di/setup_dependencies.go index ffd4dd9..1d2b970 100644 --- a/di/setup_dependencies.go +++ b/di/setup_dependencies.go @@ -19,6 +19,7 @@ func Setup(dbConn *gorm.DB) ( *usecases.TransactionService, *usecases.NotificationService, *usecases.MissionService, + *usecases.GameService, ) { // Create repository instances userRepo := db.NewUserRepositoryMySQL(dbConn) @@ -31,6 +32,7 @@ func Setup(dbConn *gorm.DB) ( transactionRepo := db.NewTransactionRepositoryMySQL(dbConn) notificationRepo := db.NewNotificationRepositoryMySQL(dbConn) missionRepo := db.NewMissionRepositoryMySQL(dbConn) + gameRepo := db.NewGameRepositoryMySQL(dbConn) // Create service instances userService := usecases.NewUserService(userRepo) @@ -44,6 +46,7 @@ func Setup(dbConn *gorm.DB) ( transactionService := usecases.NewTransactionService(transactionRepo) notificationService := usecases.NewNotificationService(notificationRepo) missionService := usecases.NewMissionService(missionRepo) + gameService := usecases.NewGameService(gameRepo) return userService, authService, @@ -55,5 +58,6 @@ func Setup(dbConn *gorm.DB) ( walletService, transactionService, notificationService, - missionService + missionService, + gameService } diff --git a/go.mod b/go.mod index e39ed32..30c4272 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,8 @@ require ( require github.com/aws/aws-sdk-go-v2/service/sqs v1.35.3 +require github.com/shopspring/decimal v1.4.0 // direct + require ( github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.5 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.17.35 // indirect diff --git a/go.sum b/go.sum index be5c6ff..50cf07f 100644 --- a/go.sum +++ b/go.sum @@ -135,6 +135,8 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 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= diff --git a/internal/adapters/api/game_handler.go b/internal/adapters/api/game_handler.go new file mode 100644 index 0000000..0d0f6d7 --- /dev/null +++ b/internal/adapters/api/game_handler.go @@ -0,0 +1,49 @@ +package api + +import ( + "gcstatus/internal/resources" + "gcstatus/internal/usecases" + "gcstatus/pkg/s3" + "gcstatus/pkg/utils" + "net/http" + + "github.com/gin-gonic/gin" +) + +type GameHandler struct { + gameService *usecases.GameService + userService *usecases.UserService +} + +func NewGameHandler( + gameService *usecases.GameService, + userService *usecases.UserService, +) *GameHandler { + return &GameHandler{ + gameService: gameService, + userService: userService, + } +} + +func (h *GameHandler) FindBySlug(c *gin.Context) { + slug := c.Param("slug") + user, err := utils.Auth(c, h.userService.GetUserByID) + if err != nil { + RespondWithError(c, http.StatusUnauthorized, "Unauthorized: "+err.Error()) + return + } + + game, err := h.gameService.FindBySlug(slug, user.ID) + if err != nil { + RespondWithError(c, http.StatusInternalServerError, "Failed to fetch game: "+err.Error()) + return + } + + transformedGame := resources.TransformGame(game, s3.GlobalS3Client) + + response := resources.Response{ + Data: transformedGame, + } + + c.JSON(http.StatusOK, response) +} diff --git a/internal/adapters/db/game_repository_mysql.go b/internal/adapters/db/game_repository_mysql.go new file mode 100644 index 0000000..6b55d58 --- /dev/null +++ b/internal/adapters/db/game_repository_mysql.go @@ -0,0 +1,69 @@ +package db + +import ( + "errors" + "gcstatus/internal/domain" + "gcstatus/internal/ports" + + "gorm.io/gorm" +) + +type GameRepositoryMySQL struct { + db *gorm.DB +} + +func NewGameRepositoryMySQL(db *gorm.DB) ports.GameRepository { + return &GameRepositoryMySQL{db: db} +} + +func (h *GameRepositoryMySQL) FindBySlug(slug string, userID uint) (domain.Game, error) { + var game domain.Game + if err := h.db.Preload("Categories.Category"). + Preload("Genres.Genre"). + Preload("Tags.Tag"). + Preload("Platforms.Platform"). + Preload("Languages.Language"). + Preload("Requirements.RequirementType"). + Preload("Crack.Cracker"). + Preload("Crack.Protection"). + Preload("Torrents.TorrentProvider"). + Preload("Publishers.Publisher"). + Preload("Developers.Developer"). + Preload("Reviews.User.Profile"). + Preload("Critics.Critic"). + Preload("Stores.Store"). + Preload("Galleries"). + Preload("DLCs.Galleries"). + Preload("DLCs.Platforms.Platform"). + Preload("DLCs.Stores.Store"). + Preload("Comments", "parent_id IS NULL"). + Preload("Comments.Hearts"). + Preload("Comments.User"). + Preload("Comments.Replies.User"). + Preload("Support"). + Preload("Views"). + Preload("Hearts"). + Where("slug = ?", slug). + First(&game). + Error; err != nil { + return game, err + } + + var view domain.Viewable + if err := h.db.Where("viewable_id = ? AND viewable_type = ? AND user_id = ?", game.ID, "games", userID).First(&view).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + view = domain.Viewable{ + ViewableID: game.ID, + ViewableType: "games", + UserID: userID, + } + if err := h.db.Create(&view).Error; err != nil { + return game, err + } + } else { + return game, err + } + } + + return game, nil +} diff --git a/internal/domain/categoriable.go b/internal/domain/categoriable.go new file mode 100644 index 0000000..fa15001 --- /dev/null +++ b/internal/domain/categoriable.go @@ -0,0 +1,20 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Categoriable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + CategoriableID uint `gorm:"index"` + CategoriableType string `gorm:"index"` + CategoryID uint `gorm:"index"` + Category Category `gorm:"foreignKey:CategoryID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time + + Categoriable any `gorm:"-"` +} diff --git a/internal/domain/commentable.go b/internal/domain/commentable.go new file mode 100644 index 0000000..d75b6b1 --- /dev/null +++ b/internal/domain/commentable.go @@ -0,0 +1,32 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Commentable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Comment string `gorm:"size:255;type:text;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time + UserID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + User User `gorm:"foreignKey:UserID"` + CommentableID uint `gorm:"index"` + CommentableType string `gorm:"index"` + ParentID *uint `gorm:"index"` + Replies []Commentable `gorm:"foreignKey:ParentID;constraint:OnDelete:CASCADE"` + Hearts []Heartable `gorm:"polymorphic:Heartable"` +} + +func (c *Commentable) ValidateCommentable() error { + Init() + + if err := validate.Struct(c); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/crack.go b/internal/domain/crack.go new file mode 100644 index 0000000..9e6f2a7 --- /dev/null +++ b/internal/domain/crack.go @@ -0,0 +1,39 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +const ( + CrackedStatus = "cracked" + UncrackedStatus = "uncracked" + CrackedSameDay = "cracked-oneday" +) + +type Crack struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Status string `gorm:"size:255;not null;type:enum('cracked','uncracked','cracked-oneday');default:uncracked" validate:"required"` + CrackedAt *time.Time + CreatedAt time.Time + UpdatedAt time.Time + CrackerID uint `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` + Cracker Cracker `gorm:"foreignKey:CrackerID;references:ID;"` + ProtectionID uint `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` + Protection Protection `gorm:"foreignKey:ProtectionID;references:ID;"` + GameID uint `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID;"` +} + +func (c *Crack) ValidateCrack() error { + Init() + + err := validate.Struct(c) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/cracker.go b/internal/domain/cracker.go new file mode 100644 index 0000000..09b20c4 --- /dev/null +++ b/internal/domain/cracker.go @@ -0,0 +1,27 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Cracker struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + Acting bool `gorm:"not null" validate:"boolean"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (c *Cracker) ValidateCracker() error { + Init() + + err := validate.Struct(c) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/critic.go b/internal/domain/critic.go new file mode 100644 index 0000000..b174d29 --- /dev/null +++ b/internal/domain/critic.go @@ -0,0 +1,27 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Critic struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + URL string `gorm:"size:255;not null" validate:"required"` + Logo string `gorm:"size:255;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (c *Critic) ValidateCritic() error { + Init() + + if err := validate.Struct(c); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/criticable.go b/internal/domain/criticable.go new file mode 100644 index 0000000..38a791a --- /dev/null +++ b/internal/domain/criticable.go @@ -0,0 +1,32 @@ +package domain + +import ( + "time" + + "github.com/shopspring/decimal" + "gorm.io/gorm" +) + +type Criticable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Rate decimal.Decimal `gorm:"not null;type:decimal(10,2)" validate:"required"` + URL string `gorm:"size:255;not null" validate:"required"` + PostedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time + CriticableID uint `gorm:"index"` + CriticableType string `gorm:"index"` + CriticID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Critic Critic `gorm:"foreignKey:CriticID"` +} + +func (c *Criticable) ValidateCriticable() error { + Init() + + if err := validate.Struct(c); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/developer.go b/internal/domain/developer.go new file mode 100644 index 0000000..42e2f92 --- /dev/null +++ b/internal/domain/developer.go @@ -0,0 +1,27 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Developer struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + Acting bool `gorm:"not null" validate:"boolean"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (d *Developer) ValidateDeveloper() error { + Init() + + err := validate.Struct(d) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/dlc.go b/internal/domain/dlc.go new file mode 100644 index 0000000..a315db3 --- /dev/null +++ b/internal/domain/dlc.go @@ -0,0 +1,32 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type DLC struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + Cover string `gorm:"size:255;not null" validate:"required"` + ReleaseDate time.Time + GameID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID"` + Galleries []Galleriable `gorm:"polymorphic:Galleriable"` + Platforms []Platformable `gorm:"polymorphic:Platformable;"` + Stores []DLCStore `gorm:"foreignKey:DLCID"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (d *DLC) ValidateDLC() error { + Init() + + if err := validate.Struct(d); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/dlc_store.go b/internal/domain/dlc_store.go new file mode 100644 index 0000000..f2eb41d --- /dev/null +++ b/internal/domain/dlc_store.go @@ -0,0 +1,30 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type DLCStore struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Price uint `gorm:"not null" validate:"required"` + URL string `gorm:"size:255;not null" validate:"required"` + DLCID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + DLC DLC `gorm:"foreignKey:DLCID;references:ID"` + StoreID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Store Store `gorm:"foreignKey:StoreID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (ds *DLCStore) ValidateDLCStore() error { + Init() + + if err := validate.Struct(ds); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/galleriable.go b/internal/domain/galleriable.go new file mode 100644 index 0000000..104b94c --- /dev/null +++ b/internal/domain/galleriable.go @@ -0,0 +1,28 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Galleriable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + S3 bool `gorm:"not null;default:false" validate:"boolean"` + Path string `gorm:"size:255;not null" validate:"required"` + GalleriableID uint `gorm:"index"` + GalleriableType string `gorm:"index"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (g *Galleriable) ValidateGalleriable() error { + Init() + + if err := validate.Struct(g); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/game.go b/internal/domain/game.go new file mode 100644 index 0000000..50b67ea --- /dev/null +++ b/internal/domain/game.go @@ -0,0 +1,63 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +const ( + HotCondition = "hot" + SaleCondition = "sale" + CommomCondition = "commom" + PopularCondition = "popular" +) + +type Game struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Age int `gorm:"not null" validate:"required,numeric"` + Slug string `gorm:"size:255;uniqueIndex;not null" validate:"required"` + Title string `gorm:"size:255;not null" validate:"required"` + Condition string `gorm:"size:255;not null;type:enum('hot','sale','popular','commom');default:commom" validate:"required"` + Cover string `gorm:"size:255" validate:"required"` + About string `gorm:"type:text" validate:"required"` + Description string `gorm:"type:text" validate:"required"` + ShortDescription string `gorm:"size:255" validate:"required"` + Free bool `gorm:"not null;default:false" validate:"boolean"` + Legal *string `gorm:"size:255"` + Website *string `gorm:"size:255"` + ReleaseDate time.Time `gorm:"size:255" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time + Views []Viewable `gorm:"polymorphic:Viewable"` + Hearts []Heartable `gorm:"polymorphic:Heartable"` + Categories []Categoriable `gorm:"polymorphic:Categoriable;"` + Tags []Taggable `gorm:"polymorphic:Taggable;"` + Genres []Genreable `gorm:"polymorphic:Genreable;"` + Platforms []Platformable `gorm:"polymorphic:Platformable;"` + Reviews []Reviewable `gorm:"polymorphic:Reviewable"` + Critics []Criticable `gorm:"polymorphic:Criticable"` + Comments []Commentable `gorm:"polymorphic:Commentable"` + Galleries []Galleriable `gorm:"polymorphic:Galleriable"` + Languages []GameLanguage `gorm:"foreignKey:GameID"` + Requirements []Requirement `gorm:"foreignKey:GameID"` + Torrents []Torrent `gorm:"foreignKey:GameID"` + Publishers []GamePublisher `gorm:"foreignKey:GameID"` + Developers []GameDeveloper `gorm:"foreignKey:GameID"` + Stores []GameStore `gorm:"foreignKey:GameID"` + DLCs []DLC `gorm:"foreignKey:GameID"` + Crack *Crack `gorm:"foreignKey:GameID"` + Support *GameSupport `gorm:"foreignKey:GameID"` +} + +func (g *Game) ValidateGame() error { + Init() + + err := validate.Struct(g) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/game_developer.go b/internal/domain/game_developer.go new file mode 100644 index 0000000..3b942d9 --- /dev/null +++ b/internal/domain/game_developer.go @@ -0,0 +1,29 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type GameDeveloper struct { + gorm.Model + ID uint `gorm:"primaryKey"` + GameID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID"` + DeveloperID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Developer Developer `gorm:"foreignKey:DeveloperID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (gp *GameDeveloper) ValidateGameDeveloper() error { + Init() + + err := validate.Struct(gp) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/game_language.go b/internal/domain/game_language.go new file mode 100644 index 0000000..c5236fb --- /dev/null +++ b/internal/domain/game_language.go @@ -0,0 +1,32 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type GameLanguage struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Menu bool `gorm:"not null;default:false" validate:"boolean"` + Dubs bool `gorm:"not null;default:false" validate:"boolean"` + Subtitles bool `gorm:"not null;default:false" validate:"boolean"` + CreatedAt time.Time + UpdatedAt time.Time + LanguageID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Language Language `gorm:"foreignKey:LanguageID;references:ID"` + GameID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID"` +} + +func (gl *GameLanguage) ValidateGameLanguage() error { + Init() + + err := validate.Struct(gl) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/game_publisher.go b/internal/domain/game_publisher.go new file mode 100644 index 0000000..de71328 --- /dev/null +++ b/internal/domain/game_publisher.go @@ -0,0 +1,29 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type GamePublisher struct { + gorm.Model + ID uint `gorm:"primaryKey"` + GameID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID"` + PublisherID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Publisher Publisher `gorm:"foreignKey:PublisherID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (gp *GamePublisher) ValidateGamePublisher() error { + Init() + + err := validate.Struct(gp) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/game_store.go b/internal/domain/game_store.go new file mode 100644 index 0000000..cd756ff --- /dev/null +++ b/internal/domain/game_store.go @@ -0,0 +1,30 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type GameStore struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Price uint `gorm:"not null" validate:"required"` + URL string `gorm:"size:255;not null" validate:"required"` + GameID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID"` + StoreID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Store Store `gorm:"foreignKey:StoreID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (gs *GameStore) ValidateGameStore() error { + Init() + + if err := validate.Struct(gs); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/game_support.go b/internal/domain/game_support.go new file mode 100644 index 0000000..3d46b70 --- /dev/null +++ b/internal/domain/game_support.go @@ -0,0 +1,30 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type GameSupport struct { + gorm.Model + ID uint `gorm:"primaryKey"` + URL *string `gorm:"size:255"` + Email *string `gorm:"size:255" validate:"email"` + Contact *string `gorm:"size:255"` + GameID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (gs *GameSupport) ValidateGameSupport() error { + Init() + + err := validate.Struct(gs) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/genreable.go b/internal/domain/genreable.go new file mode 100644 index 0000000..157b2f4 --- /dev/null +++ b/internal/domain/genreable.go @@ -0,0 +1,20 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Genreable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + GenreableID uint `gorm:"index"` + GenreableType string `gorm:"index"` + GenreID uint `gorm:"index"` + Genre Genre `gorm:"foreignKey:GenreID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time + + Genreable any `gorm:"-"` +} diff --git a/internal/domain/heartable.go b/internal/domain/heartable.go new file mode 100644 index 0000000..3513044 --- /dev/null +++ b/internal/domain/heartable.go @@ -0,0 +1,28 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Heartable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + HeartableID uint `gorm:"index"` + HeartableType string `gorm:"index"` + CreatedAt time.Time + UpdatedAt time.Time + UserID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + User User `gorm:"foreignKey:UserID"` +} + +func (h *Heartable) ValidateHeartable() error { + Init() + + if err := validate.Struct(h); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/language.go b/internal/domain/language.go new file mode 100644 index 0000000..fab45eb --- /dev/null +++ b/internal/domain/language.go @@ -0,0 +1,27 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Language struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + ISO string `gorm:"size:10;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (g *Language) ValidateLanguage() error { + Init() + + err := validate.Struct(g) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/platformable.go b/internal/domain/platformable.go new file mode 100644 index 0000000..2442983 --- /dev/null +++ b/internal/domain/platformable.go @@ -0,0 +1,20 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Platformable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + PlatformableID uint `gorm:"index"` + PlatformableType string `gorm:"index"` + PlatformID uint `gorm:"index"` + Platform Platform `gorm:"foreignKey:PlatformID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time + + Platformable any `gorm:"-"` +} diff --git a/internal/domain/protection.go b/internal/domain/protection.go new file mode 100644 index 0000000..8bb657b --- /dev/null +++ b/internal/domain/protection.go @@ -0,0 +1,26 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Protection struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (p *Protection) ValidateProtection() error { + Init() + + err := validate.Struct(p) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/publisher.go b/internal/domain/publisher.go new file mode 100644 index 0000000..10c4ebd --- /dev/null +++ b/internal/domain/publisher.go @@ -0,0 +1,27 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Publisher struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + Acting bool `gorm:"not null" validate:"boolean"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (p *Publisher) ValidatePublisher() error { + Init() + + err := validate.Struct(p) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/requirement.go b/internal/domain/requirement.go new file mode 100644 index 0000000..5d9737a --- /dev/null +++ b/internal/domain/requirement.go @@ -0,0 +1,37 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Requirement struct { + gorm.Model + ID uint `gorm:"primaryKey"` + OS string `gorm:"size:255;not null" validate:"required"` + DX string `gorm:"size:255;not null" validate:"required"` + CPU string `gorm:"size:255;not null" validate:"required"` + RAM string `gorm:"size:255;not null" validate:"required"` + GPU string `gorm:"size:255;not null" validate:"required"` + ROM string `gorm:"size:255;not null" validate:"required"` + OBS *string `gorm:"size:255"` + Network string `gorm:"size:255;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time + RequirementTypeID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + RequirementType RequirementType `gorm:"foreignKey:RequirementTypeID;references:ID"` + GameID uint `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE;"` + Game Game `gorm:"foreignKey:GameID;references:ID"` +} + +func (r *Requirement) ValidateRequirement() error { + Init() + + err := validate.Struct(r) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/requirement_type.go b/internal/domain/requirement_type.go new file mode 100644 index 0000000..efc8d63 --- /dev/null +++ b/internal/domain/requirement_type.go @@ -0,0 +1,36 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +const ( + MinimumRequirementType = "minimum" + RecommendedRequirementType = "recommended" + MaximumRequirementType = "maximum" + WindowsOSRequirement = "windows" + MacOSRequirement = "mac" + LinuxOSRequirement = "linux" +) + +type RequirementType struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Potential string `gorm:"not null;type:enum('minimum','recommended','maximum')" validate:"required,enum_potential"` + OS string `gorm:"not null;type:enum('windows','mac','linux')" validate:"required,enum_os"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (rt *RequirementType) ValidateRequirementType() error { + Init() + + err := validate.Struct(rt) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/reviewable.go b/internal/domain/reviewable.go new file mode 100644 index 0000000..51351a2 --- /dev/null +++ b/internal/domain/reviewable.go @@ -0,0 +1,31 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Reviewable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Rate uint `gorm:"not nul;" validate:"required,numeric"` + Review string `gorm:"size:255;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time + ReviewableID uint `gorm:"index"` + ReviewableType string `gorm:"index"` + UserID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + User User `gorm:"foreignKey:UserID"` +} + +func (r *Reviewable) ValidateReviewable() error { + Init() + + err := validate.Struct(r) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/store.go b/internal/domain/store.go new file mode 100644 index 0000000..c032682 --- /dev/null +++ b/internal/domain/store.go @@ -0,0 +1,28 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Store struct { + gorm.Model + ID uint `gorm:"primaryKey"` + Name string `gorm:"size:255;not null" validate:"required"` + URL string `gorm:"size:255;not null" validate:"required"` + Slug string `gorm:"size:255;not null" validate:"required"` + Logo string `gorm:"size:255;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (c *Store) ValidateStore() error { + Init() + + if err := validate.Struct(c); err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/taggable.go b/internal/domain/taggable.go new file mode 100644 index 0000000..8d349cf --- /dev/null +++ b/internal/domain/taggable.go @@ -0,0 +1,20 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Taggable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + TaggableID uint `gorm:"index"` + TaggableType string `gorm:"index"` + TagID uint `gorm:"index"` + Tag Tag `gorm:"foreignKey:TagID;references:ID"` + CreatedAt time.Time + UpdatedAt time.Time + + Taggable any `gorm:"-"` +} diff --git a/internal/domain/torrent.go b/internal/domain/torrent.go new file mode 100644 index 0000000..a732da0 --- /dev/null +++ b/internal/domain/torrent.go @@ -0,0 +1,31 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Torrent struct { + gorm.Model + ID uint `gorm:"primaryKey"` + URL string `gorm:"size:255;not null" validate:"required"` + PostedAt time.Time `gorm:"not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time + TorrentProviderID uint `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + TorrentProvider TorrentProvider `gorm:"foreignKey:TorrentProviderID;references:ID"` + GameID uint `gorm:"constraint:OnDelete:CASCADE,OnUpdate:CASCADE"` + Game Game `gorm:"foreignKey:GameID;references:ID"` +} + +func (t *Torrent) ValidateTorrent() error { + Init() + + err := validate.Struct(t) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/torrent_provider.go b/internal/domain/torrent_provider.go new file mode 100644 index 0000000..28ec2fb --- /dev/null +++ b/internal/domain/torrent_provider.go @@ -0,0 +1,27 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type TorrentProvider struct { + gorm.Model + ID uint `gorm:"primaryKey"` + URL string `gorm:"size:255;not null" validate:"required"` + Name string `gorm:"size:255;not null" validate:"required"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (tp *TorrentProvider) ValidateTorrentProvider() error { + Init() + + err := validate.Struct(tp) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/domain/user.go b/internal/domain/user.go index 3f68bd1..12aa30d 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -29,6 +29,7 @@ type User struct { Notifications []Notification `json:"notifications" gorm:"foreignKey:UserID"` Missions []UserMission `gorm:"foreignKey:UserID"` MyMissions []UserMissionAssignment `gorm:"foreignKey:UserID"` + Reviews []Reviewable `gorm:"foreignKey:UserID"` } func (u *User) ValidateUser() error { diff --git a/internal/domain/validations.go b/internal/domain/validations.go index d2da5ca..1c52512 100644 --- a/internal/domain/validations.go +++ b/internal/domain/validations.go @@ -11,6 +11,20 @@ var validate *validator.Validate func Init() { validate = validator.New() + + if err := validate.RegisterValidation("enum_potential", func(fl validator.FieldLevel) bool { + potential := fl.Field().String() + return potential == "minimum" || potential == "recommended" || potential == "maximum" + }); err != nil { + fmt.Printf("Error registering validation enum_potential: %v\n", err) + } + + if err := validate.RegisterValidation("enum_os", func(fl validator.FieldLevel) bool { + os := fl.Field().String() + return os == "windows" || os == "mac" || os == "linux" + }); err != nil { + fmt.Printf("Error registering validation enum_os: %v\n", err) + } } func FormatValidationError(err error) error { @@ -23,6 +37,10 @@ func FormatValidationError(err error) error { errorMessages = append(errorMessages, fmt.Sprintf("%s is a required field", fieldName)) case "email": errorMessages = append(errorMessages, fmt.Sprintf("%s must be a valid email address", fieldName)) + case "enum_potential": + errorMessages = append(errorMessages, fmt.Sprintf("%s must be one of 'minimum', 'recommended', or 'maximum'", fieldName)) + case "enum_os": + errorMessages = append(errorMessages, fmt.Sprintf("%s must be one of 'windows', 'mac', or 'linux'", fieldName)) default: errorMessages = append(errorMessages, fmt.Sprintf("%s is not valid", fieldName)) } diff --git a/internal/domain/viewable.go b/internal/domain/viewable.go new file mode 100644 index 0000000..e4f685f --- /dev/null +++ b/internal/domain/viewable.go @@ -0,0 +1,29 @@ +package domain + +import ( + "time" + + "gorm.io/gorm" +) + +type Viewable struct { + gorm.Model + ID uint `gorm:"primaryKey"` + ViewableID uint `gorm:"index"` + ViewableType string `gorm:"index"` + UserID uint `gorm:"constraint:OnDelete:SET NULL,OnUpdate:CASCADE;"` + User User `gorm:"foreignKey:UserID"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (v *Viewable) ValidateViewable() error { + Init() + + err := validate.Struct(v) + if err != nil { + return FormatValidationError(err) + } + + return nil +} diff --git a/internal/ports/game_repository.go b/internal/ports/game_repository.go new file mode 100644 index 0000000..e801164 --- /dev/null +++ b/internal/ports/game_repository.go @@ -0,0 +1,7 @@ +package ports + +import "gcstatus/internal/domain" + +type GameRepository interface { + FindBySlug(slug string, userID uint) (domain.Game, error) +} diff --git a/internal/resources/category_resource.go b/internal/resources/category_resource.go new file mode 100644 index 0000000..369ac90 --- /dev/null +++ b/internal/resources/category_resource.go @@ -0,0 +1,25 @@ +package resources + +import "gcstatus/internal/domain" + +type CategoryResource struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +func TransformCategory(category domain.Category) CategoryResource { + return CategoryResource{ + ID: category.ID, + Name: category.Name, + } +} + +func TransformCategories(categories []domain.Category) []CategoryResource { + var resources []CategoryResource + + for _, category := range categories { + resources = append(resources, TransformCategory(category)) + } + + return resources +} diff --git a/internal/resources/commentable_resource.go b/internal/resources/commentable_resource.go new file mode 100644 index 0000000..1654afe --- /dev/null +++ b/internal/resources/commentable_resource.go @@ -0,0 +1,34 @@ +package resources + +import ( + "gcstatus/internal/domain" + "gcstatus/pkg/s3" + "gcstatus/pkg/utils" +) + +type CommentableResource struct { + ID uint `json:"id"` + Comment string `json:"comment"` + HeartsCount uint `json:"hearts_count"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + By MinimalUserResource `json:"by"` + Replies []CommentableResource `json:"replies"` +} + +func TransformCommentable(commentable domain.Commentable, s3Client s3.S3ClientInterface) CommentableResource { + replies := make([]CommentableResource, len(commentable.Replies)) + for i, reply := range commentable.Replies { + replies[i] = TransformCommentable(reply, s3Client) + } + + return CommentableResource{ + ID: commentable.ID, + Comment: commentable.Comment, + CreatedAt: utils.FormatTimestamp(commentable.CreatedAt), + UpdatedAt: utils.FormatTimestamp(commentable.UpdatedAt), + By: TransformMinimalUser(commentable.User, s3Client), + Replies: replies, + HeartsCount: uint(len(commentable.Hearts)), + } +} diff --git a/internal/resources/crack_resource.go b/internal/resources/crack_resource.go new file mode 100644 index 0000000..e10056f --- /dev/null +++ b/internal/resources/crack_resource.go @@ -0,0 +1,40 @@ +package resources + +import ( + "gcstatus/internal/domain" + "gcstatus/pkg/utils" +) + +type CrackResource struct { + ID uint `json:"id"` + Status string `json:"status"` + CrackedAt *string `json:"cracked_at"` + By *CrackerResource `json:"by"` + Protection *ProtectionResource `json:"protection"` +} + +func TransformCrack(crack *domain.Crack) *CrackResource { + resource := CrackResource{ + ID: crack.ID, + Status: crack.Status, + } + + if crack.CrackedAt != nil { + formattedTime := utils.FormatTimestamp(*crack.CrackedAt) + resource.CrackedAt = &formattedTime + } + + if crack.Cracker.ID != 0 { + resource.By = TransformCracker(crack.Cracker) + } else { + resource.By = nil + } + + if crack.Protection.ID != 0 { + resource.Protection = TransformProtection(crack.Protection) + } else { + resource.Protection = nil + } + + return &resource +} diff --git a/internal/resources/cracker_resource.go b/internal/resources/cracker_resource.go new file mode 100644 index 0000000..a3ab42b --- /dev/null +++ b/internal/resources/cracker_resource.go @@ -0,0 +1,27 @@ +package resources + +import "gcstatus/internal/domain" + +type CrackerResource struct { + ID uint `json:"id"` + Name string `json:"name"` + Acting bool `json:"acting"` +} + +func TransformCracker(cracker domain.Cracker) *CrackerResource { + return &CrackerResource{ + ID: cracker.ID, + Name: cracker.Name, + Acting: cracker.Acting, + } +} + +func TransformCrackers(crackers []domain.Cracker) []*CrackerResource { + var resources []*CrackerResource + + for _, cracker := range crackers { + resources = append(resources, TransformCracker(cracker)) + } + + return resources +} diff --git a/internal/resources/critic_resource.go b/internal/resources/critic_resource.go new file mode 100644 index 0000000..385d6eb --- /dev/null +++ b/internal/resources/critic_resource.go @@ -0,0 +1,19 @@ +package resources + +import "gcstatus/internal/domain" + +type CriticResource struct { + ID uint `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Logo string `json:"logo"` +} + +func TransformCritic(critic domain.Critic) CriticResource { + return CriticResource{ + ID: critic.ID, + Name: critic.Name, + URL: critic.URL, + Logo: critic.Logo, + } +} diff --git a/internal/resources/criticable_resource.go b/internal/resources/criticable_resource.go new file mode 100644 index 0000000..4495669 --- /dev/null +++ b/internal/resources/criticable_resource.go @@ -0,0 +1,31 @@ +package resources + +import ( + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + + "github.com/shopspring/decimal" +) + +type CriticableResource struct { + ID uint `json:"id"` + URL string `json:"url"` + Rate decimal.Decimal `json:"rate"` + PostedAt string `json:"posted_at"` + Critic CriticResource `json:"critic"` +} + +func TransformCriticable(criticable domain.Criticable) CriticableResource { + var postedAt string + if !criticable.PostedAt.IsZero() { + postedAt = utils.FormatTimestamp(criticable.PostedAt) + } + + return CriticableResource{ + ID: criticable.ID, + URL: criticable.URL, + Rate: criticable.Rate, + PostedAt: postedAt, + Critic: TransformCritic(criticable.Critic), + } +} diff --git a/internal/resources/developer_resource.go b/internal/resources/developer_resource.go new file mode 100644 index 0000000..1e8bd93 --- /dev/null +++ b/internal/resources/developer_resource.go @@ -0,0 +1,27 @@ +package resources + +import "gcstatus/internal/domain" + +type DeveloperResource struct { + ID uint `json:"id"` + Name string `json:"name"` + Acting bool `json:"acting"` +} + +func TransformDeveloper(Developer domain.Developer) DeveloperResource { + return DeveloperResource{ + ID: Developer.ID, + Name: Developer.Name, + Acting: Developer.Acting, + } +} + +func TransformDevelopers(Developers []domain.Developer) []DeveloperResource { + var resources []DeveloperResource + + for _, Developer := range Developers { + resources = append(resources, TransformDeveloper(Developer)) + } + + return resources +} diff --git a/internal/resources/dlc_resource.go b/internal/resources/dlc_resource.go new file mode 100644 index 0000000..8ced837 --- /dev/null +++ b/internal/resources/dlc_resource.go @@ -0,0 +1,51 @@ +package resources + +import ( + "context" + "gcstatus/internal/domain" + "gcstatus/pkg/s3" + "gcstatus/pkg/utils" + "log" + "time" +) + +type DLCResource struct { + ID uint `json:"id"` + Name string `json:"name"` + Cover string `json:"cover"` + ReleaseDate string `json:"release_date"` + Galleries []GalleriableResource `json:"galleries"` + Platforms []PlatformResource `json:"platforms"` + Stores []DLCStoreResource `json:"stores"` +} + +func TransformDLC(DLC domain.DLC, s3Client s3.S3ClientInterface) DLCResource { + resource := DLCResource{ + ID: DLC.ID, + Name: DLC.Name, + ReleaseDate: utils.FormatTimestamp(DLC.ReleaseDate), + } + + url, err := s3Client.GetPresignedURL(context.TODO(), DLC.Cover, time.Hour*3) + if err != nil { + log.Printf("Error generating presigned URL: %v", err) + } + + resource.Cover = url + resource.Platforms = transformPlatforms(DLC.Platforms) + resource.Galleries = transformGalleries(DLC.Galleries, s3Client) + resource.Stores = transformDLCStores(DLC.Stores) + + return resource +} + +func transformDLCStores(stores []domain.DLCStore) []DLCStoreResource { + storeResources := make([]DLCStoreResource, 0) + for _, s := range stores { + if s.ID != 0 { + storeResources = append(storeResources, TransformDLCtore(s)) + } + } + + return storeResources +} diff --git a/internal/resources/dlc_store_resource.go b/internal/resources/dlc_store_resource.go new file mode 100644 index 0000000..88dcdb1 --- /dev/null +++ b/internal/resources/dlc_store_resource.go @@ -0,0 +1,19 @@ +package resources + +import "gcstatus/internal/domain" + +type DLCStoreResource struct { + ID uint `json:"id"` + Price uint `json:"price"` + URL string `json:"url"` + Store StoreResource `json:"store"` +} + +func TransformDLCtore(DLCStore domain.DLCStore) DLCStoreResource { + return DLCStoreResource{ + ID: DLCStore.ID, + Price: DLCStore.Price, + URL: DLCStore.URL, + Store: TransformStore(DLCStore.Store), + } +} diff --git a/internal/resources/galleriable_resource.go b/internal/resources/galleriable_resource.go new file mode 100644 index 0000000..fe9d2c9 --- /dev/null +++ b/internal/resources/galleriable_resource.go @@ -0,0 +1,33 @@ +package resources + +import ( + "context" + "gcstatus/internal/domain" + "gcstatus/pkg/s3" + "log" + "time" +) + +type GalleriableResource struct { + ID uint `json:"id"` + Path string `json:"path"` +} + +func TransformGalleriable(galleriable domain.Galleriable, s3Client s3.S3ClientInterface) GalleriableResource { + resource := GalleriableResource{ + ID: galleriable.ID, + } + + if galleriable.S3 { + url, err := s3Client.GetPresignedURL(context.TODO(), galleriable.Path, time.Hour*3) + if err != nil { + log.Printf("Error generating presigned URL: %v", err) + } + + resource.Path = url + } else { + resource.Path = galleriable.Path + } + + return resource +} diff --git a/internal/resources/game_language_resource.go b/internal/resources/game_language_resource.go new file mode 100644 index 0000000..5447378 --- /dev/null +++ b/internal/resources/game_language_resource.go @@ -0,0 +1,21 @@ +package resources + +import "gcstatus/internal/domain" + +type GameLanguageResource struct { + ID uint `json:"id"` + Menu bool `json:"menu"` + Dubs bool `json:"dubs"` + Subtitles bool `json:"subtitles"` + Language LanguageResource `json:"language"` +} + +func TransformGameLanguage(gameLanguage domain.GameLanguage) GameLanguageResource { + return GameLanguageResource{ + ID: gameLanguage.ID, + Menu: gameLanguage.Menu, + Dubs: gameLanguage.Dubs, + Subtitles: gameLanguage.Subtitles, + Language: TransformLanguage(gameLanguage.Language), + } +} diff --git a/internal/resources/game_resource.go b/internal/resources/game_resource.go new file mode 100644 index 0000000..62d0bf5 --- /dev/null +++ b/internal/resources/game_resource.go @@ -0,0 +1,269 @@ +package resources + +import ( + "gcstatus/internal/domain" + "gcstatus/pkg/s3" + "gcstatus/pkg/utils" +) + +type GameResource struct { + ID uint `json:"id"` + Age uint `json:"age"` + Slug string `json:"slug"` + Title string `json:"title"` + Condition string `json:"condition"` + Cover string `json:"cover"` + About string `json:"about"` + Description string `json:"description"` + ShortDescription string `json:"short_description"` + Free bool `json:"is_free"` + Legal *string `json:"legal"` + Website *string `json:"website"` + ReleaseDate string `json:"release_date"` + ViewsCount uint `json:"views_count"` + HeartsCount uint `json:"hearts_count"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + Categories []CategoryResource `json:"categories"` + Platforms []PlatformResource `json:"platforms"` + Genres []GenreResource `json:"genres"` + Tags []TagResource `json:"tags"` + Languages []GameLanguageResource `json:"languages"` + Requirements []RequirementResource `json:"requirements"` + Torrents []TorrentResource `json:"torrents"` + Publishers []PublisherResource `json:"publishers"` + Developers []DeveloperResource `json:"developers"` + Reviews []ReviewResource `json:"reviews"` + Critics []CriticableResource `json:"critics"` + Stores []GameStoreResource `json:"stores"` + Comments []CommentableResource `json:"comments"` + Galleries []GalleriableResource `json:"galleries"` + DLCs []DLCResource `json:"dlcs"` + Crack *CrackResource `json:"crack"` + Support *SupportResource `json:"support"` +} + +func TransformGame(game domain.Game, s3Client s3.S3ClientInterface) GameResource { + resource := GameResource{ + ID: game.ID, + Age: uint(game.Age), + Slug: game.Slug, + Title: game.Title, + Condition: game.Condition, + Cover: game.Cover, + About: game.About, + Description: game.Description, + ShortDescription: game.ShortDescription, + Free: game.Free, + Legal: game.Legal, + Website: game.Website, + ViewsCount: uint(len(game.Views)), + HeartsCount: uint(len(game.Hearts)), + ReleaseDate: utils.FormatTimestamp(game.ReleaseDate), + CreatedAt: utils.FormatTimestamp(game.CreatedAt), + UpdatedAt: utils.FormatTimestamp(game.UpdatedAt), + } + + resource.Categories = transformCategories(game.Categories) + resource.Platforms = transformPlatforms(game.Platforms) + resource.Genres = transformGenres(game.Genres) + resource.Tags = transformTags(game.Tags) + resource.Languages = transformLanguages(game.Languages) + resource.Requirements = transformRequirements(game.Requirements) + resource.Torrents = transformTorrents(game.Torrents) + resource.Publishers = transformPublishers(game.Publishers) + resource.Developers = transformDevelopers(game.Developers) + resource.Reviews = transformReviews(game.Reviews, s3Client) + resource.Critics = transformCritics(game.Critics) + resource.Stores = transformStores(game.Stores) + resource.Comments = transformComments(game.Comments, s3Client) + resource.Galleries = transformGalleries(game.Galleries, s3Client) + resource.DLCs = transformDLCs(game.DLCs, s3Client) + + if game.Crack != nil && game.Crack.ID != 0 { + resource.Crack = TransformCrack(game.Crack) + } + + if game.Support != nil && game.Support.ID != 0 { + resource.Support = TransformSupport(game.Support) + } + + return resource +} + +func TransformGames(games []domain.Game, s3Client s3.S3ClientInterface) []GameResource { + var resources []GameResource + + resources = make([]GameResource, 0, len(games)) + + for _, game := range games { + resources = append(resources, TransformGame(game, s3Client)) + } + + return resources +} + +func transformCategories(categories []domain.Categoriable) []CategoryResource { + categoryResources := make([]CategoryResource, 0) + for _, c := range categories { + if c.Category.ID != 0 { + categoryResources = append(categoryResources, TransformCategory(c.Category)) + } + } + + return categoryResources +} + +func transformPlatforms(platforms []domain.Platformable) []PlatformResource { + platformResources := make([]PlatformResource, 0) + for _, p := range platforms { + if p.Platform.ID != 0 { + platformResources = append(platformResources, TransformPlatform(p.Platform)) + } + } + + return platformResources +} + +func transformGenres(genres []domain.Genreable) []GenreResource { + genreResources := make([]GenreResource, 0) + for _, g := range genres { + if g.Genre.ID != 0 { + genreResources = append(genreResources, TransformGenre(g.Genre)) + } + } + + return genreResources +} + +func transformTags(tags []domain.Taggable) []TagResource { + tagResources := make([]TagResource, 0) + for _, t := range tags { + if t.Tag.ID != 0 { + tagResources = append(tagResources, TransformTag(t.Tag)) + } + } + + return tagResources +} + +func transformLanguages(languages []domain.GameLanguage) []GameLanguageResource { + languageResources := make([]GameLanguageResource, 0) + for _, l := range languages { + if l.Language.ID != 0 { + languageResources = append(languageResources, TransformGameLanguage(l)) + } + } + + return languageResources +} + +func transformRequirements(requirements []domain.Requirement) []RequirementResource { + requirementResources := make([]RequirementResource, 0) + for _, r := range requirements { + if r.ID != 0 { + requirementResources = append(requirementResources, TransformRequirement(r)) + } + } + + return requirementResources +} + +func transformTorrents(torrents []domain.Torrent) []TorrentResource { + torrentResources := make([]TorrentResource, 0) + for _, t := range torrents { + if t.ID != 0 { + torrentResources = append(torrentResources, TransformTorrent(t)) + } + } + + return torrentResources +} + +func transformPublishers(publishers []domain.GamePublisher) []PublisherResource { + publisherResources := make([]PublisherResource, 0) + for _, p := range publishers { + if p.Publisher.ID != 0 { + publisherResources = append(publisherResources, TransformPublisher(p.Publisher)) + } + } + + return publisherResources +} + +func transformDevelopers(developers []domain.GameDeveloper) []DeveloperResource { + developerResources := make([]DeveloperResource, 0) + for _, d := range developers { + if d.Developer.ID != 0 { + developerResources = append(developerResources, TransformDeveloper(d.Developer)) + } + } + + return developerResources +} + +func transformReviews(reviews []domain.Reviewable, s3Client s3.S3ClientInterface) []ReviewResource { + reviewResources := make([]ReviewResource, 0) + for _, r := range reviews { + if r.ID != 0 { + reviewResources = append(reviewResources, TransformReview(r, s3Client)) + } + } + + return reviewResources +} + +func transformCritics(critics []domain.Criticable) []CriticableResource { + criticResources := make([]CriticableResource, 0) + for _, c := range critics { + if c.ID != 0 { + criticResources = append(criticResources, TransformCriticable(c)) + } + } + + return criticResources +} + +func transformStores(stores []domain.GameStore) []GameStoreResource { + storeResources := make([]GameStoreResource, 0) + for _, s := range stores { + if s.ID != 0 { + storeResources = append(storeResources, TransformGameStore(s)) + } + } + + return storeResources +} + +func transformComments(comments []domain.Commentable, s3Client s3.S3ClientInterface) []CommentableResource { + commentResources := make([]CommentableResource, 0) + for _, c := range comments { + if c.ID != 0 { + commentResources = append(commentResources, TransformCommentable(c, s3Client)) + } + } + + return commentResources +} + +func transformGalleries(galleries []domain.Galleriable, s3Client s3.S3ClientInterface) []GalleriableResource { + galleryResources := make([]GalleriableResource, 0) + for _, g := range galleries { + if g.ID != 0 { + galleryResources = append(galleryResources, TransformGalleriable(g, s3Client)) + } + } + + return galleryResources +} + +func transformDLCs(DLCs []domain.DLC, s3Client s3.S3ClientInterface) []DLCResource { + DLCResources := make([]DLCResource, 0) + for _, d := range DLCs { + if d.ID != 0 { + DLCResources = append(DLCResources, TransformDLC(d, s3Client)) + } + } + + return DLCResources +} diff --git a/internal/resources/game_store_resource.go b/internal/resources/game_store_resource.go new file mode 100644 index 0000000..ede78cf --- /dev/null +++ b/internal/resources/game_store_resource.go @@ -0,0 +1,19 @@ +package resources + +import "gcstatus/internal/domain" + +type GameStoreResource struct { + ID uint `json:"id"` + Price uint `json:"price"` + URL string `json:"url"` + Store StoreResource `json:"store"` +} + +func TransformGameStore(gameStore domain.GameStore) GameStoreResource { + return GameStoreResource{ + ID: gameStore.ID, + Price: gameStore.Price, + URL: gameStore.URL, + Store: TransformStore(gameStore.Store), + } +} diff --git a/internal/resources/genre_resource.go b/internal/resources/genre_resource.go new file mode 100644 index 0000000..bca2a07 --- /dev/null +++ b/internal/resources/genre_resource.go @@ -0,0 +1,25 @@ +package resources + +import "gcstatus/internal/domain" + +type GenreResource struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +func TransformGenre(genre domain.Genre) GenreResource { + return GenreResource{ + ID: genre.ID, + Name: genre.Name, + } +} + +func TransformGenres(genres []domain.Genre) []GenreResource { + var resources []GenreResource + + for _, genre := range genres { + resources = append(resources, TransformGenre(genre)) + } + + return resources +} diff --git a/internal/resources/language_resource.go b/internal/resources/language_resource.go new file mode 100644 index 0000000..f13bcb8 --- /dev/null +++ b/internal/resources/language_resource.go @@ -0,0 +1,27 @@ +package resources + +import "gcstatus/internal/domain" + +type LanguageResource struct { + ID uint `json:"id"` + Name string `json:"name"` + ISO string `json:"iso"` +} + +func TransformLanguage(language domain.Language) LanguageResource { + return LanguageResource{ + ID: language.ID, + Name: language.Name, + ISO: language.ISO, + } +} + +func TransformLanguages(languages []domain.Language) []LanguageResource { + var resources []LanguageResource + + for _, language := range languages { + resources = append(resources, TransformLanguage(language)) + } + + return resources +} diff --git a/internal/resources/platform_resource.go b/internal/resources/platform_resource.go new file mode 100644 index 0000000..8e12a4d --- /dev/null +++ b/internal/resources/platform_resource.go @@ -0,0 +1,25 @@ +package resources + +import "gcstatus/internal/domain" + +type PlatformResource struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +func TransformPlatform(platform domain.Platform) PlatformResource { + return PlatformResource{ + ID: platform.ID, + Name: platform.Name, + } +} + +func TransformPlatforms(platforms []domain.Platform) []PlatformResource { + var resources []PlatformResource + + for _, platform := range platforms { + resources = append(resources, TransformPlatform(platform)) + } + + return resources +} diff --git a/internal/resources/protection_resource.go b/internal/resources/protection_resource.go new file mode 100644 index 0000000..eb5126f --- /dev/null +++ b/internal/resources/protection_resource.go @@ -0,0 +1,25 @@ +package resources + +import "gcstatus/internal/domain" + +type ProtectionResource struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +func TransformProtection(protection domain.Protection) *ProtectionResource { + return &ProtectionResource{ + ID: protection.ID, + Name: protection.Name, + } +} + +func TransformProtections(protections []domain.Protection) []*ProtectionResource { + var resources []*ProtectionResource + + for _, protection := range protections { + resources = append(resources, TransformProtection(protection)) + } + + return resources +} diff --git a/internal/resources/publisher_resource.go b/internal/resources/publisher_resource.go new file mode 100644 index 0000000..68f72d0 --- /dev/null +++ b/internal/resources/publisher_resource.go @@ -0,0 +1,27 @@ +package resources + +import "gcstatus/internal/domain" + +type PublisherResource struct { + ID uint `json:"id"` + Name string `json:"name"` + Acting bool `json:"acting"` +} + +func TransformPublisher(publisher domain.Publisher) PublisherResource { + return PublisherResource{ + ID: publisher.ID, + Name: publisher.Name, + Acting: publisher.Acting, + } +} + +func TransformPublishers(Publishers []domain.Publisher) []PublisherResource { + var resources []PublisherResource + + for _, Publisher := range Publishers { + resources = append(resources, TransformPublisher(Publisher)) + } + + return resources +} diff --git a/internal/resources/requirement_resource.go b/internal/resources/requirement_resource.go new file mode 100644 index 0000000..4eca347 --- /dev/null +++ b/internal/resources/requirement_resource.go @@ -0,0 +1,31 @@ +package resources + +import "gcstatus/internal/domain" + +type RequirementResource struct { + ID uint `json:"id"` + OS string `json:"os"` + DX string `json:"dx"` + CPU string `json:"cpu"` + RAM string `json:"ram"` + GPU string `json:"gpu"` + ROM string `json:"rom"` + OBS *string `json:"obs,omitempty"` + Network string `json:"network"` + RequirementType RequirementTypeResource `json:"requirement_type"` +} + +func TransformRequirement(requirement domain.Requirement) RequirementResource { + return RequirementResource{ + ID: requirement.ID, + OS: requirement.OS, + DX: requirement.DX, + CPU: requirement.CPU, + RAM: requirement.RAM, + GPU: requirement.GPU, + ROM: requirement.ROM, + OBS: requirement.OBS, + Network: requirement.Network, + RequirementType: TransformRequirementType(requirement.RequirementType), + } +} diff --git a/internal/resources/requirement_type_resource.go b/internal/resources/requirement_type_resource.go new file mode 100644 index 0000000..ead09b5 --- /dev/null +++ b/internal/resources/requirement_type_resource.go @@ -0,0 +1,27 @@ +package resources + +import "gcstatus/internal/domain" + +type RequirementTypeResource struct { + ID uint `json:"id"` + OS string `json:"os"` + Potential string `json:"potential"` +} + +func TransformRequirementType(requirementType domain.RequirementType) RequirementTypeResource { + return RequirementTypeResource{ + ID: requirementType.ID, + OS: requirementType.OS, + Potential: requirementType.Potential, + } +} + +func TransformRequirementTypes(requirementTypes []domain.RequirementType) []RequirementTypeResource { + var resources []RequirementTypeResource + + for _, requirementType := range requirementTypes { + resources = append(resources, TransformRequirementType(requirementType)) + } + + return resources +} diff --git a/internal/resources/review_resource.go b/internal/resources/review_resource.go new file mode 100644 index 0000000..74437f6 --- /dev/null +++ b/internal/resources/review_resource.go @@ -0,0 +1,42 @@ +package resources + +import ( + "gcstatus/internal/domain" + "gcstatus/pkg/s3" + "gcstatus/pkg/utils" +) + +type ReviewResource struct { + ID uint `json:"id"` + Rate uint `json:"rate"` + Review string `json:"review"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` + User MinimalUserResource `json:"user"` +} + +func TransformReview(review domain.Reviewable, s3Client s3.S3ClientInterface) ReviewResource { + reviewResource := ReviewResource{ + ID: review.ID, + Rate: review.Rate, + Review: review.Review, + CreatedAt: utils.FormatTimestamp(review.CreatedAt), + UpdatedAt: utils.FormatTimestamp(review.UpdatedAt), + } + + if review.User.ID != 0 { + reviewResource.User = TransformMinimalUser(review.User, s3Client) + } + + return reviewResource +} + +func TransformReviews(reviews []domain.Reviewable, s3Client s3.S3ClientInterface) []ReviewResource { + var resources []ReviewResource + + for _, review := range reviews { + resources = append(resources, TransformReview(review, s3Client)) + } + + return resources +} diff --git a/internal/resources/store_resource.go b/internal/resources/store_resource.go new file mode 100644 index 0000000..3cd73b6 --- /dev/null +++ b/internal/resources/store_resource.go @@ -0,0 +1,21 @@ +package resources + +import "gcstatus/internal/domain" + +type StoreResource struct { + ID uint `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + URL string `json:"url"` + Logo string `json:"logo"` +} + +func TransformStore(store domain.Store) StoreResource { + return StoreResource{ + ID: store.ID, + Name: store.Name, + Slug: store.Slug, + URL: store.URL, + Logo: store.Logo, + } +} diff --git a/internal/resources/support_resource.go b/internal/resources/support_resource.go new file mode 100644 index 0000000..721c4a2 --- /dev/null +++ b/internal/resources/support_resource.go @@ -0,0 +1,21 @@ +package resources + +import ( + "gcstatus/internal/domain" +) + +type SupportResource struct { + ID uint `json:"id"` + URL *string `json:"url"` + Email *string `json:"email"` + Contact *string `json:"contact"` +} + +func TransformSupport(support *domain.GameSupport) *SupportResource { + return &SupportResource{ + ID: support.ID, + URL: support.URL, + Email: support.Email, + Contact: support.Contact, + } +} diff --git a/internal/resources/tag_resource.go b/internal/resources/tag_resource.go new file mode 100644 index 0000000..86f69c8 --- /dev/null +++ b/internal/resources/tag_resource.go @@ -0,0 +1,25 @@ +package resources + +import "gcstatus/internal/domain" + +type TagResource struct { + ID uint `json:"id"` + Name string `json:"name"` +} + +func TransformTag(tag domain.Tag) TagResource { + return TagResource{ + ID: tag.ID, + Name: tag.Name, + } +} + +func TransformTags(tags []domain.Tag) []TagResource { + var resources []TagResource + + for _, tag := range tags { + resources = append(resources, TransformTag(tag)) + } + + return resources +} diff --git a/internal/resources/torrent_provider_resource.go b/internal/resources/torrent_provider_resource.go new file mode 100644 index 0000000..bd37c81 --- /dev/null +++ b/internal/resources/torrent_provider_resource.go @@ -0,0 +1,17 @@ +package resources + +import "gcstatus/internal/domain" + +type TorrentProviderResource struct { + ID uint `json:"id"` + URL string `json:"url"` + Name string `json:"name"` +} + +func TransformTorrentProvider(torrentProvider domain.TorrentProvider) TorrentProviderResource { + return TorrentProviderResource{ + ID: torrentProvider.ID, + URL: torrentProvider.URL, + Name: torrentProvider.Name, + } +} diff --git a/internal/resources/torrent_resource.go b/internal/resources/torrent_resource.go new file mode 100644 index 0000000..943aa08 --- /dev/null +++ b/internal/resources/torrent_resource.go @@ -0,0 +1,32 @@ +package resources + +import ( + "gcstatus/internal/domain" + "gcstatus/pkg/utils" +) + +type TorrentResource struct { + ID uint `json:"id"` + URL string `json:"url"` + PostedAt string `json:"posted_in"` + Provider TorrentProviderResource `json:"provider"` +} + +func TransformTorrent(torrent domain.Torrent) TorrentResource { + return TorrentResource{ + ID: torrent.ID, + URL: torrent.URL, + PostedAt: utils.FormatTimestamp(torrent.PostedAt), + Provider: TransformTorrentProvider(torrent.TorrentProvider), + } +} + +func TransformTorrents(torrents []domain.Torrent) []TorrentResource { + var resources []TorrentResource + + for _, torrent := range torrents { + resources = append(resources, TransformTorrent(torrent)) + } + + return resources +} diff --git a/internal/resources/user_resource.go b/internal/resources/user_resource.go index 2f575b5..a697856 100644 --- a/internal/resources/user_resource.go +++ b/internal/resources/user_resource.go @@ -1,9 +1,12 @@ package resources import ( + "context" "gcstatus/internal/domain" "gcstatus/pkg/s3" "gcstatus/pkg/utils" + "log" + "time" ) type UserResource struct { @@ -21,6 +24,15 @@ type UserResource struct { Wallet *WalletResource `json:"wallet"` } +type MinimalUserResource struct { + ID uint `json:"id"` + Name string `json:"name"` + Photo *string `json:"photo"` + Email string `json:"email"` + Nickname string `json:"nickname"` + CreatedAt string `json:"created_at"` +} + func TransformUser(user domain.User, s3Client s3.S3ClientInterface) UserResource { userResource := UserResource{ ID: user.ID, @@ -70,3 +82,24 @@ func TransformUsers(users []domain.User, s3Client s3.S3ClientInterface) []UserRe return resources } + +func TransformMinimalUser(user domain.User, s3Client s3.S3ClientInterface) MinimalUserResource { + userResource := MinimalUserResource{ + ID: user.ID, + Name: user.Name, + Email: user.Email, + Nickname: user.Nickname, + CreatedAt: utils.FormatTimestamp(user.CreatedAt), + } + + if user.Profile.Photo != "" { + url, err := s3Client.GetPresignedURL(context.TODO(), user.Profile.Photo, time.Hour*3) + if err != nil { + log.Printf("Error generating presigned URL: %v", err) + } else { + userResource.Photo = &url + } + } + + return userResource +} diff --git a/internal/usecases/game_service.go b/internal/usecases/game_service.go new file mode 100644 index 0000000..c711cd8 --- /dev/null +++ b/internal/usecases/game_service.go @@ -0,0 +1,18 @@ +package usecases + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/ports" +) + +type GameService struct { + repo ports.GameRepository +} + +func NewGameService(repo ports.GameRepository) *GameService { + return &GameService{repo: repo} +} + +func (h *GameService) FindBySlug(slug string, userID uint) (domain.Game, error) { + return h.repo.FindBySlug(slug, userID) +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index b408ae4..213b315 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -268,6 +268,14 @@ func StringPtr(s string) *string { return &s } +func UintPtr(u uint) *uint { + return &u +} + +func TimePtr(t time.Time) *time.Time { + return &t +} + func FormatTimestamp(t time.Time) string { return t.Format("2006-01-02T15:04:05") } diff --git a/tests/unit/db/game_repository_mysql_test.go b/tests/unit/db/game_repository_mysql_test.go new file mode 100644 index 0000000..89ea9f3 --- /dev/null +++ b/tests/unit/db/game_repository_mysql_test.go @@ -0,0 +1,356 @@ +package tests + +import ( + "errors" + "gcstatus/internal/adapters/db" + "gcstatus/internal/domain" + "gcstatus/tests" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestGameRepositoryMySQL_FindBySlug(t *testing.T) { + fixedTime := time.Now() + gormDB, mock := tests.Setup(t) + mockRepo := db.NewGameRepositoryMySQL(gormDB) + + testCases := map[string]struct { + userID uint + slug string + wantErr bool + expectedErr error + wantGame domain.Game + mockBehavior func(slug string) + }{ + "game found": { + userID: 1, + slug: "valid", + wantErr: false, + wantGame: domain.Game{ + ID: 1, + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(slug string) { + rows := mock.NewRows([]string{"id", "age", "slug", "title", "condition", "cover", "about", "description", "short_description", "free", "release_date", "created_at", "updated_at"}). + AddRow(1, 18, "valid", "Game Test", domain.CommomCondition, "https://placehold.co/600x400/EEE/31343C", "About game", "Description", "Short description", false, fixedTime, fixedTime, fixedTime) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `games` WHERE slug = ? AND `games`.`deleted_at` IS NULL ORDER BY `games`.`id` LIMIT ?")). + WithArgs(slug, 1). + WillReturnRows(rows) + + categoriableRows := mock.NewRows([]string{"id", "categoriable_id", "categoriable_type", "category_id"}). + AddRow(1, 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `categoriables` WHERE `categoriable_type` = ? AND `categoriables`.`categoriable_id` = ? AND `categoriables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(categoriableRows) + + categoriesRows := mock.NewRows([]string{"id", "name"}). + AddRow(1, "FPS") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `categories` WHERE `categories`.`id` = ? AND `categories`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(categoriesRows) + + commentablesRows := mock.NewRows([]string{"id", "comment", "user_id", "commentable_id", "commentable_type"}). + AddRow(1, "Fake comment", 1, 1, "games") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `commentables` WHERE `commentable_type` = ? AND `commentables`.`commentable_id` = ? AND parent_id IS NULL AND `commentables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(commentablesRows) + + commentableHeartsRows := mock.NewRows([]string{"id", "heartable_id", "heartable_type", "user_id"}). + AddRow(1, 1, "commentables", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `heartables` WHERE `heartable_type` = ? AND `heartables`.`heartable_id` = ? AND `heartables`.`deleted_at` IS NULL")). + WithArgs("commentables", 1). + WillReturnRows(commentableHeartsRows) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `commentables` WHERE `commentables`.`parent_id` = ? AND `commentables`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(commentablesRows) + + userCommentablessRows := mock.NewRows([]string{"id", "name", "email", "nickname", "created_at", "updated_at"}). + AddRow(1, "Fake", "fake@gmail.com", "fake", fixedTime, fixedTime) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE `users`.`id` = ? AND `users`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(userCommentablessRows) + + crackRows := mock.NewRows([]string{"id", "status", "cracked_at", "cracker_id", "protection_id", "game_id"}). + AddRow(1, "uncracked", fixedTime, 1, 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `cracks` WHERE `cracks`.`game_id` = ? AND `cracks`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(crackRows) + + crackerRows := mock.NewRows([]string{"id", "name", "acting"}). + AddRow(1, "GOLDBERG", true) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `crackers` WHERE `crackers`.`id` = ? AND `crackers`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(crackerRows) + + protectionRows := mock.NewRows([]string{"id", "name"}). + AddRow(1, "Denuvo") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `protections` WHERE `protections`.`id` = ? AND `protections`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(protectionRows) + + criticablesRows := mock.NewRows([]string{"id", "rate", "url", "posted_at", "criticable_id", "criticable_type", "critic_id"}). + AddRow(1, 7.9, "https://google.com", fixedTime, 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `criticables` WHERE `criticable_type` = ? AND `criticables`.`criticable_id` = ? AND `criticables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(criticablesRows) + + criticsRows := mock.NewRows([]string{"id", "name", "url", "logo"}). + AddRow(1, "Metacritic", "https://google.com", "https://placehold.co/600x400/EEE/31343C") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `critics` WHERE `critics`.`id` = ? AND `critics`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(criticsRows) + + dlcsRows := mock.NewRows([]string{"id", "name", "cover", "release_date", "game_id"}). + AddRow(1, "DLC 1", "https://placehold.co/600x400/EEE/31343C", fixedTime, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `dlcs` WHERE `dlcs`.`game_id` = ? AND `dlcs`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(dlcsRows) + + dlcGalleriesRows := mock.NewRows([]string{"id", "path", "s3", "galleriable_id", "galleriable_type"}). + AddRow(1, "Game Science", false, 1, "dlcs") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `galleriables` WHERE `galleriable_type` = ? AND `galleriables`.`galleriable_id` = ? AND `galleriables`.`deleted_at` IS NULL")). + WithArgs("dlcs", 1). + WillReturnRows(dlcGalleriesRows) + + platformableDlcsRows := mock.NewRows([]string{"id", "platformable_id", "platformable_type", "platform_id"}). + AddRow(1, 1, "dlcs", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `platformables` WHERE `platformable_type` = ? AND `platformables`.`platformable_id` = ? AND `platformables`.`deleted_at` IS NULL")). + WithArgs("dlcs", 1). + WillReturnRows(platformableDlcsRows) + + platformsRows := mock.NewRows([]string{"id", "name"}). + AddRow(1, "PC") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `platforms` WHERE `platforms`.`id` = ? AND `platforms`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(platformsRows) + + dlcStoresRows := mock.NewRows([]string{"id", "price", "url", "dlc_id", "store_id"}). + AddRow(1, 2200, "https://google.com", 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `dlc_stores` WHERE `dlc_stores`.`dlc_id` = ? AND `dlc_stores`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(dlcStoresRows) + + storesRows := mock.NewRows([]string{"id", "name", "url", "slug", "logo"}). + AddRow(1, "Store 1", "https://photo.co", "store-1", "https://photo.co") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `stores` WHERE `stores`.`id` = ? AND `stores`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(storesRows) + + gameDevelopersRows := mock.NewRows([]string{"id", "developer_id", "game_id"}). + AddRow(1, 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `game_developers` WHERE `game_developers`.`game_id` = ? AND `game_developers`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(gameDevelopersRows) + + developersRows := mock.NewRows([]string{"id", "name", "acting"}). + AddRow(1, "Game Science", true) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `developers` WHERE `developers`.`id` = ? AND `developers`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(developersRows) + + galleriablesRows := mock.NewRows([]string{"id", "path", "s3", "galleriable_id", "galleriable_type"}). + AddRow(1, "Game Science", false, 1, "games") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `galleriables` WHERE `galleriable_type` = ? AND `galleriables`.`galleriable_id` = ? AND `galleriables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(galleriablesRows) + + genreableRows := mock.NewRows([]string{"id", "genreable_id", "genreable_type", "genre_id"}). + AddRow(1, 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `genreables` WHERE `genreable_type` = ? AND `genreables`.`genreable_id` = ? AND `genreables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(genreableRows) + + genresRows := mock.NewRows([]string{"id", "name"}). + AddRow(1, "Action") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `genres` WHERE `genres`.`id` = ? AND `genres`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(genresRows) + + heartsRows := mock.NewRows([]string{"id", "heartable_id", "heartable_type", "user_id"}). + AddRow(1, 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `heartables` WHERE `heartable_type` = ? AND `heartables`.`heartable_id` = ? AND `heartables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(heartsRows) + + gameLanguageRows := mock.NewRows([]string{"id", "menu", "dubs", "subtitles", "game_id", "language_id"}). + AddRow(1, false, true, false, 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `game_languages` WHERE `game_languages`.`game_id` = ? AND `game_languages`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(gameLanguageRows) + + languageRows := mock.NewRows([]string{"id", "name", "iso"}). + AddRow(1, "Portuguese", "pt_BR") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `languages` WHERE `languages`.`id` = ? AND `languages`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(languageRows) + + platformableRows := mock.NewRows([]string{"id", "platformable_id", "platformable_type", "platform_id"}). + AddRow(1, 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `platformables` WHERE `platformable_type` = ? AND `platformables`.`platformable_id` = ? AND `platformables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(platformableRows) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `platforms` WHERE `platforms`.`id` = ? AND `platforms`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(platformsRows) + + gamePublishersRows := mock.NewRows([]string{"id", "publisher_id", "game_id"}). + AddRow(1, 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `game_publishers` WHERE `game_publishers`.`game_id` = ? AND `game_publishers`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(gamePublishersRows) + + publishersRows := mock.NewRows([]string{"id", "name", "acting"}). + AddRow(1, "Game Science", true) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `publishers` WHERE `publishers`.`id` = ? AND `publishers`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(publishersRows) + + requirementRows := mock.NewRows([]string{"id", "os", "dx", "cpu", "ram", "gpu", "rom", "obs", "network", "requirement_type_id", "game_id"}). + AddRow(1, "Windows 11 64-bit", "DirectX 12", "Ryzen 5 3600", "16GB", "GeForce RTX 3090 Ti", "90GB", "Test", "Non necessary", 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `requirements` WHERE `requirements`.`game_id` = ? AND `requirements`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(requirementRows) + + requirementTypeRows := mock.NewRows([]string{"id", "os", "potential"}). + AddRow(1, "windows", "minimum") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `requirement_types` WHERE `requirement_types`.`id` = ? AND `requirement_types`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(requirementTypeRows) + + reviewablesRows := mock.NewRows([]string{"id", "rate", "review", "reviewable_id", "reviewable_type", "user_id"}). + AddRow(1, 5, "Good game!", 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `reviewables` WHERE `reviewable_type` = ? AND `reviewables`.`reviewable_id` = ? AND `reviewables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(reviewablesRows) + + usersRows := mock.NewRows([]string{"id", "name", "email", "nickname", "created_at", "updated_at"}). + AddRow(1, "Fake", "fake@gmail.com", "fake", fixedTime, fixedTime) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE `users`.`id` = ? AND `users`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(usersRows) + + profilesRows := mock.NewRows([]string{"id", "share", "photo", "user_id"}). + AddRow(1, true, "https://photo.co", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `profiles` WHERE `profiles`.`user_id` = ? AND `profiles`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(profilesRows) + + gameStoresRows := mock.NewRows([]string{"id", "price", "url", "game_id", "store_id"}). + AddRow(1, 22999, "https://photo.co", 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `game_stores` WHERE `game_stores`.`game_id` = ? AND `game_stores`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(gameStoresRows) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `stores` WHERE `stores`.`id` = ? AND `stores`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(storesRows) + + supportsRows := mock.NewRows([]string{"id", "url", "email", "contact", "game_id"}). + AddRow(1, "https://google.com", "email@example.com", "fakeContact", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `game_supports` WHERE `game_supports`.`game_id` = ? AND `game_supports`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(supportsRows) + + taggablesRows := mock.NewRows([]string{"id", "taggable_id", "taggable_type", "tag_id"}). + AddRow(1, 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `taggables` WHERE `taggable_type` = ? AND `taggables`.`taggable_id` = ? AND `taggables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(taggablesRows) + + tagsRows := mock.NewRows([]string{"id", "name"}). + AddRow(1, "Adventure") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `tags` WHERE `tags`.`id` = ? AND `tags`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(tagsRows) + + torrentsRows := mock.NewRows([]string{"id", "url", "posted_at", "torrent_provider_id", "game_id"}). + AddRow(1, "https://google.com", fixedTime, 1, 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `torrents` WHERE `torrents`.`game_id` = ? AND `torrents`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(torrentsRows) + + torrentProvidersRows := mock.NewRows([]string{"id", "url", "name"}). + AddRow(1, "https://google.com", "Google") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `torrent_providers` WHERE `torrent_providers`.`id` = ? AND `torrent_providers`.`deleted_at` IS NULL")). + WithArgs(1). + WillReturnRows(torrentProvidersRows) + + viewRows := mock.NewRows([]string{"id", "viewable_id", "viewable_type", "user_id"}). + AddRow(1, 1, "games", 1) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `viewables` WHERE `viewable_type` = ? AND `viewables`.`viewable_id` = ? AND `viewables`.`deleted_at` IS NULL")). + WithArgs("games", 1). + WillReturnRows(viewRows) + + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `viewables` WHERE (viewable_id = ? AND viewable_type = ? AND user_id = ?) AND `viewables`.`deleted_at` IS NULL ORDER BY `viewables`.`id` LIMIT ?")). + WithArgs(1, "games", 1, 1). + WillReturnRows(viewRows) + + mock.ExpectBegin() + mock.ExpectExec(regexp.QuoteMeta("INSERT INTO `viewables` (`created_at`,`updated_at`,`deleted_at`,`viewable_id`,`viewable_type`,`user_id`) VALUES (?,?,?,?,?,?)")). + WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), sqlmock.AnyArg(), 1, "games", 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + }, + "game not found": { + userID: 1, + slug: "invalid", + wantErr: true, + expectedErr: errors.New("record not found"), + wantGame: domain.Game{}, + mockBehavior: func(slug string) { + rows := mock.NewRows([]string{"id", "age", "slug", "title", "condition", "cover", "about", "description", "short_description", "free", "release_date", "created_at", "updated_at"}) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `games` WHERE slug = ? AND `games`.`deleted_at` IS NULL ORDER BY `games`.`id` LIMIT ?")). + WithArgs(slug, 1). + WillReturnRows(rows) + }, + }, + "db error": { + userID: 1, + slug: "valid", + wantErr: true, + expectedErr: errors.New("db error"), + wantGame: domain.Game{}, + mockBehavior: func(slug string) { + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `games` WHERE slug = ? AND `games`.`deleted_at` IS NULL ORDER BY `games`.`id` LIMIT ?")). + WithArgs(slug, 1). + WillReturnError(errors.New("db error")) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(tc.slug) + + game, err := mockRepo.FindBySlug(tc.slug, tc.userID) + + assert.Equal(t, tc.expectedErr, err) + if err == gorm.ErrRecordNotFound { + assert.Equal(t, uint(0), game.ID) + } else { + assert.Equal(t, tc.wantGame.ID, game.ID) + } + + assert.NoError(t, mock.ExpectationsWereMet()) + }) + } +} diff --git a/tests/unit/domains/categoriable_test.go b/tests/unit/domains/categoriable_test.go new file mode 100644 index 0000000..6fd2ab9 --- /dev/null +++ b/tests/unit/domains/categoriable_test.go @@ -0,0 +1,216 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateCategoriable(t *testing.T) { + testCases := map[string]struct { + categoriable domain.Categoriable + mockBehavior func(mock sqlmock.Sqlmock, categoriable domain.Categoriable) + expectError bool + }{ + "Success": { + categoriable: domain.Categoriable{ + CategoriableID: 1, + CategoriableType: "games", + CategoryID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, categoriable domain.Categoriable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `categoriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + categoriable.CategoriableID, + categoriable.CategoriableType, + categoriable.CategoryID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + categoriable: domain.Categoriable{ + CategoriableID: 1, + CategoriableType: "games", + CategoryID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, categoriable domain.Categoriable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `categoriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + categoriable.CategoriableID, + categoriable.CategoriableType, + categoriable.CategoryID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.categoriable) + + err := db.Create(&tc.categoriable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateCategoriable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + categoriable domain.Categoriable + mockBehavior func(mock sqlmock.Sqlmock, categoriable domain.Categoriable) + expectError bool + }{ + "Success": { + categoriable: domain.Categoriable{ + ID: 1, + CategoriableID: 1, + CategoriableType: "games", + CategoryID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, categoriable domain.Categoriable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `categoriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + categoriable.CategoriableID, + categoriable.CategoriableType, + categoriable.CategoryID, + categoriable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + categoriable: domain.Categoriable{ + ID: 1, + CategoriableID: 1, + CategoriableType: "games", + CategoryID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, categoriable domain.Categoriable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `categoriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + categoriable.CategoriableID, + categoriable.CategoriableType, + categoriable.CategoryID, + categoriable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.categoriable) + + err := db.Save(&tc.categoriable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteCategoriable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + categoriableID uint + mockBehavior func(mock sqlmock.Sqlmock, categoriableID uint) + wantErr bool + }{ + "Can soft delete a Categoriable": { + categoriableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, categoriableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `categoriables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), categoriableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + categoriableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, categoriableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `categoriables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete categoriable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.categoriableID) + + err := db.Delete(&domain.Categoriable{}, tc.categoriableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} diff --git a/tests/unit/domains/commentable_test.go b/tests/unit/domains/commentable_test.go new file mode 100644 index 0000000..9f311ce --- /dev/null +++ b/tests/unit/domains/commentable_test.go @@ -0,0 +1,292 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateCommentable(t *testing.T) { + testCases := map[string]struct { + commentable domain.Commentable + mockBehavior func(mock sqlmock.Sqlmock, commentable domain.Commentable) + expectError bool + }{ + "Success": { + commentable: domain.Commentable{ + Comment: "Comment", + CommentableID: 1, + CommentableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, commentable domain.Commentable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `commentables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + commentable.Comment, + commentable.UserID, + commentable.CommentableID, + commentable.CommentableType, + commentable.ParentID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + commentable: domain.Commentable{ + Comment: "Comment", + CommentableID: 1, + CommentableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, commentable domain.Commentable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `commentables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + commentable.Comment, + commentable.UserID, + commentable.CommentableID, + commentable.CommentableType, + commentable.ParentID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.commentable) + + err := db.Create(&tc.commentable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateCommentable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + commentable domain.Commentable + mockBehavior func(mock sqlmock.Sqlmock, commentable domain.Commentable) + expectError bool + }{ + "Success": { + commentable: domain.Commentable{ + ID: 1, + Comment: "Comment", + CommentableID: 1, + CommentableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, commentable domain.Commentable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `commentables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + commentable.Comment, + commentable.UserID, + commentable.CommentableID, + commentable.CommentableType, + commentable.ParentID, + commentable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + commentable: domain.Commentable{ + ID: 1, + Comment: "Comment", + CommentableID: 1, + CommentableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, commentable domain.Commentable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `commentables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + commentable.Comment, + commentable.UserID, + commentable.CommentableID, + commentable.CommentableType, + commentable.ParentID, + commentable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.commentable) + + err := db.Save(&tc.commentable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteCommentable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + commentableID uint + mockBehavior func(mock sqlmock.Sqlmock, commentableID uint) + wantErr bool + }{ + "Can soft delete a Commentable": { + commentableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, commentableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `commentables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), commentableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + commentableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, commentableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `commentables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete commentable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.commentableID) + + err := db.Delete(&domain.Commentable{}, tc.commentableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateCommentableValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + commentable domain.Commentable + }{ + "Can empty validations errors": { + commentable: domain.Commentable{ + Comment: "Comment", + CommentableID: 1, + CommentableType: "games", + User: domain.User{ + Name: "John Doe", + Email: "johndoe@example.com", + Nickname: "johnny", + Blocked: false, + Experience: 500, + Birthdate: fixedTime, + Password: "supersecretpassword", + Profile: domain.Profile{ + Share: true, + }, + Wallet: domain.Wallet{ + Amount: 10, + }, + Level: domain.Level{ + Level: 1, + Experience: 500, + Coins: 10, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.commentable.ValidateCommentable() + assert.NoError(t, err) + }) + } +} + +func TestCreateCommentableWithMissingFields(t *testing.T) { + testCases := map[string]struct { + commentable domain.Commentable + wantErr string + }{ + "Missing required fields": { + commentable: domain.Commentable{}, + wantErr: "Name is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.commentable.ValidateCommentable() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/crack_test.go b/tests/unit/domains/crack_test.go new file mode 100644 index 0000000..7c4cf37 --- /dev/null +++ b/tests/unit/domains/crack_test.go @@ -0,0 +1,315 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateCrack(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + crack domain.Crack + mockBehavior func(mock sqlmock.Sqlmock, crack domain.Crack) + expectError bool + }{ + "Success": { + crack: domain.Crack{ + CrackedAt: utils.TimePtr(fixedTime), + Status: domain.UncrackedStatus, + CrackerID: 1, + ProtectionID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, crack domain.Crack) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `cracks`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + crack.Status, + crack.CrackedAt, + crack.CrackerID, + crack.ProtectionID, + crack.GameID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + crack: domain.Crack{ + Status: domain.UncrackedStatus, + CrackerID: 1, + ProtectionID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, crack domain.Crack) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `cracks`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + crack.Status, + crack.CrackedAt, + crack.CrackerID, + crack.ProtectionID, + crack.GameID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.crack) + + err := db.Create(&tc.crack).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateCrack(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + crack domain.Crack + mockBehavior func(mock sqlmock.Sqlmock, crack domain.Crack) + expectError bool + }{ + "Success": { + crack: domain.Crack{ + ID: 1, + Status: domain.UncrackedStatus, + CrackerID: 1, + ProtectionID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, crack domain.Crack) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `cracks`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + crack.Status, + crack.CrackedAt, + crack.CrackerID, + crack.ProtectionID, + crack.GameID, + crack.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + crack: domain.Crack{ + ID: 1, + Status: domain.UncrackedStatus, + CrackerID: 1, + ProtectionID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, crack domain.Crack) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `cracks`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + crack.Status, + crack.CrackedAt, + crack.CrackerID, + crack.ProtectionID, + crack.GameID, + crack.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.crack) + + err := db.Save(&tc.crack).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteCrack(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + crackID uint + mockBehavior func(mock sqlmock.Sqlmock, crackID uint) + wantErr bool + }{ + "Can soft delete a Crack": { + crackID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, crackID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `cracks` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), crackID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + crackID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, crackID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `cracks` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Crack")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.crackID) + + err := db.Delete(&domain.Crack{}, tc.crackID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateCrack(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + crack domain.Crack + }{ + "Can empty validations errors": { + crack: domain.Crack{ + Status: domain.UncrackedStatus, + Cracker: domain.Cracker{ + Name: "GOLDBERG", + Acting: true, + }, + Protection: domain.Protection{ + Name: "Denuvo", + }, + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.crack.ValidateCrack() + assert.NoError(t, err) + }) + } +} + +func TestCreateCrackWithMissingFields(t *testing.T) { + testCases := map[string]struct { + crack domain.Crack + wantErr string + }{ + "Missing required fields": { + crack: domain.Crack{}, + wantErr: ` + Status is a required field, + Name is a required field, + Name is a required field, + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.crack.ValidateCrack() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/cracker_test.go b/tests/unit/domains/cracker_test.go new file mode 100644 index 0000000..ae276da --- /dev/null +++ b/tests/unit/domains/cracker_test.go @@ -0,0 +1,252 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateCracker(t *testing.T) { + testCases := map[string]struct { + cracker domain.Cracker + mockBehavior func(mock sqlmock.Sqlmock, cracker domain.Cracker) + expectError bool + }{ + "Success": { + cracker: domain.Cracker{ + Name: "GOLDBERG", + Acting: false, + }, + mockBehavior: func(mock sqlmock.Sqlmock, cracker domain.Cracker) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `crackers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + cracker.Name, + cracker.Acting, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + cracker: domain.Cracker{ + Name: "GOLDBERG", + Acting: false, + }, + mockBehavior: func(mock sqlmock.Sqlmock, cracker domain.Cracker) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `crackers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + cracker.Name, + cracker.Acting, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.cracker) + + err := db.Create(&tc.cracker).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateCracker(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + cracker domain.Cracker + mockBehavior func(mock sqlmock.Sqlmock, cracker domain.Cracker) + expectError bool + }{ + "Success": { + cracker: domain.Cracker{ + ID: 1, + Name: "GOLDBERG", + Acting: true, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, cracker domain.Cracker) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `crackers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + cracker.Name, + cracker.Acting, + cracker.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + cracker: domain.Cracker{ + ID: 1, + Name: "GOLDBERG", + Acting: true, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, cracker domain.Cracker) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `crackers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + cracker.Name, + cracker.Acting, + cracker.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.cracker) + + err := db.Save(&tc.cracker).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteCracker(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + crackerID uint + mockBehavior func(mock sqlmock.Sqlmock, crackerID uint) + wantErr bool + }{ + "Can soft delete a Cracker": { + crackerID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, crackerID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `crackers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), crackerID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + crackerID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, crackerID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `crackers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Cracker")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.crackerID) + + err := db.Delete(&domain.Cracker{}, tc.crackerID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateCracker(t *testing.T) { + testCases := map[string]struct { + cracker domain.Cracker + }{ + "Can empty validations errors": { + cracker: domain.Cracker{ + Name: "GOLDBERG", + Acting: true, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.cracker.ValidateCracker() + assert.NoError(t, err) + }) + } +} + +func TestCreateCrackerWithMissingFields(t *testing.T) { + testCases := map[string]struct { + cracker domain.Cracker + wantErr string + }{ + "Missing required fields": { + cracker: domain.Cracker{}, + wantErr: ` + Name is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.cracker.ValidateCracker() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/critic_test.go b/tests/unit/domains/critic_test.go new file mode 100644 index 0000000..85ee1f8 --- /dev/null +++ b/tests/unit/domains/critic_test.go @@ -0,0 +1,314 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateCritic(t *testing.T) { + testCases := map[string]struct { + critic domain.Critic + mockBehavior func(mock sqlmock.Sqlmock, critic domain.Critic) + expectError bool + }{ + "Success": { + critic: domain.Critic{ + Name: "Critic 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + mockBehavior: func(mock sqlmock.Sqlmock, critic domain.Critic) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `critics`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + critic.Name, + critic.URL, + critic.Logo, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + critic: domain.Critic{ + Name: "Critic 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + mockBehavior: func(mock sqlmock.Sqlmock, critic domain.Critic) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `critics`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + critic.Name, + critic.URL, + critic.Logo, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.critic) + + err := db.Create(&tc.critic).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateCritic(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + critic domain.Critic + mockBehavior func(mock sqlmock.Sqlmock, critic domain.Critic) + expectError bool + }{ + "Success": { + critic: domain.Critic{ + ID: 1, + Name: "Critic 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, critic domain.Critic) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `critics`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + critic.Name, + critic.URL, + critic.Logo, + critic.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + critic: domain.Critic{ + ID: 1, + Name: "Critic 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, critic domain.Critic) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `critics`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + critic.Name, + critic.URL, + critic.Logo, + critic.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.critic) + + err := db.Save(&tc.critic).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteCritic(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + criticID uint + mockBehavior func(mock sqlmock.Sqlmock, criticID uint) + wantErr bool + }{ + "Can soft delete a Critic": { + criticID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, criticID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `critics` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), criticID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + criticID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, criticID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `critics` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Critic")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.criticID) + + err := db.Delete(&domain.Critic{}, tc.criticID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestGetCriticByID(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + criticID uint + mockFunc func() + wantCritic domain.Critic + wantError bool + }{ + "Valid Critic fetch": { + criticID: 1, + wantCritic: domain.Critic{ + ID: 1, + Name: "Critic 1", + }, + mockFunc: func() { + rows := sqlmock.NewRows([]string{"id", "name"}). + AddRow(1, "Critic 1") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `critics` WHERE `critics`.`id` = ? AND `critics`.`deleted_at` IS NULL ORDER BY `critics`.`id` LIMIT ?")). + WithArgs(1, 1).WillReturnRows(rows) + }, + wantError: false, + }, + "Critic not found": { + criticID: 2, + wantCritic: domain.Critic{}, + wantError: true, + mockFunc: func() { + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `critics` WHERE `critics`.`id` = ? AND `critics`.`deleted_at` IS NULL ORDER BY `critics`.`id` LIMIT ?")). + WithArgs(2, 1).WillReturnError(fmt.Errorf("record not found")) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockFunc() + + var critic domain.Critic + err := db.First(&critic, tc.criticID).Error + + if tc.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.wantCritic, critic) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateCriticValidData(t *testing.T) { + testCases := map[string]struct { + critic domain.Critic + }{ + "Can empty validations errors": { + critic: domain.Critic{ + Name: "Critic 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.critic.ValidateCritic() + assert.NoError(t, err) + }) + } +} + +func TestCreateCriticWithMissingFields(t *testing.T) { + testCases := map[string]struct { + critic domain.Critic + wantErr string + }{ + "Missing required fields": { + critic: domain.Critic{}, + wantErr: "Name is a required field, URL is a required field, Logo is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.critic.ValidateCritic() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/criticable_test.go b/tests/unit/domains/criticable_test.go new file mode 100644 index 0000000..4690c7d --- /dev/null +++ b/tests/unit/domains/criticable_test.go @@ -0,0 +1,288 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" +) + +func TestCreateCriticable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + criticable domain.Criticable + mockBehavior func(mock sqlmock.Sqlmock, criticable domain.Criticable) + expectError bool + }{ + "Success": { + criticable: domain.Criticable{ + Rate: decimal.NewFromUint64(5), + URL: "https://google.com", + PostedAt: fixedTime, + CriticID: 1, + CriticableID: 1, + CriticableType: "games", + }, + mockBehavior: func(mock sqlmock.Sqlmock, criticable domain.Criticable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `criticables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + criticable.Rate, + criticable.URL, + criticable.PostedAt, + criticable.CriticableID, + criticable.CriticableType, + criticable.CriticID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + criticable: domain.Criticable{ + Rate: decimal.NewFromUint64(5), + URL: "https://google.com", + CriticID: 1, + CriticableID: 1, + CriticableType: "games", + }, + mockBehavior: func(mock sqlmock.Sqlmock, criticable domain.Criticable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `criticables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + criticable.Rate, + criticable.URL, + criticable.PostedAt, + criticable.CriticableID, + criticable.CriticableType, + criticable.CriticID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.criticable) + + err := db.Create(&tc.criticable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateCriticable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + criticable domain.Criticable + mockBehavior func(mock sqlmock.Sqlmock, criticable domain.Criticable) + expectError bool + }{ + "Success": { + criticable: domain.Criticable{ + ID: 1, + Rate: decimal.NewFromUint64(5), + URL: "https://google.com", + CriticID: 1, + CriticableID: 1, + CriticableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, criticable domain.Criticable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `criticables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + criticable.Rate, + criticable.URL, + criticable.PostedAt, + criticable.CriticableID, + criticable.CriticableType, + criticable.CriticID, + criticable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + criticable: domain.Criticable{ + ID: 1, + Rate: decimal.NewFromUint64(5), + URL: "https://google.com", + CriticID: 1, + CriticableID: 1, + CriticableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, criticable domain.Criticable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `criticables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + criticable.Rate, + criticable.URL, + criticable.PostedAt, + criticable.CriticableID, + criticable.CriticableType, + criticable.CriticID, + criticable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.criticable) + + err := db.Save(&tc.criticable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteCriticable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + criticableID uint + mockBehavior func(mock sqlmock.Sqlmock, criticableID uint) + wantErr bool + }{ + "Can soft delete a Criticable": { + criticableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, criticableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `criticables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), criticableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + criticableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, criticableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `criticables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Criticable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.criticableID) + + err := db.Delete(&domain.Criticable{}, tc.criticableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateCriticableValidData(t *testing.T) { + testCases := map[string]struct { + criticable domain.Criticable + }{ + "Can empty validations errors": { + criticable: domain.Criticable{ + Rate: decimal.NewFromUint64(5), + URL: "https://google.com", + CriticableID: 1, + CriticableType: "games", + Critic: domain.Critic{ + Name: "Test", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.criticable.ValidateCriticable() + assert.NoError(t, err) + }) + } +} + +func TestCreateCriticableWithMissingFields(t *testing.T) { + testCases := map[string]struct { + criticable domain.Criticable + wantErr string + }{ + "Missing required fields": { + criticable: domain.Criticable{}, + wantErr: "Name is a required field, URL is a required field, Logo is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.criticable.ValidateCriticable() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/developer_test.go b/tests/unit/domains/developer_test.go new file mode 100644 index 0000000..73dd9a1 --- /dev/null +++ b/tests/unit/domains/developer_test.go @@ -0,0 +1,252 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateDeveloper(t *testing.T) { + testCases := map[string]struct { + developer domain.Developer + mockBehavior func(mock sqlmock.Sqlmock, developer domain.Developer) + expectError bool + }{ + "Success": { + developer: domain.Developer{ + Name: "Game Science", + Acting: false, + }, + mockBehavior: func(mock sqlmock.Sqlmock, developer domain.Developer) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + developer.Name, + developer.Acting, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + developer: domain.Developer{ + Name: "Game Science", + Acting: false, + }, + mockBehavior: func(mock sqlmock.Sqlmock, developer domain.Developer) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + developer.Name, + developer.Acting, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.developer) + + err := db.Create(&tc.developer).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateDeveloper(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + developer domain.Developer + mockBehavior func(mock sqlmock.Sqlmock, developer domain.Developer) + expectError bool + }{ + "Success": { + developer: domain.Developer{ + ID: 1, + Name: "Game Science", + Acting: true, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, developer domain.Developer) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + developer.Name, + developer.Acting, + developer.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + developer: domain.Developer{ + ID: 1, + Name: "Game Science", + Acting: false, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, developer domain.Developer) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + developer.Name, + developer.Acting, + developer.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.developer) + + err := db.Save(&tc.developer).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteDeveloper(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + developerID uint + mockBehavior func(mock sqlmock.Sqlmock, developerID uint) + wantErr bool + }{ + "Can soft delete a Developer": { + developerID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, developerID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `developers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), developerID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + developerID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, developerID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `developers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Developer")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.developerID) + + err := db.Delete(&domain.Developer{}, tc.developerID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateDeveloper(t *testing.T) { + testCases := map[string]struct { + developer domain.Developer + }{ + "Can empty validations errors": { + developer: domain.Developer{ + Name: "Game Science", + Acting: true, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.developer.ValidateDeveloper() + assert.NoError(t, err) + }) + } +} + +func TestCreateDeveloperWithMissingFields(t *testing.T) { + testCases := map[string]struct { + developer domain.Developer + wantErr string + }{ + "Missing required fields": { + developer: domain.Developer{}, + wantErr: ` + Name is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.developer.ValidateDeveloper() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/dlc_store_test.go b/tests/unit/domains/dlc_store_test.go new file mode 100644 index 0000000..46a4a99 --- /dev/null +++ b/tests/unit/domains/dlc_store_test.go @@ -0,0 +1,312 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateDLCStore(t *testing.T) { + testCases := map[string]struct { + DLCStore domain.DLCStore + mockBehavior func(mock sqlmock.Sqlmock, DLCStore domain.DLCStore) + expectError bool + }{ + "Success": { + DLCStore: domain.DLCStore{ + Price: 2200, + URL: "https://google.com", + DLCID: 1, + StoreID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLCStore domain.DLCStore) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `dlc_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLCStore.Price, + DLCStore.URL, + DLCStore.DLCID, + DLCStore.StoreID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + DLCStore: domain.DLCStore{ + Price: 2200, + URL: "https://google.com", + DLCID: 1, + StoreID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLCStore domain.DLCStore) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `dlc_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLCStore.Price, + DLCStore.URL, + DLCStore.DLCID, + DLCStore.StoreID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.DLCStore) + + err := db.Create(&tc.DLCStore).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateDLCStore(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + DLCStore domain.DLCStore + mockBehavior func(mock sqlmock.Sqlmock, DLCStore domain.DLCStore) + expectError bool + }{ + "Success": { + DLCStore: domain.DLCStore{ + ID: 1, + Price: 2200, + URL: "https://google.com", + DLCID: 1, + StoreID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLCStore domain.DLCStore) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlc_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLCStore.Price, + DLCStore.URL, + DLCStore.DLCID, + DLCStore.StoreID, + DLCStore.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + DLCStore: domain.DLCStore{ + ID: 1, + Price: 2200, + URL: "https://google.com", + DLCID: 1, + StoreID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLCStore domain.DLCStore) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlc_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLCStore.Price, + DLCStore.URL, + DLCStore.DLCID, + DLCStore.StoreID, + DLCStore.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.DLCStore) + + err := db.Save(&tc.DLCStore).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteDLCStore(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + DLCStoreID uint + mockBehavior func(mock sqlmock.Sqlmock, DLCStoreID uint) + wantErr bool + }{ + "Can soft delete a DLCStore": { + DLCStoreID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, DLCStoreID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlc_stores` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), DLCStoreID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + DLCStoreID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, DLCStoreID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlc_stores` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete DLCStore")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.DLCStoreID) + + err := db.Delete(&domain.DLCStore{}, tc.DLCStoreID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateDLCStore(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + DLCStore domain.DLCStore + }{ + "Can empty validations errors": { + DLCStore: domain.DLCStore{ + Price: 2200, + URL: "https://google.com", + DLC: domain.DLC{ + Name: "Game Science", + Cover: "https://google.com", + ReleaseDate: fixedTime, + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + Store: domain.Store{ + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.DLCStore.ValidateDLCStore() + assert.NoError(t, err) + }) + } +} + +func TestCreateDLCStoreWithMissingFields(t *testing.T) { + testCases := map[string]struct { + DLCStore domain.DLCStore + wantErr string + }{ + "Missing required fields": { + DLCStore: domain.DLCStore{}, + wantErr: ` + Name is a required field, + Cover is a required field, + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.DLCStore.ValidateDLCStore() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/dlc_test.go b/tests/unit/domains/dlc_test.go new file mode 100644 index 0000000..50569ed --- /dev/null +++ b/tests/unit/domains/dlc_test.go @@ -0,0 +1,304 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateDLC(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + DLC domain.DLC + mockBehavior func(mock sqlmock.Sqlmock, DLC domain.DLC) + expectError bool + }{ + "Success": { + DLC: domain.DLC{ + Name: "Game Science", + Cover: "https://google.com", + ReleaseDate: fixedTime, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLC domain.DLC) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `dlcs`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLC.Name, + DLC.Cover, + DLC.ReleaseDate, + DLC.GameID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + DLC: domain.DLC{ + Name: "Game Science", + Cover: "https://google.com", + ReleaseDate: fixedTime, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLC domain.DLC) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `dlcs`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLC.Name, + DLC.Cover, + DLC.ReleaseDate, + DLC.GameID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.DLC) + + err := db.Create(&tc.DLC).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateDLC(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + DLC domain.DLC + mockBehavior func(mock sqlmock.Sqlmock, DLC domain.DLC) + expectError bool + }{ + "Success": { + DLC: domain.DLC{ + ID: 1, + Name: "Game Science", + Cover: "https://google.com", + ReleaseDate: fixedTime, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLC domain.DLC) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlcs`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLC.Name, + DLC.Cover, + DLC.ReleaseDate, + DLC.GameID, + DLC.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + DLC: domain.DLC{ + ID: 1, + Name: "Game Science", + Cover: "https://google.com", + ReleaseDate: fixedTime, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, DLC domain.DLC) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlcs`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + DLC.Name, + DLC.Cover, + DLC.ReleaseDate, + DLC.GameID, + DLC.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.DLC) + + err := db.Save(&tc.DLC).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteDLC(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + DLCID uint + mockBehavior func(mock sqlmock.Sqlmock, DLCID uint) + wantErr bool + }{ + "Can soft delete a DLC": { + DLCID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, DLCID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlcs` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), DLCID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + DLCID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, DLCID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `dlcs` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete DLC")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.DLCID) + + err := db.Delete(&domain.DLC{}, tc.DLCID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateDLC(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + DLC domain.DLC + }{ + "Can empty validations errors": { + DLC: domain.DLC{ + Name: "Game Science", + Cover: "https://google.com", + ReleaseDate: fixedTime, + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.DLC.ValidateDLC() + assert.NoError(t, err) + }) + } +} + +func TestCreateDLCWithMissingFields(t *testing.T) { + testCases := map[string]struct { + DLC domain.DLC + wantErr string + }{ + "Missing required fields": { + DLC: domain.DLC{}, + wantErr: ` + Name is a required field, + Cover is a required field, + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.DLC.ValidateDLC() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/galleriable_test.go b/tests/unit/domains/galleriable_test.go new file mode 100644 index 0000000..6750d96 --- /dev/null +++ b/tests/unit/domains/galleriable_test.go @@ -0,0 +1,277 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGalleriable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + galleriable domain.Galleriable + mockBehavior func(mock sqlmock.Sqlmock, galleriable domain.Galleriable) + expectError bool + }{ + "Success": { + galleriable: domain.Galleriable{ + S3: false, + Path: "https://google.com", + GalleriableID: 1, + GalleriableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, galleriable domain.Galleriable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `galleriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + galleriable.S3, + galleriable.Path, + galleriable.GalleriableID, + galleriable.GalleriableType, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + galleriable: domain.Galleriable{ + S3: false, + Path: "https://google.com", + GalleriableID: 1, + GalleriableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, galleriable domain.Galleriable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `galleriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + galleriable.S3, + galleriable.Path, + galleriable.GalleriableID, + galleriable.GalleriableType, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.galleriable) + + err := db.Create(&tc.galleriable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGalleriable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + galleriable domain.Galleriable + mockBehavior func(mock sqlmock.Sqlmock, galleriable domain.Galleriable) + expectError bool + }{ + "Success": { + galleriable: domain.Galleriable{ + ID: 1, + S3: false, + Path: "https://google.com", + GalleriableID: 1, + GalleriableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, galleriable domain.Galleriable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `galleriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + galleriable.S3, + galleriable.Path, + galleriable.GalleriableID, + galleriable.GalleriableType, + galleriable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + galleriable: domain.Galleriable{ + ID: 1, + S3: false, + Path: "https://google.com", + GalleriableID: 1, + GalleriableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, galleriable domain.Galleriable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `galleriables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + galleriable.S3, + galleriable.Path, + galleriable.GalleriableID, + galleriable.GalleriableType, + galleriable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.galleriable) + + err := db.Save(&tc.galleriable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGalleriable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + galleriableID uint + mockBehavior func(mock sqlmock.Sqlmock, galleriableID uint) + wantErr bool + }{ + "Can soft delete a Galleriable": { + galleriableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, galleriableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `galleriables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), galleriableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + galleriableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, galleriableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `galleriables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Galleriable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.galleriableID) + + err := db.Delete(&domain.Galleriable{}, tc.galleriableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateGalleriableValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + galleriable domain.Galleriable + }{ + "Can empty validations errors": { + galleriable: domain.Galleriable{ + S3: false, + Path: "https://google.com", + GalleriableID: 1, + GalleriableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.galleriable.ValidateGalleriable() + assert.NoError(t, err) + }) + } +} + +func TestCreateGalleriableWithMissingFields(t *testing.T) { + testCases := map[string]struct { + galleriable domain.Galleriable + wantErr string + }{ + "Missing required fields": { + galleriable: domain.Galleriable{}, + wantErr: "Path is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.galleriable.ValidateGalleriable() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/game_developer_test.go b/tests/unit/domains/game_developer_test.go new file mode 100644 index 0000000..e174919 --- /dev/null +++ b/tests/unit/domains/game_developer_test.go @@ -0,0 +1,287 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGameDeveloper(t *testing.T) { + testCases := map[string]struct { + gameDeveloper domain.GameDeveloper + mockBehavior func(mock sqlmock.Sqlmock, gameDeveloper domain.GameDeveloper) + expectError bool + }{ + "Success": { + gameDeveloper: domain.GameDeveloper{ + GameID: 1, + DeveloperID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameDeveloper domain.GameDeveloper) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameDeveloper.GameID, + gameDeveloper.DeveloperID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + gameDeveloper: domain.GameDeveloper{ + GameID: 1, + DeveloperID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameDeveloper domain.GameDeveloper) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameDeveloper.GameID, + gameDeveloper.DeveloperID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameDeveloper) + + err := db.Create(&tc.gameDeveloper).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGameDeveloper(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameDeveloper domain.GameDeveloper + mockBehavior func(mock sqlmock.Sqlmock, gameDeveloper domain.GameDeveloper) + expectError bool + }{ + "Success": { + gameDeveloper: domain.GameDeveloper{ + ID: 1, + GameID: 1, + DeveloperID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameDeveloper domain.GameDeveloper) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameDeveloper.GameID, + gameDeveloper.DeveloperID, + gameDeveloper.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + gameDeveloper: domain.GameDeveloper{ + ID: 1, + GameID: 1, + DeveloperID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameDeveloper domain.GameDeveloper) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_developers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameDeveloper.GameID, + gameDeveloper.DeveloperID, + gameDeveloper.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameDeveloper) + + err := db.Save(&tc.gameDeveloper).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGameDeveloper(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + gameDeveloperID uint + mockBehavior func(mock sqlmock.Sqlmock, gameDeveloperID uint) + wantErr bool + }{ + "Can soft delete a GameDeveloper": { + gameDeveloperID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, gameDeveloperID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_developers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), gameDeveloperID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + gameDeveloperID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, GameDeveloper uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_developers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete GameDeveloper")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.gameDeveloperID) + + err := db.Delete(&domain.GameDeveloper{}, tc.gameDeveloperID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateGameDeveloper(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameDeveloper domain.GameDeveloper + }{ + "Can empty validations errors": { + gameDeveloper: domain.GameDeveloper{ + Developer: domain.Developer{ + Name: "Game Science", + Acting: true, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameDeveloper.ValidateGameDeveloper() + assert.NoError(t, err) + }) + } +} + +func TestCreateGameDeveloperWithMissingFields(t *testing.T) { + testCases := map[string]struct { + gameDeveloper domain.GameDeveloper + wantErr string + }{ + "Missing required fields": { + gameDeveloper: domain.GameDeveloper{}, + wantErr: ` + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameDeveloper.ValidateGameDeveloper() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/game_language_test.go b/tests/unit/domains/game_language_test.go new file mode 100644 index 0000000..603060c --- /dev/null +++ b/tests/unit/domains/game_language_test.go @@ -0,0 +1,321 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGameLanguage(t *testing.T) { + testCases := map[string]struct { + gameLanguage domain.GameLanguage + mockBehavior func(mock sqlmock.Sqlmock, gameLanguage domain.GameLanguage) + expectError bool + }{ + "Success": { + gameLanguage: domain.GameLanguage{ + Menu: false, + Dubs: true, + Subtitles: false, + LanguageID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameLanguage domain.GameLanguage) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameLanguage.Menu, + gameLanguage.Dubs, + gameLanguage.Subtitles, + gameLanguage.LanguageID, + gameLanguage.GameID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + gameLanguage: domain.GameLanguage{ + Menu: false, + Dubs: true, + Subtitles: false, + LanguageID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameLanguage domain.GameLanguage) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameLanguage.Menu, + gameLanguage.Dubs, + gameLanguage.Subtitles, + gameLanguage.LanguageID, + gameLanguage.GameID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameLanguage) + + err := db.Create(&tc.gameLanguage).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGameLanguage(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameLanguage domain.GameLanguage + mockBehavior func(mock sqlmock.Sqlmock, gameLanguage domain.GameLanguage) + expectError bool + }{ + "Success": { + gameLanguage: domain.GameLanguage{ + ID: 1, + Menu: false, + Dubs: true, + Subtitles: false, + LanguageID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameLanguage domain.GameLanguage) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameLanguage.Menu, + gameLanguage.Dubs, + gameLanguage.Subtitles, + gameLanguage.LanguageID, + gameLanguage.GameID, + gameLanguage.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + gameLanguage: domain.GameLanguage{ + ID: 1, + Menu: false, + Dubs: true, + Subtitles: false, + LanguageID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameLanguage domain.GameLanguage) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameLanguage.Menu, + gameLanguage.Dubs, + gameLanguage.Subtitles, + gameLanguage.LanguageID, + gameLanguage.GameID, + gameLanguage.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameLanguage) + + err := db.Save(&tc.gameLanguage).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGameLanguage(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + gameLanguageID uint + mockBehavior func(mock sqlmock.Sqlmock, gameLanguageID uint) + wantErr bool + }{ + "Can soft delete a GameLanguage": { + gameLanguageID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, gameLanguageID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_languages` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), gameLanguageID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + gameLanguageID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, gameLanguageID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_languages` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete GameLanguage")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.gameLanguageID) + + err := db.Delete(&domain.GameLanguage{}, tc.gameLanguageID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateGameLanguageLanguageValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameLanguage domain.GameLanguage + }{ + "Can empty validations errors": { + gameLanguage: domain.GameLanguage{ + Menu: false, + Dubs: true, + Subtitles: false, + LanguageID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Language: domain.Language{ + ID: 1, + Name: "Portuguese", + ISO: "pt_BR", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + Game: domain.Game{ + ID: 1, + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameLanguage.ValidateGameLanguage() + assert.NoError(t, err) + }) + } +} + +func TestCreateGameLanguageWithMissingFields(t *testing.T) { + testCases := map[string]struct { + gameLanguage domain.GameLanguage + wantErr string + }{ + "Missing required fields": { + gameLanguage: domain.GameLanguage{}, + wantErr: ` + Name is a required field, + ISO is a required field, + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameLanguage.ValidateGameLanguage() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/game_publisher_test.go b/tests/unit/domains/game_publisher_test.go new file mode 100644 index 0000000..51164ce --- /dev/null +++ b/tests/unit/domains/game_publisher_test.go @@ -0,0 +1,287 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGamePublisher(t *testing.T) { + testCases := map[string]struct { + gamePublisher domain.GamePublisher + mockBehavior func(mock sqlmock.Sqlmock, gamePublisher domain.GamePublisher) + expectError bool + }{ + "Success": { + gamePublisher: domain.GamePublisher{ + GameID: 1, + PublisherID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gamePublisher domain.GamePublisher) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gamePublisher.GameID, + gamePublisher.PublisherID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + gamePublisher: domain.GamePublisher{ + GameID: 1, + PublisherID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gamePublisher domain.GamePublisher) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gamePublisher.GameID, + gamePublisher.PublisherID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gamePublisher) + + err := db.Create(&tc.gamePublisher).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGamePublisher(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gamePublisher domain.GamePublisher + mockBehavior func(mock sqlmock.Sqlmock, gamePublisher domain.GamePublisher) + expectError bool + }{ + "Success": { + gamePublisher: domain.GamePublisher{ + ID: 1, + GameID: 1, + PublisherID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gamePublisher domain.GamePublisher) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gamePublisher.GameID, + gamePublisher.PublisherID, + gamePublisher.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + gamePublisher: domain.GamePublisher{ + ID: 1, + GameID: 1, + PublisherID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gamePublisher domain.GamePublisher) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gamePublisher.GameID, + gamePublisher.PublisherID, + gamePublisher.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gamePublisher) + + err := db.Save(&tc.gamePublisher).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGamePublisher(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + gamePublisherID uint + mockBehavior func(mock sqlmock.Sqlmock, gamePublisherID uint) + wantErr bool + }{ + "Can soft delete a GamePublisher": { + gamePublisherID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, gamePublisherID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_publishers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), gamePublisherID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + gamePublisherID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, GamePublisher uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_publishers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete GamePublisher")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.gamePublisherID) + + err := db.Delete(&domain.GamePublisher{}, tc.gamePublisherID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateGamePublisher(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gamePublisher domain.GamePublisher + }{ + "Can empty validations errors": { + gamePublisher: domain.GamePublisher{ + Publisher: domain.Publisher{ + Name: "Game Science", + Acting: true, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gamePublisher.ValidateGamePublisher() + assert.NoError(t, err) + }) + } +} + +func TestCreateGamePublisherWithMissingFields(t *testing.T) { + testCases := map[string]struct { + gamePublisher domain.GamePublisher + wantErr string + }{ + "Missing required fields": { + gamePublisher: domain.GamePublisher{}, + wantErr: ` + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gamePublisher.ValidateGamePublisher() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/game_store_test.go b/tests/unit/domains/game_store_test.go new file mode 100644 index 0000000..455f653 --- /dev/null +++ b/tests/unit/domains/game_store_test.go @@ -0,0 +1,309 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGameStore(t *testing.T) { + testCases := map[string]struct { + gameStore domain.GameStore + mockBehavior func(mock sqlmock.Sqlmock, gameStore domain.GameStore) + expectError bool + }{ + "Success": { + gameStore: domain.GameStore{ + Price: 22999, + URL: "https://google.com", + GameID: 1, + StoreID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameStore domain.GameStore) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameStore.Price, + gameStore.URL, + gameStore.GameID, + gameStore.StoreID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + gameStore: domain.GameStore{ + Price: 22999, + URL: "https://google.com", + GameID: 1, + StoreID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameStore domain.GameStore) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameStore.Price, + gameStore.URL, + gameStore.GameID, + gameStore.StoreID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameStore) + + err := db.Create(&tc.gameStore).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGameStore(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameStore domain.GameStore + mockBehavior func(mock sqlmock.Sqlmock, gameStore domain.GameStore) + expectError bool + }{ + "Success": { + gameStore: domain.GameStore{ + ID: 1, + Price: 22999, + URL: "https://google.com", + GameID: 1, + StoreID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameStore domain.GameStore) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameStore.Price, + gameStore.URL, + gameStore.GameID, + gameStore.StoreID, + gameStore.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + gameStore: domain.GameStore{ + ID: 1, + Price: 22999, + URL: "https://google.com", + GameID: 1, + StoreID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameStore domain.GameStore) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameStore.Price, + gameStore.URL, + gameStore.GameID, + gameStore.StoreID, + gameStore.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameStore) + + err := db.Save(&tc.gameStore).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGameStore(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + gameStoreID uint + mockBehavior func(mock sqlmock.Sqlmock, gameStoreID uint) + wantErr bool + }{ + "Can soft delete a GameStore": { + gameStoreID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, gameStoreID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_stores` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), gameStoreID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + gameStoreID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, gameStoreID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_stores` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete GameStore")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.gameStoreID) + + err := db.Delete(&domain.GameStore{}, tc.gameStoreID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateGameStoreValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameStore domain.GameStore + }{ + "Can empty validations errors": { + gameStore: domain.GameStore{ + Price: 22999, + URL: "https://google.com", + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + Store: domain.Store{ + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameStore.ValidateGameStore() + assert.NoError(t, err) + }) + } +} + +func TestCreateGameStoreWithMissingFields(t *testing.T) { + testCases := map[string]struct { + gameStore domain.GameStore + wantErr string + }{ + "Missing required fields": { + gameStore: domain.GameStore{}, + wantErr: ` + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field, + Name is a required field, + URL is a required field, + Slug is a required field, + Logo is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameStore.ValidateGameStore() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/game_support_test.go b/tests/unit/domains/game_support_test.go new file mode 100644 index 0000000..27c8258 --- /dev/null +++ b/tests/unit/domains/game_support_test.go @@ -0,0 +1,300 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGameSupport(t *testing.T) { + testCases := map[string]struct { + gameSupport domain.GameSupport + mockBehavior func(mock sqlmock.Sqlmock, gameSupport domain.GameSupport) + expectError bool + }{ + "Success": { + gameSupport: domain.GameSupport{ + URL: utils.StringPtr("https://google.com"), + Email: utils.StringPtr("fake@example.com"), + Contact: utils.StringPtr("fakecontact"), + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameSupport domain.GameSupport) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_supports`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameSupport.URL, + gameSupport.Email, + gameSupport.Contact, + gameSupport.GameID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + gameSupport: domain.GameSupport{ + URL: utils.StringPtr("https://google.com"), + Email: utils.StringPtr("fake@example.com"), + Contact: utils.StringPtr("fakecontact"), + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameSupport domain.GameSupport) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `game_supports`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameSupport.URL, + gameSupport.Email, + gameSupport.Contact, + gameSupport.GameID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameSupport) + + err := db.Create(&tc.gameSupport).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGameSupport(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameSupport domain.GameSupport + mockBehavior func(mock sqlmock.Sqlmock, gameSupport domain.GameSupport) + expectError bool + }{ + "Success": { + gameSupport: domain.GameSupport{ + ID: 1, + URL: utils.StringPtr("https://google.com"), + Email: utils.StringPtr("fake@example.com"), + Contact: utils.StringPtr("fakecontact"), + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameSupport domain.GameSupport) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_supports`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameSupport.URL, + gameSupport.Email, + gameSupport.Contact, + gameSupport.GameID, + gameSupport.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + gameSupport: domain.GameSupport{ + ID: 1, + URL: utils.StringPtr("https://google.com"), + Email: utils.StringPtr("fake@example.com"), + Contact: utils.StringPtr("fakecontact"), + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, gameSupport domain.GameSupport) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_supports`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + gameSupport.URL, + gameSupport.Email, + gameSupport.Contact, + gameSupport.GameID, + gameSupport.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.gameSupport) + + err := db.Save(&tc.gameSupport).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGameSupport(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + gameSupportID uint + mockBehavior func(mock sqlmock.Sqlmock, gameSupportID uint) + wantErr bool + }{ + "Can soft delete a GameSupport": { + gameSupportID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, gameSupportID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_supports` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), gameSupportID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + gameSupportID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, GameSupport uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `game_supports` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete GameSupport")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.gameSupportID) + + err := db.Delete(&domain.GameSupport{}, tc.gameSupportID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateGameSupport(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + gameSupport domain.GameSupport + }{ + "Can empty validations errors": { + gameSupport: domain.GameSupport{ + URL: utils.StringPtr("https://google.com"), + Email: utils.StringPtr("fake@example.com"), + Contact: utils.StringPtr("fakecontact"), + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameSupport.ValidateGameSupport() + assert.NoError(t, err) + }) + } +} + +func TestCreateGameSupportWithMissingFields(t *testing.T) { + testCases := map[string]struct { + gameSupport domain.GameSupport + wantErr string + }{ + "Missing required fields": { + gameSupport: domain.GameSupport{}, + wantErr: ` + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.gameSupport.ValidateGameSupport() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/game_test.go b/tests/unit/domains/game_test.go new file mode 100644 index 0000000..c4b729e --- /dev/null +++ b/tests/unit/domains/game_test.go @@ -0,0 +1,414 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGame(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + game domain.Game + mockBehavior func(mock sqlmock.Sqlmock, game domain.Game) + expectError bool + }{ + "Success": { + game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, game domain.Game) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `games`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + game.Age, + game.Slug, + game.Title, + game.Condition, + game.Cover, + game.About, + game.Description, + game.ShortDescription, + game.Free, + game.Legal, + game.Website, + sqlmock.AnyArg(), + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, game domain.Game) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `games`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + game.Age, + game.Slug, + game.Title, + game.Condition, + game.Cover, + game.About, + game.Description, + game.ShortDescription, + game.Free, + game.Legal, + game.Website, + sqlmock.AnyArg(), + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.game) + + err := db.Create(&tc.game).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGame(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + game domain.Game + mockBehavior func(mock sqlmock.Sqlmock, game domain.Game) + expectError bool + }{ + "Success": { + game: domain.Game{ + ID: 1, + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, game domain.Game) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `games`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + game.Age, + game.Slug, + game.Title, + game.Condition, + game.Cover, + game.About, + game.Description, + game.ShortDescription, + game.Free, + game.Legal, + game.Website, + sqlmock.AnyArg(), + game.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + game: domain.Game{ + ID: 1, + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, game domain.Game) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `games`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + game.Age, + game.Slug, + game.Title, + game.Condition, + game.Cover, + game.About, + game.Description, + game.ShortDescription, + game.Free, + game.Legal, + game.Website, + sqlmock.AnyArg(), + game.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.game) + + err := db.Save(&tc.game).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGame(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + gameID uint + mockBehavior func(mock sqlmock.Sqlmock, gameID uint) + wantErr bool + }{ + "Can soft delete a Game": { + gameID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, gameID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `games` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), gameID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + gameID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, gameID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `games` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Game")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.gameID) + + err := db.Delete(&domain.Game{}, tc.gameID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestGetGameBySlug(t *testing.T) { + fixedTime := time.Now() + db, mock := tests.Setup(t) + + testCases := map[string]struct { + gameSlug string + mockFunc func() + wantGame domain.Game + wantError bool + }{ + "Valid game fetch": { + gameSlug: "valid", + wantGame: domain.Game{ + ID: 1, + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockFunc: func() { + rows := sqlmock.NewRows([]string{"id", "title", "slug", "age", "cover", "about", "description", "short_description", "free", "release_date", "condition", "created_at", "updated_at"}). + AddRow(1, "Game Test", "valid", 18, "https://placehold.co/600x400/EEE/31343C", "About game", "Description", "Short description", false, fixedTime, domain.CommomCondition, fixedTime, fixedTime) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `games` WHERE slug = ? AND `games`.`deleted_at` IS NULL ORDER BY `games`.`id` LIMIT ?")). + WithArgs("valid", 1).WillReturnRows(rows) + }, + wantError: false, + }, + "Game not found": { + gameSlug: "invalid", + wantGame: domain.Game{}, + wantError: true, + mockFunc: func() { + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `games` WHERE slug = ? AND `games`.`deleted_at` IS NULL ORDER BY `games`.`id` LIMIT ?")). + WithArgs("invalid", 1).WillReturnError(fmt.Errorf("record not found")) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockFunc() + + var game domain.Game + err := db.Where("slug = ?", tc.gameSlug).First(&game).Error + + if tc.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.wantGame, game) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateGameValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + game domain.Game + }{ + "Can empty validations errors": { + game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.game.ValidateGame() + assert.NoError(t, err) + }) + } +} + +func TestCreateGameWithMissingFields(t *testing.T) { + testCases := map[string]struct { + game domain.Game + wantErr string + }{ + "Missing required fields": { + game: domain.Game{}, + wantErr: "Age is a required field, Slug is a required field, Title is a required field, Condition is a required field, Cover is a required field, About is a required field, Description is a required field, ShortDescription is a required field, ReleaseDate is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.game.ValidateGame() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/genreable_test.go b/tests/unit/domains/genreable_test.go new file mode 100644 index 0000000..d9688d6 --- /dev/null +++ b/tests/unit/domains/genreable_test.go @@ -0,0 +1,216 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateGenreable(t *testing.T) { + testCases := map[string]struct { + genreable domain.Genreable + mockBehavior func(mock sqlmock.Sqlmock, genreable domain.Genreable) + expectError bool + }{ + "Success": { + genreable: domain.Genreable{ + GenreableID: 1, + GenreableType: "games", + GenreID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, genreable domain.Genreable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `genreables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + genreable.GenreableID, + genreable.GenreableType, + genreable.GenreID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + genreable: domain.Genreable{ + GenreableID: 1, + GenreableType: "games", + GenreID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, genreable domain.Genreable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `genreables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + genreable.GenreableID, + genreable.GenreableType, + genreable.GenreID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.genreable) + + err := db.Create(&tc.genreable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateGenreable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + genreable domain.Genreable + mockBehavior func(mock sqlmock.Sqlmock, genreable domain.Genreable) + expectError bool + }{ + "Success": { + genreable: domain.Genreable{ + ID: 1, + GenreableID: 1, + GenreableType: "games", + GenreID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, genreable domain.Genreable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `genreables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + genreable.GenreableID, + genreable.GenreableType, + genreable.GenreID, + genreable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + genreable: domain.Genreable{ + ID: 1, + GenreableID: 1, + GenreableType: "games", + GenreID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, genreable domain.Genreable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `genreables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + genreable.GenreableID, + genreable.GenreableType, + genreable.GenreID, + genreable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.genreable) + + err := db.Save(&tc.genreable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteGenreable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + genreableID uint + mockBehavior func(mock sqlmock.Sqlmock, genreableID uint) + wantErr bool + }{ + "Can soft delete a Genreable": { + genreableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, genreableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `genreables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), genreableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + genreableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, genreableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `genreables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Genreable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.genreableID) + + err := db.Delete(&domain.Genreable{}, tc.genreableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} diff --git a/tests/unit/domains/heartable_test.go b/tests/unit/domains/heartable_test.go new file mode 100644 index 0000000..b6f8d50 --- /dev/null +++ b/tests/unit/domains/heartable_test.go @@ -0,0 +1,282 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateHeartable(t *testing.T) { + testCases := map[string]struct { + heartable domain.Heartable + mockBehavior func(mock sqlmock.Sqlmock, heartable domain.Heartable) + expectError bool + }{ + "Success": { + heartable: domain.Heartable{ + HeartableID: 1, + HeartableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, heartable domain.Heartable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `heartables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + heartable.HeartableID, + heartable.HeartableType, + heartable.UserID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + heartable: domain.Heartable{ + HeartableID: 1, + HeartableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, heartable domain.Heartable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `heartables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + heartable.HeartableID, + heartable.HeartableType, + heartable.UserID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.heartable) + + err := db.Create(&tc.heartable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateHeartable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + heartable domain.Heartable + mockBehavior func(mock sqlmock.Sqlmock, heartable domain.Heartable) + expectError bool + }{ + "Success": { + heartable: domain.Heartable{ + ID: 1, + HeartableID: 1, + HeartableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, heartable domain.Heartable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `heartables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + heartable.HeartableID, + heartable.HeartableType, + heartable.UserID, + heartable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + heartable: domain.Heartable{ + ID: 1, + HeartableID: 1, + HeartableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, heartable domain.Heartable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `heartables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + heartable.HeartableID, + heartable.HeartableType, + heartable.UserID, + heartable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.heartable) + + err := db.Save(&tc.heartable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteHeartable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + HeartableID uint + mockBehavior func(mock sqlmock.Sqlmock, HeartableID uint) + wantErr bool + }{ + "Can soft delete a Heartable": { + HeartableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, HeartableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `heartables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), HeartableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + HeartableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, HeartableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `heartables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Heartable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.HeartableID) + + err := db.Delete(&domain.Heartable{}, tc.HeartableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateHeartableValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + heartable domain.Heartable + }{ + "Valid Heartable with zero amount": { + heartable: domain.Heartable{ + ID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + HeartableID: 1, + HeartableType: "games", + User: domain.User{ + Name: "John Doe", + Email: "johndoe@example.com", + Nickname: "johnny", + Blocked: false, + Experience: 500, + Birthdate: fixedTime, + Password: "supersecretpassword", + Profile: domain.Profile{ + Share: true, + }, + Wallet: domain.Wallet{ + Amount: 10, + }, + Level: domain.Level{ + Level: 1, + Experience: 500, + Coins: 10, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.heartable.ValidateHeartable() + assert.NoError(t, err) + }) + } +} + +func TestCreateHeartableWithMissingFields(t *testing.T) { + testCases := map[string]struct { + heartable domain.Heartable + wantErr string + }{ + "Missing required fields": { + heartable: domain.Heartable{}, + wantErr: "Share is a required field, Level is a required field, Experience is a required field, Coins is a required field, Amount is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.heartable.ValidateHeartable() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/language_test.go b/tests/unit/domains/language_test.go new file mode 100644 index 0000000..9152cbd --- /dev/null +++ b/tests/unit/domains/language_test.go @@ -0,0 +1,309 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateLanguage(t *testing.T) { + testCases := map[string]struct { + language domain.Language + mockBehavior func(mock sqlmock.Sqlmock, language domain.Language) + expectError bool + }{ + "Success": { + language: domain.Language{ + Name: "Portuguese", + ISO: "pt_BR", + }, + mockBehavior: func(mock sqlmock.Sqlmock, language domain.Language) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + language.Name, + language.ISO, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + language: domain.Language{ + Name: "Portuguese", + ISO: "pt_BR", + }, + mockBehavior: func(mock sqlmock.Sqlmock, language domain.Language) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + language.Name, + language.ISO, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.language) + + err := db.Create(&tc.language).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateLanguage(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + language domain.Language + mockBehavior func(mock sqlmock.Sqlmock, language domain.Language) + expectError bool + }{ + "Success": { + language: domain.Language{ + ID: 1, + Name: "Portuguese", + ISO: "pt_BR", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, language domain.Language) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + language.Name, + language.ISO, + language.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + language: domain.Language{ + ID: 1, + Name: "Portuguese", + ISO: "pt_BR", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, language domain.Language) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `languages`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + language.Name, + language.ISO, + language.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.language) + + err := db.Save(&tc.language).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteLanguage(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + languageID uint + mockBehavior func(mock sqlmock.Sqlmock, languageID uint) + wantErr bool + }{ + "Can soft delete a Language": { + languageID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, languageID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `languages` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), languageID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + languageID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, languageID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `languages` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Language")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.languageID) + + err := db.Delete(&domain.Language{}, tc.languageID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestGetLanguageByID(t *testing.T) { + fixedTime := time.Now() + db, mock := tests.Setup(t) + + testCases := map[string]struct { + languageID uint + mockFunc func() + wantLanguage domain.Language + wantError bool + }{ + "Valid Language fetch": { + languageID: 1, + wantLanguage: domain.Language{ + ID: 1, + Name: "Portuguese", + ISO: "pt_BR", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockFunc: func() { + rows := sqlmock.NewRows([]string{"id", "name", "iso", "created_at", "updated_at"}). + AddRow(1, "Portuguese", "pt_BR", fixedTime, fixedTime) + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `languages` WHERE `languages`.`id` = ? AND `languages`.`deleted_at` IS NULL ORDER BY `languages`.`id` LIMIT ?")). + WithArgs(1, 1).WillReturnRows(rows) + }, + wantError: false, + }, + "Language not found": { + languageID: 2, + wantLanguage: domain.Language{}, + wantError: true, + mockFunc: func() { + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `languages` WHERE `languages`.`id` = ? AND `languages`.`deleted_at` IS NULL ORDER BY `languages`.`id` LIMIT ?")). + WithArgs(2, 1).WillReturnError(fmt.Errorf("record not found")) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockFunc() + + var Language domain.Language + err := db.First(&Language, tc.languageID).Error + + if tc.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.wantLanguage, Language) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateLanguageValidData(t *testing.T) { + testCases := map[string]struct { + language domain.Language + }{ + "Can empty validations errors": { + language: domain.Language{ + Name: "Portuguese", + ISO: "pt_BR", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.language.ValidateLanguage() + assert.NoError(t, err) + }) + } +} + +func TestCreateLanguageWithMissingFields(t *testing.T) { + testCases := map[string]struct { + language domain.Language + wantErr string + }{ + "Missing required fields": { + language: domain.Language{}, + wantErr: "Name is a required field, ISO is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.language.ValidateLanguage() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/platformable_test.go b/tests/unit/domains/platformable_test.go new file mode 100644 index 0000000..b9e65f5 --- /dev/null +++ b/tests/unit/domains/platformable_test.go @@ -0,0 +1,216 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreatePlatformable(t *testing.T) { + testCases := map[string]struct { + platformable domain.Platformable + mockBehavior func(mock sqlmock.Sqlmock, platformable domain.Platformable) + expectError bool + }{ + "Success": { + platformable: domain.Platformable{ + Platformable: 1, + PlatformableType: "games", + PlatformID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, platformable domain.Platformable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `platformables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + platformable.PlatformableID, + platformable.PlatformableType, + platformable.PlatformID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + platformable: domain.Platformable{ + Platformable: 1, + PlatformableType: "games", + PlatformID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, platformable domain.Platformable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `platformables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + platformable.PlatformableID, + platformable.PlatformableType, + platformable.PlatformID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.platformable) + + err := db.Create(&tc.platformable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdatePlatformable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + platformable domain.Platformable + mockBehavior func(mock sqlmock.Sqlmock, platformable domain.Platformable) + expectError bool + }{ + "Success": { + platformable: domain.Platformable{ + ID: 1, + Platformable: 1, + PlatformableType: "games", + PlatformID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, platformable domain.Platformable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `platformables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + platformable.PlatformableID, + platformable.PlatformableType, + platformable.PlatformID, + platformable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + platformable: domain.Platformable{ + ID: 1, + Platformable: 1, + PlatformableType: "games", + PlatformID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, platformable domain.Platformable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `platformables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + platformable.PlatformableID, + platformable.PlatformableType, + platformable.PlatformID, + platformable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.platformable) + + err := db.Save(&tc.platformable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeletePlatformable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + platformable uint + mockBehavior func(mock sqlmock.Sqlmock, platformable uint) + wantErr bool + }{ + "Can soft delete a Platformable": { + platformable: 1, + mockBehavior: func(mock sqlmock.Sqlmock, platformable uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `platformables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), platformable).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + platformable: 2, + mockBehavior: func(mock sqlmock.Sqlmock, platformable uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `platformables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Platformable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.platformable) + + err := db.Delete(&domain.Platformable{}, tc.platformable).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} diff --git a/tests/unit/domains/protection_test.go b/tests/unit/domains/protection_test.go new file mode 100644 index 0000000..7ecd00b --- /dev/null +++ b/tests/unit/domains/protection_test.go @@ -0,0 +1,243 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateProtection(t *testing.T) { + testCases := map[string]struct { + protection domain.Protection + mockBehavior func(mock sqlmock.Sqlmock, protection domain.Protection) + expectError bool + }{ + "Success": { + protection: domain.Protection{ + Name: "Denuvo", + }, + mockBehavior: func(mock sqlmock.Sqlmock, protection domain.Protection) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `protections`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + protection.Name, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + protection: domain.Protection{ + Name: "Denuvo", + }, + mockBehavior: func(mock sqlmock.Sqlmock, protection domain.Protection) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `protections`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + protection.Name, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.protection) + + err := db.Create(&tc.protection).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateProtection(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + protection domain.Protection + mockBehavior func(mock sqlmock.Sqlmock, protection domain.Protection) + expectError bool + }{ + "Success": { + protection: domain.Protection{ + ID: 1, + Name: "Denuvo", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, protection domain.Protection) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `protections`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + protection.Name, + protection.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + protection: domain.Protection{ + ID: 1, + Name: "Denuvo", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, protection domain.Protection) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `protections`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + protection.Name, + protection.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.protection) + + err := db.Save(&tc.protection).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteProtection(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + protectionID uint + mockBehavior func(mock sqlmock.Sqlmock, protectionID uint) + wantErr bool + }{ + "Can soft delete a Protection": { + protectionID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, protectionID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `protections` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), protectionID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + protectionID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, protectionID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `protections` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Protection")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.protectionID) + + err := db.Delete(&domain.Protection{}, tc.protectionID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateProtection(t *testing.T) { + testCases := map[string]struct { + protection domain.Protection + }{ + "Can empty validations errors": { + protection: domain.Protection{ + Name: "Denuvo", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.protection.ValidateProtection() + assert.NoError(t, err) + }) + } +} + +func TestCreateProtectionWithMissingFields(t *testing.T) { + testCases := map[string]struct { + protection domain.Protection + wantErr string + }{ + "Missing required fields": { + protection: domain.Protection{}, + wantErr: ` + Name is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.protection.ValidateProtection() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/publisher_test.go b/tests/unit/domains/publisher_test.go new file mode 100644 index 0000000..4b9f3e7 --- /dev/null +++ b/tests/unit/domains/publisher_test.go @@ -0,0 +1,252 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreatePublisher(t *testing.T) { + testCases := map[string]struct { + publisher domain.Publisher + mockBehavior func(mock sqlmock.Sqlmock, publisher domain.Publisher) + expectError bool + }{ + "Success": { + publisher: domain.Publisher{ + Name: "Game Science", + Acting: false, + }, + mockBehavior: func(mock sqlmock.Sqlmock, publisher domain.Publisher) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + publisher.Name, + publisher.Acting, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + publisher: domain.Publisher{ + Name: "Game Science", + Acting: false, + }, + mockBehavior: func(mock sqlmock.Sqlmock, publisher domain.Publisher) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + publisher.Name, + publisher.Acting, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.publisher) + + err := db.Create(&tc.publisher).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdatePublisher(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + publisher domain.Publisher + mockBehavior func(mock sqlmock.Sqlmock, publisher domain.Publisher) + expectError bool + }{ + "Success": { + publisher: domain.Publisher{ + ID: 1, + Name: "Game Science", + Acting: true, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, publisher domain.Publisher) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + publisher.Name, + publisher.Acting, + publisher.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + publisher: domain.Publisher{ + ID: 1, + Name: "Game Science", + Acting: false, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, publisher domain.Publisher) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `publishers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + publisher.Name, + publisher.Acting, + publisher.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.publisher) + + err := db.Save(&tc.publisher).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeletePublisher(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + publisherID uint + mockBehavior func(mock sqlmock.Sqlmock, publisherID uint) + wantErr bool + }{ + "Can soft delete a Publisher": { + publisherID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, publisherID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `publishers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), publisherID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + publisherID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, publisherID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `publishers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Publisher")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.publisherID) + + err := db.Delete(&domain.Publisher{}, tc.publisherID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidatePublisher(t *testing.T) { + testCases := map[string]struct { + publisher domain.Publisher + }{ + "Can empty validations errors": { + publisher: domain.Publisher{ + Name: "Game Science", + Acting: true, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.publisher.ValidatePublisher() + assert.NoError(t, err) + }) + } +} + +func TestCreatePublisherWithMissingFields(t *testing.T) { + testCases := map[string]struct { + publisher domain.Publisher + wantErr string + }{ + "Missing required fields": { + publisher: domain.Publisher{}, + wantErr: ` + Name is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.publisher.ValidatePublisher() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/requirement_test.go b/tests/unit/domains/requirement_test.go new file mode 100644 index 0000000..1884554 --- /dev/null +++ b/tests/unit/domains/requirement_test.go @@ -0,0 +1,367 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateRequirement(t *testing.T) { + testCases := map[string]struct { + requirement domain.Requirement + mockBehavior func(mock sqlmock.Sqlmock, requirement domain.Requirement) + expectError bool + }{ + "Success": { + requirement: domain.Requirement{ + OS: "Windows 11 64 bits", + DX: "DirectX 12", + CPU: "Ryzen 5 3600", + RAM: "16GB", + GPU: "RTX 3090 TI", + ROM: "90GB", + OBS: utils.StringPtr("Some observation"), + Network: "Non required", + RequirementTypeID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirement domain.Requirement) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `requirements`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirement.OS, + requirement.DX, + requirement.CPU, + requirement.RAM, + requirement.GPU, + requirement.ROM, + requirement.OBS, + requirement.Network, + requirement.RequirementTypeID, + requirement.GameID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + requirement: domain.Requirement{ + OS: "Windows 11 64 bits", + DX: "DirectX 12", + CPU: "Ryzen 5 3600", + RAM: "16GB", + GPU: "RTX 3090 TI", + ROM: "90GB", + OBS: utils.StringPtr("Some observation"), + Network: "Non required", + RequirementTypeID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirement domain.Requirement) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `requirements`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirement.OS, + requirement.DX, + requirement.CPU, + requirement.RAM, + requirement.GPU, + requirement.ROM, + requirement.OBS, + requirement.Network, + requirement.RequirementTypeID, + requirement.GameID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.requirement) + + err := db.Create(&tc.requirement).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateRequirement(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + requirement domain.Requirement + mockBehavior func(mock sqlmock.Sqlmock, requirement domain.Requirement) + expectError bool + }{ + "Success": { + requirement: domain.Requirement{ + ID: 1, + OS: "Windows 11 64 bits", + DX: "DirectX 12", + CPU: "Ryzen 5 3600", + RAM: "16GB", + GPU: "RTX 3090 TI", + ROM: "90GB", + OBS: utils.StringPtr("Some observation"), + Network: "Non required", + RequirementTypeID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirement domain.Requirement) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirements`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirement.OS, + requirement.DX, + requirement.CPU, + requirement.RAM, + requirement.GPU, + requirement.ROM, + requirement.OBS, + requirement.Network, + requirement.RequirementTypeID, + requirement.GameID, + requirement.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + requirement: domain.Requirement{ + ID: 1, + OS: "Windows 11 64 bits", + DX: "DirectX 12", + CPU: "Ryzen 5 3600", + RAM: "16GB", + GPU: "RTX 3090 TI", + ROM: "90GB", + OBS: utils.StringPtr("Some observation"), + Network: "Non required", + RequirementTypeID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirement domain.Requirement) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirements`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirement.OS, + requirement.DX, + requirement.CPU, + requirement.RAM, + requirement.GPU, + requirement.ROM, + requirement.OBS, + requirement.Network, + requirement.RequirementTypeID, + requirement.GameID, + requirement.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.requirement) + + err := db.Save(&tc.requirement).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteRequirement(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + requirementID uint + mockBehavior func(mock sqlmock.Sqlmock, requirementID uint) + wantErr bool + }{ + "Can soft delete a Requirement": { + requirementID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, requirementID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirements` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), requirementID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + requirementID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, requirementID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirements` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Requirement")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.requirementID) + + err := db.Delete(&domain.Requirement{}, tc.requirementID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateRequirement(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + requirement domain.Requirement + }{ + "Can empty validations errors": { + requirement: domain.Requirement{ + OS: "Windows 11 64 bits", + DX: "DirectX 12", + CPU: "Ryzen 5 3600", + RAM: "16GB", + GPU: "RTX 3090 TI", + ROM: "90GB", + OBS: utils.StringPtr("Some observation"), + Network: "Non required", + RequirementType: domain.RequirementType{ + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + Game: domain.Game{ + ID: 1, + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.requirement.ValidateRequirement() + assert.NoError(t, err) + }) + } +} + +func TestCreateRequirementWithMissingFields(t *testing.T) { + testCases := map[string]struct { + requirement domain.Requirement + wantErr string + }{ + "Missing required fields": { + requirement: domain.Requirement{}, + wantErr: ` + OS is a required field, + DX is a required field, + CPU is a required field, + RAM is a required field, + GPU is a required field, + ROM is a required field, + Network is a required field, + Potential is a required field, + OS is a required field, + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.requirement.ValidateRequirement() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/requirement_type_test.go b/tests/unit/domains/requirement_type_test.go new file mode 100644 index 0000000..a522580 --- /dev/null +++ b/tests/unit/domains/requirement_type_test.go @@ -0,0 +1,282 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateRequirementType(t *testing.T) { + testCases := map[string]struct { + requirementType domain.RequirementType + mockBehavior func(mock sqlmock.Sqlmock, requirementType domain.RequirementType) + expectError bool + }{ + "Success": { + requirementType: domain.RequirementType{ + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirementType domain.RequirementType) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `requirement_types`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirementType.Potential, + requirementType.OS, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + requirementType: domain.RequirementType{ + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirementType domain.RequirementType) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `requirement_types`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirementType.Potential, + requirementType.OS, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.requirementType) + + err := db.Create(&tc.requirementType).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateRequirementType(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + requirementType domain.RequirementType + mockBehavior func(mock sqlmock.Sqlmock, requirementType domain.RequirementType) + expectError bool + }{ + "Success": { + requirementType: domain.RequirementType{ + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirementType domain.RequirementType) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirement_types`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirementType.Potential, + requirementType.OS, + requirementType.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + requirementType: domain.RequirementType{ + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, requirementType domain.RequirementType) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirement_types`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + requirementType.Potential, + requirementType.OS, + requirementType.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.requirementType) + + err := db.Save(&tc.requirementType).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteRequirementType(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + requirementTypeID uint + mockBehavior func(mock sqlmock.Sqlmock, requirementTypeID uint) + wantErr bool + }{ + "Can soft delete a RequirementType": { + requirementTypeID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, requirementTypeID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirement_types` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), requirementTypeID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + requirementTypeID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, requirementTypeID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `requirement_types` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete RequirementType")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.requirementTypeID) + + err := db.Delete(&domain.RequirementType{}, tc.requirementTypeID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateRequirementTypeLanguageValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + requirementType domain.RequirementType + }{ + "Can empty validations errors": { + requirementType: domain.RequirementType{ + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.requirementType.ValidateRequirementType() + assert.NoError(t, err) + }) + } +} + +func TestCreateRequirementTypeWithMissingFields(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + requirementType domain.RequirementType + wantErr string + }{ + "Missing required fields": { + requirementType: domain.RequirementType{}, + wantErr: "Potential is a required field, OS is a required field", + }, + "Invalid potential": { + requirementType: domain.RequirementType{ + Potential: "invalid", + OS: domain.WindowsOSRequirement, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + wantErr: "Potential must be one of 'minimum', 'recommended', or 'maximum'", + }, + "invalid os": { + requirementType: domain.RequirementType{ + Potential: domain.MinimumRequirementType, + OS: "invalid", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + wantErr: "OS must be one of 'windows', 'mac', or 'linux'", + }, + "both invalid": { + requirementType: domain.RequirementType{ + Potential: "invalid", + OS: "invalid", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + wantErr: "Potential must be one of 'minimum', 'recommended', or 'maximum', OS must be one of 'windows', 'mac', or 'linux'", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.requirementType.ValidateRequirementType() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/reviewable_test.go b/tests/unit/domains/reviewable_test.go new file mode 100644 index 0000000..95bb121 --- /dev/null +++ b/tests/unit/domains/reviewable_test.go @@ -0,0 +1,314 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateReviewable(t *testing.T) { + testCases := map[string]struct { + reviewable domain.Reviewable + mockBehavior func(mock sqlmock.Sqlmock, reviewable domain.Reviewable) + expectError bool + }{ + "Success": { + reviewable: domain.Reviewable{ + Rate: 5, + Review: "Good game!", + ReviewableID: 1, + ReviewableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, reviewable domain.Reviewable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `reviewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + reviewable.Rate, + reviewable.Review, + reviewable.ReviewableID, + reviewable.ReviewableType, + reviewable.UserID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + reviewable: domain.Reviewable{ + Rate: 5, + Review: "Good game!", + ReviewableID: 1, + ReviewableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, reviewable domain.Reviewable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `reviewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + reviewable.Rate, + reviewable.Review, + reviewable.ReviewableID, + reviewable.ReviewableType, + reviewable.UserID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.reviewable) + + err := db.Create(&tc.reviewable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateReviewable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + reviewable domain.Reviewable + mockBehavior func(mock sqlmock.Sqlmock, reviewable domain.Reviewable) + expectError bool + }{ + "Success": { + reviewable: domain.Reviewable{ + ID: 1, + Rate: 5, + Review: "Good game!", + ReviewableID: 1, + ReviewableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, reviewable domain.Reviewable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `reviewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + reviewable.Rate, + reviewable.Review, + reviewable.ReviewableID, + reviewable.ReviewableType, + reviewable.UserID, + reviewable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + reviewable: domain.Reviewable{ + ID: 1, + Rate: 5, + Review: "Good game!", + ReviewableID: 1, + ReviewableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, reviewable domain.Reviewable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `reviewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + reviewable.Rate, + reviewable.Review, + reviewable.ReviewableID, + reviewable.ReviewableType, + reviewable.UserID, + reviewable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.reviewable) + + err := db.Save(&tc.reviewable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteReviewable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + reviewableID uint + mockBehavior func(mock sqlmock.Sqlmock, reviewableID uint) + wantErr bool + }{ + "Can soft delete a Reviewable": { + reviewableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, reviewableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `reviewables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), reviewableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + reviewableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, reviewableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `reviewables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Reviewable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.reviewableID) + + err := db.Delete(&domain.Reviewable{}, tc.reviewableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateReviewableValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + reviewable domain.Reviewable + }{ + "Valid Reviewable with zero amount": { + reviewable: domain.Reviewable{ + ID: 1, + Rate: 5, + Review: "Good game!", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + ReviewableID: 1, + ReviewableType: "games", + User: domain.User{ + Name: "John Doe", + Email: "johndoe@example.com", + Nickname: "johnny", + Blocked: false, + Experience: 500, + Birthdate: fixedTime, + Password: "supersecretpassword", + Profile: domain.Profile{ + Share: true, + }, + Wallet: domain.Wallet{ + Amount: 10, + }, + Level: domain.Level{ + Level: 1, + Experience: 500, + Coins: 10, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.reviewable.ValidateReviewable() + assert.NoError(t, err) + }) + } +} + +func TestCreateReviewableWithMissingFields(t *testing.T) { + testCases := map[string]struct { + reviewable domain.Reviewable + wantErr string + }{ + "Missing required fields": { + reviewable: domain.Reviewable{}, + wantErr: ` + Rate is a required field, + Review is a required field, + Name is a required field, + Email is a required field, + Nickname is a required field, + Birthdate is a required field, + Password is a required field, + Share is a required field, + Level is a required field, + Experience is a required field, + Coins is a required field, + Amount is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.reviewable.ValidateReviewable() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/store_test.go b/tests/unit/domains/store_test.go new file mode 100644 index 0000000..76b6e4e --- /dev/null +++ b/tests/unit/domains/store_test.go @@ -0,0 +1,323 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateStore(t *testing.T) { + testCases := map[string]struct { + store domain.Store + mockBehavior func(mock sqlmock.Sqlmock, store domain.Store) + expectError bool + }{ + "Success": { + store: domain.Store{ + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + mockBehavior: func(mock sqlmock.Sqlmock, store domain.Store) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + store.Name, + store.URL, + store.Slug, + store.Logo, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + store: domain.Store{ + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + mockBehavior: func(mock sqlmock.Sqlmock, store domain.Store) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + store.Name, + store.URL, + store.Slug, + store.Logo, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.store) + + err := db.Create(&tc.store).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateStore(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + store domain.Store + mockBehavior func(mock sqlmock.Sqlmock, store domain.Store) + expectError bool + }{ + "Success": { + store: domain.Store{ + ID: 1, + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, store domain.Store) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + store.Name, + store.URL, + store.Slug, + store.Logo, + store.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + store: domain.Store{ + ID: 1, + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, store domain.Store) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `stores`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + store.Name, + store.URL, + store.Slug, + store.Logo, + store.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.store) + + err := db.Save(&tc.store).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteStore(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + storeID uint + mockBehavior func(mock sqlmock.Sqlmock, storeID uint) + wantErr bool + }{ + "Can soft delete a Store": { + storeID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, storeID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `stores` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), storeID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + storeID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, storeID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `stores` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Store")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.storeID) + + err := db.Delete(&domain.Store{}, tc.storeID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestGetStoreByID(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + storeID uint + mockFunc func() + wantStore domain.Store + wantError bool + }{ + "Valid Store fetch": { + storeID: 1, + wantStore: domain.Store{ + ID: 1, + Name: "Store 1", + }, + mockFunc: func() { + rows := sqlmock.NewRows([]string{"id", "name"}). + AddRow(1, "Store 1") + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `stores` WHERE `stores`.`id` = ? AND `stores`.`deleted_at` IS NULL ORDER BY `stores`.`id` LIMIT ?")). + WithArgs(1, 1).WillReturnRows(rows) + }, + wantError: false, + }, + "Store not found": { + storeID: 2, + wantStore: domain.Store{}, + wantError: true, + mockFunc: func() { + mock.ExpectQuery(regexp.QuoteMeta("SELECT * FROM `stores` WHERE `stores`.`id` = ? AND `stores`.`deleted_at` IS NULL ORDER BY `stores`.`id` LIMIT ?")). + WithArgs(2, 1).WillReturnError(fmt.Errorf("record not found")) + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockFunc() + + var store domain.Store + err := db.First(&store, tc.storeID).Error + + if tc.wantError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.wantStore, store) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateStoreValidData(t *testing.T) { + testCases := map[string]struct { + store domain.Store + }{ + "Can empty validations errors": { + store: domain.Store{ + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.store.ValidateStore() + assert.NoError(t, err) + }) + } +} + +func TestCreateStoreWithMissingFields(t *testing.T) { + testCases := map[string]struct { + store domain.Store + wantErr string + }{ + "Missing required fields": { + store: domain.Store{}, + wantErr: "Name is a required field, URL is a required field, Slug is a required field, Logo is a required field", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.store.ValidateStore() + + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + }) + } +} diff --git a/tests/unit/domains/taggable_test.go b/tests/unit/domains/taggable_test.go new file mode 100644 index 0000000..740282c --- /dev/null +++ b/tests/unit/domains/taggable_test.go @@ -0,0 +1,216 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateTaggable(t *testing.T) { + testCases := map[string]struct { + taggable domain.Taggable + mockBehavior func(mock sqlmock.Sqlmock, taggable domain.Taggable) + expectError bool + }{ + "Success": { + taggable: domain.Taggable{ + TaggableID: 1, + TaggableType: "games", + TagID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, taggable domain.Taggable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `taggables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + taggable.TaggableID, + taggable.TaggableType, + taggable.TagID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + taggable: domain.Taggable{ + TaggableID: 1, + TaggableType: "games", + TagID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, taggable domain.Taggable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `taggables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + taggable.TaggableID, + taggable.TaggableType, + taggable.TagID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.taggable) + + err := db.Create(&tc.taggable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateTaggable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + taggable domain.Taggable + mockBehavior func(mock sqlmock.Sqlmock, taggable domain.Taggable) + expectError bool + }{ + "Success": { + taggable: domain.Taggable{ + ID: 1, + TaggableID: 1, + TaggableType: "games", + TagID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, taggable domain.Taggable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `taggables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + taggable.TaggableID, + taggable.TaggableType, + taggable.TagID, + taggable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + taggable: domain.Taggable{ + ID: 1, + TaggableID: 1, + TaggableType: "games", + TagID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, taggable domain.Taggable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `taggables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + taggable.TaggableID, + taggable.TaggableType, + taggable.TagID, + taggable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.taggable) + + err := db.Save(&tc.taggable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteTaggable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + taggableID uint + mockBehavior func(mock sqlmock.Sqlmock, taggableID uint) + wantErr bool + }{ + "Can soft delete a Taggable": { + taggableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, taggableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `taggables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), taggableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + taggableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, taggableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `taggables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Taggable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.taggableID) + + err := db.Delete(&domain.Taggable{}, tc.taggableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} diff --git a/tests/unit/domains/torrent_provider_test.go b/tests/unit/domains/torrent_provider_test.go new file mode 100644 index 0000000..4b30c5d --- /dev/null +++ b/tests/unit/domains/torrent_provider_test.go @@ -0,0 +1,253 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateTorrentProvider(t *testing.T) { + testCases := map[string]struct { + torrentProvider domain.TorrentProvider + mockBehavior func(mock sqlmock.Sqlmock, torrentProvider domain.TorrentProvider) + expectError bool + }{ + "Success": { + torrentProvider: domain.TorrentProvider{ + URL: "https:google.com", + Name: "Google", + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrentProvider domain.TorrentProvider) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `torrent_providers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrentProvider.URL, + torrentProvider.Name, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + torrentProvider: domain.TorrentProvider{ + URL: "https:google.com", + Name: "Google", + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrentProvider domain.TorrentProvider) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `torrent_providers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrentProvider.URL, + torrentProvider.Name, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.torrentProvider) + + err := db.Create(&tc.torrentProvider).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateTorrentProvider(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + torrentProvider domain.TorrentProvider + mockBehavior func(mock sqlmock.Sqlmock, torrentProvider domain.TorrentProvider) + expectError bool + }{ + "Success": { + torrentProvider: domain.TorrentProvider{ + ID: 1, + URL: "https:google.com", + Name: "Google", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrentProvider domain.TorrentProvider) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrent_providers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrentProvider.URL, + torrentProvider.Name, + torrentProvider.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + torrentProvider: domain.TorrentProvider{ + ID: 1, + URL: "https:google.com", + Name: "Google", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrentProvider domain.TorrentProvider) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrent_providers`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrentProvider.URL, + torrentProvider.Name, + torrentProvider.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.torrentProvider) + + err := db.Save(&tc.torrentProvider).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteTorrentProvider(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + torrentProviderID uint + mockBehavior func(mock sqlmock.Sqlmock, torrentProviderID uint) + wantErr bool + }{ + "Can soft delete a TorrentProvider": { + torrentProviderID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, torrentProviderID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrent_providers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), torrentProviderID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + torrentProviderID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, torrentProviderID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrent_providers` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete TorrentProvider")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.torrentProviderID) + + err := db.Delete(&domain.TorrentProvider{}, tc.torrentProviderID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateTorrentProvider(t *testing.T) { + testCases := map[string]struct { + torrentProvider domain.TorrentProvider + }{ + "Can empty validations errors": { + torrentProvider: domain.TorrentProvider{ + URL: "https:google.com", + Name: "Google", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.torrentProvider.ValidateTorrentProvider() + assert.NoError(t, err) + }) + } +} + +func TestCreateTorrentProviderWithMissingFields(t *testing.T) { + testCases := map[string]struct { + torrentProvider domain.TorrentProvider + wantErr string + }{ + "Missing required fields": { + torrentProvider: domain.TorrentProvider{}, + wantErr: ` + URL is a required field, + Name is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.torrentProvider.ValidateTorrentProvider() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/torrent_test.go b/tests/unit/domains/torrent_test.go new file mode 100644 index 0000000..9de8fac --- /dev/null +++ b/tests/unit/domains/torrent_test.go @@ -0,0 +1,307 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/pkg/utils" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateTorrent(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + torrent domain.Torrent + mockBehavior func(mock sqlmock.Sqlmock, torrent domain.Torrent) + expectError bool + }{ + "Success": { + torrent: domain.Torrent{ + URL: "https:google.com", + PostedAt: fixedTime, + TorrentProviderID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrent domain.Torrent) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `torrents`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrent.URL, + torrent.PostedAt, + torrent.TorrentProviderID, + torrent.GameID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + torrent: domain.Torrent{ + URL: "https:google.com", + PostedAt: fixedTime, + TorrentProviderID: 1, + GameID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrent domain.Torrent) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `torrents`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrent.URL, + torrent.PostedAt, + torrent.TorrentProviderID, + torrent.GameID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.torrent) + + err := db.Create(&tc.torrent).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateTorrent(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + torrent domain.Torrent + mockBehavior func(mock sqlmock.Sqlmock, torrent domain.Torrent) + expectError bool + }{ + "Success": { + torrent: domain.Torrent{ + ID: 1, + URL: "https:google.com", + PostedAt: fixedTime, + TorrentProviderID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrent domain.Torrent) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrents`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrent.URL, + torrent.PostedAt, + torrent.TorrentProviderID, + torrent.GameID, + torrent.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + torrent: domain.Torrent{ + ID: 1, + URL: "https:google.com", + PostedAt: fixedTime, + TorrentProviderID: 1, + GameID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, torrent domain.Torrent) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrents`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + torrent.URL, + torrent.PostedAt, + torrent.TorrentProviderID, + torrent.GameID, + torrent.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.torrent) + + err := db.Save(&tc.torrent).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteTorrent(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + torrentID uint + mockBehavior func(mock sqlmock.Sqlmock, torrentID uint) + wantErr bool + }{ + "Can soft delete a Torrent": { + torrentID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, torrentID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrents` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), torrentID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + torrentID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, torrentID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `torrents` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Torrent")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.torrentID) + + err := db.Delete(&domain.Torrent{}, tc.torrentID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateTorrent(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + torrent domain.Torrent + }{ + "Can empty validations errors": { + torrent: domain.Torrent{ + URL: "https:google.com", + PostedAt: fixedTime, + TorrentProvider: domain.TorrentProvider{ + URL: "http://google.com", + Name: "Google", + }, + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.torrent.ValidateTorrent() + assert.NoError(t, err) + }) + } +} + +func TestCreateTorrentWithMissingFields(t *testing.T) { + testCases := map[string]struct { + torrent domain.Torrent + wantErr string + }{ + "Missing required fields": { + torrent: domain.Torrent{}, + wantErr: ` + URL is a required field, + Name is a required field, + Age is a required field, + Slug is a required field, + Title is a required field, + Condition is a required field, + Cover is a required field, + About is a required field, + Description is a required field, + ShortDescription is a required field, + ReleaseDate is a required field + `, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.torrent.ValidateTorrent() + + assert.Error(t, err) + assert.Contains(t, err.Error(), utils.NormalizeWhitespace(tc.wantErr)) + }) + } +} diff --git a/tests/unit/domains/viewable_test.go b/tests/unit/domains/viewable_test.go new file mode 100644 index 0000000..6153ef0 --- /dev/null +++ b/tests/unit/domains/viewable_test.go @@ -0,0 +1,261 @@ +package tests + +import ( + "fmt" + "gcstatus/internal/domain" + "gcstatus/tests" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/stretchr/testify/assert" +) + +func TestCreateViewable(t *testing.T) { + testCases := map[string]struct { + viewable domain.Viewable + mockBehavior func(mock sqlmock.Sqlmock, viewable domain.Viewable) + expectError bool + }{ + "Success": { + viewable: domain.Viewable{ + ViewableID: 1, + ViewableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, viewable domain.Viewable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `viewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + viewable.ViewableID, + viewable.ViewableType, + viewable.UserID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Insert Error": { + viewable: domain.Viewable{ + ViewableID: 1, + ViewableType: "games", + UserID: 1, + }, + mockBehavior: func(mock sqlmock.Sqlmock, viewable domain.Viewable) { + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO `viewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + viewable.ViewableID, + viewable.ViewableType, + viewable.UserID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.viewable) + + err := db.Create(&tc.viewable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestUpdateViewable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + viewable domain.Viewable + mockBehavior func(mock sqlmock.Sqlmock, viewable domain.Viewable) + expectError bool + }{ + "Success": { + viewable: domain.Viewable{ + ID: 1, + ViewableID: 1, + ViewableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, viewable domain.Viewable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `viewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + viewable.ViewableID, + viewable.ViewableType, + viewable.UserID, + viewable.ID, + ). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + expectError: false, + }, + "Failure - Update Error": { + viewable: domain.Viewable{ + ID: 1, + ViewableID: 1, + ViewableType: "games", + UserID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + mockBehavior: func(mock sqlmock.Sqlmock, viewable domain.Viewable) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `viewables`"). + WithArgs( + sqlmock.AnyArg(), + sqlmock.AnyArg(), + sqlmock.AnyArg(), + viewable.ViewableID, + viewable.ViewableType, + viewable.UserID, + viewable.ID, + ). + WillReturnError(fmt.Errorf("some error")) + mock.ExpectRollback() + }, + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + db, mock := tests.Setup(t) + + tc.mockBehavior(mock, tc.viewable) + + err := db.Save(&tc.viewable).Error + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestSoftDeleteViewable(t *testing.T) { + db, mock := tests.Setup(t) + + testCases := map[string]struct { + viewableID uint + mockBehavior func(mock sqlmock.Sqlmock, viewableID uint) + wantErr bool + }{ + "Can soft delete a Viewable": { + viewableID: 1, + mockBehavior: func(mock sqlmock.Sqlmock, viewableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `viewables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), viewableID).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + }, + wantErr: false, + }, + "Soft delete fails": { + viewableID: 2, + mockBehavior: func(mock sqlmock.Sqlmock, viewableID uint) { + mock.ExpectBegin() + mock.ExpectExec("UPDATE `viewables` SET `deleted_at`").WithArgs(sqlmock.AnyArg(), 2). + WillReturnError(fmt.Errorf("failed to delete Viewable")) + mock.ExpectRollback() + }, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + tc.mockBehavior(mock, tc.viewableID) + + err := db.Delete(&domain.Viewable{}, tc.viewableID).Error + + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + }) + } +} + +func TestValidateViewableValidData(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + viewable domain.Viewable + }{ + "Valid Viewable with zero amount": { + viewable: domain.Viewable{ + ID: 1, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + ViewableID: 1, + ViewableType: "games", + User: domain.User{ + Name: "John Doe", + Email: "johndoe@example.com", + Nickname: "johnny", + Blocked: false, + Experience: 500, + Birthdate: fixedTime, + Password: "supersecretpassword", + Profile: domain.Profile{ + Share: true, + }, + Wallet: domain.Wallet{ + Amount: 10, + }, + Level: domain.Level{ + Level: 1, + Experience: 500, + Coins: 10, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + err := tc.viewable.ValidateViewable() + assert.NoError(t, err) + }) + } +} diff --git a/tests/unit/ports/game_repository_test.go b/tests/unit/ports/game_repository_test.go new file mode 100644 index 0000000..b3583a8 --- /dev/null +++ b/tests/unit/ports/game_repository_test.go @@ -0,0 +1,95 @@ +package tests + +import ( + "errors" + "gcstatus/internal/domain" + "testing" + "time" +) + +type MockGameRepository struct { + games map[uint]*domain.Game +} + +func NewMockGameRepository() *MockGameRepository { + return &MockGameRepository{ + games: make(map[uint]*domain.Game), + } +} +func (m *MockGameRepository) FindBySlug(slug string) (*domain.Game, error) { + for _, game := range m.games { + if game.Slug == slug { + return game, nil + } + } + + return nil, errors.New("game not found") +} + +func (m *MockGameRepository) CreateGame(game *domain.Game) error { + if game == nil { + return errors.New("invalid game data") + } + m.games[game.ID] = game + return nil +} + +func TestMockGameRepository_FindBySlug(t *testing.T) { + fixedTime := time.Now() + + mockRepo := NewMockGameRepository() + if err := mockRepo.CreateGame(&domain.Game{ + ID: 1, + Slug: "valid", + Age: 18, + Title: "Game Test", + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }); err != nil { + t.Fatalf("failed to create the slug: %s", err.Error()) + } + + testCases := map[string]struct { + gameSlug string + expectError bool + }{ + "valid game slug": { + gameSlug: "valid", + expectError: false, + }, + "invalid game slug": { + gameSlug: "invalid", + expectError: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + game, err := mockRepo.FindBySlug(tc.gameSlug) + + if tc.expectError { + if err == nil { + t.Fatalf("expected error, got nil") + } + if game != nil { + t.Fatalf("expected nil game, got %v", game) + } + } else { + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if game == nil || game.Slug != tc.gameSlug { + t.Fatalf("expected game Slug %s, got %v", tc.gameSlug, game) + } + } + }) + } +} diff --git a/tests/unit/resources/category_resource_test.go b/tests/unit/resources/category_resource_test.go new file mode 100644 index 0000000..ffbd131 --- /dev/null +++ b/tests/unit/resources/category_resource_test.go @@ -0,0 +1,103 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformCategory(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input domain.Category + expected resources.CategoryResource + }{ + "as null": { + input: domain.Category{}, + expected: resources.CategoryResource{}, + }, + "valid category": { + input: domain.Category{ + ID: 1, + Name: "Category 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + expected: resources.CategoryResource{ + ID: 1, + Name: "Category 1", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + categoryResource := resources.TransformCategory(test.input) + + if !reflect.DeepEqual(categoryResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, categoryResource) + } + }) + } +} + +func TestTransformCategories(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input []domain.Category + expected []resources.CategoryResource + }{ + "as null": { + input: []domain.Category{}, + expected: []resources.CategoryResource{}, + }, + "multiple categories": { + input: []domain.Category{ + { + ID: 1, + Name: "Category 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + { + ID: 2, + Name: "Category 2", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: []resources.CategoryResource{ + { + ID: 1, + Name: "Category 1", + }, + { + ID: 2, + Name: "Category 2", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + categoriesResource := resources.TransformCategories(test.input) + + if categoriesResource == nil { + categoriesResource = []resources.CategoryResource{} + } + + if !reflect.DeepEqual(categoriesResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, categoriesResource) + } + }) + } +} diff --git a/tests/unit/resources/commentable_resource_test.go b/tests/unit/resources/commentable_resource_test.go new file mode 100644 index 0000000..27949bf --- /dev/null +++ b/tests/unit/resources/commentable_resource_test.go @@ -0,0 +1,174 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "testing" + "time" +) + +func TestTransformCommentable(t *testing.T) { + fixedTime := time.Now() + formattedTime := utils.FormatTimestamp(fixedTime) + + testCases := map[string]struct { + input domain.Commentable + expected resources.CommentableResource + }{ + "without replies": { + input: domain.Commentable{ + ID: 1, + Comment: "Fake comment", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + User: domain.User{ + ID: 1, + Name: "John Doe", + Email: "johndoe@example.com", + Nickname: "johnny", + CreatedAt: fixedTime, + Profile: domain.Profile{ + Share: true, + Photo: "photo-key-1", + }, + }, + }, + expected: resources.CommentableResource{ + ID: 1, + Comment: "Fake comment", + CreatedAt: formattedTime, + UpdatedAt: formattedTime, + By: resources.MinimalUserResource{ + ID: 1, + Name: "John Doe", + Photo: utils.StringPtr("https://mock-presigned-url.com/photo-key-1"), + Email: "johndoe@example.com", + Nickname: "johnny", + CreatedAt: formattedTime, + }, + Replies: []resources.CommentableResource{}, + }, + }, + "with replies": { + input: domain.Commentable{ + ID: 1, + Comment: "Main comment", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + User: domain.User{ + ID: 1, + Name: "Main User", + Email: "mainuser@example.com", + Nickname: "mainuser", + CreatedAt: fixedTime, + }, + Replies: []domain.Commentable{ + { + ID: 2, + Comment: "Reply to main comment", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + User: domain.User{ + ID: 2, + Name: "Reply User", + Email: "replyuser@example.com", + Nickname: "replyuser", + CreatedAt: fixedTime, + Profile: domain.Profile{ + Share: true, + }, + }, + }, + }, + }, + expected: resources.CommentableResource{ + ID: 1, + Comment: "Main comment", + CreatedAt: formattedTime, + UpdatedAt: formattedTime, + By: resources.MinimalUserResource{ + ID: 1, + Name: "Main User", + Photo: nil, + Email: "mainuser@example.com", + Nickname: "mainuser", + CreatedAt: formattedTime, + }, + Replies: []resources.CommentableResource{ + { + ID: 2, + Comment: "Reply to main comment", + CreatedAt: formattedTime, + UpdatedAt: formattedTime, + By: resources.MinimalUserResource{ + ID: 2, + Name: "Reply User", + Photo: nil, + Email: "replyuser@example.com", + Nickname: "replyuser", + CreatedAt: formattedTime, + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + mockS3Client := &MockS3Client{} + result := resources.TransformCommentable(tc.input, mockS3Client) + + if !compareCommentableResources(tc.expected, result) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} + +func compareCommentableResources(expected, actual resources.CommentableResource) bool { + if expected.ID != actual.ID || + expected.Comment != actual.Comment || + expected.CreatedAt != actual.CreatedAt || + expected.UpdatedAt != actual.UpdatedAt { + return false + } + + if !compareMinimalUserResources(expected.By, actual.By) { + return false + } + + if len(expected.Replies) != len(actual.Replies) { + return false + } + + for i := range expected.Replies { + if !compareCommentableResources(expected.Replies[i], actual.Replies[i]) { + return false + } + } + + return true +} + +func compareMinimalUserResources(expected, actual resources.MinimalUserResource) bool { + if expected.ID != actual.ID || + expected.Name != actual.Name || + expected.Email != actual.Email || + expected.Nickname != actual.Nickname || + expected.CreatedAt != actual.CreatedAt { + return false + } + + if (expected.Photo == nil && actual.Photo != nil) || + (expected.Photo != nil && actual.Photo == nil) { + return false + } + + if expected.Photo != nil && actual.Photo != nil && *expected.Photo != *actual.Photo { + return false + } + + return true +} diff --git a/tests/unit/resources/crack_resource_test.go b/tests/unit/resources/crack_resource_test.go new file mode 100644 index 0000000..2bc399c --- /dev/null +++ b/tests/unit/resources/crack_resource_test.go @@ -0,0 +1,85 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "reflect" + "testing" + "time" +) + +func TestTransformCrack(t *testing.T) { + fixedTime := time.Now() + formattedTime := utils.FormatTimestamp(fixedTime) + + testCases := map[string]struct { + input *domain.Crack + expected *resources.CrackResource + }{ + "basic transformation": { + input: &domain.Crack{ + ID: 1, + Status: "cracked", + CrackedAt: &fixedTime, + Cracker: domain.Cracker{ + ID: 1, + Name: "Cracker 1", + }, + Protection: domain.Protection{ + ID: 1, + Name: "Protection 1", + }, + }, + expected: &resources.CrackResource{ + ID: 1, + Status: "cracked", + CrackedAt: &formattedTime, + By: &resources.CrackerResource{ID: 1, Name: "Cracker 1"}, + Protection: &resources.ProtectionResource{ID: 1, Name: "Protection 1"}, + }, + }, + "nil CrackedAt field": { + input: &domain.Crack{ + ID: 2, + Status: "uncracked", + CrackedAt: nil, + Cracker: domain.Cracker{ID: 1, Name: "Cracker 2"}, + Protection: domain.Protection{ID: 1, Name: "Protection 2"}, + }, + expected: &resources.CrackResource{ + ID: 2, + Status: "uncracked", + CrackedAt: nil, + By: &resources.CrackerResource{ID: 1, Name: "Cracker 2"}, + Protection: &resources.ProtectionResource{ID: 1, Name: "Protection 2"}, + }, + }, + "nil Cracker and Protection": { + input: &domain.Crack{ + ID: 3, + Status: "uncracked", + CrackedAt: &fixedTime, + Cracker: domain.Cracker{ID: 0}, + Protection: domain.Protection{ID: 0}, + }, + expected: &resources.CrackResource{ + ID: 3, + Status: "uncracked", + CrackedAt: &formattedTime, + By: nil, + Protection: nil, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformCrack(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/cracker_resource_test.go b/tests/unit/resources/cracker_resource_test.go new file mode 100644 index 0000000..2f6328c --- /dev/null +++ b/tests/unit/resources/cracker_resource_test.go @@ -0,0 +1,89 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformCracker(t *testing.T) { + testCases := map[string]struct { + input domain.Cracker + expected *resources.CrackerResource + }{ + "single Cracker": { + input: domain.Cracker{ + ID: 1, + Name: "Cracker 1", + Acting: false, + }, + expected: &resources.CrackerResource{ + ID: 1, + Name: "Cracker 1", + Acting: false, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformCracker(tc.input) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} + +func TestTransformCrackers(t *testing.T) { + testCases := map[string]struct { + input []domain.Cracker + expected []*resources.CrackerResource + }{ + "empty slice": { + input: []domain.Cracker{}, + expected: []*resources.CrackerResource{}, + }, + "multiple Crackers": { + input: []domain.Cracker{ + { + ID: 1, + Name: "Cracker 1", + Acting: true, + }, + { + ID: 2, + Name: "Cracker 2", + Acting: false, + }, + }, + expected: []*resources.CrackerResource{ + { + ID: 1, + Name: "Cracker 1", + Acting: true, + }, + { + ID: 2, + Name: "Cracker 2", + Acting: false, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformCrackers(tc.input) + + if result == nil { + result = []*resources.CrackerResource{} + } + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/critic_resource_test.go b/tests/unit/resources/critic_resource_test.go new file mode 100644 index 0000000..413469a --- /dev/null +++ b/tests/unit/resources/critic_resource_test.go @@ -0,0 +1,49 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformCritic(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.Critic + expected resources.CriticResource + }{ + "as nil": { + input: domain.Critic{}, + expected: resources.CriticResource{}, + }, + "basic transformation": { + input: domain.Critic{ + ID: 1, + Name: "Critic 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + expected: resources.CriticResource{ + ID: 1, + Name: "Critic 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformCritic(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/criticable_resource_test.go b/tests/unit/resources/criticable_resource_test.go new file mode 100644 index 0000000..aa3a0c5 --- /dev/null +++ b/tests/unit/resources/criticable_resource_test.go @@ -0,0 +1,66 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "reflect" + "testing" + "time" + + "github.com/shopspring/decimal" +) + +func TestTransformCriticable(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.Criticable + expected resources.CriticableResource + }{ + "as nil": { + input: domain.Criticable{}, + expected: resources.CriticableResource{}, + }, + "basic transformation": { + input: domain.Criticable{ + ID: 1, + URL: "https://google.com", + Rate: decimal.NewFromFloat32(5.8), + PostedAt: fixedTime, + CriticableID: 1, + CriticableType: "games", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Critic: domain.Critic{ + ID: 1, + Name: "Criticable 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + expected: resources.CriticableResource{ + ID: 1, + Rate: decimal.NewFromFloat32(5.8), + URL: "https://google.com", + PostedAt: utils.FormatTimestamp(fixedTime), + Critic: resources.CriticResource{ + ID: 1, + Name: "Criticable 1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformCriticable(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/developer_resource_test.go b/tests/unit/resources/developer_resource_test.go new file mode 100644 index 0000000..227aa58 --- /dev/null +++ b/tests/unit/resources/developer_resource_test.go @@ -0,0 +1,89 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformDeveloper(t *testing.T) { + testCases := map[string]struct { + input domain.Developer + expected resources.DeveloperResource + }{ + "single Developer": { + input: domain.Developer{ + ID: 1, + Name: "Developer 1", + Acting: false, + }, + expected: resources.DeveloperResource{ + ID: 1, + Name: "Developer 1", + Acting: false, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformDeveloper(tc.input) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} + +func TestTransformDevelopers(t *testing.T) { + testCases := map[string]struct { + input []domain.Developer + expected []resources.DeveloperResource + }{ + "empty slice": { + input: []domain.Developer{}, + expected: []resources.DeveloperResource{}, + }, + "multiple Developers": { + input: []domain.Developer{ + { + ID: 1, + Name: "Developer 1", + Acting: true, + }, + { + ID: 2, + Name: "Developer 2", + Acting: false, + }, + }, + expected: []resources.DeveloperResource{ + { + ID: 1, + Name: "Developer 1", + Acting: true, + }, + { + ID: 2, + Name: "Developer 2", + Acting: false, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformDevelopers(tc.input) + + if result == nil { + result = []resources.DeveloperResource{} + } + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/dlc_resource_test.go b/tests/unit/resources/dlc_resource_test.go new file mode 100644 index 0000000..7a44556 --- /dev/null +++ b/tests/unit/resources/dlc_resource_test.go @@ -0,0 +1,127 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "reflect" + "testing" + "time" +) + +func TestTransformDLC(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.DLC + expected resources.DLCResource + }{ + "empty relations": { + input: domain.DLC{ + ID: 1, + Name: "DLC 1", + Cover: "photo-key-1", + ReleaseDate: fixedTime, + Galleries: []domain.Galleriable{}, + Platforms: []domain.Platformable{}, + Stores: []domain.DLCStore{}, + }, + expected: resources.DLCResource{ + ID: 1, + Name: "DLC 1", + Cover: "https://mock-presigned-url.com/photo-key-1", + ReleaseDate: utils.FormatTimestamp(fixedTime), + Galleries: []resources.GalleriableResource{}, + Platforms: []resources.PlatformResource{}, + Stores: []resources.DLCStoreResource{}, + }, + }, + "fully relations": { + input: domain.DLC{ + ID: 1, + Name: "DLC 1", + Cover: "photo-key-1", + ReleaseDate: fixedTime, + Galleries: []domain.Galleriable{ + { + ID: 1, + S3: false, + Path: "https://google.com", + GalleriableID: 1, + GalleriableType: "dlcs", + }, + }, + Platforms: []domain.Platformable{ + { + ID: 1, + PlatformableID: 1, + PlatformableType: "dlcs", + PlatformID: 1, + Platform: domain.Platform{ + ID: 1, + Name: "Platform 1", + }, + }, + }, + Stores: []domain.DLCStore{ + { + ID: 1, + Price: 2200, + URL: "https://google.com", + DLCID: 1, + StoreID: 1, + Store: domain.Store{ + ID: 1, + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://google.com", + }, + }, + }, + }, + expected: resources.DLCResource{ + ID: 1, + Name: "DLC 1", + Cover: "https://mock-presigned-url.com/photo-key-1", + ReleaseDate: utils.FormatTimestamp(fixedTime), + Galleries: []resources.GalleriableResource{ + { + ID: 1, + Path: "https://google.com", + }, + }, + Platforms: []resources.PlatformResource{ + { + ID: 1, + Name: "Platform 1", + }, + }, + Stores: []resources.DLCStoreResource{ + { + ID: 1, + Price: 2200, + URL: "https://google.com", + Store: resources.StoreResource{ + ID: 1, + Name: "Store 1", + URL: "https://google.com", + Slug: "store-1", + Logo: "https://google.com", + }, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformDLC(tc.input, &MockS3Client{}) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/dlc_store_resource_test.go b/tests/unit/resources/dlc_store_resource_test.go new file mode 100644 index 0000000..6ea98bd --- /dev/null +++ b/tests/unit/resources/dlc_store_resource_test.go @@ -0,0 +1,72 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformDLCStore(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.DLCStore + expected resources.DLCStoreResource + }{ + "as nil": { + input: domain.DLCStore{}, + expected: resources.DLCStoreResource{}, + }, + "basic transformation": { + input: domain.DLCStore{ + ID: 1, + Price: 22999, + URL: "https://google.com", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + DLC: domain.DLC{ + ID: 1, + Name: "DLC 1", + Cover: "photo-key-1", + ReleaseDate: fixedTime, + Galleries: []domain.Galleriable{}, + Platforms: []domain.Platformable{}, + Stores: []domain.DLCStore{}, + }, + Store: domain.Store{ + ID: 1, + Name: "Store 1", + Slug: "store-1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: resources.DLCStoreResource{ + ID: 1, + Price: 22999, + URL: "https://google.com", + Store: resources.StoreResource{ + ID: 1, + Name: "Store 1", + Slug: "store-1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformDLCtore(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/galleriable_resource_test.go b/tests/unit/resources/galleriable_resource_test.go new file mode 100644 index 0000000..9e8a8ba --- /dev/null +++ b/tests/unit/resources/galleriable_resource_test.go @@ -0,0 +1,56 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformGalleriable(t *testing.T) { + testCases := map[string]struct { + input domain.Galleriable + expected resources.GalleriableResource + }{ + "as nil": { + input: domain.Galleriable{}, + expected: resources.GalleriableResource{}, + }, + "no s3": { + input: domain.Galleriable{ + ID: 1, + S3: false, + Path: "https://placehold.co/600x400/EEE/31343C", + GalleriableID: 1, + GalleriableType: "games", + }, + expected: resources.GalleriableResource{ + ID: 1, + Path: "https://placehold.co/600x400/EEE/31343C", + }, + }, + "as s3": { + input: domain.Galleriable{ + ID: 1, + S3: true, + Path: "photo-key-1", + GalleriableID: 1, + GalleriableType: "games", + }, + expected: resources.GalleriableResource{ + ID: 1, + Path: "https://mock-presigned-url.com/photo-key-1", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformGalleriable(tc.input, &MockS3Client{}) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/game_language_resource_test.go b/tests/unit/resources/game_language_resource_test.go new file mode 100644 index 0000000..8130c53 --- /dev/null +++ b/tests/unit/resources/game_language_resource_test.go @@ -0,0 +1,74 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformGameLanguage(t *testing.T) { + testCases := map[string]struct { + input domain.GameLanguage + expected resources.GameLanguageResource + }{ + "basic transformation": { + input: domain.GameLanguage{ + ID: 1, + Menu: true, + Dubs: false, + Subtitles: true, + Language: domain.Language{ + ID: 1, + Name: "English", + ISO: "en", + }, + }, + expected: resources.GameLanguageResource{ + ID: 1, + Menu: true, + Dubs: false, + Subtitles: true, + Language: resources.LanguageResource{ + ID: 1, + Name: "English", + ISO: "en", + }, + }, + }, + "language with nil values": { + input: domain.GameLanguage{ + ID: 2, + Menu: false, + Dubs: false, + Subtitles: false, + Language: domain.Language{ + ID: 0, + Name: "", + ISO: "", + }, + }, + expected: resources.GameLanguageResource{ + ID: 2, + Menu: false, + Dubs: false, + Subtitles: false, + Language: resources.LanguageResource{ + ID: 0, + Name: "", + ISO: "", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformGameLanguage(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/game_resource_test.go b/tests/unit/resources/game_resource_test.go new file mode 100644 index 0000000..f5239ac --- /dev/null +++ b/tests/unit/resources/game_resource_test.go @@ -0,0 +1,362 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTransformGame(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.Game + expected resources.GameResource + }{ + "No Morph Relations": { + input: domain.Game{ + ID: 1, + Age: 16, + Slug: "test-game", + Title: "Test Game", + Condition: "New", + Cover: "test-cover.jpg", + About: "About Test Game", + Description: "Detailed description of Test Game", + ShortDescription: "Short description", + Free: true, + Legal: utils.StringPtr("Some legal info"), + Website: utils.StringPtr("http://testgame.com"), + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Categories: []domain.Categoriable{}, + Platforms: []domain.Platformable{}, + Genres: []domain.Genreable{}, + Tags: []domain.Taggable{}, + }, + expected: resources.GameResource{ + ID: 1, + Age: 16, + Slug: "test-game", + Title: "Test Game", + Condition: "New", + Cover: "test-cover.jpg", + About: "About Test Game", + Description: "Detailed description of Test Game", + ShortDescription: "Short description", + Free: true, + Legal: utils.StringPtr("Some legal info"), + Website: utils.StringPtr("http://testgame.com"), + ReleaseDate: utils.FormatTimestamp(fixedTime), + CreatedAt: utils.FormatTimestamp(fixedTime), + UpdatedAt: utils.FormatTimestamp(fixedTime), + Categories: []resources.CategoryResource{}, + Platforms: []resources.PlatformResource{}, + Genres: []resources.GenreResource{}, + Tags: []resources.TagResource{}, + Languages: []resources.GameLanguageResource{}, + Requirements: []resources.RequirementResource{}, + Torrents: []resources.TorrentResource{}, + Publishers: []resources.PublisherResource{}, + Developers: []resources.DeveloperResource{}, + Reviews: []resources.ReviewResource{}, + Critics: []resources.CriticableResource{}, + Stores: []resources.GameStoreResource{}, + Comments: []resources.CommentableResource{}, + Galleries: []resources.GalleriableResource{}, + DLCs: []resources.DLCResource{}, + }, + }, + "With One Category": { + input: domain.Game{ + ID: 2, + Age: 18, + Slug: "fps-game", + Title: "FPS Game", + Condition: "Used", + Cover: "fps-cover.jpg", + About: "About FPS Game", + Description: "Detailed description of FPS Game", + Free: false, + Legal: nil, + Website: nil, + ReleaseDate: time.Date(2022, 5, 15, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Categories: []domain.Categoriable{ + { + Category: domain.Category{ + ID: 1, + Name: "FPS", + }, + }, + }, + Platforms: []domain.Platformable{}, + Genres: []domain.Genreable{}, + Tags: []domain.Taggable{}, + }, + expected: resources.GameResource{ + ID: 2, + Age: 18, + Slug: "fps-game", + Title: "FPS Game", + Condition: "Used", + Cover: "fps-cover.jpg", + About: "About FPS Game", + Description: "Detailed description of FPS Game", + ShortDescription: "", + Free: false, + Legal: nil, + Website: nil, + ReleaseDate: utils.FormatTimestamp(time.Date(2022, 5, 15, 0, 0, 0, 0, time.UTC)), + CreatedAt: utils.FormatTimestamp(fixedTime), + UpdatedAt: utils.FormatTimestamp(fixedTime), + Categories: []resources.CategoryResource{ + {ID: 1, Name: "FPS"}, + }, + Platforms: []resources.PlatformResource{}, + Genres: []resources.GenreResource{}, + Tags: []resources.TagResource{}, + Languages: []resources.GameLanguageResource{}, + Requirements: []resources.RequirementResource{}, + Torrents: []resources.TorrentResource{}, + Publishers: []resources.PublisherResource{}, + Developers: []resources.DeveloperResource{}, + Reviews: []resources.ReviewResource{}, + Critics: []resources.CriticableResource{}, + Stores: []resources.GameStoreResource{}, + Comments: []resources.CommentableResource{}, + Galleries: []resources.GalleriableResource{}, + DLCs: []resources.DLCResource{}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + mockS3Client := &MockS3Client{} + result := resources.TransformGame(tc.input, mockS3Client) + assert.Equal(t, tc.expected, result) + }) + } +} + +func TestTransformGames(t *testing.T) { + testCases := map[string]struct { + input []domain.Game + expected []resources.GameResource + }{ + "Empty Game List": { + input: []domain.Game{}, + expected: []resources.GameResource{}, + }, + "Single Game With No Morph Relations": { + input: []domain.Game{ + { + ID: 1, + Age: 16, + Slug: "test-game", + Title: "Test Game", + Condition: "New", + Cover: "test-cover.jpg", + About: "About Test Game", + Description: "Detailed description of Test Game", + ShortDescription: "Short description", + Free: true, + Legal: utils.StringPtr("Some legal info"), + Website: utils.StringPtr("http://testgame.com"), + ReleaseDate: time.Date(2023, 10, 6, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Categories: []domain.Categoriable{}, + Platforms: []domain.Platformable{}, + Genres: []domain.Genreable{}, + Tags: []domain.Taggable{}, + }, + }, + expected: []resources.GameResource{ + { + ID: 1, + Age: 16, + Slug: "test-game", + Title: "Test Game", + Condition: "New", + Cover: "test-cover.jpg", + About: "About Test Game", + Description: "Detailed description of Test Game", + ShortDescription: "Short description", + Free: true, + Legal: utils.StringPtr("Some legal info"), + Website: utils.StringPtr("http://testgame.com"), + ReleaseDate: utils.FormatTimestamp(time.Date(2023, 10, 6, 0, 0, 0, 0, time.UTC)), + CreatedAt: utils.FormatTimestamp(time.Now()), + UpdatedAt: utils.FormatTimestamp(time.Now()), + Categories: []resources.CategoryResource{}, + Platforms: []resources.PlatformResource{}, + Genres: []resources.GenreResource{}, + Tags: []resources.TagResource{}, + Languages: []resources.GameLanguageResource{}, + Requirements: []resources.RequirementResource{}, + Torrents: []resources.TorrentResource{}, + Publishers: []resources.PublisherResource{}, + Developers: []resources.DeveloperResource{}, + Reviews: []resources.ReviewResource{}, + Critics: []resources.CriticableResource{}, + Stores: []resources.GameStoreResource{}, + Comments: []resources.CommentableResource{}, + Galleries: []resources.GalleriableResource{}, + DLCs: []resources.DLCResource{}, + }, + }, + }, + "Multiple Games With Mixed Morph Relations": { + input: []domain.Game{ + { + ID: 2, + Age: 18, + Slug: "fps-game", + Title: "FPS Game", + Condition: "Used", + Cover: "fps-cover.jpg", + About: "About FPS Game", + Description: "Detailed description of FPS Game", + Free: false, + ReleaseDate: time.Date(2022, 5, 15, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Categories: []domain.Categoriable{ + { + Category: domain.Category{ + ID: 1, + Name: "FPS", + }, + }, + }, + Platforms: []domain.Platformable{ + { + Platform: domain.Platform{ + ID: 1, + Name: "PC", + }, + }, + }, + Genres: []domain.Genreable{}, + Tags: []domain.Taggable{}, + }, + { + ID: 3, + Age: 13, + Slug: "rpg-game", + Title: "RPG Game", + Condition: "New", + Cover: "rpg-cover.jpg", + About: "About RPG Game", + Description: "Detailed description of RPG Game", + Free: true, + ReleaseDate: time.Date(2021, 7, 21, 0, 0, 0, 0, time.UTC), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Categories: []domain.Categoriable{}, + Platforms: []domain.Platformable{}, + Genres: []domain.Genreable{ + { + Genre: domain.Genre{ + ID: 2, + Name: "Fantasy", + }, + }, + }, + Tags: []domain.Taggable{ + { + Tag: domain.Tag{ + ID: 1, + Name: "Adventure", + }, + }, + }, + }, + }, + expected: []resources.GameResource{ + { + ID: 2, + Age: 18, + Slug: "fps-game", + Title: "FPS Game", + Condition: "Used", + Cover: "fps-cover.jpg", + About: "About FPS Game", + Description: "Detailed description of FPS Game", + Free: false, + ReleaseDate: utils.FormatTimestamp(time.Date(2022, 5, 15, 0, 0, 0, 0, time.UTC)), + CreatedAt: utils.FormatTimestamp(time.Now()), + UpdatedAt: utils.FormatTimestamp(time.Now()), + Categories: []resources.CategoryResource{ + {ID: 1, Name: "FPS"}, + }, + Platforms: []resources.PlatformResource{ + {ID: 1, Name: "PC"}, + }, + Genres: []resources.GenreResource{}, + Tags: []resources.TagResource{}, + Languages: []resources.GameLanguageResource{}, + Requirements: []resources.RequirementResource{}, + Torrents: []resources.TorrentResource{}, + Publishers: []resources.PublisherResource{}, + Developers: []resources.DeveloperResource{}, + Reviews: []resources.ReviewResource{}, + Critics: []resources.CriticableResource{}, + Stores: []resources.GameStoreResource{}, + Comments: []resources.CommentableResource{}, + Galleries: []resources.GalleriableResource{}, + DLCs: []resources.DLCResource{}, + }, + { + ID: 3, + Age: 13, + Slug: "rpg-game", + Title: "RPG Game", + Condition: "New", + Cover: "rpg-cover.jpg", + About: "About RPG Game", + Description: "Detailed description of RPG Game", + Free: true, + ReleaseDate: utils.FormatTimestamp(time.Date(2021, 7, 21, 0, 0, 0, 0, time.UTC)), + CreatedAt: utils.FormatTimestamp(time.Now()), + UpdatedAt: utils.FormatTimestamp(time.Now()), + Categories: []resources.CategoryResource{}, + Platforms: []resources.PlatformResource{}, + Genres: []resources.GenreResource{ + {ID: 2, Name: "Fantasy"}, + }, + Tags: []resources.TagResource{ + {ID: 1, Name: "Adventure"}, + }, + Languages: []resources.GameLanguageResource{}, + Requirements: []resources.RequirementResource{}, + Torrents: []resources.TorrentResource{}, + Publishers: []resources.PublisherResource{}, + Developers: []resources.DeveloperResource{}, + Reviews: []resources.ReviewResource{}, + Critics: []resources.CriticableResource{}, + Stores: []resources.GameStoreResource{}, + Comments: []resources.CommentableResource{}, + Galleries: []resources.GalleriableResource{}, + DLCs: []resources.DLCResource{}, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + mockS3Client := &MockS3Client{} + result := resources.TransformGames(tc.input, mockS3Client) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/tests/unit/resources/game_store_resource_test.go b/tests/unit/resources/game_store_resource_test.go new file mode 100644 index 0000000..b78a684 --- /dev/null +++ b/tests/unit/resources/game_store_resource_test.go @@ -0,0 +1,84 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformGameStore(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.GameStore + expected resources.GameStoreResource + }{ + "as nil": { + input: domain.GameStore{}, + expected: resources.GameStoreResource{}, + }, + "basic transformation": { + input: domain.GameStore{ + ID: 1, + Price: 22999, + URL: "https://google.com", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Game: domain.Game{ + Slug: "valid", + Age: 18, + Title: "Game Test", + Condition: domain.CommomCondition, + Cover: "https://placehold.co/600x400/EEE/31343C", + About: "About game", + Description: "Description", + ShortDescription: "Short description", + Free: false, + ReleaseDate: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + Views: []domain.Viewable{ + { + UserID: 10, + ViewableID: 1, + ViewableType: "games", + }, + }, + }, + Store: domain.Store{ + ID: 1, + Name: "Store 1", + Slug: "store-1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: resources.GameStoreResource{ + ID: 1, + Price: 22999, + URL: "https://google.com", + Store: resources.StoreResource{ + ID: 1, + Name: "Store 1", + Slug: "store-1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformGameStore(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/game_support_resource_test.go b/tests/unit/resources/game_support_resource_test.go new file mode 100644 index 0000000..4e64a4c --- /dev/null +++ b/tests/unit/resources/game_support_resource_test.go @@ -0,0 +1,58 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "testing" +) + +func TestTransformSupport(t *testing.T) { + testCases := map[string]struct { + input *domain.GameSupport + expected resources.SupportResource + }{ + "as null": { + input: &domain.GameSupport{}, + expected: resources.SupportResource{}, + }, + "basic transformation": { + input: &domain.GameSupport{ + ID: 1, + URL: utils.StringPtr("https://google.com"), + Email: utils.StringPtr("email@example.com"), + Contact: utils.StringPtr("fakeContact"), + GameID: 1, + }, + expected: resources.SupportResource{ + ID: 1, + URL: utils.StringPtr("https://google.com"), + Email: utils.StringPtr("email@example.com"), + Contact: utils.StringPtr("fakeContact"), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformSupport(tc.input) + + if result.ID != tc.expected.ID || + !CompareStringPtr(result.URL, tc.expected.URL) || + !CompareStringPtr(result.Email, tc.expected.Email) || + !CompareStringPtr(result.Contact, tc.expected.Contact) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} + +func CompareStringPtr(a, b *string) bool { + if a == nil && b == nil { + return true + } + if a != nil && b != nil { + return *a == *b + } + return false +} diff --git a/tests/unit/resources/genre_resource_test.go b/tests/unit/resources/genre_resource_test.go new file mode 100644 index 0000000..b9841d4 --- /dev/null +++ b/tests/unit/resources/genre_resource_test.go @@ -0,0 +1,103 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformGenre(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input domain.Genre + expected resources.GenreResource + }{ + "as null": { + input: domain.Genre{}, + expected: resources.GenreResource{}, + }, + "valid category": { + input: domain.Genre{ + ID: 1, + Name: "Genre 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + expected: resources.GenreResource{ + ID: 1, + Name: "Genre 1", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + genreResource := resources.TransformGenre(test.input) + + if !reflect.DeepEqual(genreResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, genreResource) + } + }) + } +} + +func TestTransformGenres(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input []domain.Genre + expected []resources.GenreResource + }{ + "as null": { + input: []domain.Genre{}, + expected: []resources.GenreResource{}, + }, + "multiple genres": { + input: []domain.Genre{ + { + ID: 1, + Name: "Genre 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + { + ID: 2, + Name: "Genre 2", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: []resources.GenreResource{ + { + ID: 1, + Name: "Genre 1", + }, + { + ID: 2, + Name: "Genre 2", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + genresResource := resources.TransformGenres(test.input) + + if genresResource == nil { + genresResource = []resources.GenreResource{} + } + + if !reflect.DeepEqual(genresResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, genresResource) + } + }) + } +} diff --git a/tests/unit/resources/language_resource_test.go b/tests/unit/resources/language_resource_test.go new file mode 100644 index 0000000..ad69313 --- /dev/null +++ b/tests/unit/resources/language_resource_test.go @@ -0,0 +1,109 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformLanguage(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input domain.Language + expected resources.LanguageResource + }{ + "as null": { + input: domain.Language{}, + expected: resources.LanguageResource{}, + }, + "multiple categories": { + input: domain.Language{ + ID: 1, + Name: "Language 1", + ISO: "pt_BR", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + expected: resources.LanguageResource{ + ID: 1, + Name: "Language 1", + ISO: "pt_BR", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + LanguageResource := resources.TransformLanguage(test.input) + + if !reflect.DeepEqual(LanguageResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, LanguageResource) + } + }) + } +} + +func TestTransformLanguages(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input []domain.Language + expected []resources.LanguageResource + }{ + "as null": { + input: []domain.Language{}, + expected: []resources.LanguageResource{}, + }, + "multiple categories": { + input: []domain.Language{ + { + ID: 1, + Name: "Language 1", + ISO: "pt_BR", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + { + ID: 2, + Name: "Language 2", + ISO: "en_US", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: []resources.LanguageResource{ + { + ID: 1, + Name: "Language 1", + ISO: "pt_BR", + }, + { + ID: 2, + Name: "Language 2", + ISO: "en_US", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + LanguagesResources := resources.TransformLanguages(test.input) + + if LanguagesResources == nil { + LanguagesResources = []resources.LanguageResource{} + } + + if !reflect.DeepEqual(LanguagesResources, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, LanguagesResources) + } + }) + } +} diff --git a/tests/unit/resources/platform_resource_test.go b/tests/unit/resources/platform_resource_test.go new file mode 100644 index 0000000..a81d066 --- /dev/null +++ b/tests/unit/resources/platform_resource_test.go @@ -0,0 +1,103 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformPlatform(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input domain.Platform + expected resources.PlatformResource + }{ + "as null": { + input: domain.Platform{}, + expected: resources.PlatformResource{}, + }, + "multiple categories": { + input: domain.Platform{ + ID: 1, + Name: "Platform 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + expected: resources.PlatformResource{ + ID: 1, + Name: "Platform 1", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + platformResource := resources.TransformPlatform(test.input) + + if !reflect.DeepEqual(platformResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, platformResource) + } + }) + } +} + +func TestTransformPlatforms(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input []domain.Platform + expected []resources.PlatformResource + }{ + "as null": { + input: []domain.Platform{}, + expected: []resources.PlatformResource{}, + }, + "multiple categories": { + input: []domain.Platform{ + { + ID: 1, + Name: "Platform 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + { + ID: 2, + Name: "Platform 2", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: []resources.PlatformResource{ + { + ID: 1, + Name: "Platform 1", + }, + { + ID: 2, + Name: "Platform 2", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + platformsResources := resources.TransformPlatforms(test.input) + + if platformsResources == nil { + platformsResources = []resources.PlatformResource{} + } + + if !reflect.DeepEqual(platformsResources, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, platformsResources) + } + }) + } +} diff --git a/tests/unit/resources/protection_resource_test.go b/tests/unit/resources/protection_resource_test.go new file mode 100644 index 0000000..1775136 --- /dev/null +++ b/tests/unit/resources/protection_resource_test.go @@ -0,0 +1,83 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformProtection(t *testing.T) { + testCases := map[string]struct { + input domain.Protection + expected *resources.ProtectionResource + }{ + "single protection": { + input: domain.Protection{ + ID: 1, + Name: "Protection 1", + }, + expected: &resources.ProtectionResource{ + ID: 1, + Name: "Protection 1", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformProtection(tc.input) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} + +func TestTransformProtections(t *testing.T) { + testCases := map[string]struct { + input []domain.Protection + expected []*resources.ProtectionResource + }{ + "empty slice": { + input: []domain.Protection{}, + expected: []*resources.ProtectionResource{}, + }, + "multiple protections": { + input: []domain.Protection{ + { + ID: 1, + Name: "Protection 1", + }, + { + ID: 2, + Name: "Protection 2", + }, + }, + expected: []*resources.ProtectionResource{ + { + ID: 1, + Name: "Protection 1", + }, + { + ID: 2, + Name: "Protection 2", + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformProtections(tc.input) + + if result == nil { + result = []*resources.ProtectionResource{} + } + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/publisher_resource_test.go b/tests/unit/resources/publisher_resource_test.go new file mode 100644 index 0000000..e324603 --- /dev/null +++ b/tests/unit/resources/publisher_resource_test.go @@ -0,0 +1,89 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformPublisher(t *testing.T) { + testCases := map[string]struct { + input domain.Publisher + expected resources.PublisherResource + }{ + "single Publisher": { + input: domain.Publisher{ + ID: 1, + Name: "Publisher 1", + Acting: false, + }, + expected: resources.PublisherResource{ + ID: 1, + Name: "Publisher 1", + Acting: false, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformPublisher(tc.input) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} + +func TestTransformPublishers(t *testing.T) { + testCases := map[string]struct { + input []domain.Publisher + expected []resources.PublisherResource + }{ + "empty slice": { + input: []domain.Publisher{}, + expected: []resources.PublisherResource{}, + }, + "multiple Publishers": { + input: []domain.Publisher{ + { + ID: 1, + Name: "Publisher 1", + Acting: true, + }, + { + ID: 2, + Name: "Publisher 2", + Acting: false, + }, + }, + expected: []resources.PublisherResource{ + { + ID: 1, + Name: "Publisher 1", + Acting: true, + }, + { + ID: 2, + Name: "Publisher 2", + Acting: false, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformPublishers(tc.input) + + if result == nil { + result = []resources.PublisherResource{} + } + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/requirement_resource_test.go b/tests/unit/resources/requirement_resource_test.go new file mode 100644 index 0000000..d1cbd18 --- /dev/null +++ b/tests/unit/resources/requirement_resource_test.go @@ -0,0 +1,95 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "reflect" + "testing" +) + +func TestTransformRequirement(t *testing.T) { + testCases := map[string]struct { + input domain.Requirement + expected resources.RequirementResource + }{ + "basic transformation": { + input: domain.Requirement{ + ID: 1, + OS: "Windows 10", + DX: "12", + CPU: "Intel i5", + RAM: "8 GB", + GPU: "NVIDIA GTX 1060", + ROM: "50 GB", + OBS: nil, + Network: "Broadband", + RequirementType: domain.RequirementType{ + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + }, + expected: resources.RequirementResource{ + ID: 1, + OS: "Windows 10", + DX: "12", + CPU: "Intel i5", + RAM: "8 GB", + GPU: "NVIDIA GTX 1060", + ROM: "50 GB", + OBS: nil, + Network: "Broadband", + RequirementType: resources.RequirementTypeResource{ + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + }, + }, + "with OBS field": { + input: domain.Requirement{ + ID: 2, + OS: "Windows 11", + DX: "12", + CPU: "AMD Ryzen 5", + RAM: "16 GB", + GPU: "NVIDIA RTX 2060", + ROM: "100 GB", + OBS: utils.StringPtr("Streamable"), + Network: "High-Speed", + RequirementType: domain.RequirementType{ + ID: 2, + Potential: domain.RecommendedRequirementType, + OS: domain.WindowsOSRequirement, + }, + }, + expected: resources.RequirementResource{ + ID: 2, + OS: "Windows 11", + DX: "12", + CPU: "AMD Ryzen 5", + RAM: "16 GB", + GPU: "NVIDIA RTX 2060", + ROM: "100 GB", + OBS: utils.StringPtr("Streamable"), + Network: "High-Speed", + RequirementType: resources.RequirementTypeResource{ + ID: 2, + Potential: domain.RecommendedRequirementType, + OS: domain.WindowsOSRequirement, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformRequirement(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/requirement_type_resource_test.go b/tests/unit/resources/requirement_type_resource_test.go new file mode 100644 index 0000000..2e2e796 --- /dev/null +++ b/tests/unit/resources/requirement_type_resource_test.go @@ -0,0 +1,98 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformRequirementType(t *testing.T) { + tests := map[string]struct { + input domain.RequirementType + expected resources.RequirementTypeResource + }{ + "as null": { + input: domain.RequirementType{}, + expected: resources.RequirementTypeResource{}, + }, + "multiple categories": { + input: domain.RequirementType{ + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + expected: resources.RequirementTypeResource{ + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + RequirementTypeResource := resources.TransformRequirementType(test.input) + + if !reflect.DeepEqual(RequirementTypeResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, RequirementTypeResource) + } + }) + } +} + +func TestTransformRequirementTypes(t *testing.T) { + tests := map[string]struct { + input []domain.RequirementType + expected []resources.RequirementTypeResource + }{ + "as null": { + input: []domain.RequirementType{}, + expected: []resources.RequirementTypeResource{}, + }, + "multiple categories": { + input: []domain.RequirementType{ + { + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + { + ID: 2, + Potential: domain.RecommendedRequirementType, + OS: domain.WindowsOSRequirement, + }, + }, + expected: []resources.RequirementTypeResource{ + { + ID: 1, + Potential: domain.MinimumRequirementType, + OS: domain.WindowsOSRequirement, + }, + { + ID: 2, + Potential: domain.RecommendedRequirementType, + OS: domain.WindowsOSRequirement, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + RequirementTypesResources := resources.TransformRequirementTypes(test.input) + + if RequirementTypesResources == nil { + RequirementTypesResources = []resources.RequirementTypeResource{} + } + + if !reflect.DeepEqual(RequirementTypesResources, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, RequirementTypesResources) + } + }) + } +} diff --git a/tests/unit/resources/review_resource_test.go b/tests/unit/resources/review_resource_test.go new file mode 100644 index 0000000..dbfa35e --- /dev/null +++ b/tests/unit/resources/review_resource_test.go @@ -0,0 +1,186 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "reflect" + "testing" + "time" +) + +func TestTransformReview(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.Reviewable + expected resources.ReviewResource + }{ + "single Review": { + input: domain.Reviewable{ + ID: 1, + Rate: 5, + Review: "Good game!", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + ReviewableID: 1, + ReviewableType: "games", + User: domain.User{ + ID: 1, + Email: "fake@gmail.com", + Name: "Fake", + Nickname: "fake", + CreatedAt: fixedTime, + Profile: domain.Profile{ + ID: 1, + Share: true, + Photo: "profile-photo-key", + }, + }, + }, + expected: resources.ReviewResource{ + ID: 1, + Rate: 5, + Review: "Good game!", + CreatedAt: utils.FormatTimestamp(fixedTime), + UpdatedAt: utils.FormatTimestamp(fixedTime), + User: resources.MinimalUserResource{ + ID: 1, + Email: "fake@gmail.com", + Name: "Fake", + Nickname: "fake", + Photo: utils.StringPtr("https://mock-presigned-url.com/profile-photo-key"), + CreatedAt: utils.FormatTimestamp(fixedTime), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + mockS3Client := &MockS3Client{} + result := resources.TransformReview(tc.input, mockS3Client) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} + +func TestTransformReviews(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input []domain.Reviewable + expected []resources.ReviewResource + }{ + "empty slice": { + input: []domain.Reviewable{}, + expected: []resources.ReviewResource{}, + }, + "multiple Reviews": { + input: []domain.Reviewable{ + { + ID: 1, + Rate: 1, + Review: "Bad game!", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + ReviewableID: 1, + ReviewableType: "games", + User: domain.User{ + ID: 1, + Email: "fake@gmail.com", + Name: "Fake", + Nickname: "fake", + Experience: 0, + Birthdate: fixedTime, + Password: "fake1234", + Blocked: false, + LevelID: 1, + CreatedAt: fixedTime, + Profile: domain.Profile{ + ID: 1, + Share: true, + Photo: "photo-1-key", + }, + }, + }, + { + ID: 2, + Rate: 5, + Review: "Good game!", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + ReviewableID: 1, + ReviewableType: "games", + User: domain.User{ + ID: 1, + Email: "fake@gmail.com", + Name: "Fake", + Nickname: "fake", + Experience: 0, + Birthdate: fixedTime, + Password: "fake1234", + Blocked: false, + LevelID: 1, + CreatedAt: fixedTime, + Profile: domain.Profile{ + ID: 1, + Share: true, + Photo: "photo-2-key", + }, + }, + }, + }, + expected: []resources.ReviewResource{ + { + ID: 1, + Rate: 1, + Review: "Bad game!", + CreatedAt: utils.FormatTimestamp(fixedTime), + UpdatedAt: utils.FormatTimestamp(fixedTime), + User: resources.MinimalUserResource{ + ID: 1, + Email: "fake@gmail.com", + Name: "Fake", + Nickname: "fake", + Photo: utils.StringPtr("https://mock-presigned-url.com/photo-1-key"), + CreatedAt: utils.FormatTimestamp(fixedTime), + }, + }, + { + ID: 2, + Rate: 5, + Review: "Good game!", + CreatedAt: utils.FormatTimestamp(fixedTime), + UpdatedAt: utils.FormatTimestamp(fixedTime), + User: resources.MinimalUserResource{ + ID: 1, + Email: "fake@gmail.com", + Name: "Fake", + Nickname: "fake", + Photo: utils.StringPtr("https://mock-presigned-url.com/photo-2-key"), + CreatedAt: utils.FormatTimestamp(fixedTime), + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + mockS3Client := &MockS3Client{} + result := resources.TransformReviews(tc.input, mockS3Client) + + if result == nil { + result = []resources.ReviewResource{} + } + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/store_resource_test.go b/tests/unit/resources/store_resource_test.go new file mode 100644 index 0000000..c3a240b --- /dev/null +++ b/tests/unit/resources/store_resource_test.go @@ -0,0 +1,51 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformStore(t *testing.T) { + fixedTime := time.Now() + + testCases := map[string]struct { + input domain.Store + expected resources.StoreResource + }{ + "as nil": { + input: domain.Store{}, + expected: resources.StoreResource{}, + }, + "basic transformation": { + input: domain.Store{ + ID: 1, + Name: "Store 1", + Slug: "store-1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + expected: resources.StoreResource{ + ID: 1, + Name: "Store 1", + Slug: "store-1", + URL: "https://google.com", + Logo: "https://placehold.co/600x400/EEE/31343C", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformStore(tc.input) + + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/tag_resource_test.go b/tests/unit/resources/tag_resource_test.go new file mode 100644 index 0000000..06cd74e --- /dev/null +++ b/tests/unit/resources/tag_resource_test.go @@ -0,0 +1,103 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" + "time" +) + +func TestTransformTag(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input domain.Tag + expected resources.TagResource + }{ + "as null": { + input: domain.Tag{}, + expected: resources.TagResource{}, + }, + "multiple categories": { + input: domain.Tag{ + ID: 1, + Name: "Tag 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + expected: resources.TagResource{ + ID: 1, + Name: "Tag 1", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + categoryResource := resources.TransformTag(test.input) + + if !reflect.DeepEqual(categoryResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, categoryResource) + } + }) + } +} + +func TestTransformTags(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input []domain.Tag + expected []resources.TagResource + }{ + "as null": { + input: []domain.Tag{}, + expected: []resources.TagResource{}, + }, + "multiple categories": { + input: []domain.Tag{ + { + ID: 1, + Name: "Tag 1", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + { + ID: 2, + Name: "Tag 2", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: []resources.TagResource{ + { + ID: 1, + Name: "Tag 1", + }, + { + ID: 2, + Name: "Tag 2", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + genresResource := resources.TransformTags(test.input) + + if genresResource == nil { + genresResource = []resources.TagResource{} + } + + if !reflect.DeepEqual(genresResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, genresResource) + } + }) + } +} diff --git a/tests/unit/resources/torrent_provider_resource_test.go b/tests/unit/resources/torrent_provider_resource_test.go new file mode 100644 index 0000000..e41b4fa --- /dev/null +++ b/tests/unit/resources/torrent_provider_resource_test.go @@ -0,0 +1,41 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "reflect" + "testing" +) + +func TestTransformTorrentProvider(t *testing.T) { + testCases := map[string]struct { + input domain.TorrentProvider + expected resources.TorrentProviderResource + }{ + "as null": { + input: domain.TorrentProvider{}, + expected: resources.TorrentProviderResource{}, + }, + "valid torrent provider": { + input: domain.TorrentProvider{ + ID: 1, + Name: "Google", + URL: "https://google.com", + }, + expected: resources.TorrentProviderResource{ + ID: 1, + Name: "Google", + URL: "https://google.com", + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + result := resources.TransformTorrentProvider(tc.input) + if !reflect.DeepEqual(result, tc.expected) { + t.Errorf("Expected %+v, got %+v", tc.expected, result) + } + }) + } +} diff --git a/tests/unit/resources/torrent_resource_test.go b/tests/unit/resources/torrent_resource_test.go new file mode 100644 index 0000000..dc6fbaa --- /dev/null +++ b/tests/unit/resources/torrent_resource_test.go @@ -0,0 +1,142 @@ +package tests + +import ( + "gcstatus/internal/domain" + "gcstatus/internal/resources" + "gcstatus/pkg/utils" + "reflect" + "testing" + "time" +) + +func TestTransformTorrent(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input domain.Torrent + expected resources.TorrentResource + }{ + "valid torrent": { + input: domain.Torrent{ + ID: 1, + URL: "https://google.com", + PostedAt: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + TorrentProvider: domain.TorrentProvider{ + ID: 1, + URL: "https://google.com", + Name: "Google", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + expected: resources.TorrentResource{ + ID: 1, + URL: "https://google.com", + PostedAt: utils.FormatTimestamp(fixedTime), + Provider: resources.TorrentProviderResource{ + ID: 1, + URL: "https://google.com", + Name: "Google", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + categoryResource := resources.TransformTorrent(test.input) + + if !reflect.DeepEqual(categoryResource, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, categoryResource) + } + }) + } +} + +func TestTransformTorrents(t *testing.T) { + fixedTime := time.Now() + + tests := map[string]struct { + input []domain.Torrent + expected []resources.TorrentResource + }{ + "as null": { + input: []domain.Torrent{}, + expected: []resources.TorrentResource{}, + }, + "multiple categories": { + input: []domain.Torrent{ + { + ID: 1, + URL: "https://google.com", + PostedAt: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + TorrentProvider: domain.TorrentProvider{ + ID: 1, + URL: "https://google.com", + Name: "Google", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + { + ID: 2, + URL: "https://google2.com", + PostedAt: fixedTime, + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + TorrentProvider: domain.TorrentProvider{ + ID: 2, + URL: "https://google2.com", + Name: "Google2", + CreatedAt: fixedTime, + UpdatedAt: fixedTime, + }, + }, + }, + expected: []resources.TorrentResource{ + { + ID: 1, + URL: "https://google.com", + PostedAt: utils.FormatTimestamp(fixedTime), + Provider: resources.TorrentProviderResource{ + ID: 1, + URL: "https://google.com", + Name: "Google", + }, + }, + { + ID: 2, + URL: "https://google2.com", + PostedAt: utils.FormatTimestamp(fixedTime), + Provider: resources.TorrentProviderResource{ + ID: 2, + URL: "https://google2.com", + Name: "Google2", + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + torrentsResources := resources.TransformTorrents(test.input) + + if torrentsResources == nil { + torrentsResources = []resources.TorrentResource{} + } + + if !reflect.DeepEqual(torrentsResources, test.expected) { + t.Errorf("Expected %+v, got %+v", test.expected, torrentsResources) + } + }) + } +}