diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a47eee --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/api \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..75e1794 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Stage 1: Build the Go binary +FROM golang:1.22-alpine AS builder + +# Install necessary build tools +RUN apk add --no-cache gcc musl-dev + +# Set the Current Working Directory inside the container +WORKDIR /app + +# Copy go mod and sum files +COPY go.mod go.sum ./ + +# Download all dependencies +RUN go mod download + +# Copy the entire project source into the container +COPY . . + +# Build the Go app, specifying the directory containing the entry point +RUN go build -o ./main ./cmd/api + +## Stage 2: A minimal image to run the Go binary +FROM alpine:latest +# +## Set the Current Working Directory inside the container +WORKDIR /root/ +# +## Copy the Pre-built binary file from the builder stage +COPY --from=builder /app/main . + +# Expose port 8080 to the outside world +EXPOSE 8080 + +# Command to run the executable +CMD ["./main"] diff --git a/Makefile b/Makefile index 909b776..a079ffa 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ test: @go test ./... -v -start: - @go run cmd/api/api.go \ No newline at end of file +build: + @go build -o bin/api cmd/api/api.go + +start: build + ./bin/api diff --git a/adapter/input/adapter_http/posts_controller.go b/adapter/input/adapter_http/posts_controller.go new file mode 100644 index 0000000..6aa152d --- /dev/null +++ b/adapter/input/adapter_http/posts_controller.go @@ -0,0 +1,96 @@ +package adapter_http + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/request" + "github.com/dexfs/go-twitter-clone/adapter/input/model/response" + "github.com/dexfs/go-twitter-clone/config/validation" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/gin-gonic/gin" + "net/http" +) + +type postsController struct { + createPostUseCase input.CreatePostUseCase + createRepostUseCase input.CreateRepostUseCase + createQuoteUseCase input.CreateQuoteUseCase +} + +func NewPostsController(createPostUseCase input.CreatePostUseCase, createRepostUseCase input.CreateRepostUseCase, createQuoteUseCase input.CreateQuoteUseCase) *postsController { + return &postsController{ + createPostUseCase: createPostUseCase, + createRepostUseCase: createRepostUseCase, + createQuoteUseCase: createQuoteUseCase, + } +} + +func (pc *postsController) CreatePost(c *gin.Context) { + createPostRequest := request.CreatePostRequest{} + + if err := c.ShouldBindJSON(&createPostRequest); err != nil { + errRest := validation.RestError(err) + c.JSON(errRest.Code, errRest) + return + } + + postDomain, err := pc.createPostUseCase.Execute(input.CreatePostUseCaseInput{ + UserID: createPostRequest.UserID, + Content: createPostRequest.Content, + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusCreated, response.CreatePostResponse{ + PostID: postDomain.ID, + }) +} + +func (pc *postsController) CreateRepost(c *gin.Context) { + createRequest := request.RepostRequest{} + + if err := c.ShouldBindJSON(&createRequest); err != nil { + errRest := validation.RestError(err) + c.JSON(errRest.Code, errRest) + return + } + + postDomain, err := pc.createRepostUseCase.Execute(input.CreateRepostUseCaseInput{ + PostID: createRequest.PostID, + UserID: createRequest.UserID, + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, err) + return + } + + c.JSON(http.StatusCreated, response.CreatePostResponse{ + PostID: postDomain.ID, + }) +} + +func (pc *postsController) CreateQuote(c *gin.Context) { + createRequest := request.QuoteRequest{} + + if err := c.ShouldBindJSON(&createRequest); err != nil { + errRest := validation.RestError(err) + c.JSON(errRest.Code, errRest) + return + } + + postDomain, err := pc.createQuoteUseCase.Execute(input.CreateQuoteUseCaseInput{ + PostID: createRequest.PostID, + UserID: createRequest.UserID, + Quote: createRequest.Quote, + }) + + if err != nil { + c.JSON(http.StatusInternalServerError, err) + } + + c.JSON(http.StatusCreated, response.CreatePostResponse{ + PostID: postDomain.ID, + }) +} diff --git a/adapter/input/adapter_http/users_controller.go b/adapter/input/adapter_http/users_controller.go new file mode 100644 index 0000000..407d1af --- /dev/null +++ b/adapter/input/adapter_http/users_controller.go @@ -0,0 +1,70 @@ +package adapter_http + +import "C" +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/request" + "github.com/dexfs/go-twitter-clone/adapter/input/model/response" + "github.com/dexfs/go-twitter-clone/config/validation" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/gin-gonic/gin" + "net/http" +) + +type usersController struct { + getUserInfoUseCase input.GetUserInfoUseCase + getUserFeedUseCase input.GetUserFeedUseCase +} + +func NewUsersController(getUserInfoUseCase input.GetUserInfoUseCase, getUserFeedUseCase input.GetUserFeedUseCase) *usersController { + return &usersController{ + getUserInfoUseCase, + getUserFeedUseCase, + } +} + +func (uc *usersController) GetInfo(c *gin.Context) { + userInfoRequest := request.UserInfoRequest{} + + if err := c.ShouldBindUri(&userInfoRequest); err != nil { + errRest := validation.RestError(err) + c.JSON(errRest.Code, errRest) + return + } + userFeedDomain, err := uc.getUserInfoUseCase.Execute(userInfoRequest.Username) + if err != nil { + c.JSON(err.Code, err) + return + } + + c.JSON(http.StatusOK, response.GetUserInfoResponse{ + ID: userFeedDomain.ID, + Username: userFeedDomain.Username, + CreatedAt: userFeedDomain.CreatedAt.Format("2006-01-02"), + }) +} + +func (uc *usersController) GetFeed(c *gin.Context) { + userRequest := request.UserFeedRequest{} + + if err := c.ShouldBindUri(&userRequest); err != nil { + errRest := validation.RestError(err) + c.JSON(errRest.Code, errRest) + return + } + + userFeedDomain, err := uc.getUserFeedUseCase.Execute(userRequest.Username) + if err != nil { + c.JSON(err.Code, err) + return + } + var items []*domain.Post + if len(userFeedDomain) == 0 { + items = make([]*domain.Post, 0) + } else { + items = userFeedDomain + } + c.JSON(http.StatusOK, response.GetUserFeedResponse{ + Items: items, + }) +} diff --git a/adapter/input/model/request/post_create_request.go b/adapter/input/model/request/post_create_request.go new file mode 100644 index 0000000..6c71e29 --- /dev/null +++ b/adapter/input/model/request/post_create_request.go @@ -0,0 +1,6 @@ +package request + +type CreatePostRequest struct { + UserID string `json:"user_id" binding:"required,uuid4"` + Content string `json:"content" binding:"required,min=10,max=255"` +} diff --git a/adapter/input/model/request/quote_request.go b/adapter/input/model/request/quote_request.go new file mode 100644 index 0000000..2efd38b --- /dev/null +++ b/adapter/input/model/request/quote_request.go @@ -0,0 +1,7 @@ +package request + +type QuoteRequest struct { + UserID string `json:"user_id" binding:"required,uuid4"` + PostID string `json:"post_id" binding:"required,uuid4"` + Quote string `json:"quote" binding:"required,min=10,max=100"` +} diff --git a/adapter/input/model/request/repost_request.go b/adapter/input/model/request/repost_request.go new file mode 100644 index 0000000..d92ed7d --- /dev/null +++ b/adapter/input/model/request/repost_request.go @@ -0,0 +1,6 @@ +package request + +type RepostRequest struct { + UserID string `json:"user_id" binding:"required,uuid4"` + PostID string `json:"post_id" binding:"required,uuid4"` +} diff --git a/adapter/input/model/request/user_request.go b/adapter/input/model/request/user_request.go new file mode 100644 index 0000000..b9ae3db --- /dev/null +++ b/adapter/input/model/request/user_request.go @@ -0,0 +1,9 @@ +package request + +type UserInfoRequest struct { + Username string `uri:"username" binding:"required,min=5,max=10,alphanum,lowercase"` +} + +type UserFeedRequest struct { + Username string `uri:"username" binding:"required,min=5,max=10,alphanum,lowercase"` +} diff --git a/adapter/input/model/response/create_post_response.go b/adapter/input/model/response/create_post_response.go new file mode 100644 index 0000000..190c2a9 --- /dev/null +++ b/adapter/input/model/response/create_post_response.go @@ -0,0 +1,5 @@ +package response + +type CreatePostResponse struct { + PostID string `json:"post_id"` +} diff --git a/adapter/input/model/response/user_response.go b/adapter/input/model/response/user_response.go new file mode 100644 index 0000000..7e926e0 --- /dev/null +++ b/adapter/input/model/response/user_response.go @@ -0,0 +1,13 @@ +package response + +import "github.com/dexfs/go-twitter-clone/internal/core/domain" + +type GetUserInfoResponse struct { + ID string `json:"id"` + Username string `json:"username"` + CreatedAt string `json:"created_at"` +} + +type GetUserFeedResponse struct { + Items []*domain.Post `json:"items"` +} diff --git a/adapter/input/model/rest_errors/rest_error.go b/adapter/input/model/rest_errors/rest_error.go new file mode 100644 index 0000000..2e90bad --- /dev/null +++ b/adapter/input/model/rest_errors/rest_error.go @@ -0,0 +1,61 @@ +package rest_errors + +import "net/http" + +type RestError struct { + Message string `json:"message"` + Err string `json:"error"` + Code int `json:"code"` + Causes []Cause `json:"causes"` +} + +type Cause struct { + Field string `json:"field"` + Message string `json:"message"` +} + +func (r *RestError) Error() string { + return r.Message +} + +func NewRestError(message string, err string, code int, causes []Cause) *RestError { + return &RestError{ + Message: message, + Err: err, + Code: code, + Causes: causes, + } +} + +func NewBadRequestError(message string) *RestError { + return &RestError{ + Message: message, + Err: http.StatusText(http.StatusBadRequest), + Code: http.StatusBadRequest, + } +} + +func NewBadRequestValidationError(message string, causes []Cause) *RestError { + return &RestError{ + Message: message, + Err: http.StatusText(http.StatusBadRequest), + Code: http.StatusBadRequest, + Causes: causes, + } +} + +func NewInternalServerError(message string) *RestError { + return &RestError{ + Message: message, + Err: http.StatusText(http.StatusInternalServerError), + Code: http.StatusInternalServerError, + } +} + +func NewNotFoundError(message string) *RestError { + return &RestError{ + Message: message, + Err: http.StatusText(http.StatusNotFound), + Code: http.StatusNotFound, + } +} diff --git a/adapter/input/routes/gin_router.go b/adapter/input/routes/gin_router.go new file mode 100644 index 0000000..c8a49c9 --- /dev/null +++ b/adapter/input/routes/gin_router.go @@ -0,0 +1,62 @@ +package routes + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/adapter_http" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" + "github.com/dexfs/go-twitter-clone/internal/core/usecase" + "github.com/fvbock/endless" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +type AppServer struct { + Router *gin.Engine + addr string +} + +func NewRouter(addr string) *AppServer { + //gin.SetMode(gin.DebugMode) + //gin.ForceConsoleColor() + r := gin.Default() + r.Use(cors.Default()) + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + return &AppServer{ + Router: r, + addr: addr, + } +} + +func (s *AppServer) Run(userRepo output.UserPort, postRepo output.PostPort) error { + err := s.Router.SetTrustedProxies(nil) + if err != nil { + return err + } + + s.InitUserResources(userRepo, postRepo) + s.InitPostResources(userRepo, postRepo) + return endless.ListenAndServe(s.addr, s.Router) +} +func (s *AppServer) InitUserResources(userRepo output.UserPort, postRepo output.PostPort) { + getUserInfoService, _ := usecase.NewGetUserInfoUseCase(userRepo) + getUserFeedUseCase, _ := usecase.NewGetUserFeedUseCase(userRepo, postRepo) + + usersController := adapter_http.NewUsersController(getUserInfoService, getUserFeedUseCase) + + s.Router.GET("/users/:username/info", usersController.GetInfo) + s.Router.GET("/users/:username/feed", usersController.GetFeed) +} + +func (s *AppServer) InitPostResources(userRepo output.UserPort, postRepo output.PostPort) { + createPostUseCase, _ := usecase.NewCreatePostUseCase(postRepo, userRepo) + createRepostUseCase, _ := usecase.NewCreateRepostUseCase(postRepo, userRepo) + createQuoteUseCase, _ := usecase.NewCreateQuoteUseCase(postRepo, userRepo) + + postsController := adapter_http.NewPostsController(createPostUseCase, createRepostUseCase, createQuoteUseCase) + + s.Router.POST("/posts", postsController.CreatePost) + s.Router.POST("/posts/repost", postsController.CreateRepost) + s.Router.POST("/posts/quote", postsController.CreateQuote) +} diff --git a/adapter/output/mappers/post_mapper.go b/adapter/output/mappers/post_mapper.go new file mode 100644 index 0000000..d68091a --- /dev/null +++ b/adapter/output/mappers/post_mapper.go @@ -0,0 +1,42 @@ +package mappers + +import ( + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) + +type postMapper struct{} + +func NewPostMapper() *postMapper { + return &postMapper{} +} + +func (m *postMapper) ToPersistence(aPost *domain.Post) *inmemory_schema.PostSchema { + return &inmemory_schema.PostSchema{ + ID: aPost.ID, + UserID: aPost.User.ID, + Content: aPost.Content, + CreatedAt: aPost.CreatedAt, + IsQuote: aPost.IsQuote, + IsRepost: aPost.IsRepost, + OriginalPostID: aPost.OriginalPostID, + OriginalPostContent: aPost.OriginalPostContent, + OriginalPostUserID: aPost.OriginalPostUserID, + OriginalPostScreenName: aPost.OriginalPostScreenName, + } +} + +func (m *postMapper) FromPersistence(aPost *inmemory_schema.PostSchema, aUser *domain.User) *domain.Post { + return &domain.Post{ + ID: aPost.ID, + User: aUser, + Content: aPost.Content, + CreatedAt: aPost.CreatedAt, + IsQuote: aPost.IsQuote, + IsRepost: aPost.IsRepost, + OriginalPostID: aPost.OriginalPostID, + OriginalPostContent: aPost.OriginalPostContent, + OriginalPostUserID: aPost.OriginalPostUserID, + OriginalPostScreenName: aPost.OriginalPostScreenName, + } +} diff --git a/adapter/output/mappers/user_mapper.go b/adapter/output/mappers/user_mapper.go new file mode 100644 index 0000000..0926d14 --- /dev/null +++ b/adapter/output/mappers/user_mapper.go @@ -0,0 +1,30 @@ +package mappers + +import ( + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) + +type userMapper struct{} + +func NewUserMapper() *userMapper { + return &userMapper{} +} + +func (m *userMapper) ToPersistence(aUser *domain.User) *inmemory_schema.UserSchema { + return &inmemory_schema.UserSchema{ + ID: aUser.ID, + Username: aUser.Username, + CreatedAt: aUser.CreatedAt, + UpdatedAt: aUser.UpdatedAt, + } +} + +func (m *userMapper) FromPersistence(aUserSchema *inmemory_schema.UserSchema) *domain.User { + return &domain.User{ + ID: aUserSchema.ID, + Username: aUserSchema.Username, + CreatedAt: aUserSchema.CreatedAt, + UpdatedAt: aUserSchema.UpdatedAt, + } +} diff --git a/adapter/output/repository/inmemory/post_inmemory_repo.go b/adapter/output/repository/inmemory/post_inmemory_repo.go new file mode 100644 index 0000000..8d92bbe --- /dev/null +++ b/adapter/output/repository/inmemory/post_inmemory_repo.go @@ -0,0 +1,112 @@ +package inmemory + +import ( + "errors" + "github.com/dexfs/go-twitter-clone/adapter/output/mappers" + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/dexfs/go-twitter-clone/pkg/shared/helpers" + "log" +) + +const POST_SCHEMA_NAME = "posts" + +type inMemoryPostRepository struct { + db *database.InMemoryDB +} + +func NewInMemoryPostRepository(db *database.InMemoryDB) *inMemoryPostRepository { + return &inMemoryPostRepository{ + db, + } +} + +func (r *inMemoryPostRepository) CreatePost(aPost *domain.Post) error { + r.insert(&inmemory_schema.PostSchema{ + ID: aPost.ID, + UserID: aPost.User.ID, + Content: aPost.Content, + CreatedAt: aPost.CreatedAt, + IsQuote: aPost.IsQuote, + IsRepost: aPost.IsRepost, + OriginalPostID: aPost.OriginalPostID, + OriginalPostContent: aPost.OriginalPostContent, + OriginalPostUserID: aPost.OriginalPostUserID, + OriginalPostScreenName: aPost.OriginalPostScreenName, + }) + return nil +} + +func (r *inMemoryPostRepository) HasReachedPostingLimitDay(aUserId string, aLimit uint64) bool { + var count = uint64(0) + + for _, currentData := range r.getAll() { + matched := currentData.UserID == aUserId && helpers.IsToday(currentData.CreatedAt) + + if matched { + count++ + } + } + + reached := count >= aLimit + if reached { + return true + } else { + return false + } +} + +func (r *inMemoryPostRepository) AllByUserID(aUser *domain.User) []*domain.Post { + var feed []*domain.Post + for _, currentData := range r.getAll() { + if currentData.UserID == aUser.ID { + feed = append(feed, mappers.NewPostMapper().FromPersistence(currentData, aUser)) + } + } + + return feed +} + +func (r *inMemoryPostRepository) FindByID(aPostID string) (*domain.Post, error) { + for _, currentData := range r.getAll() { + if currentData.ID == aPostID { + var postUser *domain.User + for _, userData := range r.db.GetSchema(USER_SCHEMA_NAME).([]*inmemory_schema.UserSchema) { + if currentData.UserID == userData.ID { + postUser = mappers.NewUserMapper().FromPersistence(userData) + } + } + return mappers.NewPostMapper().FromPersistence(currentData, postUser), nil + } + } + + return nil, errors.New("post not found") +} + +func (r *inMemoryPostRepository) HasPostBeenRepostedByUser(postID string, userID string) bool { + for _, vPost := range r.getAll() { + if vPost.IsRepost { + if vPost.UserID == userID && vPost.OriginalPostID == postID { + return true + } + } + } + + return false +} + +func (r *inMemoryPostRepository) getAll() []*inmemory_schema.PostSchema { + return r.db.GetSchema(POST_SCHEMA_NAME).([]*inmemory_schema.PostSchema) +} + +func (r *inMemoryPostRepository) insert(newItem interface{}) { + existing, ok := r.db.Schemas[POST_SCHEMA_NAME].([]*inmemory_schema.PostSchema) + if !ok { + log.Fatal("schema " + POST_SCHEMA_NAME + " not found") + return + } + + updateSlice := append(existing, newItem.(*inmemory_schema.PostSchema)) + r.db.Schemas[POST_SCHEMA_NAME] = updateSlice +} diff --git a/adapter/output/repository/inmemory/schema/types.go b/adapter/output/repository/inmemory/schema/types.go new file mode 100644 index 0000000..2448d32 --- /dev/null +++ b/adapter/output/repository/inmemory/schema/types.go @@ -0,0 +1,23 @@ +package inmemory_schema + +import "time" + +type PostSchema struct { + ID string + UserID string + Content string + CreatedAt time.Time + IsQuote bool + IsRepost bool + OriginalPostID string + OriginalPostContent string + OriginalPostUserID string + OriginalPostScreenName string +} + +type UserSchema struct { + ID string + Username string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/adapter/output/repository/inmemory/user_inmemory_repo.go b/adapter/output/repository/inmemory/user_inmemory_repo.go new file mode 100644 index 0000000..ecd0c60 --- /dev/null +++ b/adapter/output/repository/inmemory/user_inmemory_repo.go @@ -0,0 +1,53 @@ +package inmemory + +import ( + "errors" + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/pkg/database" +) + +const USER_SCHEMA_NAME = "users" + +type inMemoryUserRepository struct { + db *database.InMemoryDB +} + +func NewInMemoryUserRepository(db *database.InMemoryDB) *inMemoryUserRepository { + return &inMemoryUserRepository{ + db: db, + } +} + +func (r *inMemoryUserRepository) ByUsername(username string) (*domain.User, error) { + for _, currentUser := range r.getAll() { + if currentUser.Username == username { + return &domain.User{ + ID: currentUser.ID, + Username: currentUser.Username, + CreatedAt: currentUser.CreatedAt, + UpdatedAt: currentUser.UpdatedAt, + }, nil + } + } + + return nil, errors.New("user not found") +} + +func (r *inMemoryUserRepository) FindByID(id string) (*domain.User, error) { + for _, currentUser := range r.getAll() { + if currentUser.ID == id { + return &domain.User{ + ID: currentUser.ID, + Username: currentUser.Username, + CreatedAt: currentUser.CreatedAt, + UpdatedAt: currentUser.UpdatedAt, + }, nil + } + } + return nil, errors.New("user not found") +} + +func (r *inMemoryUserRepository) getAll() []*inmemory_schema.UserSchema { + return r.db.GetSchema(USER_SCHEMA_NAME).([]*inmemory_schema.UserSchema) +} diff --git a/api/go-twitter-clone.http b/api/go-twitter-clone.http new file mode 100644 index 0000000..97f7b9f --- /dev/null +++ b/api/go-twitter-clone.http @@ -0,0 +1,32 @@ +# curl --location 'http://localhost:8001/posts' +#--header 'Content-Type: application/json' +#--data '{ +# "content": "Teste Post", +# "user_id": "4cfe67a9-defc-42b9-8410-cb5086bec2f5" +#}' +POST http://localhost:8001/posts +Content-Type: application/json + +{ + "content": "Teste Post", + "user_id": "4cfe67a9-defc-42b9-8410-cb5086bec2f5" +} + +### + +GET http://localhost:8001/users/alucard/info +Accept: application/json + +### + +GET http://localhost:8001/users/alucard/feed +Accept: application/json + +### + +GET http://localhost:8001/ping +Accept: application/json + +### + + diff --git a/cmd/api/api.go b/cmd/api/api.go deleted file mode 100644 index 61a3e6b..0000000 --- a/cmd/api/api.go +++ /dev/null @@ -1,131 +0,0 @@ -package main - -import ( - "fmt" - "github.com/dexfs/go-twitter-clone/internal/application/handlers" - app "github.com/dexfs/go-twitter-clone/internal/application/usecases" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" - "github.com/dexfs/go-twitter-clone/internal/infra/repository/inmemory" - "github.com/dexfs/go-twitter-clone/tests/mocks" - "log" - "net/http" - "time" -) - -// Middlewares - -func RequestLoggerMiddleware(next http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - log.Printf("method %s, path: %s", r.Method, r.URL.Path) - next.ServeHTTP(w, r) - } -} -func RequestJsonContentTypeMiddleware(next http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - next.ServeHTTP(w, r) - } -} - -/** @see https://www.youtube.com/watch?v=npzXQSL4oWo **/ -type APIServer struct { - addr string -} - -type Gateway struct { - userRepo interfaces.UserRepository - postRepo interfaces.PostRepository -} - -func NewAPIServer(addr string) *APIServer { - return &APIServer{ - addr: ":" + addr, - } -} - -func (s *APIServer) Run() error { - router := http.NewServeMux() - gateways := s.initGateways() - s.initUserRoutes(router, gateways) - s.initPostRoutes(router, gateways) - - // router prefix - //router.Handle("/api/v1/", http.StripPrefix("/api/v1", router)) - - server := http.Server{ - Addr: s.addr, - Handler: RequestLoggerMiddleware(RequestJsonContentTypeMiddleware(router)), - } - - log.Printf("API server listening on %s\n", s.addr) - - return server.ListenAndServe() -} - -func (s *APIServer) initGateways() *Gateway { - dbMocks := mocks.GetTestMocks() - dbMocks.MockUserDB.Insert(&domain.User{ - ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", - Username: "alucard", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }) - dbMocks.MockUserDB.Insert(&domain.User{ - ID: "b8903f77-5d16-4176-890f-f597594ff952", - Username: "alexander", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }) - dbMocks.MockUserDB.Insert(&domain.User{ - ID: "75135a97-46be-405f-8948-0821290ca83e", - Username: "seras_victoria", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }) - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - - return &Gateway{ - userRepo: userRepo, - postRepo: postRepo, - } -} - -func (s *APIServer) initUserRoutes(router *http.ServeMux, gateways *Gateway) { - - getUserFeed, err := app.NewGetUserFeedUseCase(gateways.userRepo, gateways.postRepo) - if err != nil { - log.Fatal(err) - } - - getUserInfo, err := app.NewGetUserInfoUseCase(gateways.userRepo) - if err != nil { - log.Fatal(err) - } - - router.HandleFunc("GET /users/{username}/info", handlers.NewGetUserInfoHandler(getUserInfo).Handle) - router.HandleFunc("GET /users/{username}/feed", handlers.NewGetFeedHandler(getUserFeed).Handle) -} -func (s *APIServer) initPostRoutes(router *http.ServeMux, gateways *Gateway) { - - createPostUseCase := app.NewCreatePostUseCase(gateways.userRepo, gateways.postRepo) - createPostHandler := handlers.NewCreatePostHandler(createPostUseCase) - router.HandleFunc(createPostHandler.Path, createPostHandler.Handle) - - createQuotePostUseCase := app.NewCreateQuotePostUseCase(gateways.userRepo, gateways.postRepo) - crateQuotePostHandler := handlers.NewCreateQuoteHandler(createQuotePostUseCase) - router.HandleFunc(crateQuotePostHandler.Path, crateQuotePostHandler.Handle) - - createRepostUseCase := app.NewCreateRepostUseCase(gateways.userRepo, gateways.postRepo) - createRepostHandler := handlers.NewRepostHandler(createRepostUseCase) - router.HandleFunc(createRepostHandler.Path, createRepostHandler.Handle) -} - -func main() { - server := NewAPIServer("8001") - - if err := server.Run(); err != nil { - fmt.Println(err.Error()) - } -} diff --git a/cmd/api/api_test.go b/cmd/api/api_test.go deleted file mode 100644 index eb46628..0000000 --- a/cmd/api/api_test.go +++ /dev/null @@ -1,348 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "github.com/dexfs/go-twitter-clone/internal/application/handlers" - app "github.com/dexfs/go-twitter-clone/internal/application/usecases" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/infra/repository/inmemory" - "github.com/dexfs/go-twitter-clone/tests/mocks" - "github.com/google/uuid" - "io" - "log" - "net/http" - "net/http/httptest" - "strconv" - "testing" - "time" -) - -// users -func TestUserInfoResource_WithNoFoundUser_ReturnsErrorMessage(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - - getUserInfoUseCase, err := app.NewGetUserInfoUseCase(userRepo) - if err != nil { - log.Fatal(err) - } - server.HandleFunc("/users/{username}/info", handlers.NewGetUserInfoHandler(getUserInfoUseCase).Handle) - - request, _ := http.NewRequest("GET", "/users/not_found/info", nil) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got struct { - Erro string `json:"error"` - } - if err := helperDecodeJSON(response.Body, &got); err != nil { - log.Fatal(err) - } - - want := "user not found" - - if got.Erro != want { - t.Errorf("got %q, want %q", got.Erro, want) - } -} -func TestUserInfoResource(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - - getUserInfoUseCase, err := app.NewGetUserInfoUseCase(userRepo) - if err != nil { - log.Fatal(err) - } - server.HandleFunc("/users/{username}/info", handlers.NewGetUserInfoHandler(getUserInfoUseCase).Handle) - - request, _ := http.NewRequest("GET", "/users/user0/info", nil) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got app.GetUserInfoOutput - - if err := helperDecodeJSON(response.Body, &got); err != nil { - fmt.Errorf("could not decode JSON: %v", err) - } - - want := "user0" - if got.Username != want { - t.Errorf("got %q, want %q", got.Username, want) - } -} -func TestUserFeedResource(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - - getUserFeedUseCase, err := app.NewGetUserFeedUseCase(userRepo, postRepo) - if err != nil { - log.Fatal(err) - } - server.HandleFunc("/users/{username}/feed", handlers.NewGetFeedHandler(getUserFeedUseCase).Handle) - - request, _ := http.NewRequest("GET", "/users/user0/feed", nil) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got app.GetUserFeedUseCaseOutput - - if err := helperDecodeJSON(response.Body, &got); err != nil { - fmt.Errorf("could not decode JSON: %v", err) - } - - if len(got.Items) != 2 { - t.Errorf("got %q, want %q", len(got.Items), 2) - } -} -func TestUserFeedResource_WithNoFoundUser_ReturnsErrorMessage(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - - getUserFeedUseCase, err := app.NewGetUserFeedUseCase(userRepo, postRepo) - if err != nil { - log.Fatal(err) - } - server.HandleFunc("/users/{username}/feed", handlers.NewGetFeedHandler(getUserFeedUseCase).Handle) - - request, _ := http.NewRequest("GET", "/users/not_found/feed", nil) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got struct { - Error string `json:"error"` - } - - if err := helperDecodeJSON(response.Body, &got); err != nil { - log.Fatal(err) - } - - want := "user not found" - if got.Error != want { - t.Errorf("got %q, want %q", got.Error, want) - } -} - -// posts -func TestCreatePostResource(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - createPostUseCase := app.NewCreatePostUseCase(userRepo, postRepo) - createPostHandler := handlers.NewCreatePostHandler(createPostUseCase) - server.HandleFunc(createPostHandler.Path, createPostHandler.Handle) - - userID := strconv.Quote(dbMocks.MockUserSeed[0].ID) - jsonStr := `{"user_id": ` + userID + `, "content": "test content"}` - - request, _ := http.NewRequest("POST", "/posts", bytes.NewBufferString(jsonStr)) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got app.CreatePostOutput - - if err := helperDecodeJSON(response.Body, &got); err != nil { - fmt.Errorf("could not decode JSON: %v", err) - } - - if err := uuid.Validate(got.PostID); err != nil { - t.Errorf("got %q, want valid UUID", got.PostID) - } -} -func TestCreatePostResource_WithoutLimit_ReturnsError(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - - for i := 0; i < 6; i++ { - aInput := domain.NewPostInput{ - User: dbMocks.MockUserSeed[0], - Content: "Content post" + strconv.Itoa(i), - } - aPost, _ := domain.NewPost(aInput) - dbMocks.MockPostDB.Insert(aPost) - } - - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - createPostUseCase := app.NewCreatePostUseCase(userRepo, postRepo) - createPostHandler := handlers.NewCreatePostHandler(createPostUseCase) - server.HandleFunc(createPostHandler.Path, createPostHandler.Handle) - - userID := strconv.Quote(dbMocks.MockUserSeed[0].ID) - jsonStr := `{"user_id": ` + userID + `, "content": "test content"}` - - request, _ := http.NewRequest("POST", "/posts", bytes.NewBufferString(jsonStr)) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got struct { - Error string `json:"error"` - } - - if err := helperDecodeJSON(response.Body, &got); err != nil { - log.Fatal(err) - } - want := "you reached your posts day limit" - - if got.Error != want { - t.Errorf("got %s want %s", got.Error, want) - } -} - -func TestCreatePostResource_WithNotFoundUser_ReturnsError(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - createPostUseCase := app.NewCreatePostUseCase(userRepo, postRepo) - createPostHandler := handlers.NewCreatePostHandler(createPostUseCase) - server.HandleFunc(createPostHandler.Path, createPostHandler.Handle) - - userID := strconv.Quote(uuid.NewString()) - jsonStr := `{"user_id": ` + userID + `, "content": "test content"}` - - request, _ := http.NewRequest("POST", "/posts", bytes.NewBufferString(jsonStr)) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got struct { - Error string `json:"error"` - } - - if err := helperDecodeJSON(response.Body, &got); err != nil { - log.Fatal(err) - } - - want := "user not found" - if got.Error != want { - t.Errorf("got %s want %s", got.Error, want) - } -} - -func TestCreateQuotePostResource(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - newUser := &domain.User{ - ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", - Username: "alucard", - CreatedAt: time.Time{}, - UpdatedAt: time.Time{}, - } - - dbMocks.MockUserDB.Insert(newUser) - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - createQuotePostUseCase := app.NewCreateQuotePostUseCase(userRepo, postRepo) - - createQuotePostHandler := handlers.NewCreateQuoteHandler(createQuotePostUseCase) - server.HandleFunc(createQuotePostHandler.Path, createQuotePostHandler.Handle) - - userID := strconv.Quote(newUser.ID) - postID := strconv.Quote(dbMocks.MockPostsSeed[0].ID) - jsonStr := `{"user_id": ` + userID + `, "post_id":` + postID + `, "quote": "quote post content"}` - request, _ := http.NewRequest("POST", "/posts/quote", bytes.NewBufferString(jsonStr)) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got app.CreateQuotePostUseCaseOutput - - if err := helperDecodeJSON(response.Body, &got); err != nil { - t.Fatalf("could not decode JSON: %v", err) - } - - if err := uuid.Validate(got.PostID); err != nil { - t.Errorf("got %q, want valid UUID", got.PostID) - } -} - -func TestCreateQuotePostResource_WithTheOriginalUser_ReturnsError(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - createQuotePostUseCase := app.NewCreateQuotePostUseCase(userRepo, postRepo) - - createQuotePostHandler := handlers.NewCreateQuoteHandler(createQuotePostUseCase) - server.HandleFunc(createQuotePostHandler.Path, createQuotePostHandler.Handle) - - userID := strconv.Quote(dbMocks.MockUserSeed[0].ID) - postID := strconv.Quote(dbMocks.MockPostsSeed[0].ID) - jsonStr := `{"user_id": ` + userID + `, "post_id":` + postID + `, "quote": "quote post content"}` - request, _ := http.NewRequest("POST", "/posts/quote", bytes.NewBufferString(jsonStr)) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got struct { - Error string - } - - if err := helperDecodeJSON(response.Body, &got); err != nil { - t.Fatal(err) - } - - want := "it is not possible quote your own post" - if got.Error != want { - t.Errorf("got %s, want %s", got.Error, want) - } -} -func TestCreateRepostResource(t *testing.T) { - server := http.NewServeMux() - - dbMocks := mocks.GetTestMocks() - newUser := &domain.User{ - ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", - Username: "alucard", - CreatedAt: time.Time{}, - UpdatedAt: time.Time{}, - } - - dbMocks.MockUserDB.Insert(newUser) - userRepo := inmemory.NewInMemoryUserRepo(dbMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(dbMocks.MockPostDB) - createRepostUseCase := app.NewCreateRepostUseCase(userRepo, postRepo) - - createRepostHandler := handlers.NewRepostHandler(createRepostUseCase) - server.HandleFunc(createRepostHandler.Path, createRepostHandler.Handle) - - userID := strconv.Quote(newUser.ID) - postID := strconv.Quote(dbMocks.MockPostsSeed[0].ID) - jsonStr := `{"user_id": ` + userID + `, "post_id":` + postID + `}` - request, _ := http.NewRequest("POST", "/posts/repost", bytes.NewBufferString(jsonStr)) - response := httptest.NewRecorder() - server.ServeHTTP(response, request) - - var got app.CreateRepostUseCaseOutput - - if err := helperDecodeJSON(response.Body, &got); err != nil { - t.Fatalf("could not decode JSON: %v", err) - } - - if err := uuid.Validate(got.PostID); err != nil { - t.Errorf("got %q, want valid UUID", got.PostID) - } -} - -func helperDecodeJSON(body io.Reader, v interface{}) error { - if err := json.NewDecoder(body).Decode(v); err != nil { - return fmt.Errorf("could not decode JSON: %v", err) - } - return nil -} diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..3c483a7 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/routes" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" + "github.com/dexfs/go-twitter-clone/pkg/database" + "log" + "time" +) + +var ( + db *database.InMemoryDB + userRepo output.UserPort + postRepo output.PostPort +) + +func init() { + db = database.NewInMemoryDB() + if db == nil { + log.Fatal("database is nil") + } + + initialUsers := make([]*inmemory_schema.UserSchema, 0) + initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ + ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", + Username: "alucard", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ + ID: "b8903f77-5d16-4176-890f-f597594ff952", + Username: "alexander", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + db.RegisterSchema(inmemory.USER_SCHEMA_NAME, initialUsers) + db.RegisterSchema(inmemory.POST_SCHEMA_NAME, []*inmemory_schema.PostSchema{}) + userRepo = inmemory.NewInMemoryUserRepository(db) + postRepo = inmemory.NewInMemoryPostRepository(db) +} + +type APIServer struct { + addr string +} + +func NewAPIServer(addr string) *APIServer { + return &APIServer{ + addr: ":" + addr, + } +} + +func (s *APIServer) Run() error { + router := routes.NewRouter(s.addr) + return router.Run(userRepo, postRepo) +} + +func main() { + log.Printf("Starting Application") + server := NewAPIServer("8001") + + if err := server.Run(); err != nil { + log.Fatal("Error starting server:", err) + } +} diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go new file mode 100644 index 0000000..9e0bd26 --- /dev/null +++ b/cmd/api/main_test.go @@ -0,0 +1,177 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/dexfs/go-twitter-clone/adapter/input/adapter_http" + "github.com/dexfs/go-twitter-clone/adapter/input/model/response" + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/adapter/input/routes" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" + "github.com/dexfs/go-twitter-clone/internal/core/usecase" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/dexfs/go-twitter-clone/tests/mocks" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "log" + "net/http" + "net/http/httptest" + "testing" +) + +func setUpTests() { + gin.SetMode(gin.TestMode) +} + +func InitDependencies(db *database.InMemoryDB) (userRepo output.UserPort, port output.PostPort) { + userRepo = inmemory.NewInMemoryUserRepository(db) + postRepo = inmemory.NewInMemoryPostRepository(db) + return userRepo, postRepo +} + +func helperDecodeJSON(body *bytes.Buffer, v interface{}) error { + if err := json.Unmarshal([]byte(body.String()), v); err != nil { + return fmt.Errorf("could not decode JSON: %v", err) + } + return nil +} + +func TestInvalidUserName(t *testing.T) { + var tests = []struct { + name string + input string + want string + causesLen int + }{ + {" username should be alphanumeric", "not@found", "Bad Request", 1}, + {" username should be alphanumeric", "not_found", "Bad Request", 1}, + {" username should have at least 5 characters", "nf", "Bad Request", 1}, + {" username should have max 10 characters", "notfound123456789", "Bad Request", 1}, + } + setUpTests() + dbMocks := mocks.GetTestMocks() + userRepo, _ := InitDependencies(dbMocks.MockDB) + getUserInfoService, _ := usecase.NewGetUserInfoUseCase(userRepo) + usersController := adapter_http.NewUsersController(getUserInfoService, nil) + + router := routes.NewRouter(":8002") + router.Router.GET("/users/:username/info", usersController.GetInfo) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wRecorder := httptest.NewRecorder() + url := fmt.Sprintf("/users/%s/info", tt.input) + req, _ := http.NewRequest("GET", url, nil) + router.Router.ServeHTTP(wRecorder, req) + + got := new(rest_errors.RestError) + + assert.Equal(t, http.StatusBadRequest, wRecorder.Code) + if err := helperDecodeJSON(wRecorder.Body, got); err != nil { + log.Fatal(err) + } + + assert.EqualValues(t, tt.want, got.Message) + assert.Len(t, got.Causes, tt.causesLen) + + }) + } +} + +func TestUserInfoResource_WithNoFoundUser_ReturnsErrorMessage(t *testing.T) { + setUpTests() + dbMocks := mocks.GetTestMocks() + userRepo, _ := InitDependencies(dbMocks.MockDB) + wRecorder := httptest.NewRecorder() + + router := routes.NewRouter(":8002") + + getUserInfoService, _ := usecase.NewGetUserInfoUseCase(userRepo) + usersController := adapter_http.NewUsersController(getUserInfoService, nil) + router.Router.GET("/users/:username/info", usersController.GetInfo) + + req, _ := http.NewRequest("GET", "/users/notfound/info", nil) + router.Router.ServeHTTP(wRecorder, req) + + var got rest_errors.RestError + + assert.Equal(t, http.StatusNotFound, wRecorder.Code) + if err := helperDecodeJSON(wRecorder.Body, &got); err != nil { + log.Fatal(err) + } + + assert.EqualValues(t, got.Message, "user not found") +} + +func TestUserInfoResource(t *testing.T) { + setUpTests() + dbMocks := mocks.GetTestMocks() + userRepo, _ := InitDependencies(dbMocks.MockDB) + wRecorder := httptest.NewRecorder() + + router := routes.NewRouter(":8002") + getUserInfoService, _ := usecase.NewGetUserInfoUseCase(userRepo) + usersController := adapter_http.NewUsersController(getUserInfoService, nil) + router.Router.GET("/users/:username/info", usersController.GetInfo) + + req, _ := http.NewRequest("GET", "/users/user0/info", nil) + router.Router.ServeHTTP(wRecorder, req) + + var got response.GetUserInfoResponse + assert.Equal(t, http.StatusOK, wRecorder.Code) + + if err := helperDecodeJSON(wRecorder.Body, &got); err != nil { + log.Fatal(err) + } + + assert.EqualValues(t, "user0", got.Username) +} + +func TestUserFeedResource(t *testing.T) { + setUpTests() + dbMocks := mocks.GetTestMocks() + userRepo, postRepo := InitDependencies(dbMocks.MockDB) + wRecorder := httptest.NewRecorder() + + router := routes.NewRouter(":8002") + getUserFeeUseCase, _ := usecase.NewGetUserFeedUseCase(userRepo, postRepo) + usersController := adapter_http.NewUsersController(nil, getUserFeeUseCase) + router.Router.GET("/users/:username/feed", usersController.GetFeed) + + req, _ := http.NewRequest("GET", "/users/user0/feed", nil) + router.Router.ServeHTTP(wRecorder, req) + + var got response.GetUserFeedResponse + if err := json.Unmarshal([]byte(wRecorder.Body.String()), &got); err != nil { + log.Fatal(err) + } + + assert.Len(t, got.Items, 2) +} + +func TestUserFeedResource_WithNoFoundUser_ReturnsErrorMessage(t *testing.T) { + setUpTests() + dbMocks := mocks.GetTestMocks() + userRepo, postRepo := InitDependencies(dbMocks.MockDB) + wRecorder := httptest.NewRecorder() + + router := routes.NewRouter(":8002") + getUserFeeUseCase, _ := usecase.NewGetUserFeedUseCase(userRepo, postRepo) + usersController := adapter_http.NewUsersController(nil, getUserFeeUseCase) + router.Router.GET("/users/:username/feed", usersController.GetFeed) + + req, _ := http.NewRequest("GET", "/users/notfound/feed", nil) + router.Router.ServeHTTP(wRecorder, req) + + var got rest_errors.RestError + + assert.Equal(t, http.StatusNotFound, wRecorder.Code) + + if err := helperDecodeJSON(wRecorder.Body, &got); err != nil { + log.Fatal(err) + } + + assert.EqualValues(t, "user not found", got.Message) +} diff --git a/config/validation/rest_validation.go b/config/validation/rest_validation.go new file mode 100644 index 0000000..5384ef8 --- /dev/null +++ b/config/validation/rest_validation.go @@ -0,0 +1,49 @@ +package validation + +import ( + "encoding/json" + "errors" + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/locales/en" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + en_translation "github.com/go-playground/validator/v10/translations/en" + "net/http" +) + +var ( + Validate = validator.New() + transl ut.Translator +) + +func init() { + if val, ok := binding.Validator.Engine().(*validator.Validate); ok { + en := en.New() + un := ut.New(en, en) + transl, _ = un.GetTranslator("en") + en_translation.RegisterDefaultTranslations(val, transl) + } +} + +func RestError(validation_err error) *rest_errors.RestError { + var jsonErr *json.UnmarshalTypeError + var jsonValidationError validator.ValidationErrors + + if errors.As(validation_err, &jsonErr) { + return rest_errors.NewBadRequestError("Invalid field type") + } + if errors.As(validation_err, &jsonValidationError) { + errorsCause := []rest_errors.Cause{} + for _, e := range validation_err.(validator.ValidationErrors) { + cause := rest_errors.Cause{ + Field: e.Field(), + Message: e.Translate(transl), + } + errorsCause = append(errorsCause, cause) + } + return rest_errors.NewBadRequestValidationError(http.StatusText(http.StatusBadRequest), errorsCause) + } + + return rest_errors.NewBadRequestError("Error trying to convert fields") +} diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..6ec20b6 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,7 @@ +version: "3" +services: + api: + build: . + container_name: twitter_clone_api + ports: + - 3001:8001 diff --git a/go.mod b/go.mod index 8c2df06..0f2efd9 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,41 @@ module github.com/dexfs/go-twitter-clone go 1.22.0 -require github.com/google/uuid v1.6.0 // indirect +require ( + github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-gonic/gin v1.9.1 + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.20.0 + github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/cors v1.7.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.7.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.34.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index 7790d7c..351c887 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,105 @@ +github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k= +github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg= +github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= +github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= +golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/application/handlers/helpers.go b/internal/application/handlers/helpers.go deleted file mode 100644 index 6addcae..0000000 --- a/internal/application/handlers/helpers.go +++ /dev/null @@ -1,33 +0,0 @@ -package handlers - -import ( - "encoding/json" - "fmt" - "log" - "net/http" -) - -func JSON(w http.ResponseWriter, statusCode int, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - if data != nil { - if err := json.NewEncoder(w).Encode(data); err != nil { - log.Fatal(err) - } - } -} - -func DecodeJSON(r *http.Request, v interface{}) error { - if err := json.NewDecoder(r.Body).Decode(v); err != nil { - return fmt.Errorf("could not decode JSON: %v", err) - } - return nil -} - -func JSONError(w http.ResponseWriter, statusCode int, err error) { - JSON(w, statusCode, struct { - Erro string `json:"error"` - }{ - Erro: err.Error(), - }) -} diff --git a/internal/application/handlers/post_handlers.go b/internal/application/handlers/post_handlers.go deleted file mode 100644 index eba3330..0000000 --- a/internal/application/handlers/post_handlers.go +++ /dev/null @@ -1,97 +0,0 @@ -package handlers - -import ( - app "github.com/dexfs/go-twitter-clone/internal/application/usecases" - "net/http" -) - -type CreatePostHandler struct { - Path string - useCase *app.CreatePostUseCase -} - -func NewCreatePostHandler(useCase *app.CreatePostUseCase) *CreatePostHandler { - return &CreatePostHandler{ - Path: "POST /posts", - useCase: useCase, - } -} - -func (h *CreatePostHandler) Handle(w http.ResponseWriter, r *http.Request) { - var input app.CreatePostInput - err := DecodeJSON(r, &input) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - - post, err := h.useCase.Execute(input) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - - JSON(w, http.StatusCreated, post) -} - -// Create Quote -type CreateQuoteHandler struct { - Path string - useCase *app.CreateQuotePostUseCase -} - -func NewCreateQuoteHandler(useCase *app.CreateQuotePostUseCase) *CreateQuoteHandler { - return &CreateQuoteHandler{ - Path: "POST /posts/quote", - useCase: useCase, - } -} - -func (h *CreateQuoteHandler) Handle(w http.ResponseWriter, r *http.Request) { - var input app.CreateQuotePostUseCaseInput - - err := DecodeJSON(r, &input) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - - quote, err := h.useCase.Execute(input) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - - JSON(w, http.StatusCreated, quote) -} - -// - -type CreateRepostHandler struct { - Path string - useCase *app.CreateRepostUseCase -} - -func NewRepostHandler(useCase *app.CreateRepostUseCase) *CreateRepostHandler { - return &CreateRepostHandler{ - Path: "POST /posts/repost", - useCase: useCase, - } -} - -func (h *CreateRepostHandler) Handle(w http.ResponseWriter, r *http.Request) { - var input app.CreateRepostUseCaseInput - err := DecodeJSON(r, &input) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - - repost, err := h.useCase.Execute(input) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - - JSON(w, http.StatusCreated, repost) -} diff --git a/internal/application/handlers/user_handlers.go b/internal/application/handlers/user_handlers.go deleted file mode 100644 index 885c263..0000000 --- a/internal/application/handlers/user_handlers.go +++ /dev/null @@ -1,44 +0,0 @@ -package handlers - -import ( - "github.com/dexfs/go-twitter-clone/internal/application/usecases" - "net/http" -) - -type GetUserFeedHandler struct { - useCase *app.GetUserFeedUseCase -} - -type GetUserInfoHandler struct { - useCase *app.GetUserInfoUseCase -} - -func NewGetFeedHandler(useCase *app.GetUserFeedUseCase) GetUserFeedHandler { - return GetUserFeedHandler{ - useCase: useCase, - } -} - -func NewGetUserInfoHandler(useCase *app.GetUserInfoUseCase) GetUserInfoHandler { - return GetUserInfoHandler{ - useCase: useCase, - } -} - -func (h GetUserFeedHandler) Handle(w http.ResponseWriter, r *http.Request) { - feed, err := h.useCase.Execute(r.PathValue("username")) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - JSON(w, http.StatusOK, feed) -} - -func (h GetUserInfoHandler) Handle(w http.ResponseWriter, r *http.Request) { - user, err := h.useCase.Execute(r.PathValue("username")) - if err != nil { - JSONError(w, http.StatusBadRequest, err) - return - } - JSON(w, http.StatusCreated, user) -} diff --git a/internal/application/usecases/createpost.usecase.go b/internal/application/usecases/createpost.usecase.go deleted file mode 100644 index 777cab0..0000000 --- a/internal/application/usecases/createpost.usecase.go +++ /dev/null @@ -1,59 +0,0 @@ -package app - -import ( - "errors" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" -) - -type CreatePostUseCase struct { - userRepo interfaces.UserRepository - postRepo interfaces.PostRepository -} - -func NewCreatePostUseCase(userRepo interfaces.UserRepository, postRepo interfaces.PostRepository) *CreatePostUseCase { - return &CreatePostUseCase{ - userRepo: userRepo, - postRepo: postRepo, - } -} - -type CreatePostInput struct { - UserID string `json:"user_id"` - Content string `json:"content"` -} - -type CreatePostOutput struct { - PostID string `json:"post_id"` -} - -func (uc *CreatePostUseCase) Execute(input CreatePostInput) (CreatePostOutput, error) { - // verifica se já atingiu o limite de postagens do dia retornar um erro - hasReachedLimit := uc.postRepo.HasReachedPostingLimitDay(input.UserID, 5) - if hasReachedLimit { - return CreatePostOutput{}, errors.New("you reached your posts day limit") - } - - // verifica se o usuário existe - user, err := uc.userRepo.FindByID(input.UserID) - - if err != nil { - return CreatePostOutput{}, err - } - newPostInput := domain.NewPostInput{ - User: user, - Content: input.Content, - } - - newPost, err := domain.NewPost(newPostInput) - - if err != nil { - return CreatePostOutput{}, err - } - - uc.postRepo.Insert(newPost) - - return CreatePostOutput{ - PostID: newPost.ID, - }, nil -} diff --git a/internal/application/usecases/createquotepost.usecase.go b/internal/application/usecases/createquotepost.usecase.go deleted file mode 100644 index 73f9a0f..0000000 --- a/internal/application/usecases/createquotepost.usecase.go +++ /dev/null @@ -1,54 +0,0 @@ -package app - -import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" -) - -type CreateQuotePostUseCaseInput struct { - Quote string `json:"quote"` - PostID string `json:"post_id"` - UserID string `json:"user_id"` -} - -type CreateQuotePostUseCaseOutput struct { - PostID string -} - -type CreateQuotePostUseCase struct { - userRepo interfaces.UserRepository - postRepo interfaces.PostRepository -} - -func NewCreateQuotePostUseCase(userRepo interfaces.UserRepository, postRepo interfaces.PostRepository) *CreateQuotePostUseCase { - return &CreateQuotePostUseCase{userRepo, postRepo} -} - -func (uc CreateQuotePostUseCase) Execute(input CreateQuotePostUseCaseInput) (CreateQuotePostUseCaseOutput, error) { - - user, err := uc.userRepo.FindByID(input.UserID) - - if err != nil { - return CreateQuotePostUseCaseOutput{}, err - } - - post, err := uc.postRepo.FindByID(input.PostID) - if err != nil { - return CreateQuotePostUseCaseOutput{}, err - } - - newQuotePostInput := domain.NewRepostQuoteInput{ - User: user, - Post: post, - Content: input.Quote, - } - newQuotePost, err := domain.NewQuote(newQuotePostInput) - - if err != nil { - return CreateQuotePostUseCaseOutput{}, err - } - - uc.postRepo.Insert(newQuotePost) - - return CreateQuotePostUseCaseOutput{PostID: newQuotePost.ID}, nil -} diff --git a/internal/application/usecases/createrepost.usecase.go b/internal/application/usecases/createrepost.usecase.go deleted file mode 100644 index 9b999f9..0000000 --- a/internal/application/usecases/createrepost.usecase.go +++ /dev/null @@ -1,61 +0,0 @@ -package app - -import ( - "errors" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" -) - -type CreateRepostUseCaseInput struct { - PostID string `json:"post_id"` - UserID string `json:"user_id"` -} - -type CreateRepostUseCaseOutput struct { - PostID string -} - -type CreateRepostUseCase struct { - userRepo interfaces.UserRepository - postRepo interfaces.PostRepository -} - -func NewCreateRepostUseCase(userRepo interfaces.UserRepository, postRepo interfaces.PostRepository) *CreateRepostUseCase { - return &CreateRepostUseCase{ - userRepo: userRepo, - postRepo: postRepo, - } -} - -func (uc *CreateRepostUseCase) Execute(input CreateRepostUseCaseInput) (CreateRepostUseCaseOutput, error) { - user, err := uc.userRepo.FindByID(input.UserID) - - if err != nil { - return CreateRepostUseCaseOutput{}, err - } - - reposted := uc.postRepo.HasPostBeenRepostedByUser(input.PostID, input.UserID) - if reposted { - return CreateRepostUseCaseOutput{}, errors.New("it is not possible repost a repost post") - } - - post, err := uc.postRepo.FindByID(input.PostID) - if err != nil { - return CreateRepostUseCaseOutput{}, err - } - - aRepostInput := domain.NewRepostQuoteInput{ - User: user, - Post: post, - } - - aRepost, err := domain.NewRepost(aRepostInput) - - if err != nil { - return CreateRepostUseCaseOutput{}, err - } - - uc.postRepo.Insert(aRepost) - - return CreateRepostUseCaseOutput{PostID: aRepost.ID}, nil -} diff --git a/internal/application/usecases/getuserfeed.usecase.go b/internal/application/usecases/getuserfeed.usecase.go deleted file mode 100644 index d76d892..0000000 --- a/internal/application/usecases/getuserfeed.usecase.go +++ /dev/null @@ -1,48 +0,0 @@ -package app - -import ( - "errors" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" -) - -type GetUserFeedUseCase struct { - userRepo interfaces.UserRepository - postRepo interfaces.PostRepository -} - -type GetUserFeedUseCaseOutput struct { - Items []*domain.Post `json:"items"` -} - -func NewGetUserFeedUseCase(userRepo interfaces.UserRepository, postRepo interfaces.PostRepository) (*GetUserFeedUseCase, error) { - if userRepo == nil || postRepo == nil { - return nil, errors.New("the dependencies should not be nil") - } - - return &GetUserFeedUseCase{ - userRepo: userRepo, - postRepo: postRepo, - }, nil -} - -func (uc *GetUserFeedUseCase) Execute(username string) (GetUserFeedUseCaseOutput, error) { - if len(username) == 0 { - return GetUserFeedUseCaseOutput{}, errors.New("username must not be empty") - } - user, err := uc.userRepo.ByUsername(username) - - if err != nil { - return GetUserFeedUseCaseOutput{}, err - } - - posts := uc.postRepo.GetFeedByUserID(user.ID) - - if posts == nil { - return GetUserFeedUseCaseOutput{}, nil - } - - return GetUserFeedUseCaseOutput{ - Items: posts, - }, nil -} diff --git a/internal/application/usecases/getuserfeed.usecase_test.go b/internal/application/usecases/getuserfeed.usecase_test.go deleted file mode 100644 index f0a15b7..0000000 --- a/internal/application/usecases/getuserfeed.usecase_test.go +++ /dev/null @@ -1,136 +0,0 @@ -package app - -import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/pkg/database" - "github.com/dexfs/go-twitter-clone/tests/mocks" - "reflect" - "testing" -) - -func TestExecute_WithValidUsername_ReturnsFeedItems(t *testing.T) { - TestMocks := mocks.GetTestMocks() - mockUser := mocks.UserSeed(TestMocks.MockUserDB, 1) - mocks.PostSeed(TestMocks.MockPostDB, mockUser[0], 2) - mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockUserDB) - - postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockPostDB) - - userFeedUseCase, _ := NewGetUserFeedUseCase(mockUserRepo, postRepo) - - userFeed, err := userFeedUseCase.Execute(mockUser[0].Username) - - if err != nil { - t.Errorf("want err=nil; got %v", err) - } - - if len(userFeed.Items) != 2 { - t.Errorf("want 2 posts; got %v", len(userFeed.Items)) - } -} -func TestExecute_WithEmptyUsername_ReturnsError(t *testing.T) { - TestMocks := mocks.GetTestMocks() - mockUser := mocks.UserSeed(TestMocks.MockUserDB, 1) - mocks.PostSeed(TestMocks.MockPostDB, mockUser[0], 2) - mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockPostDB) - getUserFeedUseCase, _ := NewGetUserFeedUseCase(mockUserRepo, postRepo) - userFeedOutput, err := getUserFeedUseCase.Execute("") - - var expectedOutputItems []*domain.Post - expectedOutputFeed := GetUserFeedUseCaseOutput{ - Items: expectedOutputItems, - } - - if !reflect.DeepEqual(userFeedOutput, expectedOutputFeed) { - t.Errorf("want nil; got %v", userFeedOutput.Items) - } - - if err == nil { - t.Errorf("should return an error") - } - - if err.Error() != "username must not be empty" { - t.Errorf("got %v want %s", err.Error(), "username must not be empty") - } - -} -func TestExecute_WithNonExistingUsername_ReturnsError(t *testing.T) { - TestMocks := mocks.GetTestMocks() - mockUser := mocks.UserSeed(TestMocks.MockUserDB, 1) - mocks.PostSeed(TestMocks.MockPostDB, mockUser[0], 2) - mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockPostDB) - getUserFeedUseCase, _ := NewGetUserFeedUseCase(mockUserRepo, postRepo) - userFeedOutput, err := getUserFeedUseCase.Execute("non-existing-user") - - var expectedOutputItems []*domain.Post - expectedOutputFeed := GetUserFeedUseCaseOutput{ - Items: expectedOutputItems, - } - - if !reflect.DeepEqual(userFeedOutput, expectedOutputFeed) { - t.Errorf("want nil; got %v", userFeedOutput.Items) - } - - if err == nil { - t.Errorf("should return an error") - } - - if err.Error() != "user not found" { - t.Errorf("got %v want %s", err.Error(), "user not found") - } -} -func TestExecute_WithNilUserRepository_ReturnsError(t *testing.T) { - TestMocks := mocks.GetTestMocks() - postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockPostDB) - getUserFeedUseCase, err := NewGetUserFeedUseCase(nil, postRepo) - - if getUserFeedUseCase != nil { - t.Errorf("Invalid instance of usecase") - } - - if err == nil { - t.Errorf("should return an error") - } - - if err.Error() != "the dependencies should not be nil" { - t.Errorf("got %v want %s", err.Error(), "the dependencies should not be nil") - } -} -func TestExecute_WithNilPostRepository_ReturnsError(t *testing.T) { - TestMocks := mocks.GetTestMocks() - mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockUserDB) - getUserFeedUseCase, err := NewGetUserFeedUseCase(mockUserRepo, nil) - - if getUserFeedUseCase != nil { - t.Errorf("Invalid instance of usecase") - } - - if err == nil { - t.Errorf("should return an error") - } - - if err.Error() != "the dependencies should not be nil" { - t.Errorf("got %v want %s", err.Error(), "the dependencies should not be nil") - } -} -func TestExecute_WithPostRepositoryError_ReturnsError(t *testing.T) { - TestMocks := mocks.GetTestMocks() - mockUser := mocks.UserSeed(TestMocks.MockUserDB, 1) - mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockUserDB) - mockPostDB := &database.InMemoryDB[domain.Post]{} - postRepo := mocks.MakeInMemoryPostRepo(mockPostDB) - - userFeedUseCase, _ := NewGetUserFeedUseCase(mockUserRepo, postRepo) - - userFeed, err := userFeedUseCase.Execute(mockUser[0].Username) - - if err != nil { - t.Errorf("want err=nil; got %v", err) - } - - if len(userFeed.Items) > 0 { - t.Errorf("want 0 posts; got %v", len(userFeed.Items)) - } -} diff --git a/internal/application/usecases/getuserinfo.usecase.go b/internal/application/usecases/getuserinfo.usecase.go deleted file mode 100644 index 48d0b7b..0000000 --- a/internal/application/usecases/getuserinfo.usecase.go +++ /dev/null @@ -1,31 +0,0 @@ -package app - -import ( - "errors" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" -) - -type GetUserInfoUseCase struct { - userRepo interfaces.UserRepository -} - -type GetUserInfoOutput struct { - *domain.User -} - -func NewGetUserInfoUseCase(userRepo interfaces.UserRepository) (*GetUserInfoUseCase, error) { - if userRepo == nil { - return nil, errors.New("userRepo cannot be nil") - } - return &GetUserInfoUseCase{userRepo: userRepo}, nil -} - -func (u *GetUserInfoUseCase) Execute(username string) (GetUserInfoOutput, error) { - user, err := u.userRepo.ByUsername(username) - if err != nil { - return GetUserInfoOutput{}, err - } - - return GetUserInfoOutput{user}, nil -} diff --git a/internal/application/usecases/getuserinfo.usecase_test.go b/internal/application/usecases/getuserinfo.usecase_test.go deleted file mode 100644 index b295586..0000000 --- a/internal/application/usecases/getuserinfo.usecase_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package app - -import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" - "github.com/dexfs/go-twitter-clone/internal/infra/repository/inmemory" - "github.com/dexfs/go-twitter-clone/pkg/database" - "strconv" - "testing" -) - -func TestGetUserInfoUseCase_WithValidUsername_ReturnsUserInfo(t *testing.T) { - inMemoryDb := MakeDb() - usersSeed := UserSeed(inMemoryDb) - userRepo := MakeRepoInstance(inMemoryDb) - - getInfoUseCase, _ := NewGetUserInfoUseCase(userRepo) - output, err := getInfoUseCase.Execute(usersSeed[0].Username) - if err != nil { - t.Errorf("error while executing getInfoUseCase: %v", err) - } - - if output.User != usersSeed[0] { - t.Errorf("getInfoUseCase returned wrong user info, got %v, expected %v", output, usersSeed[0]) - } -} -func TestGetUserInfoUseCase_WithNonExistingUsername_ReturnsError(t *testing.T) { - inMemoryDb := MakeDb() - userRepo := MakeRepoInstance(inMemoryDb) - - getInfoUseCase, _ := NewGetUserInfoUseCase(userRepo) - output, err := getInfoUseCase.Execute("") - if err == nil { - t.Errorf("should return error") - } - - if output.User != nil { - t.Errorf("should return empty user info") - } -} -func TestGetUserInfoUseCase_WithNilUserRepository_ReturnsError(t *testing.T) { - _, err := NewGetUserInfoUseCase(nil) - if err == nil { - t.Errorf("should return error") - } - - if err.Error() != "userRepo cannot be nil" { - t.Errorf("should return 'userRepo cannot be nil' got %v", err.Error()) - } -} - -// mocks -func MakeDb() *database.InMemoryDB[domain.User] { - return &database.InMemoryDB[domain.User]{} -} -func MakeRepoInstance(db *database.InMemoryDB[domain.User]) interfaces.UserRepository { - repo := inmemory.NewInMemoryUserRepo(db) - return repo -} -func UserSeed(db *database.InMemoryDB[domain.User]) []*domain.User { - users := make([]*domain.User, 5) - for i := 0; i < 5; i++ { - username := "user" + strconv.Itoa(i) - newUser := domain.NewUser(username) - db.Insert(newUser) - users[i] = newUser - } - return users -} diff --git a/internal/domain/post.go b/internal/core/domain/post.go similarity index 82% rename from internal/domain/post.go rename to internal/core/domain/post.go index f4c81ea..d041701 100644 --- a/internal/domain/post.go +++ b/internal/core/domain/post.go @@ -7,16 +7,16 @@ import ( ) type Post struct { - ID string `json:"id"` - User *User `json:"user"` - Content string `json:"content"` - CreatedAt time.Time `json:"created_at"` - IsQuote bool `json:"is_quote"` - IsRepost bool `json:"is_repost"` - OriginalPostID string `json:"original_post_id"` - OriginalPostContent string `json:"original_post_content"` - OriginalPostUserID string `json:"original_post_user_id"` - OriginalPostScreenName string `json:"original_post_screen_name"` + ID string + User *User + Content string + CreatedAt time.Time + IsQuote bool + IsRepost bool + OriginalPostID string + OriginalPostContent string + OriginalPostUserID string + OriginalPostScreenName string } type NewPostInput struct { diff --git a/internal/domain/post_test.go b/internal/core/domain/post_test.go similarity index 66% rename from internal/domain/post_test.go rename to internal/core/domain/post_test.go index 327d66d..76b02ba 100644 --- a/internal/domain/post_test.go +++ b/internal/core/domain/post_test.go @@ -1,6 +1,7 @@ -package domain +package domain_test import ( + "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/google/uuid" "testing" "time" @@ -8,16 +9,16 @@ import ( // Post func TestNewPost_WithValidInput_ReturnsOK(t *testing.T) { - user := NewUser("user post 1") - mockInput := NewPostInput{User: user, Content: "mock_content"} - newPost, _ := NewPost(mockInput) + user := domain.NewUser("user post 1") + mockInput := domain.NewPostInput{User: user, Content: "mock_content"} + newPost, _ := domain.NewPost(mockInput) if newPost == nil { t.Errorf("Invalid instance of Post") } - if newPost.User != user { - t.Errorf("got %q want %q", newPost.User, user) + if newPost.User.ID != user.ID { + t.Errorf("got %q want %q", newPost.User.ID, user) } if newPost.IsQuote != false || newPost.IsRepost != false { @@ -31,19 +32,21 @@ func TestNewPost_WithValidInput_ReturnsOK(t *testing.T) { t.Errorf("One or more fields are filled when they shouldn't be") } } + func TestNewPost_WithEmptyInput_ReturnsError(t *testing.T) { - mockInput := NewPostInput{} - _, err := NewPost(mockInput) + mockInput := domain.NewPostInput{} + _, err := domain.NewPost(mockInput) if err == nil { t.Errorf("Invalid instance of Post") } } + func TestNewPost_WithNilUser_ReturnsError(t *testing.T) { - mockInput := NewPostInput{ + mockInput := domain.NewPostInput{ User: nil, } - _, err := NewPost(mockInput) + _, err := domain.NewPost(mockInput) if err == nil { t.Errorf("Invalid instance of Post") @@ -52,14 +55,14 @@ func TestNewPost_WithNilUser_ReturnsError(t *testing.T) { if err.Error() != "no user provided" { t.Errorf("got %q want %q", err.Error(), "no user provided") } - } + func TestNewPost_WithEmptyPostContent_ReturnsError(t *testing.T) { - mockUser := NewUser("test_user") - mockInput := NewPostInput{ + mockUser := domain.NewUser("test_user") + mockInput := domain.NewPostInput{ User: mockUser, } - _, err := NewPost(mockInput) + _, err := domain.NewPost(mockInput) if err == nil { t.Errorf("Invalid instance of Post") @@ -72,19 +75,19 @@ func TestNewPost_WithEmptyPostContent_ReturnsError(t *testing.T) { // Repost func TestNewRepost_WithValidInput_ReturnsOK(t *testing.T) { - mockUser := NewUser("post_original_user") - mockUserRepost := NewUser("post_repost_user") - mockPostInput := NewPostInput{ + mockUser := domain.NewUser("post_original_user") + mockUserRepost := domain.NewUser("post_repost_user") + mockPostInput := domain.NewPostInput{ User: mockUser, Content: "post_original_content", } - mockOriginalPost, _ := NewPost(mockPostInput) - mockInput := NewRepostQuoteInput{ + mockOriginalPost, _ := domain.NewPost(mockPostInput) + mockInput := domain.NewRepostQuoteInput{ User: mockUserRepost, Post: mockOriginalPost, } - newRepost, err := NewRepost(mockInput) + newRepost, err := domain.NewRepost(mockInput) if err != nil { t.Errorf("Unexpected error. %v", err) @@ -100,15 +103,17 @@ func TestNewRepost_WithValidInput_ReturnsOK(t *testing.T) { t.Errorf("Expected OriginalPostID to be 'original_post_id', but got '%s'", newRepost.OriginalPostID) } } + func TestNewRepost_WithRepostPost_ReturnsError(t *testing.T) { - mockOriginalPost := GenerateOriginalPost() + mockOriginalPostUser := domain.NewUser("post_original_user") + mockOriginalPost := GenerateOriginalPost(mockOriginalPostUser) mockRepost := GenerateRepost(mockOriginalPost) - newRepostInput := NewRepostQuoteInput{ - User: mockRepost.User, + newRepostInput := domain.NewRepostQuoteInput{ + User: mockOriginalPostUser, Post: mockRepost, Content: "repost in test", } - _, err := NewRepost(newRepostInput) + _, err := domain.NewRepost(newRepostInput) if err == nil { t.Errorf("NewRepost should have returned an error") @@ -120,14 +125,16 @@ func TestNewRepost_WithRepostPost_ReturnsError(t *testing.T) { } } + func TestNewRepost_WithSameUserID_ReturnsError(t *testing.T) { - mockOriginalPost := GenerateOriginalPost() - newRepostInput := NewRepostQuoteInput{ - User: mockOriginalPost.User, + mockOriginalPostUser := domain.NewUser("post_original_user") + mockOriginalPost := GenerateOriginalPost(mockOriginalPostUser) + newRepostInput := domain.NewRepostQuoteInput{ + User: mockOriginalPostUser, Post: mockOriginalPost, Content: "repost with the same user", } - _, err := NewRepost(newRepostInput) + _, err := domain.NewRepost(newRepostInput) if err == nil { t.Errorf("NewRepost should have returned an error") @@ -138,26 +145,27 @@ func TestNewRepost_WithSameUserID_ReturnsError(t *testing.T) { t.Errorf("Returned error is not correct. got '%s' want '%s'", err.Error(), expectedMsgError) } } + func TestNewRepost_WithEmptyInput_ReturnsError(t *testing.T) {} func TestNewRepost_WithEmptyPostContent_ReturnsError(t *testing.T) {} // Quotepost func TestNewQuotepost_WithValidInput_ReturnsOK(t *testing.T) { - mockePostUser := NewUser("post_original_user") - mockQuotePostUser := NewUser("post_user_user") - mockPostInput := NewPostInput{ - User: mockePostUser, + mockOriginalPostUser := domain.NewUser("post_original_user") + mockQuotePostUser := domain.NewUser("post_user_user") + mockPostInput := domain.NewPostInput{ + User: mockOriginalPostUser, Content: "post_original_content", } - mockOriginalPost, _ := NewPost(mockPostInput) + mockOriginalPost, _ := domain.NewPost(mockPostInput) - mockInput := NewRepostQuoteInput{ + mockInput := domain.NewRepostQuoteInput{ User: mockQuotePostUser, Post: mockOriginalPost, Content: "post_quote_content", } - newQuotePost, err := NewQuote(mockInput) + newQuotePost, err := domain.NewQuote(mockInput) if err != nil { t.Errorf("Unexpected error. %v", err) @@ -184,22 +192,24 @@ func TestNewQuotepost_WithValidInput_ReturnsOK(t *testing.T) { } if newQuotePost.OriginalPostUserID != mockOriginalPost.User.ID { - t.Errorf("Expected OriginalPostUserID to be '%s', but got '%s'", mockOriginalPost.User.ID, newQuotePost.OriginalPostUserID) + t.Errorf("Expected OriginalPostUserID to be '%s', but got '%s'", mockOriginalPostUser.ID, newQuotePost.OriginalPostUserID) } - if newQuotePost.OriginalPostScreenName != mockOriginalPost.User.Username { - t.Errorf("Expected OriginalPostScreenName to be '%s', but got '%s'", mockOriginalPost.User.Username, newQuotePost.OriginalPostScreenName) + if newQuotePost.OriginalPostScreenName != mockOriginalPostUser.Username { + t.Errorf("Expected OriginalPostScreenName to be '%s', but got '%s'", mockOriginalPostUser.Username, newQuotePost.OriginalPostScreenName) } } + func TestNewQuotepost_WithSameUserID_ReturnsError(t *testing.T) { - originalPost := GenerateOriginalPost() - mockInput := NewRepostQuoteInput{ - User: originalPost.User, + mockOriginalPostUser := domain.NewUser("user original post") + originalPost := GenerateOriginalPost(mockOriginalPostUser) + mockInput := domain.NewRepostQuoteInput{ + User: mockOriginalPostUser, Post: originalPost, Content: "repost in test", } - _, err := NewQuote(mockInput) + _, err := domain.NewQuote(mockInput) if err == nil { t.Errorf("Invalid instance of QuotePost returned") @@ -209,16 +219,18 @@ func TestNewQuotepost_WithSameUserID_ReturnsError(t *testing.T) { t.Errorf("Returned error is not correct. got '%s' want '%s'", err.Error(), "it is not possible quote your own post") } } + func TestNewQuotepost_WithQuotepost_ReturnsError(t *testing.T) { - originalPost := GenerateOriginalPost() - quotePost := GenerateQuotepost(originalPost) - mockInput := NewRepostQuoteInput{ - User: originalPost.User, + mockOriginalPostUser := domain.NewUser("post_original_user") + originalPost := GenerateOriginalPost(mockOriginalPostUser) + quotePost := GenerateQuotepost(originalPost, mockOriginalPostUser) + mockInput := domain.NewRepostQuoteInput{ + User: mockOriginalPostUser, Post: quotePost, - Content: "repost in test", + Content: "quote in test", } - _, err := NewQuote(mockInput) + _, err := domain.NewQuote(mockInput) if err == nil { t.Errorf("Invalid instance of QuotePost returned") @@ -228,14 +240,16 @@ func TestNewQuotepost_WithQuotepost_ReturnsError(t *testing.T) { t.Errorf("Returned error is not correct. got '%s' want '%s'", err.Error(), "it is not possible a quote post of a quote post") } } + func TestNewQuotepost_WithEmptyPostContent_ReturnsError(t *testing.T) { - originalPost := GenerateOriginalPost() - mockInput := NewRepostQuoteInput{ - User: originalPost.User, + mockOriginalPostUser := domain.NewUser("post_original_user") + originalPost := GenerateOriginalPost(mockOriginalPostUser) + mockInput := domain.NewRepostQuoteInput{ + User: mockOriginalPostUser, Post: originalPost, } - _, err := NewQuote(mockInput) + _, err := domain.NewQuote(mockInput) if err == nil { t.Errorf("Invalid instance of QuotePost returned") @@ -245,10 +259,11 @@ func TestNewQuotepost_WithEmptyPostContent_ReturnsError(t *testing.T) { t.Errorf("Returned error is not correct. got '%s' want '%s'", err.Error(), "no content provided") } } + func TestNewQuotepost_WithNilPost_ReturnsError(t *testing.T) { - mockInput := NewRepostQuoteInput{} + mockInput := domain.NewRepostQuoteInput{} - _, err := NewQuote(mockInput) + _, err := domain.NewQuote(mockInput) if err == nil { t.Errorf("Invalid instance of QuotePost returned") @@ -258,13 +273,15 @@ func TestNewQuotepost_WithNilPost_ReturnsError(t *testing.T) { t.Errorf("Returned error is not correct. got '%s' want '%s'", err.Error(), "no post provided") } } + func TestNewQuotepost_WithNilUser_ReturnsError(t *testing.T) { - mockOriginalPost := GenerateOriginalPost() - mockInput := NewRepostQuoteInput{ + mockOriginalPostUser := domain.NewUser("post_original_user") + mockOriginalPost := GenerateOriginalPost(mockOriginalPostUser) + mockInput := domain.NewRepostQuoteInput{ Post: mockOriginalPost, } - _, err := NewQuote(mockInput) + _, err := domain.NewQuote(mockInput) if err == nil { t.Errorf("Invalid instance of QuotePost returned") @@ -275,18 +292,18 @@ func TestNewQuotepost_WithNilUser_ReturnsError(t *testing.T) { } } -// in memory seeders -func GenerateOriginalPost() *Post { - mockePostUser := NewUser("post_original_user") - mockPostInput := NewPostInput{ - User: mockePostUser, +// // in memory seeders +func GenerateOriginalPost(anUser *domain.User) *domain.Post { + mockPostInput := domain.NewPostInput{ + User: anUser, Content: "post_original_content", } - newPost, _ := NewPost(mockPostInput) + newPost, _ := domain.NewPost(mockPostInput) return newPost } -func GenerateRepost(anOriginalPost *Post) *Post { - return &Post{ + +func GenerateRepost(anOriginalPost *domain.Post) *domain.Post { + return &domain.Post{ ID: uuid.NewString(), User: anOriginalPost.User, Content: anOriginalPost.Content, @@ -299,8 +316,8 @@ func GenerateRepost(anOriginalPost *Post) *Post { OriginalPostScreenName: anOriginalPost.User.Username, } } -func GenerateQuotepost(anOriginalPost *Post) *Post { - return &Post{ +func GenerateQuotepost(anOriginalPost *domain.Post, anOriginalUser *domain.User) *domain.Post { + return &domain.Post{ ID: uuid.NewString(), User: anOriginalPost.User, Content: anOriginalPost.Content, diff --git a/internal/domain/user.go b/internal/core/domain/user.go similarity index 62% rename from internal/domain/user.go rename to internal/core/domain/user.go index 45b092a..b674e24 100644 --- a/internal/domain/user.go +++ b/internal/core/domain/user.go @@ -6,10 +6,10 @@ import ( ) type User struct { - ID string `json:"id"` - Username string `json:"username"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"-"` + ID string + Username string + CreatedAt time.Time + UpdatedAt time.Time } func NewUser(username string) *User { diff --git a/internal/domain/user_test.go b/internal/core/domain/user_test.go similarity index 73% rename from internal/domain/user_test.go rename to internal/core/domain/user_test.go index 1951add..29afdb9 100644 --- a/internal/domain/user_test.go +++ b/internal/core/domain/user_test.go @@ -1,6 +1,7 @@ -package domain +package domain_test import ( + "github.com/dexfs/go-twitter-clone/internal/core/domain" "testing" "time" ) @@ -12,15 +13,15 @@ func TestNewUser(t *testing.T) { expected string }{ { - name: "Should create a new User instance correct", - input: "User 1", - expected: "User 1", + name: "Should create a new UserID instance correct", + input: "UserID 1", + expected: "UserID 1", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - sut := NewUser(test.input) + sut := domain.NewUser(test.input) if sut.Username != test.expected { t.Errorf("got %q want %q", sut.Username, test.expected) } diff --git a/internal/core/port/input/create_quote_usecase.go b/internal/core/port/input/create_quote_usecase.go new file mode 100644 index 0000000..6503dd8 --- /dev/null +++ b/internal/core/port/input/create_quote_usecase.go @@ -0,0 +1,15 @@ +package input + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) + +type CreateQuoteUseCase interface { + Execute(anInput CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) +} +type CreateQuoteUseCaseInput struct { + PostID string + UserID string + Quote string +} diff --git a/internal/core/port/input/create_repost_usecase.go b/internal/core/port/input/create_repost_usecase.go new file mode 100644 index 0000000..a5c4de0 --- /dev/null +++ b/internal/core/port/input/create_repost_usecase.go @@ -0,0 +1,15 @@ +package input + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) + +type CreateRepostUseCaseInput struct { + PostID string + UserID string +} + +type CreateRepostUseCase interface { + Execute(input CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) +} diff --git a/internal/core/port/input/createpost_usecase.go b/internal/core/port/input/createpost_usecase.go new file mode 100644 index 0000000..7e810a7 --- /dev/null +++ b/internal/core/port/input/createpost_usecase.go @@ -0,0 +1,15 @@ +package input + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) + +type CreatePostUseCaseInput struct { + UserID string + Content string +} + +type CreatePostUseCase interface { + Execute(aInput CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) +} diff --git a/internal/core/port/input/getinfo_usecase.go b/internal/core/port/input/getinfo_usecase.go new file mode 100644 index 0000000..9608941 --- /dev/null +++ b/internal/core/port/input/getinfo_usecase.go @@ -0,0 +1,10 @@ +package input + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) + +type GetUserInfoUseCase interface { + Execute(username string) (*domain.User, *rest_errors.RestError) +} diff --git a/internal/core/port/input/getuserfeed_usecase.go b/internal/core/port/input/getuserfeed_usecase.go new file mode 100644 index 0000000..a3ead52 --- /dev/null +++ b/internal/core/port/input/getuserfeed_usecase.go @@ -0,0 +1,10 @@ +package input + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) + +type GetUserFeedUseCase interface { + Execute(username string) ([]*domain.Post, *rest_errors.RestError) +} diff --git a/internal/core/port/output/post_port.go b/internal/core/port/output/post_port.go new file mode 100644 index 0000000..ba0954f --- /dev/null +++ b/internal/core/port/output/post_port.go @@ -0,0 +1,11 @@ +package output + +import "github.com/dexfs/go-twitter-clone/internal/core/domain" + +type PostPort interface { + CreatePost(aPost *domain.Post) error + HasReachedPostingLimitDay(aUserId string, aLimit uint64) bool + HasPostBeenRepostedByUser(postID string, userID string) bool + AllByUserID(aUser *domain.User) []*domain.Post + FindByID(aPostID string) (*domain.Post, error) +} diff --git a/internal/core/port/output/user_port.go b/internal/core/port/output/user_port.go new file mode 100644 index 0000000..032ba54 --- /dev/null +++ b/internal/core/port/output/user_port.go @@ -0,0 +1,8 @@ +package output + +import "github.com/dexfs/go-twitter-clone/internal/core/domain" + +type UserPort interface { + ByUsername(username string) (*domain.User, error) + FindByID(id string) (*domain.User, error) +} diff --git a/internal/core/usecase/create_quote_usecase.go b/internal/core/usecase/create_quote_usecase.go new file mode 100644 index 0000000..ed2fcf9 --- /dev/null +++ b/internal/core/usecase/create_quote_usecase.go @@ -0,0 +1,50 @@ +package usecase + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" +) + +type createQuoteUseCase struct { + userPort output.UserPort + postPort output.PostPort +} + +func NewCreateQuoteUseCase(postPort output.PostPort, userPort output.UserPort) (*createQuoteUseCase, *rest_errors.RestError) { + if postPort == nil || userPort == nil { + return nil, rest_errors.NewInternalServerError("postPort and userPort cannot be nil") + } + + return &createQuoteUseCase{ + postPort: postPort, + userPort: userPort, + }, nil +} + +func (uc *createQuoteUseCase) Execute(anInput input.CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) { + user, err := uc.userPort.FindByID(anInput.UserID) + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } + + post, err := uc.postPort.FindByID(anInput.PostID) + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } + + quotePostInput := domain.NewRepostQuoteInput{ + User: user, + Post: post, + Content: anInput.Quote, + } + newQuotePost, err := domain.NewQuote(quotePostInput) + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } + + uc.postPort.CreatePost(newQuotePost) + + return newQuotePost, nil +} diff --git a/internal/application/usecases/createquotepost.usecase_test.go b/internal/core/usecase/create_quote_usecase_test.go similarity index 51% rename from internal/application/usecases/createquotepost.usecase_test.go rename to internal/core/usecase/create_quote_usecase_test.go index a0f5827..65f62fd 100644 --- a/internal/application/usecases/createquotepost.usecase_test.go +++ b/internal/core/usecase/create_quote_usecase_test.go @@ -1,8 +1,11 @@ -package app +package usecase_test import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/infra/repository/inmemory" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/dexfs/go-twitter-clone/internal/core/usecase" "github.com/dexfs/go-twitter-clone/tests/mocks" "github.com/google/uuid" "reflect" @@ -11,11 +14,11 @@ import ( func TestCreateQuotePostUseCase_WithNotFoundUser_ReturnsError(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) + postRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) mockNotFoundUser := domain.NewUser("not_found_user") - createQuotePostUseCase := NewCreateQuotePostUseCase(mockUserRepo, postRepo) - useCaseInput := CreateQuotePostUseCaseInput{ + createQuotePostUseCase, _ := usecase.NewCreateQuoteUseCase(postRepo, mockUserRepo) + useCaseInput := input.CreateQuoteUseCaseInput{ UserID: mockNotFoundUser.ID, PostID: TestMocks.MockPostsSeed[0].ID, Quote: "not found user", @@ -23,7 +26,7 @@ func TestCreateQuotePostUseCase_WithNotFoundUser_ReturnsError(t *testing.T) { output, err := createQuotePostUseCase.Execute(useCaseInput) - if !reflect.DeepEqual(output, CreateQuotePostUseCaseOutput{}) { + if !reflect.DeepEqual(output, &domain.Post{}) { t.Errorf("should report user not found, got: %v", output) } @@ -35,12 +38,13 @@ func TestCreateQuotePostUseCase_WithNotFoundUser_ReturnsError(t *testing.T) { t.Errorf("should report 'user not found', got: %s", err.Error()) } } + func TestCreateQuotePostUseCase_WithNotFoundPost_ReturnsError(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) - createQuotePostUseCase := NewCreateQuotePostUseCase(mockUserRepo, postRepo) - useCaseInput := CreateQuotePostUseCaseInput{ + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) + postRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) + createQuotePostUseCase, _ := usecase.NewCreateQuoteUseCase(postRepo, mockUserRepo) + useCaseInput := input.CreateQuoteUseCaseInput{ UserID: TestMocks.MockUserSeed[0].ID, PostID: uuid.New().String(), Quote: "not found user", @@ -48,7 +52,7 @@ func TestCreateQuotePostUseCase_WithNotFoundPost_ReturnsError(t *testing.T) { output, err := createQuotePostUseCase.Execute(useCaseInput) - if !reflect.DeepEqual(output, CreateQuotePostUseCaseOutput{}) { + if !reflect.DeepEqual(output, &domain.Post{}) { t.Errorf("should report user not found, got: %v", output) } @@ -60,16 +64,23 @@ func TestCreateQuotePostUseCase_WithNotFoundPost_ReturnsError(t *testing.T) { t.Errorf("should report 'post not found', got: %s", err.Error()) } } + func TestCreateQuotePostUseCase_WithValidInput_ReturnsPostID(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) mockQuoteUser := domain.NewUser("quote_user") - TestMocks.MockUserDB.Insert(mockQuoteUser) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) - createQuotePostUseCase := NewCreateQuotePostUseCase(mockUserRepo, postRepo) + mocks.InsertUserHelper(TestMocks.MockDB, &inmemory_schema.UserSchema{ + ID: mockQuoteUser.ID, + Username: mockQuoteUser.Username, + CreatedAt: mockQuoteUser.CreatedAt, + UpdatedAt: mockQuoteUser.UpdatedAt, + }) + + postRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) + createQuotePostUseCase, _ := usecase.NewCreateQuoteUseCase(postRepo, mockUserRepo) mockOriginalPost := TestMocks.MockPostsSeed[0] - useCaseInput := CreateQuotePostUseCaseInput{ + useCaseInput := input.CreateQuoteUseCaseInput{ UserID: mockQuoteUser.ID, PostID: mockOriginalPost.ID, Quote: "New quote!", @@ -77,7 +88,7 @@ func TestCreateQuotePostUseCase_WithValidInput_ReturnsPostID(t *testing.T) { output, err := createQuotePostUseCase.Execute(useCaseInput) - if reflect.DeepEqual(output, CreatePostOutput{}) { + if reflect.DeepEqual(output, &domain.Post{}) { t.Errorf("should return PostID, got: %v", output) } @@ -85,14 +96,11 @@ func TestCreateQuotePostUseCase_WithValidInput_ReturnsPostID(t *testing.T) { t.Errorf("should allow create quote post") } - getNewQuotePost, _ := postRepo.FindByID(output.PostID) - - notExpected := getNewQuotePost.ID == mockOriginalPost.ID && - getNewQuotePost.OriginalPostID != mockOriginalPost.ID && - getNewQuotePost.OriginalPostContent != mockOriginalPost.Content + notExpected := output.ID == mockOriginalPost.ID && + output.OriginalPostID != mockOriginalPost.ID && + output.OriginalPostContent != mockOriginalPost.Content if notExpected { - t.Errorf("quote should be created but new quote post not found, got: %v", getNewQuotePost) + t.Errorf("quote should be created but new quote post not found, got: %v", output) } - } diff --git a/internal/application/usecases/createrepost.usecase_test.go b/internal/core/usecase/create_repost_usecase_test.go similarity index 55% rename from internal/application/usecases/createrepost.usecase_test.go rename to internal/core/usecase/create_repost_usecase_test.go index 996fef0..bb21a77 100644 --- a/internal/application/usecases/createrepost.usecase_test.go +++ b/internal/core/usecase/create_repost_usecase_test.go @@ -1,8 +1,11 @@ -package app +package usecase_test import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/infra/repository/inmemory" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/dexfs/go-twitter-clone/internal/core/usecase" "github.com/dexfs/go-twitter-clone/tests/mocks" "github.com/google/uuid" "reflect" @@ -11,19 +14,19 @@ import ( func TestCreateRepostUseCase_WithNotFoundUser_ReturnsError(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) + postRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) mockNotFoundUser := domain.NewUser("not_found_user") - createRepostUseCase := NewCreateRepostUseCase(mockUserRepo, postRepo) - useCaseInput := CreateRepostUseCaseInput{ + createRepostUseCase, _ := usecase.NewCreateRepostUseCase(postRepo, mockUserRepo) + useCaseInput := input.CreateRepostUseCaseInput{ UserID: mockNotFoundUser.ID, PostID: TestMocks.MockPostsSeed[0].ID, } output, err := createRepostUseCase.Execute(useCaseInput) - if !reflect.DeepEqual(output, CreateRepostUseCaseOutput{}) { - t.Errorf("should report user not found, got: %v", output) + if !reflect.DeepEqual(output, &domain.Post{}) { + t.Errorf("Expected nil output and got %v", output) } if err == nil { @@ -34,19 +37,20 @@ func TestCreateRepostUseCase_WithNotFoundUser_ReturnsError(t *testing.T) { t.Errorf("should report 'user not found', got: %s", err.Error()) } } + func TestCreateRepostPostUseCase_WithNotFoundPost_ReturnsError(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) - createRepostUseCase := NewCreateRepostUseCase(mockUserRepo, postRepo) - useCaseInput := CreateRepostUseCaseInput{ + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) + postRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) + createRepostUseCase, _ := usecase.NewCreateRepostUseCase(postRepo, mockUserRepo) + useCaseInput := input.CreateRepostUseCaseInput{ UserID: TestMocks.MockUserSeed[0].ID, PostID: uuid.New().String(), } output, err := createRepostUseCase.Execute(useCaseInput) - if !reflect.DeepEqual(output, CreateRepostUseCaseOutput{}) { + if !reflect.DeepEqual(output, &domain.Post{}) { t.Errorf("should report user not found, got: %v", output) } @@ -61,17 +65,17 @@ func TestCreateRepostPostUseCase_WithNotFoundPost_ReturnsError(t *testing.T) { func TestCreateRepostPostUseCase_WithPostOwner_ReturnsError(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) - createRepostUseCase := NewCreateRepostUseCase(mockUserRepo, postRepo) - useCaseInput := CreateRepostUseCaseInput{ + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) + postRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) + createRepostUseCase, _ := usecase.NewCreateRepostUseCase(postRepo, mockUserRepo) + useCaseInput := input.CreateRepostUseCaseInput{ UserID: TestMocks.MockUserSeed[0].ID, PostID: TestMocks.MockPostsSeed[0].ID, } output, err := createRepostUseCase.Execute(useCaseInput) - if !reflect.DeepEqual(output, CreateRepostUseCaseOutput{}) { + if !reflect.DeepEqual(output, &domain.Post{}) { t.Errorf("should report user not found, got: %v", output) } @@ -86,22 +90,28 @@ func TestCreateRepostPostUseCase_WithPostOwner_ReturnsError(t *testing.T) { func TestCreateRepostPostUseCase_WithValidInput_ReturnsPostID(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - mockQuoteUser := domain.NewUser("quote_user") - TestMocks.MockUserDB.Insert(mockQuoteUser) + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) + mockQuoteUser := domain.NewUser("quote_user") + mocks.InsertUserHelper(TestMocks.MockDB, &inmemory_schema.UserSchema{ + ID: mockQuoteUser.ID, + Username: mockQuoteUser.Username, + CreatedAt: mockQuoteUser.CreatedAt, + UpdatedAt: mockQuoteUser.UpdatedAt, + }) + + postRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) mockOriginalPost := TestMocks.MockPostsSeed[0] - createRepostUseCase := NewCreateRepostUseCase(mockUserRepo, postRepo) - useCaseInput := CreateRepostUseCaseInput{ + createRepostUseCase, _ := usecase.NewCreateRepostUseCase(postRepo, mockUserRepo) + useCaseInput := input.CreateRepostUseCaseInput{ UserID: mockQuoteUser.ID, PostID: mockOriginalPost.ID, } output, err := createRepostUseCase.Execute(useCaseInput) - if reflect.DeepEqual(output, CreatePostOutput{}) { + if reflect.DeepEqual(output, &domain.Post{}) { t.Errorf("should return PostID, got: %v", output) } @@ -109,7 +119,7 @@ func TestCreateRepostPostUseCase_WithValidInput_ReturnsPostID(t *testing.T) { t.Errorf("should allow create quote post") } - getNewQuotePost, _ := postRepo.FindByID(output.PostID) + getNewQuotePost, _ := postRepo.FindByID(output.ID) notExpected := getNewQuotePost.ID == mockOriginalPost.ID && getNewQuotePost.OriginalPostID != mockOriginalPost.ID && diff --git a/internal/core/usecase/createpost_usecase.go b/internal/core/usecase/createpost_usecase.go new file mode 100644 index 0000000..e8829d4 --- /dev/null +++ b/internal/core/usecase/createpost_usecase.go @@ -0,0 +1,45 @@ +package usecase + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" +) + +type createPostUseCase struct { + postPort output.PostPort + userPort output.UserPort +} + +func NewCreatePostUseCase(postPort output.PostPort, userPort output.UserPort) (*createPostUseCase, *rest_errors.RestError) { + if postPort == nil || userPort == nil { + return nil, rest_errors.NewInternalServerError("postPort and userPort cannot be nil") + } + + return &createPostUseCase{postPort: postPort, userPort: userPort}, nil +} + +func (uc *createPostUseCase) Execute(aInput input.CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) { + hasReachedLimit := uc.postPort.HasReachedPostingLimitDay(aInput.UserID, 5) // @TODO mudar isso para vir das configurações + if hasReachedLimit { + return &domain.Post{}, rest_errors.NewBadRequestError("you reached your posts day limit") + } + + user, err := uc.userPort.FindByID(aInput.UserID) + if err != nil { + return &domain.Post{}, rest_errors.NewNotFoundError(err.Error()) + } + + aNewPost, err := domain.NewPost(domain.NewPostInput{ + User: user, + Content: aInput.Content, + }) + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } + + uc.postPort.CreatePost(aNewPost) + + return aNewPost, nil +} diff --git a/internal/application/usecases/createpost.usecase_test.go b/internal/core/usecase/createpost_usecase_test.go similarity index 51% rename from internal/application/usecases/createpost.usecase_test.go rename to internal/core/usecase/createpost_usecase_test.go index 7eda77d..b6cfcd9 100644 --- a/internal/application/usecases/createpost.usecase_test.go +++ b/internal/core/usecase/createpost_usecase_test.go @@ -1,10 +1,12 @@ -package app +package usecase_test import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/infra/repository/inmemory" + "github.com/dexfs/go-twitter-clone/adapter/output/mappers" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/dexfs/go-twitter-clone/internal/core/usecase" "github.com/dexfs/go-twitter-clone/tests/mocks" - "reflect" "strconv" "testing" ) @@ -12,22 +14,24 @@ import ( func TestCreatePostUseCase_WithUserHasReachedLimitPostForCurrentDay_ReturnsError(t *testing.T) { TestMocks := mocks.GetTestMocks() mockUser := TestMocks.MockUserSeed - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) for i := 0; i < 60; i++ { postLoop, _ := domain.NewPost(domain.NewPostInput{ User: mockUser[0], Content: "post number" + strconv.Itoa(i), }) - TestMocks.MockPostDB.Insert(postLoop) + mocks.InsertPostHelper(TestMocks.MockDB, mappers.NewPostMapper().ToPersistence(postLoop)) } - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) + mockPostRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) - createPostUseCase := NewCreatePostUseCase(mockUserRepo, postRepo) - useCaseInput := CreatePostInput{ + createPostUseCase, _ := usecase.NewCreatePostUseCase(mockPostRepo, mockUserRepo) + + useCaseInput := input.CreatePostUseCaseInput{ UserID: mockUser[0].ID, Content: "Reached limit", } + _, err := createPostUseCase.Execute(useCaseInput) if err == nil { @@ -40,20 +44,16 @@ func TestCreatePostUseCase_WithUserHasReachedLimitPostForCurrentDay_ReturnsError } func TestCreatePostUseCase_WithNotFoundUser_ReturnsError(t *testing.T) { TestMocks := mocks.GetTestMocks() - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) + mockPostRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) mockNotFoundUser := domain.NewUser("not_found_user") - createPostUseCase := NewCreatePostUseCase(mockUserRepo, postRepo) - useCaseInput := CreatePostInput{ + createPostUseCase, _ := usecase.NewCreatePostUseCase(mockPostRepo, mockUserRepo) + useCaseInput := input.CreatePostUseCaseInput{ UserID: mockNotFoundUser.ID, Content: "user not found", } - output, err := createPostUseCase.Execute(useCaseInput) - - if !reflect.DeepEqual(output, CreatePostOutput{}) { - t.Errorf("should report user not found, got: %v", output) - } + _, err := createPostUseCase.Execute(useCaseInput) if err == nil { t.Errorf("should not allow create post for reached limit user") @@ -67,19 +67,14 @@ func TestCreatePostUseCase_WithNotFoundUser_ReturnsError(t *testing.T) { func TestCreatePostUseCase_WithValidInput_ReturnsPostID(t *testing.T) { TestMocks := mocks.GetTestMocks() mockUser := TestMocks.MockUserSeed - mockUserRepo := inmemory.NewInMemoryUserRepo(TestMocks.MockUserDB) - postRepo := inmemory.NewInMemoryPostRepo(TestMocks.MockPostDB) - createPostUseCase := NewCreatePostUseCase(mockUserRepo, postRepo) - useCaseInput := CreatePostInput{ + mockUserRepo := inmemory.NewInMemoryUserRepository(TestMocks.MockDB) + mockPostRepo := inmemory.NewInMemoryPostRepository(TestMocks.MockDB) + useCaseInput := input.CreatePostUseCaseInput{ UserID: mockUser[0].ID, - Content: "a valid post", - } - - output, err := createPostUseCase.Execute(useCaseInput) - - if reflect.DeepEqual(output, CreatePostOutput{}) { - t.Errorf("should return PostID, got: %v", output) + Content: "user not found", } + createPostUseCase, _ := usecase.NewCreatePostUseCase(mockPostRepo, mockUserRepo) + _, err := createPostUseCase.Execute(useCaseInput) if err != nil { t.Errorf("should allow create post") diff --git a/internal/core/usecase/createrepost_usecase.go b/internal/core/usecase/createrepost_usecase.go new file mode 100644 index 0000000..aed4655 --- /dev/null +++ b/internal/core/usecase/createrepost_usecase.go @@ -0,0 +1,54 @@ +package usecase + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/input" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" +) + +type createRepostUseCase struct { + postPort output.PostPort + userPort output.UserPort +} + +func NewCreateRepostUseCase(postPort output.PostPort, userPort output.UserPort) (*createRepostUseCase, *rest_errors.RestError) { + if postPort == nil || userPort == nil { + return nil, rest_errors.NewInternalServerError("postPort and userPort cannot be nil") + } + + return &createRepostUseCase{postPort: postPort, userPort: userPort}, nil +} + +func (uc *createRepostUseCase) Execute(aInput input.CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) { + user, err := uc.userPort.FindByID(aInput.UserID) + + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } + + isReposted := uc.postPort.HasPostBeenRepostedByUser(aInput.PostID, aInput.UserID) + if isReposted { + return &domain.Post{}, rest_errors.NewBadRequestError("it is not possible repost a repost post") + } + + post, err := uc.postPort.FindByID(aInput.PostID) + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } + + aNewPostInput := domain.NewRepostQuoteInput{ + User: user, + Post: post, + Content: "", + } + + newRepost, err := domain.NewRepost(aNewPostInput) + if err != nil { + return &domain.Post{}, rest_errors.NewInternalServerError(err.Error()) + } + + uc.postPort.CreatePost(newRepost) + + return newRepost, nil +} diff --git a/internal/core/usecase/getuserfeed.usecase_test.go b/internal/core/usecase/getuserfeed.usecase_test.go new file mode 100644 index 0000000..8e28e6a --- /dev/null +++ b/internal/core/usecase/getuserfeed.usecase_test.go @@ -0,0 +1,119 @@ +package usecase_test + +import ( + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/usecase" + "github.com/dexfs/go-twitter-clone/tests/mocks" + "testing" +) + +func TestExecute_WithValidUsername_ReturnsFeedItems(t *testing.T) { + TestMocks := mocks.GetTestMocks() + mockUserSeed, _ := mocks.UserSeed(1) + mocks.PostSeed(mockUserSeed[0], 2) + mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockDB) + + postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockDB) + + userFeedUseCase, _ := usecase.NewGetUserFeedUseCase(mockUserRepo, postRepo) + + userFeed, err := userFeedUseCase.Execute(mockUserSeed[0].Username) + + if err != nil { + t.Errorf("want err=nil; got %v", err) + } + + if len(userFeed) != 2 { + t.Errorf("want 2 posts; got %v", len(userFeed)) + } +} + +func TestExecute_WithEmptyUsername_ReturnsError(t *testing.T) { + TestMocks := mocks.GetTestMocks() + userSeed, _ := mocks.UserSeed(1) + mocks.PostSeed(userSeed[0], 2) + mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockDB) + postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockDB) + getUserFeedUseCase, _ := usecase.NewGetUserFeedUseCase(mockUserRepo, postRepo) + _, err := getUserFeedUseCase.Execute("") + + if err == nil { + t.Errorf("should return an error") + } + + if err.Error() != "user not found" { + t.Errorf("got %v want %s", err.Error(), "username must not be empty") + } + +} +func TestExecute_WithNonExistingUsername_ReturnsError(t *testing.T) { + TestMocks := mocks.GetTestMocks() + mockeSeed, _ := mocks.UserSeed(1) + mocks.PostSeed(mockeSeed[0], 2) + mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockDB) + postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockDB) + getUserFeedUseCase, _ := usecase.NewGetUserFeedUseCase(mockUserRepo, postRepo) + _, err := getUserFeedUseCase.Execute("non-existing-user") + + if err == nil { + t.Errorf("should return an error") + } + + if err.Error() != "user not found" { + t.Errorf("got %v want %s", err.Error(), "user not found") + } +} +func TestExecute_WithNilUserRepository_ReturnsError(t *testing.T) { + TestMocks := mocks.GetTestMocks() + postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockDB) + getUserFeedUseCase, err := usecase.NewGetUserFeedUseCase(nil, postRepo) + + if getUserFeedUseCase != nil { + t.Errorf("Invalid instance of usecase") + } + + if err == nil { + t.Errorf("should return an error") + } + + if err.Error() != "user port and post port is required" { + t.Errorf("got %v want %s", err.Error(), "the dependencies should not be nil") + } +} +func TestExecute_WithNilPostRepository_ReturnsError(t *testing.T) { + TestMocks := mocks.GetTestMocks() + mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockDB) + getUserFeedUseCase, err := usecase.NewGetUserFeedUseCase(mockUserRepo, nil) + + if getUserFeedUseCase != nil { + t.Errorf("Invalid instance of usecase") + } + + if err == nil { + t.Errorf("should return an error") + } + + if err.Error() != "user port and post port is required" { + t.Errorf("got %v want %s", err.Error(), "the dependencies should not be nil") + } +} +func TestExecute_WithPostRepositoryError_ReturnsError(t *testing.T) { + TestMocks := mocks.GetTestMocks() + _, mockUser := mocks.UserSeed(1) + mockUserRepo := mocks.MakeInMemoryUserRepo(TestMocks.MockDB) + TestMocks.MockDB.RegisterSchema(inmemory.POST_SCHEMA_NAME, []*inmemory_schema.PostSchema{}) + postRepo := mocks.MakeInMemoryPostRepo(TestMocks.MockDB) + + userFeedUseCase, _ := usecase.NewGetUserFeedUseCase(mockUserRepo, postRepo) + + userFeed, err := userFeedUseCase.Execute(mockUser[0].Username) + + if err != nil { + t.Errorf("want err=nil; got %v", err) + } + + if len(userFeed) > 0 { + t.Errorf("want 0 posts; got %v", len(userFeed)) + } +} diff --git a/internal/core/usecase/getuserfeed_usecase.go b/internal/core/usecase/getuserfeed_usecase.go new file mode 100644 index 0000000..efbafaa --- /dev/null +++ b/internal/core/usecase/getuserfeed_usecase.go @@ -0,0 +1,33 @@ +package usecase + +import ( + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" +) + +type getUserFeedUseCase struct { + userPort output.UserPort + postPort output.PostPort +} + +func NewGetUserFeedUseCase(userPort output.UserPort, postPort output.PostPort) (*getUserFeedUseCase, *rest_errors.RestError) { + if userPort == nil || postPort == nil { + return nil, rest_errors.NewInternalServerError("user port and post port is required") + } + + return &getUserFeedUseCase{ + userPort: userPort, + postPort: postPort, + }, nil +} + +func (uc *getUserFeedUseCase) Execute(username string) ([]*domain.Post, *rest_errors.RestError) { + user, err := uc.userPort.ByUsername(username) + if err != nil { + return []*domain.Post{}, rest_errors.NewNotFoundError(err.Error()) + } + + posts := uc.postPort.AllByUserID(user) + return posts, nil +} diff --git a/internal/core/usecase/getuserinfo.usecase_test.go b/internal/core/usecase/getuserinfo.usecase_test.go new file mode 100644 index 0000000..71a3b00 --- /dev/null +++ b/internal/core/usecase/getuserinfo.usecase_test.go @@ -0,0 +1,51 @@ +package usecase_test + +import ( + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + "github.com/dexfs/go-twitter-clone/internal/core/usecase" + "github.com/dexfs/go-twitter-clone/tests/mocks" + "testing" +) + +func TestGetUserInfoUseCase_WithValidUsername_ReturnsUserInfo(t *testing.T) { + TestMocks := mocks.GetTestMocks() + inMemoryDb := TestMocks.MockDB + usersSeed := TestMocks.MockUserSeed + userRepo := inmemory.NewInMemoryUserRepository(inMemoryDb) + + getInfoUseCase, _ := usecase.NewGetUserInfoUseCase(userRepo) + output, err := getInfoUseCase.Execute(usersSeed[0].Username) + if err != nil { + t.Errorf("error while executing getInfoUseCase: %v", err) + } + + if output.ID != usersSeed[0].ID { + t.Errorf("getInfoUseCase returned wrong user info, got %v, expected %v", output, usersSeed[0]) + } +} +func TestGetUserInfoUseCase_WithNonExistingUsername_ReturnsError(t *testing.T) { + TestMocks := mocks.GetTestMocks() + inMemoryDb := TestMocks.MockDB + userRepo := inmemory.NewInMemoryUserRepository(inMemoryDb) + + getInfoUseCase, _ := usecase.NewGetUserInfoUseCase(userRepo) + output, err := getInfoUseCase.Execute("") + if err == nil { + t.Errorf("should return error") + } + + if output != nil { + t.Errorf("should return empty user info") + } +} +func TestGetUserInfoUseCase_WithNilUserRepository_ReturnsError(t *testing.T) { + _, err := usecase.NewGetUserInfoUseCase(nil) + + if err == nil { + t.Errorf("should return error") + } + + if err.Error() != "userPort cannot be nil" { + t.Errorf("should return 'userPort cannot be nil' got %v", err.Error()) + } +} diff --git a/internal/core/usecase/getuserinfo_usecase.go b/internal/core/usecase/getuserinfo_usecase.go new file mode 100644 index 0000000..ea8f27b --- /dev/null +++ b/internal/core/usecase/getuserinfo_usecase.go @@ -0,0 +1,30 @@ +package usecase + +import ( + "fmt" + "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" +) + +type getUserInfoUseCase struct { + userPort output.UserPort +} + +func NewGetUserInfoUseCase(userPort output.UserPort) (*getUserInfoUseCase, *rest_errors.RestError) { + if userPort == nil { + return nil, rest_errors.NewInternalServerError("userPort cannot be nil") + } + return &getUserInfoUseCase{ + userPort: userPort, + }, nil +} + +func (s *getUserInfoUseCase) Execute(username string) (*domain.User, *rest_errors.RestError) { + fmt.Sprintf("GetUserInfoService_Execute(%s)", username) + userInfoResponse, err := s.userPort.ByUsername(username) + if err != nil { + return nil, rest_errors.NewNotFoundError(err.Error()) + } + return userInfoResponse, nil +} diff --git a/internal/domain/interfaces/post_types.go b/internal/domain/interfaces/post_types.go deleted file mode 100644 index ea5e207..0000000 --- a/internal/domain/interfaces/post_types.go +++ /dev/null @@ -1,22 +0,0 @@ -package interfaces - -import ( - "github.com/dexfs/go-twitter-clone/internal/domain" -) - -type ID string -type Posts []*domain.Post -type Count uint64 -type PostingLimitReached bool -type HasRepost bool -type Post *domain.Post - -type PostRepository interface { - GetAll() Posts - CountByUser(userId string) Count - HasPostBeenRepostedByUser(postID string, userID string) HasRepost - HasReachedPostingLimitDay(userId string, limit uint64) PostingLimitReached - GetFeedByUserID(userID string) Posts - Insert(item *domain.Post) - FindByID(id string) (*domain.Post, error) -} diff --git a/internal/domain/interfaces/user_types.go b/internal/domain/interfaces/user_types.go deleted file mode 100644 index c6aa5fc..0000000 --- a/internal/domain/interfaces/user_types.go +++ /dev/null @@ -1,10 +0,0 @@ -package interfaces - -import ( - "github.com/dexfs/go-twitter-clone/internal/domain" -) - -type UserRepository interface { - ByUsername(username string) (*domain.User, error) - FindByID(id string) (*domain.User, error) -} diff --git a/internal/infra/repository/inmemory/post_inmemory_impl_repo.go b/internal/infra/repository/inmemory/post_inmemory_impl_repo.go deleted file mode 100644 index dccb4d9..0000000 --- a/internal/infra/repository/inmemory/post_inmemory_impl_repo.go +++ /dev/null @@ -1,93 +0,0 @@ -package inmemory - -import ( - "errors" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" - "github.com/dexfs/go-twitter-clone/pkg/database" - "github.com/dexfs/go-twitter-clone/pkg/shared/helpers" -) - -type InMemoryPostRepo struct { - db *database.InMemoryDB[domain.Post] -} - -func NewInMemoryPostRepo(db *database.InMemoryDB[domain.Post]) *InMemoryPostRepo { - return &InMemoryPostRepo{ - db: db, - } -} - -func (r *InMemoryPostRepo) CountByUser(userId string) interfaces.Count { - count := interfaces.Count(0) - for _, currentData := range r.db.GetAll() { - if currentData.User.ID == userId { - count++ - } - } - - return count -} - -func (r *InMemoryPostRepo) HasPostBeenRepostedByUser(postID string, userID string) interfaces.HasRepost { - for _, vPost := range r.db.GetAll() { - if vPost.IsRepost { - if vPost.User.ID == userID && vPost.OriginalPostID == postID { - return true - } - } - } - return false -} - -func (r *InMemoryPostRepo) Insert(item *domain.Post) { - r.db.Insert(item) -} - -func (r *InMemoryPostRepo) FindByID(id string) (*domain.Post, error) { - for _, currentData := range r.db.GetAll() { - if currentData.ID == id { - return currentData, nil - } - } - - return nil, errors.New("post not found") -} - -func (r *InMemoryPostRepo) Remove(item *domain.Post) { - r.db.Remove(item) -} - -func (r *InMemoryPostRepo) GetAll() interfaces.Posts { - return r.db.GetAll() -} - -func (r *InMemoryPostRepo) HasReachedPostingLimitDay(userId string, limit uint64) interfaces.PostingLimitReached { - var count = uint64(0) - - for _, currentData := range r.db.GetAll() { - matched := currentData.User.ID == userId && helpers.IsToday(currentData.CreatedAt) - - if matched { - count++ - } - } - - reached := count >= limit - if reached { - return true - } else { - return false - } -} - -func (r *InMemoryPostRepo) GetFeedByUserID(userID string) interfaces.Posts { - var feed []*domain.Post - for _, currentData := range r.db.GetAll() { - if currentData.User.ID == userID { - feed = append(feed, currentData) - } - } - - return feed -} diff --git a/internal/infra/repository/inmemory/post_inmemory_impl_repo_test.go b/internal/infra/repository/inmemory/post_inmemory_impl_repo_test.go deleted file mode 100644 index 4b8920b..0000000 --- a/internal/infra/repository/inmemory/post_inmemory_impl_repo_test.go +++ /dev/null @@ -1,200 +0,0 @@ -package inmemory - -import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" - "github.com/dexfs/go-twitter-clone/pkg/database" - "math/rand" - "testing" -) - -func TestShouldInsertAPost(t *testing.T) { - //Given - userTest := domain.NewUser("post_user_test") - db := &database.InMemoryDB[domain.Post]{} - postRepo := NewInMemoryPostRepo(db) - newPostInput := domain.NewPostInput{ - User: userTest, - Content: "post test", - } - newPost, _ := domain.NewPost(newPostInput) - // When - postRepo.Insert(newPost) - posts := postRepo.GetAll() - - // Then - if len(posts) <= 0 { - t.Errorf("got %v want 1", len(posts)) - } -} - -func TestShouldFindAPostByID(t *testing.T) { - //Given - userTest := domain.NewUser("post_user_test") - db := &database.InMemoryDB[domain.Post]{} - postRepo := NewInMemoryPostRepo(db) - newPostInput := domain.NewPostInput{ - User: userTest, - Content: "post test", - } - newPostInput2 := domain.NewPostInput{ - User: userTest, - Content: "post2 test", - } - newPost, _ := domain.NewPost(newPostInput) - domain.NewPost(newPostInput2) - - postRepo.Insert(newPost) - post, err := postRepo.FindByID(newPost.ID) - - if err != nil { - t.Errorf("got %v want no empty", post) - } - - if post == nil { - t.Errorf("got nil want post") - } - - if newPost.ID != post.ID { - t.Errorf("got %v want %v", post.ID, newPost.ID) - } - - post, err = postRepo.FindByID("not_found_id") - if err == nil { - - t.Errorf("got %v want empty", post) - } - -} - -func TestShouldRemoveAPost(t *testing.T) { - //Given - userTest := domain.NewUser("post_user_test") - db := &database.InMemoryDB[domain.Post]{} - postRepo := NewInMemoryPostRepo(db) - newPostInput := domain.NewPostInput{ - User: userTest, - Content: "post test", - } - newPostInput2 := domain.NewPostInput{ - User: userTest, - Content: "post2 test", - } - newPost, _ := domain.NewPost(newPostInput) - newPost2, _ := domain.NewPost(newPostInput2) - - // When - postRepo.Insert(newPost) - postRepo.Insert(newPost2) - postRepo.Remove(newPost) - posts := postRepo.GetAll() - - // Then - expected := 1 - if len(posts) != expected { - t.Errorf("got %v want %v", len(posts), expected) - } -} - -func TestShoulCountPostsPerUser(t *testing.T) { - //Given - userTest := domain.NewUser("post_user_test") - db := &database.InMemoryDB[domain.Post]{} - postRepo := NewInMemoryPostRepo(db) - newPostInput := domain.NewPostInput{ - User: userTest, - Content: "post test", - } - newPostInput2 := domain.NewPostInput{ - User: userTest, - Content: "post2 test", - } - newPost, _ := domain.NewPost(newPostInput) - newPost2, _ := domain.NewPost(newPostInput2) - - // When - postRepo.Insert(newPost) - postRepo.Insert(newPost2) - countPosts := postRepo.CountByUser(userTest.ID) - - // Then - expected := interfaces.Count(2) - if countPosts != expected { - t.Errorf("got %v want %v", countPosts, expected) - } -} - -func TestShouldValidateHasReachedPostingLimitDay(t *testing.T) { - //Given - userTest := domain.NewUser("post_user_test") - db := &database.InMemoryDB[domain.Post]{} - postRepo := NewInMemoryPostRepo(db) - count := 5 - for i := 0; i < count; i++ { - newPost, _ := domain.NewPost(domain.NewPostInput{ - User: userTest, - Content: generateRandomString(10), - }) - postRepo.Insert(newPost) - } - - // When - hasReached := postRepo.HasReachedPostingLimitDay(userTest.ID, uint64(count)) - - if !hasReached { - t.Errorf("got %v want %v", hasReached, true) - } - - hasReached = postRepo.HasReachedPostingLimitDay(userTest.ID, uint64(10)) - - if hasReached { - t.Errorf("got %v want %v", hasReached, false) - } -} - -func TestShouldVerifyIfAPostIsEligibleForRepost(t *testing.T) { - //Given - mockUser := domain.NewUser("post_user_test") - mockRepostUser := domain.NewUser("repost_user_test") - mockOrigionalPostInput := domain.NewPostInput{ - User: mockUser, - Content: "original_post", - } - mockOriginalPost, _ := domain.NewPost(mockOrigionalPostInput) - mockRepostInput := domain.NewRepostQuoteInput{ - User: mockRepostUser, - Post: mockOriginalPost, - Content: "repost", - } - mockRepost, _ := domain.NewRepost(mockRepostInput) - - db := &database.InMemoryDB[domain.Post]{} - db.Insert(mockOriginalPost) - db.Insert(mockRepost) - postRepo := NewInMemoryPostRepo(db) - - // When - // Then - hasPostBeenRepostedByUserRepost := postRepo.HasPostBeenRepostedByUser(mockOriginalPost.ID, mockRepostUser.ID) - - if !hasPostBeenRepostedByUserRepost { - t.Errorf("got %v want %v", hasPostBeenRepostedByUserRepost, true) - } - - hasPostBeenRepostedByUser := postRepo.HasPostBeenRepostedByUser(mockOriginalPost.ID, mockUser.ID) - - if hasPostBeenRepostedByUser { - t.Errorf("got %v want %v", hasPostBeenRepostedByUserRepost, false) - } - -} - -// utils -func generateRandomString(length int) string { - const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - b := make([]byte, length) - for i := range b { - b[i] = charset[rand.Intn(len(charset))] - } - return string(b) -} diff --git a/internal/infra/repository/inmemory/user_inmemory_impl_repo.go b/internal/infra/repository/inmemory/user_inmemory_impl_repo.go deleted file mode 100644 index 954f00a..0000000 --- a/internal/infra/repository/inmemory/user_inmemory_impl_repo.go +++ /dev/null @@ -1,48 +0,0 @@ -package inmemory - -import ( - "errors" - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/pkg/database" -) - -type InMemoryUserRepoImpl struct { - db *database.InMemoryDB[domain.User] -} - -func NewInMemoryUserRepo(db *database.InMemoryDB[domain.User]) *InMemoryUserRepoImpl { - return &InMemoryUserRepoImpl{ - db: db, - } -} - -func (r *InMemoryUserRepoImpl) ByUsername(username string) (*domain.User, error) { - for _, currentUser := range r.db.GetAll() { - if currentUser.Username == username { - return currentUser, nil - } - } - - return nil, errors.New("user not found") -} - -func (r *InMemoryUserRepoImpl) Insert(item *domain.User) { - r.db.Insert(item) -} - -func (r *InMemoryUserRepoImpl) GetAll() []*domain.User { - return r.db.GetAll() -} - -func (r *InMemoryUserRepoImpl) Remove(item *domain.User) { - r.db.Remove(item) -} - -func (r *InMemoryUserRepoImpl) FindByID(id string) (*domain.User, error) { - for _, currentUser := range r.db.GetAll() { - if currentUser.ID == id { - return currentUser, nil - } - } - return nil, errors.New("user not found") -} diff --git a/internal/infra/repository/inmemory/user_inmemory_impl_repo_test.go b/internal/infra/repository/inmemory/user_inmemory_impl_repo_test.go deleted file mode 100644 index b2b1a94..0000000 --- a/internal/infra/repository/inmemory/user_inmemory_impl_repo_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package inmemory - -import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/pkg/database" - "strconv" - "testing" -) - -func TestShouldReturnInsertedUser(t *testing.T) { - userTest := domain.NewUser("usuarion_test_1") - - db := &database.InMemoryDB[domain.User]{} - userRepo := NewInMemoryUserRepo(db) - - userRepo.Insert(userTest) - - users := userRepo.GetAll() - - if len(users) > 1 || len(users) < 1 { - t.Errorf("got %d want 1", len(users)) - } - - if users[0].Username != userTest.Username { - t.Errorf("got %v want %v", users[0], userTest) - } -} - -func TestShouldReturnUserByUsername(t *testing.T) { - userToFind := domain.NewUser("user_to_find") - db := &database.InMemoryDB[domain.User]{} - for i := 0; i < 5; i++ { - newUser := domain.NewUser("username_" + strconv.Itoa(i)) - db.Insert(newUser) - } - db.Insert(userToFind) - - userRepo := NewInMemoryUserRepo(db) - - foundUser, _ := userRepo.ByUsername(userToFind.Username) - - if foundUser.Username != userToFind.Username { - t.Errorf("got %v want %v", foundUser, userToFind.Username) - } -} - -func TestShouldRemoveUserByID(t *testing.T) { - userToDelete := domain.NewUser("user_to_find") - db := &database.InMemoryDB[domain.User]{} - for i := 0; i < 5; i++ { - newUser := domain.NewUser("username_" + strconv.Itoa(i)) - db.Insert(newUser) - } - db.Insert(userToDelete) - - userRepo := NewInMemoryUserRepo(db) - - userRepo.Remove(userToDelete) - - findByUserRemoved, err := userRepo.ByUsername(userToDelete.Username) - - if err == nil { - t.Errorf("got %v want nil", findByUserRemoved) - } -} diff --git a/pkg/database/basedatabase.go b/pkg/database/basedatabase.go deleted file mode 100644 index 254172d..0000000 --- a/pkg/database/basedatabase.go +++ /dev/null @@ -1,7 +0,0 @@ -package database - -type BaseDb[T any] interface { - Insert(entity *T) *T - FindByID(id string) *T - Update(entity *T) -} diff --git a/pkg/database/in_memory_db.go b/pkg/database/in_memory_db.go new file mode 100644 index 0000000..4de8698 --- /dev/null +++ b/pkg/database/in_memory_db.go @@ -0,0 +1,29 @@ +package database + +import "fmt" + +type InMemoryDB struct { + Schemas map[string]interface{} +} + +func NewInMemoryDB() *InMemoryDB { + fmt.Println("Creating InMemoryDB") + return &InMemoryDB{Schemas: make(map[string]interface{})} +} + +func (db *InMemoryDB) GetSchema(key string) any { + existing, ok := db.Schemas[key] + if !ok { + return nil + } + + return existing +} + +func (db *InMemoryDB) RegisterSchema(key string, value interface{}) { + db.Schemas[key] = value +} + +func (db *InMemoryDB) DropSchema(key string) { + delete(db.Schemas, key) +} diff --git a/pkg/database/inmemory_db.go b/pkg/database/inmemory_db.go deleted file mode 100644 index 72be4e5..0000000 --- a/pkg/database/inmemory_db.go +++ /dev/null @@ -1,36 +0,0 @@ -package database - -import ( - "slices" -) - -type InMemoryDB[T any] struct { - BaseDb[T] - data []*T -} - -func (db *InMemoryDB[T]) Insert(item *T) { - db.data = slices.Insert(db.data, len(db.data), item) -} - -func (db *InMemoryDB[T]) GetAll() []*T { - return db.data -} - -func (db *InMemoryDB[T]) Remove(item *T) { - for i, v := range db.data { - if v == item { - db.data = slices.Delete(db.data, i, i+1) - return - } - } -} - -func (db *InMemoryDB[T]) Update(item *T) { - for i, v := range db.data { - if v == item { - db.data = slices.Replace(db.data, i, i+1, item) - return - } - } -} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..dee62dc --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,4 @@ +aws ecr get-login-password --region us-east-1 --profile bia | docker login --username AWS --password-stdin 174597536931.dkr.ecr.us-east-1.amazonaws.com +docker build -f ../Dockerfile -t dexfs/go-twitter-clone:latest ./.. +docker tag dexfs/go-twitter-clone:latest 174597536931.dkr.ecr.us-east-1.amazonaws.com/dexfs/go-twitter-clone:latest +docker push 174597536931.dkr.ecr.us-east-1.amazonaws.com/dexfs/go-twitter-clone:latest \ No newline at end of file diff --git a/tests/mocks/mocks.go b/tests/mocks/mocks.go index 08300ea..10346ab 100644 --- a/tests/mocks/mocks.go +++ b/tests/mocks/mocks.go @@ -1,69 +1,115 @@ package mocks import ( - "github.com/dexfs/go-twitter-clone/internal/domain" - "github.com/dexfs/go-twitter-clone/internal/domain/interfaces" - "github.com/dexfs/go-twitter-clone/internal/infra/repository/inmemory" + "github.com/dexfs/go-twitter-clone/adapter/output/mappers" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" + inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/internal/core/port/output" "github.com/dexfs/go-twitter-clone/pkg/database" + "log" "strconv" ) // mocks -func MakeDb[T any]() *database.InMemoryDB[T] { - return &database.InMemoryDB[T]{} +func MakeDb() *database.InMemoryDB { + db := database.NewInMemoryDB() + return db } -func MakeInMemoryUserRepo(db *database.InMemoryDB[domain.User]) interfaces.UserRepository { - repo := inmemory.NewInMemoryUserRepo(db) +func MakeInMemoryUserRepo(db *database.InMemoryDB) output.UserPort { + repo := inmemory.NewInMemoryUserRepository(db) return repo } -func MakeInMemoryPostRepo(db *database.InMemoryDB[domain.Post]) interfaces.PostRepository { - repo := inmemory.NewInMemoryPostRepo(db) +func MakeInMemoryPostRepo(db *database.InMemoryDB) output.PostPort { + repo := inmemory.NewInMemoryPostRepository(db) return repo } -func UserSeed(db *database.InMemoryDB[domain.User], amount int) []*domain.User { + +func UserSeed(amount uint64) ([]*inmemory_schema.UserSchema, []*domain.User) { if amount <= 0 { amount = 1 } + seeds := make([]*inmemory_schema.UserSchema, amount) users := make([]*domain.User, amount) - for i := 0; i < len(users); i++ { + for i := 0; i < len(seeds); i++ { username := "user" + strconv.Itoa(i) newUser := domain.NewUser(username) - db.Insert(newUser) users[i] = newUser + seeds[i] = &inmemory_schema.UserSchema{ + ID: newUser.ID, + Username: newUser.Username, + CreatedAt: newUser.CreatedAt, + UpdatedAt: newUser.UpdatedAt, + } } - return users + return seeds, users } -func PostSeed(db *database.InMemoryDB[domain.Post], user *domain.User, amount int) []*domain.Post { + +func PostSeed(user *inmemory_schema.UserSchema, amount int) ([]*inmemory_schema.PostSchema, []*domain.Post) { + seeds := make([]*inmemory_schema.PostSchema, amount) posts := make([]*domain.Post, amount) - for i := 0; i < len(posts); i++ { + for i := 0; i < len(seeds); i++ { newPostInput := domain.NewPostInput{ - User: user, + User: mappers.NewUserMapper().FromPersistence(user), Content: "post_" + strconv.Itoa(i), } newPost, _ := domain.NewPost(newPostInput) - db.Insert(newPost) posts[i] = newPost + seeds[i] = &inmemory_schema.PostSchema{ + ID: newPost.ID, + UserID: newPost.User.ID, + Content: newPost.Content, + CreatedAt: newPost.CreatedAt, + IsQuote: newPost.IsQuote, + IsRepost: newPost.IsRepost, + OriginalPostID: newPost.OriginalPostID, + OriginalPostContent: newPost.OriginalPostContent, + OriginalPostUserID: newPost.OriginalPostUserID, + OriginalPostScreenName: newPost.OriginalPostScreenName, + } } - return posts + return seeds, posts } type TestMocks struct { - MockUserDB *database.InMemoryDB[domain.User] + MockDB *database.InMemoryDB MockUserSeed []*domain.User - MockPostDB *database.InMemoryDB[domain.Post] MockPostsSeed []*domain.Post } func GetTestMocks() TestMocks { - mockUserDB := MakeDb[domain.User]() - mockPostDB := MakeDb[domain.Post]() - mockUserSeed := UserSeed(mockUserDB, 1) - mockPostsSeed := PostSeed(mockPostDB, mockUserSeed[0], 2) + mockDB := MakeDb() + userSeeds, mockUsers := UserSeed(1) + postSeeds, mockPosts := PostSeed(userSeeds[0], 2) + + mockDB.RegisterSchema(inmemory.USER_SCHEMA_NAME, userSeeds) + mockDB.RegisterSchema(inmemory.POST_SCHEMA_NAME, postSeeds) return TestMocks{ - MockUserDB: mockUserDB, - MockUserSeed: mockUserSeed, - MockPostDB: mockPostDB, - MockPostsSeed: mockPostsSeed, + MockDB: mockDB, + MockUserSeed: mockUsers, + MockPostsSeed: mockPosts, } } + +func InsertUserHelper(db *database.InMemoryDB, newItem *inmemory_schema.UserSchema) { + existing, ok := db.Schemas[inmemory.USER_SCHEMA_NAME].([]*inmemory_schema.UserSchema) + if !ok { + log.Fatal("schema " + inmemory.USER_SCHEMA_NAME + " not found") + return + } + + updateSlice := append(existing, newItem) + db.Schemas[inmemory.USER_SCHEMA_NAME] = updateSlice +} + +func InsertPostHelper(db *database.InMemoryDB, newItem *inmemory_schema.PostSchema) { + existing, ok := db.Schemas[inmemory.POST_SCHEMA_NAME].([]*inmemory_schema.PostSchema) + if !ok { + log.Fatal("schema " + inmemory.POST_SCHEMA_NAME + " not found") + return + } + + updateSlice := append(existing, newItem) + db.Schemas[inmemory.POST_SCHEMA_NAME] = updateSlice +}