diff --git a/.env.example b/.env.example index 263928f..1a4d26c 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,7 @@ -# Application -PORT= -ALLOWED_ORIGIN= -ACCESS_TOKEN_KEY= -REFRESH_TOKEN_KEY= -ACCESS_TOKEN_EXPIRATION= -REFRESH_TOKEN_EXPIRATION= -AUTO_MIGRATE=true - # PostgreSQL -POSTGRES_HOST= +POSTGRES_HOST= POSTGRES_PORT= POSTGRES_DB= POSTGRES_USER= POSTGRES_PASSWORD= -POSTGRES_SSLMODE= - -# PGAdmin -PGADMIN_EMAIL= -PGADMIN_PASSWORD= -PGADMIN_PORT= \ No newline at end of file +POSTGRES_SSLMODE= \ No newline at end of file diff --git a/.gitignore b/.gitignore index d8783ea..55b355b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -coverage.out -.env \ No newline at end of file +bin/ +.env +coverage.out \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 2ae4828..0000000 --- a/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM golang:1.19-alpine3.17 AS builder - -WORKDIR /app - -COPY go.mod . -COPY go.sum . -RUN go mod download - -COPY . . - -RUN CGO_ENABLED=0 GOOS=linux go build cmd/main.go - -FROM scratch - -WORKDIR /app - -COPY --from=builder /app/main . -COPY --from=builder /app/migrations ./migrations - -CMD ["./main"] \ No newline at end of file diff --git a/Makefile b/Makefile index b393882..517b516 100644 --- a/Makefile +++ b/Makefile @@ -10,25 +10,20 @@ help: @echo 'Usage:' @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' -## dev: run development server -.PHONY: dev +## run: start main app +.PHONY: run dev: - @APP_ENV=dev go run cmd/main.go - -## mocks: generate or update mocks -.PHONY: mocks -mocks: - @cd internal/domain && mockery --all + @go run cmd/main.go ## test: run tests .PHONY: test test: @go test -v -cover -coverprofile coverage.out ./... -## coverage: run test coverage +## cover: run test & show coverage .PHONY: cover cover: test - @go tool cover -html=coverage.out + @go tool cover -func=coverage.out ## migrate/new name=$1: create a new database migration .PHONY: migrate/new diff --git a/README.md b/README.md index 4005678..b0984c2 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,6 @@ See the [Postman documenter](https://documenter.getpostman.com/view/25225683/2s8 Implement [Clean architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) by Robert C. Martin (Uncle Bob) -![project architecture](./docs/architecture.png) - ## License MIT licensed. See the [LICENSE](./LICENSE) file for details. diff --git a/cmd/config/config.go b/cmd/config/config.go deleted file mode 100644 index 3d91221..0000000 --- a/cmd/config/config.go +++ /dev/null @@ -1,66 +0,0 @@ -package config - -import ( - "encoding/json" - "flag" - "log" - "os" - "strconv" - - _ "github.com/joho/godotenv/autoload" - - "github.com/edwintantawi/taskit/pkg/postgres" -) - -type Config struct { - Port string - AllowedOrigin string - AccessTokenKey string - RefreshTokenKey string - AccessTokenExpiration int - RefreshTokenExpiration int - AutoMigrate bool - Postgres postgres.Config -} - -func New() Config { - var config Config - - portEnv := os.Getenv("PORT") - allowedOriginEnv := os.Getenv("ALLOWED_ORIGIN") - accessTokenKeyEnv := os.Getenv("ACCESS_TOKEN_KEY") - refreshTokenKeyEnv := os.Getenv("REFRESH_TOKEN_KEY") - accessTokenExpirationEnv, _ := strconv.Atoi(os.Getenv("ACCESS_TOKEN_EXPIRATION")) - refreshTokenExpirationEnv, _ := strconv.Atoi(os.Getenv("REFRESH_TOKEN_EXPIRATION")) - autoMigrateEnv, _ := strconv.ParseBool(os.Getenv("AUTO_MIGRATE")) - - postgresHost := os.Getenv("POSTGRES_HOST") - postgresPort := os.Getenv("POSTGRES_PORT") - postgresDB := os.Getenv("POSTGRES_DB") - postgresUser := os.Getenv("POSTGRES_USER") - postgresPassword := os.Getenv("POSTGRES_PASSWORD") - postgresSSLModeEnv := os.Getenv("POSTGRES_SSLMODE") - - flag.StringVar(&config.Port, "port", portEnv, "provide http server port address") - flag.StringVar(&config.AllowedOrigin, "allowed-origin", allowedOriginEnv, "provide allowed origin") - flag.StringVar(&config.AccessTokenKey, "access-token-key", accessTokenKeyEnv, "provide access token secret key for jwt") - flag.StringVar(&config.RefreshTokenKey, "refresh-token-key", refreshTokenKeyEnv, "provide refresh token secret key for jwt") - flag.IntVar(&config.AccessTokenExpiration, "access-token-expiration", accessTokenExpirationEnv, "provide access token expiration time in seconds") - flag.IntVar(&config.RefreshTokenExpiration, "refresh-token-expiration", refreshTokenExpirationEnv, "provide refresh token expiration time in seconds") - flag.BoolVar(&config.AutoMigrate, "auto-migrate", autoMigrateEnv, "should auto migrate database (true | false)") - - flag.StringVar(&config.Postgres.Host, "postgres-host", postgresHost, "provide postgres host") - flag.StringVar(&config.Postgres.Port, "postgres-port", postgresPort, "provide postgres port") - flag.StringVar(&config.Postgres.DB, "postgres-db", postgresDB, "provide postgres db") - flag.StringVar(&config.Postgres.User, "postgres-user", postgresUser, "provide postgres user") - flag.StringVar(&config.Postgres.Password, "postgres-password", postgresPassword, "provide postgres password") - flag.StringVar(&config.Postgres.SSLMode, "postgres-sslmode", postgresSSLModeEnv, "provide postgres ssl mode (disable | require)") - - flag.Parse() - - if os.Getenv("APP_ENV") == "dev" { - strCfg, _ := json.MarshalIndent(&config, "", " ") - log.Println("Configuration:", string(strCfg)) - } - return config -} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index a4aed67..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "log" - "net/http" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" - - "github.com/edwintantawi/taskit/cmd/config" - authHTTPHandler "github.com/edwintantawi/taskit/internal/auth/delivery/http" - authMiddleware "github.com/edwintantawi/taskit/internal/auth/delivery/http/middleware" - authRepository "github.com/edwintantawi/taskit/internal/auth/repository" - authUsecase "github.com/edwintantawi/taskit/internal/auth/usecase" - taskHTTPHandler "github.com/edwintantawi/taskit/internal/task/delivery/http" - taskRepository "github.com/edwintantawi/taskit/internal/task/repository" - taskUsecase "github.com/edwintantawi/taskit/internal/task/usecase" - userHTTPHandler "github.com/edwintantawi/taskit/internal/user/delivery/http" - userRepository "github.com/edwintantawi/taskit/internal/user/repository" - userUsecase "github.com/edwintantawi/taskit/internal/user/usecase" - "github.com/edwintantawi/taskit/pkg/httpsvr" - "github.com/edwintantawi/taskit/pkg/idgen" - "github.com/edwintantawi/taskit/pkg/postgres" - "github.com/edwintantawi/taskit/pkg/security" - "github.com/edwintantawi/taskit/pkg/validator" -) - -func main() { - cfg := config.New() - - // Create new postgres connection. - db, migrate := postgres.New(cfg.Postgres) - defer db.Close() - - // Migrate database. - if err := migrate(cfg.AutoMigrate); err != nil { - log.Fatalf("Failed to migrate database: %v", err) - } - - // Create new providers. - hashProvider := security.NewBcrypt() - idProvider := idgen.NewUUID() - validator := validator.New() - jwtProvider := security.NewJWT( - security.JWTTokenConfig{Key: cfg.AccessTokenKey, Exp: cfg.AccessTokenExpiration}, - security.JWTTokenConfig{Key: cfg.RefreshTokenKey, Exp: cfg.RefreshTokenExpiration}, - ) - - // User. - userRepository := userRepository.New(db, &idProvider) - userUsecase := userUsecase.New(&validator, &userRepository, &hashProvider) - userHTTPHandler := userHTTPHandler.New(&validator, &userUsecase) - - // Auth. - authRepository := authRepository.New(db, &idProvider) - authUsecase := authUsecase.New(&validator, &authRepository, &userRepository, &hashProvider, &jwtProvider) - authHTTPHandler := authHTTPHandler.New(&validator, &authUsecase) - authMiddleware := authMiddleware.New(&jwtProvider) - - // Task. - taskRepository := taskRepository.New(db, &idProvider) - taskUsecase := taskUsecase.New(&taskRepository) - taskHTTPHandler := taskHTTPHandler.New(&validator, &taskUsecase) - - // Create new router. - r := chi.NewRouter() - r.Use(middleware.Logger) - r.Use(cors.Handler(cors.Options{ - AllowedOrigins: []string{cfg.AllowedOrigin}, - AllowedMethods: []string{http.MethodOptions, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete}, - AllowedHeaders: []string{"Content-Type", "Authorization"}, - AllowCredentials: true, - })) - - // public routes - r.Group(func(r chi.Router) { - r.Post("/api/users", userHTTPHandler.Post) - - r.Post("/api/authentications", authHTTPHandler.Post) - r.Put("/api/authentications", authHTTPHandler.Put) - }) - - // private routes (need authentication) - r.Group(func(r chi.Router) { - r.Use(authMiddleware.Authenticate) - - r.Get("/api/authentications", authHTTPHandler.Get) - r.Delete("/api/authentications", authHTTPHandler.Delete) - - r.Post("/api/tasks", taskHTTPHandler.Post) - r.Get("/api/tasks", taskHTTPHandler.Get) - r.Get("/api/tasks/{task_id}", taskHTTPHandler.GetByID) - r.Delete("/api/tasks/{task_id}", taskHTTPHandler.Delete) - r.Put("/api/tasks/{task_id}", taskHTTPHandler.Put) - }) - - // Start HTTP server. - log.Printf("Server running at %s", cfg.Port) - svr := httpsvr.New(":"+cfg.Port, r) - if err := svr.Run(); err != nil { - log.Fatal(err) - } -} diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 0b8be91..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,51 +0,0 @@ -version: '2.7' -services: - api: - build: . - restart: always - depends_on: - - postgres - ports: - - ${PORT}:${PORT} - environment: - PORT: ${PORT} - ALLOWED_ORIGIN: ${ALLOWED_ORIGIN} - ACCESS_TOKEN_KEY: ${ACCESS_TOKEN_KEY} - REFRESH_TOKEN_KEY: ${REFRESH_TOKEN_KEY} - ACCESS_TOKEN_EXPIRATION: ${ACCESS_TOKEN_EXPIRATION} - REFRESH_TOKEN_EXPIRATION: ${REFRESH_TOKEN_EXPIRATION} - AUTO_MIGRATE: ${AUTO_MIGRATE} - POSTGRES_HOST: ${POSTGRES_HOST} - POSTGRES_PORT: ${POSTGRES_PORT} - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - POSTGRES_SSLMODE: ${POSTGRES_SSLMODE} - postgres: - image: 'postgres:15.1' - restart: always - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - POSTGRES_PORT: ${POSTGRES_PORT} - POSTGRES_DB: ${POSTGRES_DB} - POSTGRES_USER: ${POSTGRES_USER} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - pgadmin: - image: 'dpage/pgadmin4:6.18' - depends_on: - - postgres - volumes: - - pgadmin-data:/var/lib/pgadmin - environment: - PGADMIN_DEFAULT_EMAIL: ${PGADMIN_EMAIL} - PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_PASSWORD} - PGADMIN_LISTEN_PORT: ${PGADMIN_PORT} - # throw access log to /dev/null instead of stdout - # to reduce log noise - GUNICORN_ACCESS_LOGFILE: '/dev/null' - ports: - - ${PGADMIN_PORT}:${PGADMIN_PORT} -volumes: - postgres-data: - pgadmin-data: diff --git a/docs/architecture.png b/docs/architecture.png deleted file mode 100644 index cb48f7b..0000000 Binary files a/docs/architecture.png and /dev/null differ diff --git a/go.mod b/go.mod index 7760129..4802d04 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,46 @@ module github.com/edwintantawi/taskit go 1.19 require ( - github.com/DATA-DOG/go-sqlmock v1.5.0 - github.com/go-chi/chi/v5 v5.0.8 - github.com/go-chi/cors v1.2.1 - github.com/golang-jwt/jwt/v4 v4.4.3 github.com/golang-migrate/migrate/v4 v4.15.2 github.com/google/uuid v1.3.0 - github.com/joho/godotenv v1.4.0 github.com/lib/pq v1.10.7 + github.com/ory/dockertest/v3 v3.9.1 github.com/stretchr/testify v1.8.1 - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e ) require ( + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.0 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/containerd/continuity v0.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v20.10.23+incompatible // indirect + github.com/docker/docker v20.10.23+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.4 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.8.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/stretchr/objx v0.5.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect go.uber.org/atomic v1.7.0 // indirect + golang.org/x/mod v0.7.0 // indirect + golang.org/x/net v0.5.0 // indirect + golang.org/x/sys v0.4.0 // indirect + golang.org/x/tools v0.5.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 32084f5..41be04b 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,6 @@ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBp github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= @@ -86,8 +84,9 @@ github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JP github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= @@ -103,6 +102,8 @@ github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5 github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -176,6 +177,8 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= @@ -250,7 +253,6 @@ github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoT github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= github.com/containerd/containerd v1.5.8/go.mod h1:YdFSv5bTFLpG2HIYmfqDpSYYTDX+mc5qtSuYx1YUb/s= -github.com/containerd/containerd v1.6.1 h1:oa2uY0/0G+JX4X7hpGCYvkp9FjUancz56kSNnb1sG3o= github.com/containerd/containerd v1.6.1/go.mod h1:1nJz5xCZPusx6jJU8Frfct988y0NpumIq9ODB0kLtoE= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -260,6 +262,8 @@ github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= github.com/containerd/continuity v0.2.2/go.mod h1:pWygW9u7LtS1o4N/Tn0FoCFDIXZ7rxcMX7HX1Dmibvk= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= @@ -333,6 +337,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= @@ -352,14 +357,17 @@ github.com/dhui/dktest v0.3.10 h1:0frpeeoM9pHouHjhLeZDuDTJ0PqjDTrycaHaMmkJAo8= github.com/dhui/dktest v0.3.10/go.mod h1:h5Enh0nG3Qbo9WjNFRrwmKUaePEBhXMOygbz3Ww7Sz0= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v20.10.23+incompatible h1:qwyha/T3rXk9lfuVcn533cKFc7n/6IzL5GXVAgMVPBg= +github.com/docker/cli v20.10.23+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v20.10.13+incompatible h1:5s7uxnKZG+b8hYWlPYUi6x1Sjpq2MSt96d15eLZeHyw= github.com/docker/docker v20.10.13+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v20.10.23+incompatible h1:1ZQUUYAdh+oylOT85aA2ZcfRp22jmLhoaEcVEfK8dyA= +github.com/docker/docker v20.10.23+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= @@ -367,8 +375,9 @@ github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6Uezg github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -409,10 +418,6 @@ github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYis github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0= -github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= -github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= @@ -452,6 +457,7 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= @@ -499,8 +505,6 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU= -github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-migrate/migrate/v4 v4.15.2 h1:vU+M05vs6jWHKDdmE1Ecwj0BznygFc4QsdRe2E/L7kc= github.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2ZdbeUNx4sIwiOK96rE9Lw= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= @@ -538,7 +542,6 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -559,6 +562,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-containerregistry v0.5.1/go.mod h1:Ct15B4yir3PLOP5jsy0GNeYVaIZs/MK/Jz5any1wFW0= github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE= @@ -586,6 +590,8 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -652,6 +658,8 @@ github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/intel/goresctrl v0.2.0/go.mod h1:+CZdzouYFn5EsxgqAQTEzMfwKwuc0fVdMrT9FCCAVRQ= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= @@ -710,8 +718,6 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= @@ -751,9 +757,8 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= @@ -816,6 +821,8 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= @@ -827,8 +834,9 @@ github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGq github.com/moby/sys/symlink v0.2.0/go.mod h1:7uZVF2dqJjG/NsClqul95CqKOBRQyYSNnJ6BMgR/gFs= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= -github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -891,6 +899,8 @@ github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rm github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= github.com/opencontainers/runc v1.1.0/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= +github.com/opencontainers/runc v1.1.4 h1:nRCz/8sKg6K6jgYAFLDlXzPeITBZJyX28DBVhWD+5dg= +github.com/opencontainers/runc v1.1.4/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= @@ -903,6 +913,8 @@ github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3 github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= +github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= @@ -915,7 +927,6 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pierrec/lz4/v4 v4.1.8/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20210706143420-7d21f8c997e2/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -966,9 +977,6 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -983,6 +991,7 @@ github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -995,8 +1004,9 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= @@ -1071,8 +1081,13 @@ github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs= github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= @@ -1166,8 +1181,6 @@ golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= -golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1215,6 +1228,8 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1280,8 +1295,9 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220111093109-d55c255bac03/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1312,6 +1328,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1431,8 +1448,10 @@ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf h1:Fm4IcnUL803i92qDlmB0obyHmosDrxZWxJL3gIeNqOw= golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1534,11 +1553,14 @@ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= +golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= @@ -1657,7 +1679,6 @@ google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 h1:ErU+UA6wxadoU8nWrsy5MZUVBs75K17zUCsUCIfrXCE= google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1691,7 +1712,6 @@ google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9K google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.43.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= @@ -1706,7 +1726,6 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -1739,19 +1758,23 @@ gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/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= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/auth/delivery/http/handler.go b/internal/auth/delivery/http/handler.go deleted file mode 100644 index 24cc436..0000000 --- a/internal/auth/delivery/http/handler.go +++ /dev/null @@ -1,131 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/pkg/errorx" -) - -type HTTPHandler struct { - validator domain.ValidatorProvider - authUsecase domain.AuthUsecase -} - -// New creates a new auth handler -func New(validator domain.ValidatorProvider, authUsecase domain.AuthUsecase) HTTPHandler { - return HTTPHandler{validator: validator, authUsecase: authUsecase} -} - -// POST /authentications to login user -func (h *HTTPHandler) Post(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.AuthLoginIn - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - w.WriteHeader(http.StatusBadRequest) - encoder.Encode(domain.NewErrorResponse(http.StatusBadRequest, "Invalid request body")) - return - } - if err := h.validator.Validate(&payload); err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - output, err := h.authUsecase.Login(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, "Successfully logged in user", output)) -} - -// DELETE /authentications to logout from current authentication -func (h *HTTPHandler) Delete(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.AuthLogoutIn - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - w.WriteHeader(http.StatusBadRequest) - encoder.Encode(domain.NewErrorResponse(http.StatusBadRequest, "Invalid request body")) - return - } - if err := h.validator.Validate(&payload); err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - err := h.authUsecase.Logout(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, "Successfully logout user", nil)) -} - -// GET /authentications to get user authenticated profile -func (h *HTTPHandler) Get(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.AuthProfileIn - payload.UserID = entity.GetAuthContext(r.Context()) - - output, err := h.authUsecase.GetProfile(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), output)) -} - -// PUT /authentications to refresh authentication token -func (h *HTTPHandler) Put(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.AuthRefreshIn - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - w.WriteHeader(http.StatusBadRequest) - encoder.Encode(domain.NewErrorResponse(http.StatusBadRequest, "Invalid request body")) - return - } - if err := h.validator.Validate(&payload); err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - output, err := h.authUsecase.Refresh(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, "Successfully refreshed authentication token", output)) -} diff --git a/internal/auth/delivery/http/handler_test.go b/internal/auth/delivery/http/handler_test.go deleted file mode 100644 index b30e334..0000000 --- a/internal/auth/delivery/http/handler_test.go +++ /dev/null @@ -1,514 +0,0 @@ -package http - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/pkg/errorx" - "github.com/edwintantawi/taskit/test" -) - -type AuthHTTPHandlerTestSuite struct { - suite.Suite -} - -func TestAuthHTTPHandlerSuite(t *testing.T) { - suite.Run(t, new(AuthHTTPHandlerTestSuite)) -} - -type dependency struct { - req *http.Request - validator *mocks.ValidatorProvider - authUsecase *mocks.AuthUsecase -} - -func (s *AuthHTTPHandlerTestSuite) TestPost() { - type args struct { - requestBody []byte - } - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when request body is invalid or not provided", - isError: true, - args: args{ - requestBody: []byte(`{`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusBadRequest, - message: http.StatusText(http.StatusBadRequest), - error: "Invalid request body", - }, - setup: func(d *dependency) {}, - }, - { - name: "it should response with error when payload is not valid", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthLoginIn{}). - Return(test.ErrValidator) - }, - }, - - { - name: "it should response with error when auth usecase Login return unexpected error", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthLoginIn{}). - Return(nil) - - d.authUsecase.On("Login", mock.Anything, &dto.AuthLoginIn{}). - Return(dto.AuthLoginOut{}, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: "Successfully logged in user", - payload: map[string]any{ - "access_token": "xxxxx.xxxxx.xxxxx", - "refresh_token": "yyyyy.yyyyy.yyyyy", - }, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthLoginIn{}). - Return(nil) - - d.authUsecase.On("Login", mock.Anything, &dto.AuthLoginIn{}). - Return(dto.AuthLoginOut{AccessToken: "xxxxx.xxxxx.xxxxx", RefreshToken: "yyyyy.yyyyy.yyyyy"}, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - reqBody := bytes.NewReader(t.args.requestBody) - rr := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", reqBody) - - d := &dependency{ - validator: &mocks.ValidatorProvider{}, - authUsecase: &mocks.AuthUsecase{}, - } - t.setup(d) - - handler := New(d.validator, d.authUsecase) - handler.Post(rr, req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - payloadMap := resBody.Payload.(map[string]any) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.payload, payloadMap) - } - }) - } -} - -func (s *AuthHTTPHandlerTestSuite) TestDelete() { - type args struct { - requestBody []byte - } - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when request body is invalid or not provided", - isError: true, - args: args{ - requestBody: []byte(`{`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusBadRequest, - message: http.StatusText(http.StatusBadRequest), - error: "Invalid request body", - }, - setup: func(d *dependency) {}, - }, - { - name: "it should response with error when payload is not valid", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthLogoutIn{}). - Return(test.ErrValidator) - }, - }, - { - name: "it should response with error when auth usecase Logout return unexpected error", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthLogoutIn{}). - Return(nil) - - d.authUsecase.On("Logout", mock.Anything, &dto.AuthLogoutIn{}). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: "Successfully logout user", - payload: nil, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthLogoutIn{}). - Return(nil) - - d.authUsecase.On("Logout", mock.Anything, &dto.AuthLogoutIn{}). - Return(nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - reqBody := bytes.NewReader(t.args.requestBody) - rr := httptest.NewRecorder() - req := httptest.NewRequest("DELETE", "/", reqBody) - - d := &dependency{ - validator: &mocks.ValidatorProvider{}, - authUsecase: &mocks.AuthUsecase{}, - } - t.setup(d) - - handler := New(d.validator, d.authUsecase) - handler.Delete(rr, req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Nil(resBody.Payload) - } - }) - } -} - -func (s *AuthHTTPHandlerTestSuite) TestGet() { - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when auth usecase GetProfile return unexpected error", - isError: true, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.authUsecase.On("GetProfile", mock.Anything, &dto.AuthProfileIn{UserID: entity.UserID("user-xxxxx")}). - Return(dto.AuthProfileOut{}, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: http.StatusText(http.StatusOK), - payload: map[string]any{ - "id": "user-xxxxx", - "name": "Gopher", - "email": "gopher@go.dev", - }, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.authUsecase.On("GetProfile", mock.Anything, &dto.AuthProfileIn{UserID: entity.UserID("user-xxxxx")}). - Return(dto.AuthProfileOut{ID: "user-xxxxx", Name: "Gopher", Email: "gopher@go.dev"}, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) - - d := &dependency{ - req: req, - validator: &mocks.ValidatorProvider{}, - authUsecase: &mocks.AuthUsecase{}, - } - t.setup(d) - - handler := New(d.validator, d.authUsecase) - handler.Get(rr, d.req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - payloadMap := resBody.Payload.(map[string]any) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.payload, payloadMap) - } - }) - } -} - -func (s *AuthHTTPHandlerTestSuite) TestPut() { - type args struct { - requestBody []byte - } - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when request body is invalid or not provided", - isError: true, - args: args{ - requestBody: []byte(`{`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusBadRequest, - message: http.StatusText(http.StatusBadRequest), - error: "Invalid request body", - }, - setup: func(d *dependency) {}, - }, - { - name: "it should response with error when payload is not valid", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthRefreshIn{}). - Return(test.ErrValidator) - }, - }, - { - name: "it should response with error when auth usecase Refresh return unexpected error", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthRefreshIn{}). - Return(nil) - - d.authUsecase.On("Refresh", mock.Anything, &dto.AuthRefreshIn{}). - Return(dto.AuthRefreshOut{}, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: "Successfully refreshed authentication token", - payload: map[string]any{ - "access_token": "xxxxx.xxxxx.xxxxx", - "refresh_token": "yyyyy.yyyyy.yyyyy", - }, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.AuthRefreshIn{}). - Return(nil) - - d.authUsecase.On("Refresh", mock.Anything, &dto.AuthRefreshIn{}). - Return(dto.AuthRefreshOut{AccessToken: "xxxxx.xxxxx.xxxxx", RefreshToken: "yyyyy.yyyyy.yyyyy"}, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - reqBody := bytes.NewReader(t.args.requestBody) - rr := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/", reqBody) - - d := &dependency{ - validator: &mocks.ValidatorProvider{}, - authUsecase: &mocks.AuthUsecase{}, - } - t.setup(d) - - handler := New(d.validator, d.authUsecase) - handler.Put(rr, req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.payload, resBody.Payload) - } - }) - } -} diff --git a/internal/auth/delivery/http/middleware/middleware.go b/internal/auth/delivery/http/middleware/middleware.go deleted file mode 100644 index b4eaedb..0000000 --- a/internal/auth/delivery/http/middleware/middleware.go +++ /dev/null @@ -1,48 +0,0 @@ -package middleware - -import ( - "context" - "encoding/json" - "net/http" - "strings" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/pkg/errorx" -) - -type Middleware struct { - jwtProvider domain.JWTProvider -} - -// New creates a new HTTP auth middleware. -func New(jwtProvider domain.JWTProvider) Middleware { - return Middleware{jwtProvider: jwtProvider} -} - -// Authenticate authenticates the request. -func (m *Middleware) Authenticate(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - bearerToken := r.Header.Get("Authorization") - if !strings.Contains(bearerToken, "Bearer") { - w.WriteHeader(http.StatusUnauthorized) - encoder.Encode(domain.NewErrorResponse(http.StatusUnauthorized, "Authentication bearer token are not provided")) - return - } - - rawToken := strings.TrimPrefix(bearerToken, "Bearer ") - userId, err := m.jwtProvider.VerifyAccessToken(rawToken) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - ctx := context.WithValue(r.Context(), entity.AuthUserIDKey, userId) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} diff --git a/internal/auth/delivery/http/middleware/middleware_test.go b/internal/auth/delivery/http/middleware/middleware_test.go deleted file mode 100644 index 8e550c8..0000000 --- a/internal/auth/delivery/http/middleware/middleware_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package middleware - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/pkg/errorx" - "github.com/edwintantawi/taskit/test" -) - -type HTTPAuthMiddlewareTestSuite struct { - suite.Suite -} - -func TestHTTPAuthMiddlewareSuite(t *testing.T) { - suite.Run(t, new(HTTPAuthMiddlewareTestSuite)) -} - -type dependency struct { - req *http.Request - jwtProvider *mocks.JWTProvider -} - -func (s *HTTPAuthMiddlewareTestSuite) TestAuthentication() { - type args struct { - handler http.Handler - } - type expected struct { - contentType string - statusCode int - message string - error string - body string - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when authorization header is not provided", - isError: true, - args: args{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusUnauthorized, - message: http.StatusText(http.StatusUnauthorized), - error: "Authentication bearer token are not provided", - }, - setup: func(d *dependency) { - d.req.Header.Del("Authorization") - }, - }, - { - name: "it should return error when authorization header token is not valid", - isError: true, - args: args{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req.Header.Set("Authorization", "Bearer xxxxx.xxxxx.xxxxx") - - d.jwtProvider.On("VerifyAccessToken", "xxxxx.xxxxx.xxxxx"). - Return(entity.UserID(""), test.ErrUnexpected) - }, - }, - { - name: "it should forward to next handler when authorization header is valid", - isError: false, - args: args{ - handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - userID := entity.GetAuthContext(r.Context()) - w.Write([]byte(userID)) - }), - }, - expected: expected{ - statusCode: http.StatusOK, - body: "user-xxxxx", - }, - setup: func(d *dependency) { - d.req.Header.Set("Authorization", "Bearer xxxxx.xxxxx.xxxxx") - - d.jwtProvider.On("VerifyAccessToken", "xxxxx.xxxxx.xxxxx"). - Return(entity.UserID("user-xxxxx"), nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - req := httptest.NewRequest("GET", "/", nil) - dep := &dependency{ - jwtProvider: &mocks.JWTProvider{}, - req: req, - } - t.setup(dep) - - rr := httptest.NewRecorder() - middleware := New(dep.jwtProvider) - handler := middleware.Authenticate(t.args.handler) - - handler.ServeHTTP(rr, dep.req) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - s.Equal(t.expected.statusCode, rr.Code) - s.Equal(t.expected.body, rr.Body.String()) - } - }) - } -} diff --git a/internal/auth/repository/repository.go b/internal/auth/repository/repository.go deleted file mode 100644 index 10d707f..0000000 --- a/internal/auth/repository/repository.go +++ /dev/null @@ -1,69 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "errors" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -type Repository struct { - db *sql.DB - idProvider domain.IDProvider -} - -// New create a new auth repository. -func New(db *sql.DB, idProvider domain.IDProvider) Repository { - return Repository{db: db, idProvider: idProvider} -} - -// Store save a new auth to database. -func (r *Repository) Store(ctx context.Context, a *entity.Auth) error { - id := r.idProvider.Generate() - q := `INSERT INTO authentications (id, user_id, token, expires_at) VALUES ($1, $2, $3, $4)` - _, err := r.db.ExecContext(ctx, q, id, a.UserID, a.Token, a.ExpiresAt) - if err != nil { - return err - } - return nil -} - -// VerifyAvailableByToken check if a authentication is available by token. -func (r *Repository) VerifyAvailableByToken(ctx context.Context, token string) error { - var id entity.AuthID - q := `SELECT id FROM authentications WHERE token = $1` - row := r.db.QueryRowContext(ctx, q, token) - err := row.Scan(&id) - if errors.Is(err, sql.ErrNoRows) { - return domain.ErrAuthNotFound - } else if err != nil { - return err - } - return nil -} - -// Delete remove an auth from database. -func (r *Repository) DeleteByToken(ctx context.Context, token string) error { - q := `DELETE FROM authentications WHERE token = $1` - _, err := r.db.ExecContext(ctx, q, token) - if err != nil { - return err - } - return nil -} - -// FindByToken find an auth by token. -func (r *Repository) FindByToken(ctx context.Context, token string) (entity.Auth, error) { - var a entity.Auth - q := `SELECT id, user_id, token, expires_at FROM authentications WHERE token = $1` - row := r.db.QueryRowContext(ctx, q, token) - err := row.Scan(&a.ID, &a.UserID, &a.Token, &a.ExpiresAt) - if errors.Is(err, sql.ErrNoRows) { - return a, domain.ErrAuthNotFound - } else if err != nil { - return a, err - } - return a, nil -} diff --git a/internal/auth/repository/repository_test.go b/internal/auth/repository/repository_test.go deleted file mode 100644 index 905577e..0000000 --- a/internal/auth/repository/repository_test.go +++ /dev/null @@ -1,374 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "regexp" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/test" -) - -type AuthRepositoryTestSuite struct { - suite.Suite -} - -func TestAuthRepositorySuite(t *testing.T) { - suite.Run(t, new(AuthRepositoryTestSuite)) -} - -type dependency struct { - mockDB sqlmock.Sqlmock - idProvider *mocks.IDProvider -} - -func (s *AuthRepositoryTestSuite) TestStore() { - type args struct { - ctx context.Context - auth *entity.Auth - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to store", - args: args{ - ctx: context.Background(), - auth: &entity.Auth{ - UserID: "user-xxxxx", - Token: "yyyyy.yyyyy.yyyyy", - ExpiresAt: test.TimeAfterNow, - }}, - expected: expected{ - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.idProvider.On("Generate").Return(string("auth-xxxxx")) - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO authentications (id, user_id, token, expires_at) VALUES ($1, $2, $3, $4)`)). - WithArgs("auth-xxxxx", "user-xxxxx", "yyyyy.yyyyy.yyyyy", test.TimeAfterNow). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error nil when successfully store", - args: args{ - ctx: context.Background(), - auth: &entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}, - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - d.idProvider.On("Generate").Return(string("auth-xxxxx")) - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO authentications (id, user_id, token, expires_at) VALUES ($1, $2, $3, $4)`)). - WithArgs("auth-xxxxx", "user-xxxxx", "yyyyy.yyyyy.yyyyy", test.TimeAfterNow). - WillReturnResult(sqlmock.NewResult(1, 1)) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - idProvider: &mocks.IDProvider{}, - } - t.setup(d) - - repository := New(db, d.idProvider) - err = repository.Store(t.args.ctx, t.args.auth) - - s.Equal(t.expected.err, err) - }) - } -} - -func (s *AuthRepositoryTestSuite) TestVerifyAvailableByID() { - type args struct { - ctx context.Context - token string - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error when authentication not found", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - err: domain.ErrAuthNotFound, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnError(sql.ErrNoRows) - }, - }, - { - name: "it should return error when database fail to scan", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - err: test.ErrRowScan, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnError(test.ErrRowScan) - }, - }, - { - name: "it should return error nil when authentication found", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id"}).AddRow("auth-xxxxx") - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnRows(mockRow) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - err = repository.VerifyAvailableByToken(t.args.ctx, t.args.token) - - s.Equal(t.expected.err, err) - }) - } -} - -func (s *AuthRepositoryTestSuite) TestDelete() { - type args struct { - ctx context.Context - token string - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to delete", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta(`DELETE FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error nil when successfully delete", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta(`DELETE FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnResult(sqlmock.NewResult(1, 1)) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - err = repository.DeleteByToken(t.args.ctx, t.args.token) - - s.Equal(t.expected.err, err) - }) - } -} - -func (s *AuthRepositoryTestSuite) TestFindByToken() { - type args struct { - ctx context.Context - token string - } - type expected struct { - auth entity.Auth - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to find", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - auth: entity.Auth{}, - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, token, expires_at FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error ErrAuthNotFound when row not found", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - auth: entity.Auth{}, - err: domain.ErrAuthNotFound, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, token, expires_at FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnError(sql.ErrNoRows) - }, - }, - { - name: "it should return error when fail to scan row", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - auth: entity.Auth{}, - err: test.ErrRowScan, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, token, expires_at FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnError(test.ErrRowScan) - }, - }, - { - name: "it should return error nil and authentication when found", - args: args{ - ctx: context.Background(), - token: "yyyyy.yyyyy.yyyyy", - }, - expected: expected{ - auth: entity.Auth{ - ID: "auth-xxxxx", - UserID: "user-xxxxx", - Token: "yyyyy.yyyyy.yyyyy", - ExpiresAt: test.TimeAfterNow, - }, - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "user_id", "token", "expires_at"}). - AddRow("auth-xxxxx", "user-xxxxx", "yyyyy.yyyyy.yyyyy", test.TimeAfterNow) - - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, user_id, token, expires_at FROM authentications WHERE token = $1`)). - WithArgs("yyyyy.yyyyy.yyyyy"). - WillReturnRows(mockRow) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - auth, err := repository.FindByToken(t.args.ctx, t.args.token) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.auth, auth) - }) - } -} diff --git a/internal/auth/usecase/usecase.go b/internal/auth/usecase/usecase.go deleted file mode 100644 index 2e16dbd..0000000 --- a/internal/auth/usecase/usecase.go +++ /dev/null @@ -1,124 +0,0 @@ -package usecase - -import ( - "context" - "errors" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -type Usecase struct { - validator domain.ValidatorProvider - authRepository domain.AuthRepository - userRepository domain.UserRepository - hashProvider domain.HashProvider - jwtProvider domain.JWTProvider -} - -// New create a new auth usecase. -func New( - validator domain.ValidatorProvider, - authRepository domain.AuthRepository, - userRepository domain.UserRepository, - hashProvider domain.HashProvider, - jwtProvider domain.JWTProvider, -) Usecase { - return Usecase{ - validator: validator, - authRepository: authRepository, - userRepository: userRepository, - hashProvider: hashProvider, - jwtProvider: jwtProvider, - } -} - -// Login authenticates a user. -func (u *Usecase) Login(ctx context.Context, payload *dto.AuthLoginIn) (dto.AuthLoginOut, error) { - user := entity.User{Email: payload.Email, Password: payload.Password} - if err := u.validator.Validate(&user); err != nil { - return dto.AuthLoginOut{}, err - } - - targetUser, err := u.userRepository.FindByEmail(ctx, user.Email) - if errors.Is(err, domain.ErrUserNotFound) { - return dto.AuthLoginOut{}, domain.ErrEmailNotExist - } else if err != nil { - return dto.AuthLoginOut{}, err - } - - if err := u.hashProvider.Compare(user.Password, targetUser.Password); err != nil { - return dto.AuthLoginOut{}, domain.ErrPasswordIncorrect - } - - accessToken, _, err := u.jwtProvider.GenerateAccessToken(targetUser.ID) - if err != nil { - return dto.AuthLoginOut{}, err - } - refreshToken, expires, err := u.jwtProvider.GenerateRefreshToken(targetUser.ID) - if err != nil { - return dto.AuthLoginOut{}, err - } - - auth := &entity.Auth{UserID: targetUser.ID, Token: refreshToken, ExpiresAt: expires} - if err := u.authRepository.Store(ctx, auth); err != nil { - return dto.AuthLoginOut{}, err - } - - return dto.AuthLoginOut{AccessToken: accessToken, RefreshToken: refreshToken}, nil -} - -// Logout remove user authentication. -func (u *Usecase) Logout(ctx context.Context, payload *dto.AuthLogoutIn) error { - auth := &entity.Auth{Token: payload.RefreshToken} - - if err := u.authRepository.VerifyAvailableByToken(ctx, auth.Token); err != nil { - return err - } - if err := u.authRepository.DeleteByToken(ctx, auth.Token); err != nil { - return err - } - - return nil -} - -// GetProfile get user authenticated profile. -func (u *Usecase) GetProfile(ctx context.Context, payload *dto.AuthProfileIn) (dto.AuthProfileOut, error) { - user, err := u.userRepository.FindByID(ctx, payload.UserID) - if err != nil { - return dto.AuthProfileOut{}, err - } - return dto.AuthProfileOut{ID: user.ID, Name: user.Name, Email: user.Email}, nil -} - -// Refresh refresh user authentication token. -func (u *Usecase) Refresh(ctx context.Context, payload *dto.AuthRefreshIn) (dto.AuthRefreshOut, error) { - auth, err := u.authRepository.FindByToken(ctx, payload.RefreshToken) - if err != nil { - return dto.AuthRefreshOut{}, err - } - if err := auth.VerifyTokenExpires(); err != nil { - return dto.AuthRefreshOut{}, err - } - - accessToken, _, err := u.jwtProvider.GenerateAccessToken(auth.UserID) - if err != nil { - return dto.AuthRefreshOut{}, err - } - refreshToken, expires, err := u.jwtProvider.GenerateRefreshToken(auth.UserID) - if err != nil { - return dto.AuthRefreshOut{}, err - } - - if err := u.authRepository.DeleteByToken(ctx, auth.Token); err != nil { - return dto.AuthRefreshOut{}, err - } - - newAuth := &entity.Auth{UserID: auth.UserID, Token: refreshToken, ExpiresAt: expires} - if err := u.authRepository.Store(ctx, newAuth); err != nil { - return dto.AuthRefreshOut{}, err - } - - return dto.AuthRefreshOut{AccessToken: accessToken, RefreshToken: refreshToken}, nil -} diff --git a/internal/auth/usecase/usecase_test.go b/internal/auth/usecase/usecase_test.go deleted file mode 100644 index 6defa3c..0000000 --- a/internal/auth/usecase/usecase_test.go +++ /dev/null @@ -1,631 +0,0 @@ -package usecase - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/test" -) - -type AuthUsecaseTestSuite struct { - suite.Suite -} - -func TestAuthUsecaseSuite(t *testing.T) { - suite.Run(t, new(AuthUsecaseTestSuite)) -} - -type dependency struct { - validator *mocks.ValidatorProvider - authRepository *mocks.AuthRepository - userRepository *mocks.UserRepository - hashProvider *mocks.HashProvider - jwtProvider *mocks.JWTProvider -} - -func (s *AuthUsecaseTestSuite) TestLogin() { - type args struct { - ctx context.Context - payload *dto.AuthLoginIn - } - type expected struct { - output dto.AuthLoginOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when user validation failed", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{}, - }, - expected: expected{ - output: dto.AuthLoginOut{}, - err: test.ErrValidator, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(test.ErrValidator) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{}, domain.ErrUserNotFound) - }, - }, - { - name: "it should return error ErrEmailNotExist when email not found", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{ - Email: "gopher@go.dev", - }, - }, - expected: expected{ - output: dto.AuthLoginOut{}, - err: domain.ErrEmailNotExist, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{}, domain.ErrUserNotFound) - }, - }, - { - name: "it should return error when user repository FindByEmail return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{ - Email: "gopher@go.dev", - }, - }, - expected: expected{ - output: dto.AuthLoginOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error ErrPasswordIncorrect when password is incorrect", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{ - Email: "gopher@go.dev", - Password: "secret_password", - }, - }, - expected: expected{ - output: dto.AuthLoginOut{}, - err: domain.ErrPasswordIncorrect, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{Password: "secret_hashed_password"}, nil) - - d.hashProvider.On("Compare", "secret_password", "secret_hashed_password"). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error when generate access token failed", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{ - Email: "gopher@go.dev", - Password: "secret_password", - }, - }, - expected: expected{ - output: dto.AuthLoginOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{ID: "user-xxxxx", Password: "secret_hashed_password"}, nil) - - d.hashProvider.On("Compare", "secret_password", "secret_hashed_password"). - Return(nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("", time.Time{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error when generate refresh token failed", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{ - Email: "gopher@go.dev", - Password: "secret_password", - }, - }, - expected: expected{ - output: dto.AuthLoginOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{ID: "user-xxxxx", Password: "secret_hashed_password"}, nil) - - d.hashProvider.On("Compare", "secret_password", "secret_hashed_password"). - Return(nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("xxxxx.xxxxx.xxxxx", test.TimeAfterNow, nil) - - d.jwtProvider.On("GenerateRefreshToken", entity.UserID("user-xxxxx")). - Return("", time.Time{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error when auth respository Store return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{ - Email: "gopher@go.dev", - Password: "secret_password", - }, - }, - expected: expected{ - output: dto.AuthLoginOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{ID: "user-xxxxx", Password: "secret_hashed_password"}, nil) - - d.hashProvider.On("Compare", "secret_password", "secret_hashed_password"). - Return(nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("xxxxx.xxxxx.xxxxx", test.TimeAfterNow, nil) - - d.jwtProvider.On("GenerateRefreshToken", entity.UserID("user-xxxxx")). - Return("yyyyy.yyyyy.yyyyy", test.TimeAfterNow, nil) - - d.authRepository.On("Store", context.Background(), &entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error nil and output when success", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLoginIn{ - Email: "gopher@go.dev", - Password: "secret_password", - }, - }, - expected: expected{ - output: dto.AuthLoginOut{ - AccessToken: "xxxxx.xxxxx.xxxxx", - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - err: nil, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.userRepository.On("FindByEmail", context.Background(), "gopher@go.dev"). - Return(entity.User{ID: "user-xxxxx", Password: "secret_hashed_password"}, nil) - - d.hashProvider.On("Compare", "secret_password", "secret_hashed_password"). - Return(nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("xxxxx.xxxxx.xxxxx", test.TimeAfterNow, nil) - - d.jwtProvider.On("GenerateRefreshToken", entity.UserID("user-xxxxx")). - Return("yyyyy.yyyyy.yyyyy", test.TimeAfterNow, nil) - - d.authRepository.On("Store", context.Background(), &entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}). - Return(nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - validator: &mocks.ValidatorProvider{}, - userRepository: &mocks.UserRepository{}, - authRepository: &mocks.AuthRepository{}, - hashProvider: &mocks.HashProvider{}, - jwtProvider: &mocks.JWTProvider{}, - } - t.setup(d) - - usecase := New(d.validator, d.authRepository, d.userRepository, d.hashProvider, d.jwtProvider) - output, err := usecase.Login(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - }) - } -} - -func (s *AuthUsecaseTestSuite) TestLogout() { - type args struct { - ctx context.Context - payload *dto.AuthLogoutIn - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when auth VerifyAvailableByToken return error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLogoutIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.authRepository.On("VerifyAvailableByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error when auth Delete repository return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLogoutIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.authRepository.On("VerifyAvailableByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(nil) - - d.authRepository.On("DeleteByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error nil when successfully delete authentication", - args: args{ - ctx: context.Background(), - payload: &dto.AuthLogoutIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - d.authRepository.On("VerifyAvailableByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(nil) - - d.authRepository.On("DeleteByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - authRepository: &mocks.AuthRepository{}, - } - t.setup(d) - - usecase := New(nil, d.authRepository, d.userRepository, d.hashProvider, d.jwtProvider) - err := usecase.Logout(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - }) - } -} - -func (s *AuthUsecaseTestSuite) TestGetProfile() { - type args struct { - ctx context.Context - payload *dto.AuthProfileIn - } - type expected struct { - output dto.AuthProfileOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when user repository FindByID return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthProfileIn{ - UserID: "user-xxxxx", - }, - }, - expected: expected{ - output: dto.AuthProfileOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.userRepository.On("FindByID", context.Background(), entity.UserID("user-xxxxx")). - Return(entity.User{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error nil and output when success", - args: args{ - ctx: context.Background(), - payload: &dto.AuthProfileIn{ - UserID: "user-xxxxx", - }, - }, - expected: expected{ - output: dto.AuthProfileOut{ - ID: "user-xxxxx", - Name: "Gopher", - Email: "gopher@go.dev", - }, - err: nil, - }, - setup: func(d *dependency) { - d.userRepository.On("FindByID", context.Background(), entity.UserID("user-xxxxx")). - Return(entity.User{ID: entity.UserID("user-xxxxx"), Name: "Gopher", Email: "gopher@go.dev"}, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - userRepository: &mocks.UserRepository{}, - } - t.setup(d) - - usecase := New(nil, d.authRepository, d.userRepository, d.hashProvider, d.jwtProvider) - output, err := usecase.GetProfile(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - }) - } -} - -func (s *AuthUsecaseTestSuite) TestRefresh() { - type args struct { - ctx context.Context - payload *dto.AuthRefreshIn - } - type expected struct { - output dto.AuthRefreshOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when auth repository FindByToken return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthRefreshIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - output: dto.AuthRefreshOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.authRepository.On("FindByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(entity.Auth{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error ErrAuthTokenExpired when token is expired", - args: args{ - ctx: context.Background(), - payload: &dto.AuthRefreshIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - output: dto.AuthRefreshOut{}, - err: entity.ErrAuthTokenExpired, - }, - setup: func(d *dependency) { - d.authRepository.On("FindByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(entity.Auth{ExpiresAt: test.TimeBeforeNow}, nil) - }, - }, - { - name: "it should return error when generate new access token failed", - args: args{ - ctx: context.Background(), - payload: &dto.AuthRefreshIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - output: dto.AuthRefreshOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.authRepository.On("FindByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}, nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("", time.Time{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error when generate new refresh token failed", - args: args{ - ctx: context.Background(), - payload: &dto.AuthRefreshIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - output: dto.AuthRefreshOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.authRepository.On("FindByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}, nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("xxxxx.xxxxx.xxxxx", test.TimeAfterNow, nil) - - d.jwtProvider.On("GenerateRefreshToken", entity.UserID("user-xxxxx")). - Return("", time.Time{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error when auth respository Delete return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthRefreshIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - output: dto.AuthRefreshOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.authRepository.On("FindByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}, nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("xxxxx.xxxxx.xxxxx", test.TimeAfterNow, nil) - - d.jwtProvider.On("GenerateRefreshToken", entity.UserID("user-xxxxx")). - Return("zzzzz.zzzzz.zzzzz", test.TimeAfterNow, nil) - - d.authRepository.On("DeleteByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error when auth respository Store return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.AuthRefreshIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - output: dto.AuthRefreshOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.authRepository.On("FindByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}, nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("xxxxx.xxxxx.xxxxx", test.TimeAfterNow, nil) - - d.jwtProvider.On("GenerateRefreshToken", entity.UserID("user-xxxxx")). - Return("zzzzz.zzzzz.zzzzz", test.TimeAfterNow, nil) - - d.authRepository.On("DeleteByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(nil) - - d.authRepository.On("Store", context.Background(), &entity.Auth{UserID: "user-xxxxx", Token: "zzzzz.zzzzz.zzzzz", ExpiresAt: test.TimeAfterNow}). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error nil and output when success", - args: args{ - ctx: context.Background(), - payload: &dto.AuthRefreshIn{ - RefreshToken: "yyyyy.yyyyy.yyyyy", - }, - }, - expected: expected{ - output: dto.AuthRefreshOut{ - AccessToken: "xxxxx.xxxxx.xxxxx", - RefreshToken: "zzzzz.zzzzz.zzzzz", - }, - err: nil, - }, - setup: func(d *dependency) { - d.authRepository.On("FindByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(entity.Auth{UserID: "user-xxxxx", Token: "yyyyy.yyyyy.yyyyy", ExpiresAt: test.TimeAfterNow}, nil) - - d.jwtProvider.On("GenerateAccessToken", entity.UserID("user-xxxxx")). - Return("xxxxx.xxxxx.xxxxx", test.TimeAfterNow, nil) - - d.jwtProvider.On("GenerateRefreshToken", entity.UserID("user-xxxxx")). - Return("zzzzz.zzzzz.zzzzz", test.TimeAfterNow, nil) - - d.authRepository.On("DeleteByToken", context.Background(), "yyyyy.yyyyy.yyyyy"). - Return(nil) - - d.authRepository.On("Store", context.Background(), &entity.Auth{UserID: "user-xxxxx", Token: "zzzzz.zzzzz.zzzzz", ExpiresAt: test.TimeAfterNow}). - Return(nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - authRepository: &mocks.AuthRepository{}, - jwtProvider: &mocks.JWTProvider{}, - } - t.setup(d) - - usecase := New(nil, d.authRepository, d.userRepository, d.hashProvider, d.jwtProvider) - output, err := usecase.Refresh(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - }) - } -} diff --git a/internal/domain/dto/auth.go b/internal/domain/dto/auth.go deleted file mode 100644 index 172f14e..0000000 --- a/internal/domain/dto/auth.go +++ /dev/null @@ -1,69 +0,0 @@ -package dto - -import "github.com/edwintantawi/taskit/internal/domain/entity" - -// AuthLoginIn represent login input. -type AuthLoginIn struct { - Email string `json:"email"` - Password string `json:"password"` -} - -func (a *AuthLoginIn) Validate() error { - switch { - case a.Email == "": - return ErrEmailEmpty - case a.Password == "": - return ErrPasswordEmpty - } - return nil -} - -// AuthLoginOut represent login output. -type AuthLoginOut struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` -} - -// AuthLogoutIn represent logout input. -type AuthLogoutIn struct { - RefreshToken string `json:"refresh_token"` -} - -func (a *AuthLogoutIn) Validate() error { - switch { - case a.RefreshToken == "": - return ErrRefreshTokenEmpty - } - return nil -} - -// AuthProfileIn represent get profile input. -type AuthProfileIn struct { - UserID entity.UserID `json:"-"` -} - -// AuthProfileOut represent get profile output. -type AuthProfileOut struct { - ID entity.UserID `json:"id"` - Name string `json:"name"` - Email string `json:"email"` -} - -// AuthRefreshIn represent refresh input. -type AuthRefreshIn struct { - RefreshToken string `json:"refresh_token"` -} - -func (a *AuthRefreshIn) Validate() error { - switch { - case a.RefreshToken == "": - return ErrRefreshTokenEmpty - } - return nil -} - -// AuthRefreshOut represent refresh output. -type AuthRefreshOut struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` -} diff --git a/internal/domain/dto/auth_test.go b/internal/domain/dto/auth_test.go deleted file mode 100644 index 8e3d220..0000000 --- a/internal/domain/dto/auth_test.go +++ /dev/null @@ -1,70 +0,0 @@ -package dto - -import ( - "testing" - - "github.com/stretchr/testify/suite" -) - -type AuthDTOTestSuite struct { - suite.Suite -} - -func TestAuthDTOSuite(t *testing.T) { - suite.Run(t, new(AuthDTOTestSuite)) -} - -func (s *AuthDTOTestSuite) TestAuthLoginIn() { - tests := []struct { - name string - input AuthLoginIn - expected error - }{ - {name: "it should return error when email is empty", input: AuthLoginIn{}, expected: ErrEmailEmpty}, - {name: "it should return error when password is empty", input: AuthLoginIn{Email: "gopher@go.dev"}, expected: ErrPasswordEmpty}, - {name: "it should return nil when all fields are valid", input: AuthLoginIn{Email: "gopher@go.dev", Password: "secret_password"}, expected: nil}, - } - - for _, test := range tests { - s.Run(test.name, func() { - err := test.input.Validate() - s.Equal(test.expected, err) - }) - } -} - -func (s *AuthDTOTestSuite) TestAuthLogoutIn() { - tests := []struct { - name string - input AuthLogoutIn - expected error - }{ - {name: "it should return error when password is empty", input: AuthLogoutIn{RefreshToken: ""}, expected: ErrRefreshTokenEmpty}, - {name: "it should return nil when all fields are valid", input: AuthLogoutIn{RefreshToken: "yyyyy.yyyyy.yyyyy"}, expected: nil}, - } - - for _, test := range tests { - s.Run(test.name, func() { - err := test.input.Validate() - s.Equal(test.expected, err) - }) - } -} - -func (s *AuthDTOTestSuite) TestAuthRefreshIn() { - tests := []struct { - name string - input AuthRefreshIn - expected error - }{ - {name: "it should return error when password is empty", input: AuthRefreshIn{RefreshToken: ""}, expected: ErrRefreshTokenEmpty}, - {name: "it should return nil when all fields are valid", input: AuthRefreshIn{RefreshToken: "yyyyy.yyyyy.yyyyy"}, expected: nil}, - } - - for _, test := range tests { - s.Run(test.name, func() { - err := test.input.Validate() - s.Equal(test.expected, err) - }) - } -} diff --git a/internal/domain/dto/errors.go b/internal/domain/dto/errors.go deleted file mode 100644 index 05288a8..0000000 --- a/internal/domain/dto/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package dto - -import "errors" - -var ( - ErrEmailEmpty = errors.New("dto.email_empty") - ErrPasswordEmpty = errors.New("dto.password_empty") - ErrNameEmpty = errors.New("dto.name_empty") - - ErrRefreshTokenEmpty = errors.New("dto.refresh_token_empty") - - ErrContentEmpty = errors.New("dto.content_empty") -) diff --git a/internal/domain/dto/task.go b/internal/domain/dto/task.go deleted file mode 100644 index 637308f..0000000 --- a/internal/domain/dto/task.go +++ /dev/null @@ -1,90 +0,0 @@ -package dto - -import ( - "time" - - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -// TaskCreateIn represents the input of task creation. -type TaskCreateIn struct { - UserID entity.UserID `json:"-"` - Content string `json:"content"` - Description string `json:"description"` - DueDate entity.NullTime `json:"due_date"` -} - -func (t *TaskCreateIn) Validate() error { - switch { - case t.Content == "": - return ErrContentEmpty - } - return nil -} - -// TaskCreateOut represents the output of task creation. -type TaskCreateOut struct { - ID entity.TaskID `json:"id"` -} - -// TaskGetAllIn represents the input of task retrieval. -type TaskGetAllIn struct { - UserID entity.UserID `json:"-"` -} - -// TaskGetAllOut represents the output of task retrieval. -type TaskGetAllOut struct { - ID entity.TaskID `json:"id"` - Content string `json:"content"` - Description string `json:"description"` - IsCompleted bool `json:"is_completed"` - DueDate entity.NullTime `json:"due_date"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// TaskRemoveIn represents the input of task removal. -type TaskRemoveIn struct { - TaskID entity.TaskID `json:"-"` - UserID entity.UserID `json:"-"` -} - -// TaskGetByIDIn represents the input of task retrieval. -type TaskGetByIDIn struct { - TaskID entity.TaskID `json:"-"` - UserID entity.UserID `json:"-"` -} - -// TaskGetByIDOut represents the output of task retrieval. -type TaskGetByIDOut struct { - ID entity.TaskID `json:"id"` - Content string `json:"content"` - Description string `json:"description"` - IsCompleted bool `json:"is_completed"` - DueDate entity.NullTime `json:"due_date"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// TaskUpdateIn represents the input of task update -type TaskUpdateIn struct { - TaskID entity.TaskID `json:"-"` - UserID entity.UserID `json:"-"` - Content string `json:"content"` - Description string `json:"description"` - IsCompleted bool `json:"is_completed"` - DueDate entity.NullTime `json:"due_date"` -} - -func (t *TaskUpdateIn) Validate() error { - switch { - case t.Content == "": - return ErrContentEmpty - } - return nil -} - -// TaskUpdateOut represents the output of task update -type TaskUpdateOut struct { - ID entity.TaskID `json:"id"` -} diff --git a/internal/domain/dto/task_test.go b/internal/domain/dto/task_test.go deleted file mode 100644 index f6f462b..0000000 --- a/internal/domain/dto/task_test.go +++ /dev/null @@ -1,71 +0,0 @@ -package dto - -import ( - "testing" - - "github.com/stretchr/testify/suite" -) - -type TaskDTOTestSuite struct { - suite.Suite -} - -func TestTaskDTOSuite(t *testing.T) { - suite.Run(t, new(TaskDTOTestSuite)) -} - -func (s *TaskDTOTestSuite) TestTaskCreateIn() { - tests := []struct { - name string - input TaskCreateIn - expected error - }{ - { - name: "it should return error when content is empty", - input: TaskCreateIn{}, - expected: ErrContentEmpty, - }, - { - name: "it should return nil when all fields are valid", - input: TaskCreateIn{ - Content: "content", - }, - expected: nil, - }, - } - - for _, test := range tests { - s.Run(test.name, func() { - err := test.input.Validate() - s.Equal(test.expected, err) - }) - } -} - -func (s *TaskDTOTestSuite) TestTaskUpdateIn() { - tests := []struct { - name string - input TaskUpdateIn - expected error - }{ - { - name: "it should return error when content is empty", - input: TaskUpdateIn{}, - expected: ErrContentEmpty, - }, - { - name: "it should return nil when all fields are valid", - input: TaskUpdateIn{ - Content: "content", - }, - expected: nil, - }, - } - - for _, test := range tests { - s.Run(test.name, func() { - err := test.input.Validate() - s.Equal(test.expected, err) - }) - } -} diff --git a/internal/domain/dto/user.go b/internal/domain/dto/user.go deleted file mode 100644 index c336659..0000000 --- a/internal/domain/dto/user.go +++ /dev/null @@ -1,30 +0,0 @@ -package dto - -import ( - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -// UserCreateIn represents the input of user creation. -type UserCreateIn struct { - Name string `json:"name"` - Email string `json:"email"` - Password string `json:"password"` -} - -func (u *UserCreateIn) Validate() error { - switch { - case u.Email == "": - return ErrEmailEmpty - case u.Password == "": - return ErrPasswordEmpty - case u.Name == "": - return ErrNameEmpty - } - return nil -} - -// UserCreateOut represents the output of user creation. -type UserCreateOut struct { - ID entity.UserID `json:"id"` - Email string `json:"email"` -} diff --git a/internal/domain/dto/user_test.go b/internal/domain/dto/user_test.go deleted file mode 100644 index cfed0ea..0000000 --- a/internal/domain/dto/user_test.go +++ /dev/null @@ -1,35 +0,0 @@ -package dto - -import ( - "testing" - - "github.com/stretchr/testify/suite" -) - -type UserDTOTestSuite struct { - suite.Suite -} - -func TestUserDTOSuite(t *testing.T) { - suite.Run(t, new(UserDTOTestSuite)) -} - -func (s *UserDTOTestSuite) TestUserCreateIn() { - tests := []struct { - name string - input UserCreateIn - expected error - }{ - {name: "it should return error when email is empty", input: UserCreateIn{}, expected: ErrEmailEmpty}, - {name: "it should return error when password is empty", input: UserCreateIn{Email: "gopher@go.dev"}, expected: ErrPasswordEmpty}, - {name: "it should return error when name is empty", input: UserCreateIn{Email: "gopher@go.dev", Password: "123456"}, expected: ErrNameEmpty}, - {name: "it should return nil when all fields are valid", input: UserCreateIn{Email: "gopher@go.dev", Password: "123456", Name: "Gopher"}, expected: nil}, - } - - for _, test := range tests { - s.Run(test.name, func() { - err := test.input.Validate() - s.Equal(test.expected, err) - }) - } -} diff --git a/internal/domain/entity/auth.go b/internal/domain/entity/auth.go deleted file mode 100644 index e749022..0000000 --- a/internal/domain/entity/auth.go +++ /dev/null @@ -1,43 +0,0 @@ -package entity - -import ( - "context" - "errors" - "time" -) - -// Auth entity errors. -var ( - ErrAuthTokenExpired = errors.New("auth.entity.token.expired") -) - -type AuthID string -type authUserIDKey string - -// AuthUserIDKey is the key for the user_id value in the context. -const AuthUserIDKey = authUserIDKey("user_id") - -// Auth represents an authentication in the system. -type Auth struct { - ID AuthID - UserID UserID - Token string - ExpiresAt time.Time -} - -// VerifyTokenExpires checks if the token has expired. -func (a *Auth) VerifyTokenExpires() error { - if a.ExpiresAt.Before(time.Now()) { - return ErrAuthTokenExpired - } - return nil -} - -// GetAuthContext get the AuthUserIDKey from the context. -func GetAuthContext(ctx context.Context) UserID { - userID := ctx.Value(AuthUserIDKey) - if userID == nil { - panic("Auth Context: Cannot get auth context, required context value user_id from auth middleware") - } - return userID.(UserID) -} diff --git a/internal/domain/entity/auth_test.go b/internal/domain/entity/auth_test.go deleted file mode 100644 index 28c40ab..0000000 --- a/internal/domain/entity/auth_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package entity - -import ( - "context" - "testing" - "time" - - "github.com/stretchr/testify/suite" -) - -type AuthEntityTestSuite struct { - suite.Suite -} - -func TestAuthEntitySuite(t *testing.T) { - suite.Run(t, new(AuthEntityTestSuite)) -} - -func (s *AuthEntityTestSuite) TestVerifyTokenExpires() { - tests := []struct { - name string - input Auth - expected error - }{ - {name: "it should return error when auth is expired", input: Auth{ExpiresAt: time.Now().Add(-1 * time.Hour)}, expected: ErrAuthTokenExpired}, - {name: "it should return nill when auth is not expired", input: Auth{ExpiresAt: time.Now().Add(1 * time.Hour)}, expected: nil}, - } - - for _, test := range tests { - s.Run(test.name, func() { - s.Equal(test.expected, test.input.VerifyTokenExpires()) - }) - } -} - -func (s *AuthEntityTestSuite) TestGetAuthContext() { - s.Run("it should panic when auth context is not set", func() { - s.Panics(func() { - GetAuthContext(context.Background()) - }) - }) - - s.Run("it should return user id when auth context is set", func() { - userID := UserID("xxxxx") - ctx := context.WithValue(context.Background(), AuthUserIDKey, userID) - s.Equal(userID, GetAuthContext(ctx)) - }) -} diff --git a/internal/domain/entity/primitive.go b/internal/domain/entity/primitive.go deleted file mode 100644 index 58ac735..0000000 --- a/internal/domain/entity/primitive.go +++ /dev/null @@ -1,34 +0,0 @@ -package entity - -import ( - "bytes" - "database/sql" - "encoding/json" -) - -// nullBytes represent the bytes for null. -var nullBytes = []byte("null") - -// NullTime that may be null. NullTime embed sql.NullTime and implement json Unmarshaler and Marshaler -type NullTime struct { - sql.NullTime -} - -func (t *NullTime) UnmarshalJSON(data []byte) error { - if bytes.Equal(data, nullBytes) { - t.Valid = false - return nil - } - if err := json.Unmarshal(data, &t.Time); err != nil { - return err - } - t.Valid = true - return nil -} - -func (t NullTime) MarshalJSON() ([]byte, error) { - if !t.Valid { - return nullBytes, nil - } - return json.Marshal(t.Time) -} diff --git a/internal/domain/entity/primitive_test.go b/internal/domain/entity/primitive_test.go deleted file mode 100644 index db00ac3..0000000 --- a/internal/domain/entity/primitive_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package entity - -import ( - "database/sql" - "encoding/json" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/suite" -) - -type PrimitiveTestSuite struct { - suite.Suite -} - -func TestPrimitiveSuite(t *testing.T) { - suite.Run(t, new(PrimitiveTestSuite)) -} - -func (s *PrimitiveTestSuite) TestNullTimeUnmarshalJSON() { - s.Run("it should return error when fail to unmarshal with invalid json", func() { - rawJson := `[]` - var dateTime NullTime - - err := json.Unmarshal([]byte(rawJson), &dateTime) - - s.Error(err) - s.False(dateTime.Valid) - s.Empty(dateTime.Time) - }) - - s.Run("it should successfully unmarshal and return valid false and time is zero value", func() { - rawJson := `null` - var dateTime NullTime - - err := json.Unmarshal([]byte(rawJson), &dateTime) - - s.NoError(err) - s.False(dateTime.Valid) - s.Empty(dateTime.Time) - }) - - s.Run("it should successfully unmarshal and return valid true and time is actual time form json", func() { - rawJson := `"2022-12-25T00:00:00.000Z"` - var dateTime NullTime - - err := json.Unmarshal([]byte(rawJson), &dateTime) - - s.NoError(err) - s.True(dateTime.Valid) - s.Equal("2022-12-25 00:00:00 +0000 UTC", dateTime.Time.String()) - }) -} - -func (s *PrimitiveTestSuite) TestNullTimeMarshalJSON() { - s.Run("it should successfully marshal and return json null when not valid", func() { - dateTime := NullTime{ - NullTime: sql.NullTime{ - Time: time.Time{}, - Valid: false, - }, - } - - r, err := json.Marshal(dateTime) - - s.NoError(err) - s.Equal("null", string(r)) - }) - - s.Run("it should successfully marshal and return json time correctly", func() { - currentTime := time.Now() - - dateTime := NullTime{ - NullTime: sql.NullTime{ - Time: currentTime, - Valid: true, - }, - } - - r, err := json.Marshal(dateTime) - - s.NoError(err) - s.Equal(fmt.Sprintf("\"%s\"", currentTime.Format(time.RFC3339Nano)), string(r)) - }) -} diff --git a/internal/domain/entity/task.go b/internal/domain/entity/task.go deleted file mode 100644 index ef0e51d..0000000 --- a/internal/domain/entity/task.go +++ /dev/null @@ -1,17 +0,0 @@ -package entity - -import "time" - -type TaskID string - -// Task represents a task in the system. -type Task struct { - ID TaskID - UserID UserID - Content string - Description string - IsCompleted bool - DueDate NullTime - CreatedAt time.Time - UpdatedAt time.Time -} diff --git a/internal/domain/entity/user.go b/internal/domain/entity/user.go deleted file mode 100644 index e044375..0000000 --- a/internal/domain/entity/user.go +++ /dev/null @@ -1,44 +0,0 @@ -package entity - -import ( - "errors" - "regexp" - "time" -) - -const ( - emailRegexStr = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" - - MinPasswordLength = 6 -) - -var emailRegex = regexp.MustCompile(emailRegexStr) - -// User entity errors. -var ( - ErrEmailInvalid = errors.New("user.entity.email_invalid") - ErrPasswordTooShort = errors.New("user.entity.password_too_short") -) - -type UserID string - -// User represents a user in the system. -type User struct { - ID UserID - Name string - Email string - Password string - CreatedAt time.Time - UpdatedAt time.Time -} - -// Validate user fields. -func (u *User) Validate() error { - switch { - case !emailRegex.MatchString(u.Email): - return ErrEmailInvalid - case len(u.Password) < MinPasswordLength: - return ErrPasswordTooShort - } - return nil -} diff --git a/internal/domain/entity/user_test.go b/internal/domain/entity/user_test.go deleted file mode 100644 index e410889..0000000 --- a/internal/domain/entity/user_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package entity - -import ( - "testing" - - "github.com/stretchr/testify/suite" -) - -type UserEntityTestSuite struct { - suite.Suite -} - -func TestUserEntitySuite(t *testing.T) { - suite.Run(t, new(UserEntityTestSuite)) -} - -func (s *UserEntityTestSuite) TestValidate() { - tests := []struct { - name string - input User - expected error - }{ - {name: "it should return error when email is invalid", input: User{Email: "invalid"}, expected: ErrEmailInvalid}, - {name: "it should return error when password is too short", input: User{Email: "gopher@go.dev", Password: "123"}, expected: ErrPasswordTooShort}, - {name: "it should return nil when all fields are valid", input: User{Email: "gopher@go.dev", Password: "123456", Name: "Gopher"}, expected: nil}, - } - - for _, test := range tests { - s.Run(test.name, func() { - err := test.input.Validate() - s.Equal(test.expected, err) - }) - } -} diff --git a/internal/domain/mocks/AuthRepository.go b/internal/domain/mocks/AuthRepository.go deleted file mode 100644 index b11065b..0000000 --- a/internal/domain/mocks/AuthRepository.go +++ /dev/null @@ -1,94 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - entity "github.com/edwintantawi/taskit/internal/domain/entity" - - mock "github.com/stretchr/testify/mock" -) - -// AuthRepository is an autogenerated mock type for the AuthRepository type -type AuthRepository struct { - mock.Mock -} - -// DeleteByToken provides a mock function with given fields: ctx, token -func (_m *AuthRepository) DeleteByToken(ctx context.Context, token string) error { - ret := _m.Called(ctx, token) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, token) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// FindByToken provides a mock function with given fields: ctx, token -func (_m *AuthRepository) FindByToken(ctx context.Context, token string) (entity.Auth, error) { - ret := _m.Called(ctx, token) - - var r0 entity.Auth - if rf, ok := ret.Get(0).(func(context.Context, string) entity.Auth); ok { - r0 = rf(ctx, token) - } else { - r0 = ret.Get(0).(entity.Auth) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, token) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Store provides a mock function with given fields: ctx, a -func (_m *AuthRepository) Store(ctx context.Context, a *entity.Auth) error { - ret := _m.Called(ctx, a) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *entity.Auth) error); ok { - r0 = rf(ctx, a) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// VerifyAvailableByToken provides a mock function with given fields: ctx, token -func (_m *AuthRepository) VerifyAvailableByToken(ctx context.Context, token string) error { - ret := _m.Called(ctx, token) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, token) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewAuthRepository interface { - mock.TestingT - Cleanup(func()) -} - -// NewAuthRepository creates a new instance of AuthRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAuthRepository(t mockConstructorTestingTNewAuthRepository) *AuthRepository { - mock := &AuthRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/AuthUsecase.go b/internal/domain/mocks/AuthUsecase.go deleted file mode 100644 index 39d6bcd..0000000 --- a/internal/domain/mocks/AuthUsecase.go +++ /dev/null @@ -1,108 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - dto "github.com/edwintantawi/taskit/internal/domain/dto" - - mock "github.com/stretchr/testify/mock" -) - -// AuthUsecase is an autogenerated mock type for the AuthUsecase type -type AuthUsecase struct { - mock.Mock -} - -// GetProfile provides a mock function with given fields: ctx, payload -func (_m *AuthUsecase) GetProfile(ctx context.Context, payload *dto.AuthProfileIn) (dto.AuthProfileOut, error) { - ret := _m.Called(ctx, payload) - - var r0 dto.AuthProfileOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.AuthProfileIn) dto.AuthProfileOut); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Get(0).(dto.AuthProfileOut) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.AuthProfileIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Login provides a mock function with given fields: ctx, payload -func (_m *AuthUsecase) Login(ctx context.Context, payload *dto.AuthLoginIn) (dto.AuthLoginOut, error) { - ret := _m.Called(ctx, payload) - - var r0 dto.AuthLoginOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.AuthLoginIn) dto.AuthLoginOut); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Get(0).(dto.AuthLoginOut) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.AuthLoginIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Logout provides a mock function with given fields: ctx, payload -func (_m *AuthUsecase) Logout(ctx context.Context, payload *dto.AuthLogoutIn) error { - ret := _m.Called(ctx, payload) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *dto.AuthLogoutIn) error); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Refresh provides a mock function with given fields: ctx, payload -func (_m *AuthUsecase) Refresh(ctx context.Context, payload *dto.AuthRefreshIn) (dto.AuthRefreshOut, error) { - ret := _m.Called(ctx, payload) - - var r0 dto.AuthRefreshOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.AuthRefreshIn) dto.AuthRefreshOut); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Get(0).(dto.AuthRefreshOut) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.AuthRefreshIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewAuthUsecase interface { - mock.TestingT - Cleanup(func()) -} - -// NewAuthUsecase creates a new instance of AuthUsecase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewAuthUsecase(t mockConstructorTestingTNewAuthUsecase) *AuthUsecase { - mock := &AuthUsecase{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/HashProvider.go b/internal/domain/mocks/HashProvider.go deleted file mode 100644 index 584d905..0000000 --- a/internal/domain/mocks/HashProvider.go +++ /dev/null @@ -1,62 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import mock "github.com/stretchr/testify/mock" - -// HashProvider is an autogenerated mock type for the HashProvider type -type HashProvider struct { - mock.Mock -} - -// Compare provides a mock function with given fields: raw, hashed -func (_m *HashProvider) Compare(raw string, hashed string) error { - ret := _m.Called(raw, hashed) - - var r0 error - if rf, ok := ret.Get(0).(func(string, string) error); ok { - r0 = rf(raw, hashed) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Hash provides a mock function with given fields: raw -func (_m *HashProvider) Hash(raw string) ([]byte, error) { - ret := _m.Called(raw) - - var r0 []byte - if rf, ok := ret.Get(0).(func(string) []byte); ok { - r0 = rf(raw) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]byte) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(raw) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewHashProvider interface { - mock.TestingT - Cleanup(func()) -} - -// NewHashProvider creates a new instance of HashProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewHashProvider(t mockConstructorTestingTNewHashProvider) *HashProvider { - mock := &HashProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/IDProvider.go b/internal/domain/mocks/IDProvider.go deleted file mode 100644 index fdbee61..0000000 --- a/internal/domain/mocks/IDProvider.go +++ /dev/null @@ -1,39 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import mock "github.com/stretchr/testify/mock" - -// IDProvider is an autogenerated mock type for the IDProvider type -type IDProvider struct { - mock.Mock -} - -// Generate provides a mock function with given fields: -func (_m *IDProvider) Generate() string { - ret := _m.Called() - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -type mockConstructorTestingTNewIDProvider interface { - mock.TestingT - Cleanup(func()) -} - -// NewIDProvider creates a new instance of IDProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewIDProvider(t mockConstructorTestingTNewIDProvider) *IDProvider { - mock := &IDProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/JWTProvider.go b/internal/domain/mocks/JWTProvider.go deleted file mode 100644 index e51b904..0000000 --- a/internal/domain/mocks/JWTProvider.go +++ /dev/null @@ -1,107 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - entity "github.com/edwintantawi/taskit/internal/domain/entity" - mock "github.com/stretchr/testify/mock" - - time "time" -) - -// JWTProvider is an autogenerated mock type for the JWTProvider type -type JWTProvider struct { - mock.Mock -} - -// GenerateAccessToken provides a mock function with given fields: userID -func (_m *JWTProvider) GenerateAccessToken(userID entity.UserID) (string, time.Time, error) { - ret := _m.Called(userID) - - var r0 string - if rf, ok := ret.Get(0).(func(entity.UserID) string); ok { - r0 = rf(userID) - } else { - r0 = ret.Get(0).(string) - } - - var r1 time.Time - if rf, ok := ret.Get(1).(func(entity.UserID) time.Time); ok { - r1 = rf(userID) - } else { - r1 = ret.Get(1).(time.Time) - } - - var r2 error - if rf, ok := ret.Get(2).(func(entity.UserID) error); ok { - r2 = rf(userID) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// GenerateRefreshToken provides a mock function with given fields: userID -func (_m *JWTProvider) GenerateRefreshToken(userID entity.UserID) (string, time.Time, error) { - ret := _m.Called(userID) - - var r0 string - if rf, ok := ret.Get(0).(func(entity.UserID) string); ok { - r0 = rf(userID) - } else { - r0 = ret.Get(0).(string) - } - - var r1 time.Time - if rf, ok := ret.Get(1).(func(entity.UserID) time.Time); ok { - r1 = rf(userID) - } else { - r1 = ret.Get(1).(time.Time) - } - - var r2 error - if rf, ok := ret.Get(2).(func(entity.UserID) error); ok { - r2 = rf(userID) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// VerifyAccessToken provides a mock function with given fields: rawToken -func (_m *JWTProvider) VerifyAccessToken(rawToken string) (entity.UserID, error) { - ret := _m.Called(rawToken) - - var r0 entity.UserID - if rf, ok := ret.Get(0).(func(string) entity.UserID); ok { - r0 = rf(rawToken) - } else { - r0 = ret.Get(0).(entity.UserID) - } - - var r1 error - if rf, ok := ret.Get(1).(func(string) error); ok { - r1 = rf(rawToken) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewJWTProvider interface { - mock.TestingT - Cleanup(func()) -} - -// NewJWTProvider creates a new instance of JWTProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewJWTProvider(t mockConstructorTestingTNewJWTProvider) *JWTProvider { - mock := &JWTProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/TaskRepository.go b/internal/domain/mocks/TaskRepository.go deleted file mode 100644 index 9a004ef..0000000 --- a/internal/domain/mocks/TaskRepository.go +++ /dev/null @@ -1,145 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - entity "github.com/edwintantawi/taskit/internal/domain/entity" - - mock "github.com/stretchr/testify/mock" -) - -// TaskRepository is an autogenerated mock type for the TaskRepository type -type TaskRepository struct { - mock.Mock -} - -// DeleteByID provides a mock function with given fields: ctx, taskID -func (_m *TaskRepository) DeleteByID(ctx context.Context, taskID entity.TaskID) error { - ret := _m.Called(ctx, taskID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, entity.TaskID) error); ok { - r0 = rf(ctx, taskID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// FindAllByUserID provides a mock function with given fields: ctx, userID -func (_m *TaskRepository) FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Task, error) { - ret := _m.Called(ctx, userID) - - var r0 []entity.Task - if rf, ok := ret.Get(0).(func(context.Context, entity.UserID) []entity.Task); ok { - r0 = rf(ctx, userID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]entity.Task) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, entity.UserID) error); ok { - r1 = rf(ctx, userID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// FindByID provides a mock function with given fields: ctx, taskID -func (_m *TaskRepository) FindByID(ctx context.Context, taskID entity.TaskID) (entity.Task, error) { - ret := _m.Called(ctx, taskID) - - var r0 entity.Task - if rf, ok := ret.Get(0).(func(context.Context, entity.TaskID) entity.Task); ok { - r0 = rf(ctx, taskID) - } else { - r0 = ret.Get(0).(entity.Task) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, entity.TaskID) error); ok { - r1 = rf(ctx, taskID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Store provides a mock function with given fields: ctx, t -func (_m *TaskRepository) Store(ctx context.Context, t *entity.Task) (entity.TaskID, error) { - ret := _m.Called(ctx, t) - - var r0 entity.TaskID - if rf, ok := ret.Get(0).(func(context.Context, *entity.Task) entity.TaskID); ok { - r0 = rf(ctx, t) - } else { - r0 = ret.Get(0).(entity.TaskID) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *entity.Task) error); ok { - r1 = rf(ctx, t) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Update provides a mock function with given fields: ctx, t -func (_m *TaskRepository) Update(ctx context.Context, t *entity.Task) (entity.TaskID, error) { - ret := _m.Called(ctx, t) - - var r0 entity.TaskID - if rf, ok := ret.Get(0).(func(context.Context, *entity.Task) entity.TaskID); ok { - r0 = rf(ctx, t) - } else { - r0 = ret.Get(0).(entity.TaskID) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *entity.Task) error); ok { - r1 = rf(ctx, t) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// VerifyAvailableByID provides a mock function with given fields: ctx, taskID -func (_m *TaskRepository) VerifyAvailableByID(ctx context.Context, taskID entity.TaskID) error { - ret := _m.Called(ctx, taskID) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, entity.TaskID) error); ok { - r0 = rf(ctx, taskID) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewTaskRepository interface { - mock.TestingT - Cleanup(func()) -} - -// NewTaskRepository creates a new instance of TaskRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewTaskRepository(t mockConstructorTestingTNewTaskRepository) *TaskRepository { - mock := &TaskRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/TaskUsecase.go b/internal/domain/mocks/TaskUsecase.go deleted file mode 100644 index 5841de7..0000000 --- a/internal/domain/mocks/TaskUsecase.go +++ /dev/null @@ -1,131 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - dto "github.com/edwintantawi/taskit/internal/domain/dto" - - mock "github.com/stretchr/testify/mock" -) - -// TaskUsecase is an autogenerated mock type for the TaskUsecase type -type TaskUsecase struct { - mock.Mock -} - -// Create provides a mock function with given fields: ctx, payload -func (_m *TaskUsecase) Create(ctx context.Context, payload *dto.TaskCreateIn) (dto.TaskCreateOut, error) { - ret := _m.Called(ctx, payload) - - var r0 dto.TaskCreateOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.TaskCreateIn) dto.TaskCreateOut); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Get(0).(dto.TaskCreateOut) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.TaskCreateIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetAll provides a mock function with given fields: ctx, payload -func (_m *TaskUsecase) GetAll(ctx context.Context, payload *dto.TaskGetAllIn) ([]dto.TaskGetAllOut, error) { - ret := _m.Called(ctx, payload) - - var r0 []dto.TaskGetAllOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.TaskGetAllIn) []dto.TaskGetAllOut); ok { - r0 = rf(ctx, payload) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]dto.TaskGetAllOut) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.TaskGetAllIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// GetByID provides a mock function with given fields: ctx, payload -func (_m *TaskUsecase) GetByID(ctx context.Context, payload *dto.TaskGetByIDIn) (dto.TaskGetByIDOut, error) { - ret := _m.Called(ctx, payload) - - var r0 dto.TaskGetByIDOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.TaskGetByIDIn) dto.TaskGetByIDOut); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Get(0).(dto.TaskGetByIDOut) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.TaskGetByIDIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Remove provides a mock function with given fields: ctx, payload -func (_m *TaskUsecase) Remove(ctx context.Context, payload *dto.TaskRemoveIn) error { - ret := _m.Called(ctx, payload) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *dto.TaskRemoveIn) error); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// Update provides a mock function with given fields: ctx, payload -func (_m *TaskUsecase) Update(ctx context.Context, payload *dto.TaskUpdateIn) (dto.TaskUpdateOut, error) { - ret := _m.Called(ctx, payload) - - var r0 dto.TaskUpdateOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.TaskUpdateIn) dto.TaskUpdateOut); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Get(0).(dto.TaskUpdateOut) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.TaskUpdateIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewTaskUsecase interface { - mock.TestingT - Cleanup(func()) -} - -// NewTaskUsecase creates a new instance of TaskUsecase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewTaskUsecase(t mockConstructorTestingTNewTaskUsecase) *TaskUsecase { - mock := &TaskUsecase{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/UserRepository.go b/internal/domain/mocks/UserRepository.go deleted file mode 100644 index ead8409..0000000 --- a/internal/domain/mocks/UserRepository.go +++ /dev/null @@ -1,108 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - entity "github.com/edwintantawi/taskit/internal/domain/entity" - - mock "github.com/stretchr/testify/mock" -) - -// UserRepository is an autogenerated mock type for the UserRepository type -type UserRepository struct { - mock.Mock -} - -// FindByEmail provides a mock function with given fields: ctx, email -func (_m *UserRepository) FindByEmail(ctx context.Context, email string) (entity.User, error) { - ret := _m.Called(ctx, email) - - var r0 entity.User - if rf, ok := ret.Get(0).(func(context.Context, string) entity.User); ok { - r0 = rf(ctx, email) - } else { - r0 = ret.Get(0).(entity.User) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, email) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// FindByID provides a mock function with given fields: ctx, id -func (_m *UserRepository) FindByID(ctx context.Context, id entity.UserID) (entity.User, error) { - ret := _m.Called(ctx, id) - - var r0 entity.User - if rf, ok := ret.Get(0).(func(context.Context, entity.UserID) entity.User); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Get(0).(entity.User) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, entity.UserID) error); ok { - r1 = rf(ctx, id) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Store provides a mock function with given fields: ctx, u -func (_m *UserRepository) Store(ctx context.Context, u *entity.User) (entity.UserID, error) { - ret := _m.Called(ctx, u) - - var r0 entity.UserID - if rf, ok := ret.Get(0).(func(context.Context, *entity.User) entity.UserID); ok { - r0 = rf(ctx, u) - } else { - r0 = ret.Get(0).(entity.UserID) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *entity.User) error); ok { - r1 = rf(ctx, u) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// VerifyAvailableEmail provides a mock function with given fields: ctx, email -func (_m *UserRepository) VerifyAvailableEmail(ctx context.Context, email string) error { - ret := _m.Called(ctx, email) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { - r0 = rf(ctx, email) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewUserRepository interface { - mock.TestingT - Cleanup(func()) -} - -// NewUserRepository creates a new instance of UserRepository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewUserRepository(t mockConstructorTestingTNewUserRepository) *UserRepository { - mock := &UserRepository{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/UserUsecase.go b/internal/domain/mocks/UserUsecase.go deleted file mode 100644 index 9e02ba2..0000000 --- a/internal/domain/mocks/UserUsecase.go +++ /dev/null @@ -1,52 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - dto "github.com/edwintantawi/taskit/internal/domain/dto" - - mock "github.com/stretchr/testify/mock" -) - -// UserUsecase is an autogenerated mock type for the UserUsecase type -type UserUsecase struct { - mock.Mock -} - -// Create provides a mock function with given fields: ctx, payload -func (_m *UserUsecase) Create(ctx context.Context, payload *dto.UserCreateIn) (dto.UserCreateOut, error) { - ret := _m.Called(ctx, payload) - - var r0 dto.UserCreateOut - if rf, ok := ret.Get(0).(func(context.Context, *dto.UserCreateIn) dto.UserCreateOut); ok { - r0 = rf(ctx, payload) - } else { - r0 = ret.Get(0).(dto.UserCreateOut) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *dto.UserCreateIn) error); ok { - r1 = rf(ctx, payload) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -type mockConstructorTestingTNewUserUsecase interface { - mock.TestingT - Cleanup(func()) -} - -// NewUserUsecase creates a new instance of UserUsecase. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewUserUsecase(t mockConstructorTestingTNewUserUsecase) *UserUsecase { - mock := &UserUsecase{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/Validater.go b/internal/domain/mocks/Validater.go deleted file mode 100644 index fd99c78..0000000 --- a/internal/domain/mocks/Validater.go +++ /dev/null @@ -1,39 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import mock "github.com/stretchr/testify/mock" - -// Validater is an autogenerated mock type for the Validater type -type Validater struct { - mock.Mock -} - -// Validate provides a mock function with given fields: -func (_m *Validater) Validate() error { - ret := _m.Called() - - var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewValidater interface { - mock.TestingT - Cleanup(func()) -} - -// NewValidater creates a new instance of Validater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewValidater(t mockConstructorTestingTNewValidater) *Validater { - mock := &Validater{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/mocks/ValidatorProvider.go b/internal/domain/mocks/ValidatorProvider.go deleted file mode 100644 index 6c90131..0000000 --- a/internal/domain/mocks/ValidatorProvider.go +++ /dev/null @@ -1,42 +0,0 @@ -// Code generated by mockery v2.15.0. DO NOT EDIT. - -package mocks - -import ( - domain "github.com/edwintantawi/taskit/internal/domain" - mock "github.com/stretchr/testify/mock" -) - -// ValidatorProvider is an autogenerated mock type for the ValidatorProvider type -type ValidatorProvider struct { - mock.Mock -} - -// Validate provides a mock function with given fields: validater -func (_m *ValidatorProvider) Validate(validater domain.Validater) error { - ret := _m.Called(validater) - - var r0 error - if rf, ok := ret.Get(0).(func(domain.Validater) error); ok { - r0 = rf(validater) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewValidatorProvider interface { - mock.TestingT - Cleanup(func()) -} - -// NewValidatorProvider creates a new instance of ValidatorProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewValidatorProvider(t mockConstructorTestingTNewValidatorProvider) *ValidatorProvider { - mock := &ValidatorProvider{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/internal/domain/provider.go b/internal/domain/provider.go deleted file mode 100644 index 514822d..0000000 --- a/internal/domain/provider.go +++ /dev/null @@ -1,35 +0,0 @@ -package domain - -import ( - "time" - - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -// IDProvider represent id generator contract -type IDProvider interface { - Generate() string -} - -// HashProvider represent hasher contract -type HashProvider interface { - Hash(raw string) ([]byte, error) - Compare(raw string, hashed string) error -} - -// JWTProvider represent jwt generator contract. -type JWTProvider interface { - GenerateAccessToken(userID entity.UserID) (string, time.Time, error) - GenerateRefreshToken(userID entity.UserID) (string, time.Time, error) - VerifyAccessToken(rawToken string) (entity.UserID, error) -} - -// Validater represent object with validate method. -type Validater interface { - Validate() error -} - -// ValidatorProvider represent validator contract. -type ValidatorProvider interface { - Validate(validater Validater) error -} diff --git a/internal/domain/repository.go b/internal/domain/repository.go deleted file mode 100644 index 65330e5..0000000 --- a/internal/domain/repository.go +++ /dev/null @@ -1,50 +0,0 @@ -package domain - -import ( - "context" - "errors" - - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -// User repository errors. -var ( - ErrEmailNotAvailable = errors.New("user.repository.email_not_available") - ErrUserNotFound = errors.New("user.repository.user_not_found") -) - -// Auth repository errors. -var ( - ErrAuthNotFound = errors.New("auth.repository.auth_not_found") -) - -// Task repository errors. -var ( - ErrTaskNotFound = errors.New("task.repository.task_not_found") -) - -// UserRepository represent user repository contract. -type UserRepository interface { - Store(ctx context.Context, u *entity.User) (entity.UserID, error) - VerifyAvailableEmail(ctx context.Context, email string) error - FindByEmail(ctx context.Context, email string) (entity.User, error) - FindByID(ctx context.Context, id entity.UserID) (entity.User, error) -} - -// AuthRepository represent auth repository contract. -type AuthRepository interface { - Store(ctx context.Context, a *entity.Auth) error - VerifyAvailableByToken(ctx context.Context, token string) error - DeleteByToken(ctx context.Context, token string) error - FindByToken(ctx context.Context, token string) (entity.Auth, error) -} - -// TaskRepository represent task repository contract. -type TaskRepository interface { - Store(ctx context.Context, t *entity.Task) (entity.TaskID, error) - FindByID(ctx context.Context, taskID entity.TaskID) (entity.Task, error) - FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Task, error) - VerifyAvailableByID(ctx context.Context, taskID entity.TaskID) error - DeleteByID(ctx context.Context, taskID entity.TaskID) error - Update(ctx context.Context, t *entity.Task) (entity.TaskID, error) -} diff --git a/internal/domain/response.go b/internal/domain/response.go deleted file mode 100644 index 1ddb339..0000000 --- a/internal/domain/response.go +++ /dev/null @@ -1,35 +0,0 @@ -package domain - -import "net/http" - -// SuccessResponse represents the success response with payload. -type SuccessResponse struct { - StatusCode int `json:"status_code"` - Message string `json:"message"` - Payload any `json:"payload"` -} - -// ErrorResponse represents the error response. -type ErrorResponse struct { - StatusCode int `json:"status_code"` - Message string `json:"message"` - Error string `json:"error"` -} - -// NewSuccessResponse creates a new ResponseSuccess. -func NewSuccessResponse(statusCode int, message string, payload any) SuccessResponse { - return SuccessResponse{ - StatusCode: statusCode, - Message: message, - Payload: payload, - } -} - -// NewErrorResponse creates a new ResponseError and translate an error. -func NewErrorResponse(statusCode int, errorMessage string) ErrorResponse { - return ErrorResponse{ - StatusCode: statusCode, - Message: http.StatusText(statusCode), - Error: errorMessage, - } -} diff --git a/internal/domain/usecase.go b/internal/domain/usecase.go deleted file mode 100644 index b534eb7..0000000 --- a/internal/domain/usecase.go +++ /dev/null @@ -1,41 +0,0 @@ -package domain - -import ( - "context" - "errors" - - "github.com/edwintantawi/taskit/internal/domain/dto" -) - -// Auth usecase errors. -var ( - ErrEmailNotExist = errors.New("auth.usecase.email_not_exist") - ErrPasswordIncorrect = errors.New("auth.usecase.password_incorrect") -) - -// Task usecase errors. -var ( - ErrTaskAuthorization = errors.New("task.usecase.task_forbidden") -) - -// UserUsecase represent user usecase contract. -type UserUsecase interface { - Create(ctx context.Context, payload *dto.UserCreateIn) (dto.UserCreateOut, error) -} - -// AuthUsecase represent auth usecase contract. -type AuthUsecase interface { - Login(ctx context.Context, payload *dto.AuthLoginIn) (dto.AuthLoginOut, error) - Logout(ctx context.Context, payload *dto.AuthLogoutIn) error - GetProfile(ctx context.Context, payload *dto.AuthProfileIn) (dto.AuthProfileOut, error) - Refresh(ctx context.Context, payload *dto.AuthRefreshIn) (dto.AuthRefreshOut, error) -} - -// TaskUsecase represent task usecase contract. -type TaskUsecase interface { - Create(ctx context.Context, payload *dto.TaskCreateIn) (dto.TaskCreateOut, error) - GetAll(ctx context.Context, payload *dto.TaskGetAllIn) ([]dto.TaskGetAllOut, error) - Remove(ctx context.Context, payload *dto.TaskRemoveIn) error - GetByID(ctx context.Context, payload *dto.TaskGetByIDIn) (dto.TaskGetByIDOut, error) - Update(ctx context.Context, payload *dto.TaskUpdateIn) (dto.TaskUpdateOut, error) -} diff --git a/internal/entity/added_user.go b/internal/entity/added_user.go new file mode 100644 index 0000000..ff65f85 --- /dev/null +++ b/internal/entity/added_user.go @@ -0,0 +1,7 @@ +package entity + +// AddedUser represents a user that has been added/created. +type AddedUser struct { + ID string + Email string +} diff --git a/internal/entity/new_user.go b/internal/entity/new_user.go new file mode 100644 index 0000000..a1d3353 --- /dev/null +++ b/internal/entity/new_user.go @@ -0,0 +1,41 @@ +package entity + +import ( + "errors" + "fmt" + "regexp" +) + +const ( + emailRegexStr = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$" + MinPasswordLength = 6 +) + +var ( + ErrEmptyName = errors.New("name is required") + ErrInvalidEmail = errors.New("email is not valid") + ErrTooShortPassword = fmt.Errorf("password must be at least %d characters long", MinPasswordLength) +) + +// EmailRegex is a regular expression for validating email addresses. +var EmailRegex = regexp.MustCompile(emailRegexStr) + +// NewUser represents a new user. +type NewUser struct { + Name string + Email string + Password string +} + +func (nu NewUser) Validate() error { + if nu.Name == "" { + return ErrEmptyName + } + if !EmailRegex.MatchString(nu.Email) { + return ErrInvalidEmail + } + if len(nu.Password) < MinPasswordLength { + return ErrTooShortPassword + } + return nil +} diff --git a/internal/entity/new_user_test.go b/internal/entity/new_user_test.go new file mode 100644 index 0000000..b5a6a2d --- /dev/null +++ b/internal/entity/new_user_test.go @@ -0,0 +1,49 @@ +package entity + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewUser_Validate(t *testing.T) { + tests := map[string]struct { + args NewUser + expected error + }{ + "it should return error ErrEmptyName when name is empty": { + args: NewUser{}, + expected: ErrEmptyName, + }, + "it should return error ErrInvalidEmail when email is not valid": { + args: NewUser{ + Name: "gopher", + Email: "invalid-email", + }, + expected: ErrInvalidEmail, + }, + "it should return error ErrTooShortPassword when the password less than 6 characters": { + args: NewUser{ + Name: "gopher", + Email: "gopher@go.dev", + Password: "12345", + }, + expected: ErrTooShortPassword, + }, + "it should return error nil when no validation error": { + args: NewUser{ + Name: "gopher", + Email: "gopher@go.dev", + Password: "123456", + }, + expected: nil, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + err := tc.args.Validate() + assert.Equal(t, tc.expected, err) + }) + } +} diff --git a/internal/entity/user.go b/internal/entity/user.go new file mode 100644 index 0000000..4e65f98 --- /dev/null +++ b/internal/entity/user.go @@ -0,0 +1,12 @@ +package entity + +import "time" + +type User struct { + ID string + Name string + Email string + Password string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/task/delivery/http/handler.go b/internal/task/delivery/http/handler.go deleted file mode 100644 index a8e202b..0000000 --- a/internal/task/delivery/http/handler.go +++ /dev/null @@ -1,149 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - - "github.com/go-chi/chi/v5" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/pkg/errorx" -) - -type HTTPHandler struct { - validator domain.ValidatorProvider - taskUsecase domain.TaskUsecase -} - -// New creates a new HTTPHandler. -func New(validator domain.ValidatorProvider, taskUsecase domain.TaskUsecase) HTTPHandler { - return HTTPHandler{validator: validator, taskUsecase: taskUsecase} -} - -// POST /tasks to create new task. -func (h *HTTPHandler) Post(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.TaskCreateIn - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - w.WriteHeader(http.StatusBadRequest) - encoder.Encode(domain.NewErrorResponse(http.StatusBadRequest, "Invalid request body")) - return - } - payload.UserID = entity.GetAuthContext(r.Context()) - - if err := h.validator.Validate(&payload); err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - output, err := h.taskUsecase.Create(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusCreated) - encoder.Encode(domain.NewSuccessResponse(http.StatusCreated, "Successfully created new task", output)) -} - -// GET /tasks to get all tasks. -func (h *HTTPHandler) Get(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.TaskGetAllIn - payload.UserID = entity.GetAuthContext(r.Context()) - - output, err := h.taskUsecase.GetAll(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), output)) -} - -// DELETE /tasks/{task_id} to remove task. -func (h *HTTPHandler) Delete(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.TaskRemoveIn - payload.UserID = entity.GetAuthContext(r.Context()) - payload.TaskID = entity.TaskID(chi.URLParam(r, "task_id")) - - if err := h.taskUsecase.Remove(r.Context(), &payload); err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, "Successfully deleted task", nil)) -} - -// GET /tasks/{task_id} to get task by task id. -func (h *HTTPHandler) GetByID(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.TaskGetByIDIn - payload.UserID = entity.GetAuthContext(r.Context()) - payload.TaskID = entity.TaskID(chi.URLParam(r, "task_id")) - - output, err := h.taskUsecase.GetByID(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, http.StatusText(http.StatusOK), output)) -} - -// PUT /tasks/{task_id} to update task by task id. -func (h *HTTPHandler) Put(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.TaskUpdateIn - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - w.WriteHeader(http.StatusBadRequest) - encoder.Encode(domain.NewErrorResponse(http.StatusBadRequest, "Invalid request body")) - return - } - payload.UserID = entity.GetAuthContext(r.Context()) - payload.TaskID = entity.TaskID(chi.URLParam(r, "task_id")) - - if err := h.validator.Validate(&payload); err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - output, err := h.taskUsecase.Update(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusOK) - encoder.Encode(domain.NewSuccessResponse(http.StatusOK, "Successfully updated task", output)) -} diff --git a/internal/task/delivery/http/handler_test.go b/internal/task/delivery/http/handler_test.go deleted file mode 100644 index 9c5bfe3..0000000 --- a/internal/task/delivery/http/handler_test.go +++ /dev/null @@ -1,617 +0,0 @@ -package http - -import ( - "bytes" - "database/sql" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/pkg/errorx" - "github.com/edwintantawi/taskit/test" -) - -type TaskHTTPHandlerTestSuite struct { - suite.Suite -} - -func TestTaskHTTPHandlerSuite(t *testing.T) { - suite.Run(t, new(TaskHTTPHandlerTestSuite)) -} - -type dependency struct { - req *http.Request - validator *mocks.ValidatorProvider - taskUsecase *mocks.TaskUsecase -} - -func (s *TaskHTTPHandlerTestSuite) TestPost() { - type args struct { - requestBody []byte - } - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when request body is invalid or not provided", - isError: true, - args: args{ - requestBody: []byte(`{`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusBadRequest, - message: http.StatusText(http.StatusBadRequest), - error: "Invalid request body", - }, - setup: func(d *dependency) {}, - }, - { - name: "it should response with error when payload is not valid", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.validator.On("Validate", mock.Anything). - Return(test.ErrValidator) - }, - }, - { - name: "it should response with error when taskUsecase Create returns unexpected error", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.taskUsecase.On("Create", mock.Anything, &dto.TaskCreateIn{UserID: "user-xxxxx"}). - Return(dto.TaskCreateOut{}, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusCreated, - message: "Successfully created new task", - payload: map[string]any{ - "id": "task-xxxxx", - }, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.taskUsecase.On("Create", mock.Anything, &dto.TaskCreateIn{UserID: "user-xxxxx"}). - Return(dto.TaskCreateOut{ID: "task-xxxxx"}, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - reqBody := bytes.NewReader(t.args.requestBody) - rr := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", reqBody) - - d := &dependency{ - req: req, - validator: &mocks.ValidatorProvider{}, - taskUsecase: &mocks.TaskUsecase{}, - } - t.setup(d) - - handler := New(d.validator, d.taskUsecase) - handler.Post(rr, d.req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - payloadMap := resBody.Payload.(map[string]any) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.payload, payloadMap) - } - }) - } -} - -func (s *TaskHTTPHandlerTestSuite) TestGet() { - type expected struct { - contentType string - statusCode int - message string - error string - payload []map[string]any - } - tests := []struct { - name string - isError bool - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when task usecase return unexpected error", - isError: true, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.taskUsecase.On("GetAll", mock.Anything, &dto.TaskGetAllIn{UserID: "user-xxxxx"}). - Return(nil, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: http.StatusText(http.StatusOK), - payload: []map[string]any{ - {"id": "task-xxxxx", "content": "task_xxxxx_content", "description": "task_xxxxx_description", "is_completed": false, "due_date": nil, "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, - {"id": "task-yyyyy", "content": "task_yyyyy_content", "description": "task_yyyyy_description", "is_completed": true, "due_date": test.TimeAfterNow.Format(time.RFC3339Nano), "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano)}, - }, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.taskUsecase.On("GetAll", mock.Anything, &dto.TaskGetAllIn{UserID: "user-xxxxx"}). - Return([]dto.TaskGetAllOut{ - {ID: "task-xxxxx", Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "task-yyyyy", Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - }, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/", nil) - - d := &dependency{ - req: req, - taskUsecase: &mocks.TaskUsecase{}, - } - t.setup(d) - - handler := New(nil, d.taskUsecase) - handler.Get(rr, d.req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - payloadList := resBody.Payload.([]any) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - - for i, payload := range t.expected.payload { - s.Equal(payload, payloadList[i].(map[string]any)) - } - } - }) - } -} - -func (s *TaskHTTPHandlerTestSuite) TestDelete() { - type args struct { - params map[string]string - } - type expected struct { - contentType string - statusCode int - message string - error string - payload any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when task usecase Remove return unexpected error", - isError: true, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.taskUsecase.On("Remove", mock.Anything, &dto.TaskRemoveIn{TaskID: "", UserID: "user-xxxxx"}). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - args: args{ - params: map[string]string{ - "task_id": "task-xxxxx", - }, - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: "Successfully deleted task", - payload: nil, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.taskUsecase.On("Remove", mock.Anything, &dto.TaskRemoveIn{TaskID: "task-xxxxx", UserID: "user-xxxxx"}). - Return(nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - rr := httptest.NewRecorder() - req := httptest.NewRequest("DELETE", "/{task_id}", nil) - - req = test.InjectChiRouterParams(req, t.args.params) - - d := &dependency{ - req: req, - taskUsecase: &mocks.TaskUsecase{}, - } - t.setup(d) - - handler := New(nil, d.taskUsecase) - handler.Delete(rr, d.req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - - s.Equal(t.expected.payload, nil) - } - }) - } -} - -func (s *TaskHTTPHandlerTestSuite) TestGetByID() { - type args struct { - params map[string]string - } - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when task usecase GetByID return unexpected error", - isError: true, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.taskUsecase.On("GetByID", mock.Anything, &dto.TaskGetByIDIn{TaskID: "", UserID: "user-xxxxx"}). - Return(dto.TaskGetByIDOut{}, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - args: args{ - params: map[string]string{ - "task_id": "task-xxxxx", - }, - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: http.StatusText(http.StatusOK), - payload: map[string]any{ - "id": "task-xxxxx", "content": "task_xxxxx_content", "description": "task_xxxxx_description", "is_completed": true, "due_date": test.TimeAfterNow.Format(time.RFC3339Nano), "created_at": test.TimeBeforeNow.Format(time.RFC3339Nano), "updated_at": test.TimeBeforeNow.Format(time.RFC3339Nano), - }, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.taskUsecase.On("GetByID", mock.Anything, &dto.TaskGetByIDIn{TaskID: "task-xxxxx", UserID: "user-xxxxx"}). - Return(dto.TaskGetByIDOut{ - ID: "task-xxxxx", - Content: "task_xxxxx_content", - Description: "task_xxxxx_description", - IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - rr := httptest.NewRecorder() - req := httptest.NewRequest("GET", "/{task_id}", nil) - - req = test.InjectChiRouterParams(req, t.args.params) - - d := &dependency{ - req: req, - taskUsecase: &mocks.TaskUsecase{}, - } - t.setup(d) - - handler := New(nil, d.taskUsecase) - handler.GetByID(rr, d.req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - payloadMap := resBody.Payload.(map[string]any) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.payload, payloadMap) - } - }) - } -} - -func (s *TaskHTTPHandlerTestSuite) TestPut() { - type args struct { - requestBody []byte - params map[string]string - } - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when request body is invalid or not provided", - isError: true, - args: args{ - requestBody: []byte(`{`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusBadRequest, - message: http.StatusText(http.StatusBadRequest), - error: "Invalid request body", - }, - setup: func(d *dependency) {}, - }, - { - name: "it should response with error when payload is not valid", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.validator.On("Validate", mock.Anything). - Return(test.ErrValidator) - }, - }, - { - name: "it should response with error when task usecase GetByID return unexpected error", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.taskUsecase.On("Update", mock.Anything, &dto.TaskUpdateIn{TaskID: "", UserID: "user-xxxxx"}). - Return(dto.TaskUpdateOut{}, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - args: args{ - requestBody: []byte(`{}`), - params: map[string]string{ - "task_id": "task-xxxxx", - }, - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusOK, - message: "Successfully updated task", - payload: map[string]any{ - "id": "task-xxxxx", - }, - }, - setup: func(d *dependency) { - d.req = test.InjectAuthContext(d.req, entity.UserID("user-xxxxx")) - - d.validator.On("Validate", mock.Anything). - Return(nil) - - d.taskUsecase.On("Update", mock.Anything, &dto.TaskUpdateIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - }).Return(dto.TaskUpdateOut{ - ID: "task-xxxxx", - }, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - reqBody := bytes.NewReader(t.args.requestBody) - rr := httptest.NewRecorder() - req := httptest.NewRequest("PUT", "/{task_id}", reqBody) - - req = test.InjectChiRouterParams(req, t.args.params) - - d := &dependency{ - req: req, - validator: &mocks.ValidatorProvider{}, - taskUsecase: &mocks.TaskUsecase{}, - } - t.setup(d) - - handler := New(d.validator, d.taskUsecase) - handler.Put(rr, d.req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - payloadMap := resBody.Payload.(map[string]any) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.payload, payloadMap) - } - }) - } -} diff --git a/internal/task/repository/repository.go b/internal/task/repository/repository.go deleted file mode 100644 index d895871..0000000 --- a/internal/task/repository/repository.go +++ /dev/null @@ -1,106 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "errors" - "time" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -type Repository struct { - db *sql.DB - idProvider domain.IDProvider -} - -// New create a new task repository. -func New(db *sql.DB, idProvider domain.IDProvider) Repository { - return Repository{db: db, idProvider: idProvider} -} - -// Store save a new task. -func (r *Repository) Store(ctx context.Context, t *entity.Task) (entity.TaskID, error) { - id := r.idProvider.Generate() - q := `INSERT INTO tasks (id, user_id, content, description, due_date) VALUES ($1, $2, $3, $4, $5)` - _, err := r.db.ExecContext(ctx, q, id, t.UserID, t.Content, t.Description, t.DueDate) - if err != nil { - return "", err - } - return entity.TaskID(id), nil -} - -// FindByID get task by id. -func (r *Repository) FindByID(ctx context.Context, taskID entity.TaskID) (entity.Task, error) { - var task entity.Task - q := `SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1` - row := r.db.QueryRowContext(ctx, q, taskID) - err := row.Scan(&task.ID, &task.UserID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) - if errors.Is(err, sql.ErrNoRows) { - return entity.Task{}, domain.ErrTaskNotFound - } else if err != nil { - return entity.Task{}, err - } - return task, nil -} - -// FindAllByUserID get all tasks owned by a user by user id. -func (r *Repository) FindAllByUserID(ctx context.Context, userID entity.UserID) ([]entity.Task, error) { - q := `SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1` - rows, err := r.db.QueryContext(ctx, q, userID) - if err != nil { - return nil, err - } - defer rows.Close() - - tasks := make([]entity.Task, 0) - for rows.Next() { - var task entity.Task - err := rows.Scan(&task.ID, &task.Content, &task.Description, &task.IsCompleted, &task.DueDate, &task.CreatedAt, &task.UpdatedAt) - if err != nil { - return nil, err - } - tasks = append(tasks, task) - } - if err := rows.Err(); err != nil { - return nil, err - } - - return tasks, nil -} - -// VerifyAvailableByID check if a task is available by id. -func (r *Repository) VerifyAvailableByID(ctx context.Context, taskID entity.TaskID) error { - var id string - q := `SELECT id FROM tasks WHERE id = $1` - row := r.db.QueryRowContext(ctx, q, taskID) - err := row.Scan(&id) - if errors.Is(err, sql.ErrNoRows) { - return domain.ErrTaskNotFound - } else if err != nil { - return err - } - return nil -} - -// DeleteByID delete a task by id. -func (r *Repository) DeleteByID(ctx context.Context, taskID entity.TaskID) error { - q := `DELETE FROM tasks WHERE id = $1` - _, err := r.db.ExecContext(ctx, q, taskID) - if err != nil { - return err - } - return nil -} - -// Update update task by id. -func (r *Repository) Update(ctx context.Context, t *entity.Task) (entity.TaskID, error) { - t.UpdatedAt = time.Now() - q := `UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1` - _, err := r.db.ExecContext(ctx, q, t.ID, t.Content, t.Description, t.IsCompleted, t.DueDate, t.UpdatedAt) - if err != nil { - return "", err - } - return t.ID, nil -} diff --git a/internal/task/repository/repository_test.go b/internal/task/repository/repository_test.go deleted file mode 100644 index 1bc8d39..0000000 --- a/internal/task/repository/repository_test.go +++ /dev/null @@ -1,627 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "errors" - "regexp" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/test" -) - -type TaskRepositoryTestSuite struct { - suite.Suite -} - -func TestTaskRepositorySuite(t *testing.T) { - suite.Run(t, new(TaskRepositoryTestSuite)) -} - -type dependency struct { - mockDB sqlmock.Sqlmock - idProvider *mocks.IDProvider -} - -func (s *TaskRepositoryTestSuite) TestStore() { - type args struct { - ctx context.Context - task *entity.Task - } - type expected struct { - taskID entity.TaskID - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to store", - args: args{ - ctx: context.Background(), - task: &entity.Task{ - UserID: "user-xxxxx", - Content: "task_content", - Description: "task_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - }, - }, - expected: expected{ - taskID: "", - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.idProvider.On("Generate").Return("task-xxxxx") - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO tasks (id, user_id, content, description, due_date)`)). - WithArgs("task-xxxxx", "user-xxxxx", "task_content", "task_description", &test.TimeAfterNow). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error nil and task id when successfully store", - args: args{ - ctx: context.Background(), - task: &entity.Task{ - UserID: "user-xxxxx", - Content: "task_content", - Description: "task_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - }, - }, - expected: expected{ - taskID: "task-xxxxx", - err: nil, - }, - setup: func(d *dependency) { - d.idProvider.On("Generate").Return("task-xxxxx") - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO tasks (id, user_id, content, description, due_date)`)). - WithArgs("task-xxxxx", "user-xxxxx", "task_content", "task_description", &test.TimeAfterNow). - WillReturnResult(sqlmock.NewResult(1, 1)) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - idProvider: &mocks.IDProvider{}, - } - t.setup(d) - - repository := New(db, d.idProvider) - taskID, err := repository.Store(t.args.ctx, t.args.task) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.taskID, taskID) - }) - } -} - -func (s *TaskRepositoryTestSuite) TestFindByID() { - type args struct { - ctx context.Context - taskID entity.TaskID - } - type expected struct { - task entity.Task - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - task: entity.Task{}, - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). - WithArgs("task-xxxxx"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error ErrTaskNotFound when task not found", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - task: entity.Task{}, - err: domain.ErrTaskNotFound, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). - WithArgs("task-xxxxx"). - WillReturnError(sql.ErrNoRows) - }, - }, - { - name: "it should return error when database scan fail", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - task: entity.Task{}, - err: test.ErrRowScan, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). - WithArgs("task-xxxxx"). - WillReturnError(test.ErrRowScan) - }, - }, - { - name: "it should return error nil and task when success", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - task: entity.Task{ - ID: "task-xxxxx", - UserID: "user-xxxxx", - Content: "task_content", - Description: "task_description", - IsCompleted: true, - DueDate: entity.NullTime{ - NullTime: sql.NullTime{ - Time: test.TimeAfterNow, - Valid: true, - }, - }, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "user_id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "user-xxxxx", "task_content", "task_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) - - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, user_id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE id = $1")). - WithArgs("task-xxxxx"). - WillReturnRows(mockRow) - }, - }, - } - - for _, t := range tests { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - task, err := repository.FindByID(t.args.ctx, t.args.taskID) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.task, task) - } -} - -func (s *TaskRepositoryTestSuite) TestFindAllByUserID() { - type args struct { - ctx context.Context - userID entity.UserID - } - type expected struct { - tasks []entity.Task - allowAnyError bool - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - tasks: nil, - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). - WithArgs("user-xxxxx"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error when database rows fail to scan", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - tasks: nil, - allowAnyError: true, - err: errors.New("anything"), - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow(nil, "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow) - - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). - WithArgs("user-xxxxx"). - WillReturnRows(mockRow) - }, - }, - { - name: "it should return error when database rows error", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - tasks: nil, - err: test.ErrRows, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "task_xxxxx_content", "task_yyyyy_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). - AddRow("task-yyyyy", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow). - RowError(1, test.ErrRows) - - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). - WithArgs("user-xxxxx"). - WillReturnRows(mockRow) - }, - }, - { - name: "it should return error nil and empty slice task when successfully query with no tasks", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - tasks: []entity.Task{}, - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}) - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). - WithArgs("user-xxxxx"). - WillReturnRows(mockRow) - }, - }, - { - name: "it should return error nil and all task when successfully query", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - tasks: []entity.Task{ - { - ID: "task-xxxxx", - Content: "task_xxxxx_content", - Description: "task_xxxxx_description", - IsCompleted: false, - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, - { - ID: "task-yyyyy", - Content: "task_yyyyy_content", - Description: "task_yyyyy_description", - IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow, - }, - }, - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "content", "description", "is_completed", "due_date", "created_at", "updated_at"}). - AddRow("task-xxxxx", "task_xxxxx_content", "task_xxxxx_description", false, nil, test.TimeBeforeNow, test.TimeBeforeNow). - AddRow("task-yyyyy", "task_yyyyy_content", "task_yyyyy_description", true, test.TimeAfterNow, test.TimeBeforeNow, test.TimeBeforeNow) - - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id, content, description, is_completed, due_date, created_at, updated_at FROM tasks WHERE user_id = $1`)). - WithArgs("user-xxxxx"). - WillReturnRows(mockRow) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, d.idProvider) - tasks, err := repository.FindAllByUserID(t.args.ctx, t.args.userID) - - if t.expected.allowAnyError { - s.Error(err) - } else { - s.Equal(t.expected.err, err) - } - s.Equal(t.expected.tasks, tasks) - }) - } -} - -func (s *TaskRepositoryTestSuite) TestVerifyAvailableByID() { - type args struct { - ctx context.Context - taskID entity.TaskID - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM tasks WHERE id = $1`)). - WithArgs("task-xxxxx"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error ErrTaskNotFound when task not found", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - err: domain.ErrTaskNotFound, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM tasks WHERE id = $1`)). - WithArgs("task-xxxxx"). - WillReturnError(sql.ErrNoRows) - }, - }, - { - name: "it should return error when fail to scan row", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - err: test.ErrRowScan, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM tasks WHERE id = $1`)). - WithArgs("task-xxxxx"). - WillReturnError(test.ErrRowScan) - }, - }, - { - name: "it should return error nil when task found", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id"}).AddRow("task-xxxxx") - d.mockDB.ExpectQuery(regexp.QuoteMeta(`SELECT id FROM tasks WHERE id = $1`)). - WithArgs("task-xxxxx"). - WillReturnRows(mockRow) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - err = repository.VerifyAvailableByID(t.args.ctx, t.args.taskID) - - s.Equal(t.expected.err, err) - }) - } -} - -func (s *TaskRepositoryTestSuite) TestDeleteByID() { - type args struct { - ctx context.Context - taskID entity.TaskID - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta(`DELETE FROM tasks WHERE id = $1`)). - WithArgs("task-xxxxx"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return nil when success to delete", - args: args{ - ctx: context.Background(), - taskID: "task-xxxxx", - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta(`DELETE FROM tasks WHERE id = $1`)). - WithArgs("task-xxxxx"). - WillReturnResult(sqlmock.NewResult(1, 1)) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - err = repository.DeleteByID(t.args.ctx, t.args.taskID) - - s.Equal(t.expected.err, err) - }) - } -} - -func (s *TaskRepositoryTestSuite) TestUpdate() { - type args struct { - ctx context.Context - task *entity.Task - } - type expected struct { - taskID entity.TaskID - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail", - args: args{ - ctx: context.Background(), - task: &entity.Task{}, - }, - expected: expected{ - taskID: "", - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1")). - WithArgs("", "", "", false, entity.NullTime{NullTime: sql.NullTime{Valid: false}}, sqlmock.AnyArg()). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error nil and task id when success update", - args: args{ - ctx: context.Background(), - task: &entity.Task{ - ID: "task-xxxxx", - UserID: "user-xxxxx", - Content: "task_content", - Description: "task_description", - IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, - }, - expected: expected{ - taskID: "task-xxxxx", - err: nil, - }, - setup: func(d *dependency) { - d.mockDB.ExpectExec(regexp.QuoteMeta("UPDATE tasks SET content = $2, description = $3, is_completed = $4, due_date = $5, updated_at = $6 WHERE id = $1")). - WithArgs("task-xxxxx", "task_content", "task_description", true, entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, sqlmock.AnyArg()). - WillReturnResult(sqlmock.NewResult(1, 1)) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - taskID, err := repository.Update(t.args.ctx, t.args.task) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.taskID, taskID) - }) - } -} diff --git a/internal/task/usecase/usecase.go b/internal/task/usecase/usecase.go deleted file mode 100644 index 2b6610b..0000000 --- a/internal/task/usecase/usecase.go +++ /dev/null @@ -1,109 +0,0 @@ -package usecase - -import ( - "context" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -type Usecase struct { - taskRepository domain.TaskRepository -} - -// New create a new usecase. -func New(taskRepository domain.TaskRepository) Usecase { - return Usecase{taskRepository: taskRepository} -} - -// Create create a new task. -func (u *Usecase) Create(ctx context.Context, payload *dto.TaskCreateIn) (dto.TaskCreateOut, error) { - task := &entity.Task{UserID: payload.UserID, Content: payload.Content, Description: payload.Description, DueDate: payload.DueDate} - - taskID, err := u.taskRepository.Store(ctx, task) - if err != nil { - return dto.TaskCreateOut{}, err - } - return dto.TaskCreateOut{ID: taskID}, nil -} - -// GetAll get all tasks. -func (u *Usecase) GetAll(ctx context.Context, payload *dto.TaskGetAllIn) ([]dto.TaskGetAllOut, error) { - tasks, err := u.taskRepository.FindAllByUserID(ctx, payload.UserID) - if err != nil { - return nil, err - } - - output := make([]dto.TaskGetAllOut, len(tasks)) - for i, task := range tasks { - output[i] = dto.TaskGetAllOut{ - ID: task.ID, - Content: task.Content, - Description: task.Description, - IsCompleted: task.IsCompleted, - DueDate: task.DueDate, - CreatedAt: task.CreatedAt, - UpdatedAt: task.UpdatedAt, - } - } - return output, nil -} - -// Remove remove a task. -func (u *Usecase) Remove(ctx context.Context, payload *dto.TaskRemoveIn) error { - task, err := u.taskRepository.FindByID(ctx, payload.TaskID) - if err != nil { - return err - } - if task.UserID != payload.UserID { - return domain.ErrTaskAuthorization - } - if err := u.taskRepository.DeleteByID(ctx, payload.TaskID); err != nil { - return err - } - return nil -} - -// GetByID get task by id. -func (u *Usecase) GetByID(ctx context.Context, payload *dto.TaskGetByIDIn) (dto.TaskGetByIDOut, error) { - task, err := u.taskRepository.FindByID(ctx, payload.TaskID) - if err != nil { - return dto.TaskGetByIDOut{}, err - } - if task.UserID != payload.UserID { - return dto.TaskGetByIDOut{}, domain.ErrTaskAuthorization - } - - output := dto.TaskGetByIDOut{ - ID: task.ID, - Content: task.Content, - Description: task.Description, - IsCompleted: task.IsCompleted, - DueDate: task.DueDate, - CreatedAt: task.CreatedAt, - UpdatedAt: task.UpdatedAt, - } - return output, nil -} - -func (u *Usecase) Update(ctx context.Context, payload *dto.TaskUpdateIn) (dto.TaskUpdateOut, error) { - task, err := u.taskRepository.FindByID(ctx, payload.TaskID) - if err != nil { - return dto.TaskUpdateOut{}, err - } - if task.UserID != payload.UserID { - return dto.TaskUpdateOut{}, domain.ErrTaskAuthorization - } - - task.Content = payload.Content - task.Description = payload.Description - task.IsCompleted = payload.IsCompleted - task.DueDate = payload.DueDate - - taskID, err := u.taskRepository.Update(ctx, &task) - if err != nil { - return dto.TaskUpdateOut{}, err - } - return dto.TaskUpdateOut{ID: taskID}, nil -} diff --git a/internal/task/usecase/usecase_test.go b/internal/task/usecase/usecase_test.go deleted file mode 100644 index 5dc33a3..0000000 --- a/internal/task/usecase/usecase_test.go +++ /dev/null @@ -1,505 +0,0 @@ -package usecase - -import ( - "context" - "database/sql" - "testing" - - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/test" -) - -type TaskUsecaseTestSuite struct { - suite.Suite -} - -func TestTaskUsecaseSuite(t *testing.T) { - suite.Run(t, new(TaskUsecaseTestSuite)) -} - -type dependency struct { - taskRepository *mocks.TaskRepository -} - -func (s *TaskUsecaseTestSuite) TestCreate() { - type args struct { - ctx context.Context - payload *dto.TaskCreateIn - } - type expected struct { - output dto.TaskCreateOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when task respository return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskCreateIn{ - UserID: "user-xxxxx", - Content: "task_content", - Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, - }, - }, - expected: expected{ - output: dto.TaskCreateOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.taskRepository.On("Store", context.Background(), &entity.Task{ - UserID: "user-xxxxx", - Content: "task_content", - Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, - }).Return(entity.TaskID(""), test.ErrUnexpected) - }, - }, - { - name: "it should return error nil and output when task respository return nil error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskCreateIn{ - UserID: "user-xxxxx", - Content: "task_content", - Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, - }, - }, - expected: expected{ - output: dto.TaskCreateOut{ID: "task-xxxxx"}, - err: nil, - }, - setup: func(d *dependency) { - d.taskRepository.On("Store", context.Background(), &entity.Task{ - UserID: "user-xxxxx", - Content: "task_content", - Description: "content_description", - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, - }).Return(entity.TaskID("task-xxxxx"), nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - taskRepository: &mocks.TaskRepository{}, - } - t.setup(d) - - usecase := New(d.taskRepository) - output, err := usecase.Create(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - }) - } -} - -func (s *TaskUsecaseTestSuite) TestGetAll() { - type args struct { - ctx context.Context - payload *dto.TaskGetAllIn - } - type expected struct { - output []dto.TaskGetAllOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when task respository return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskGetAllIn{UserID: "user-xxxxx"}, - }, - expected: expected{ - output: nil, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindAllByUserID", context.Background(), entity.UserID("user-xxxxx")). - Return(nil, test.ErrUnexpected) - }, - }, - { - name: "it should return error nil and tasks when success", - args: args{ - ctx: context.Background(), - payload: &dto.TaskGetAllIn{UserID: "user-xxxxx"}, - }, - expected: expected{ - output: []dto.TaskGetAllOut{ - {ID: "task-xxxxx", Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "task-yyyyy", Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - }, - }, - setup: func(d *dependency) { - tasks := []entity.Task{ - {ID: "task-xxxxx", Content: "task_xxxxx_content", Description: "task_xxxxx_description", IsCompleted: false, DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - {ID: "task-yyyyy", Content: "task_yyyyy_content", Description: "task_yyyyy_description", IsCompleted: true, DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, CreatedAt: test.TimeBeforeNow, UpdatedAt: test.TimeBeforeNow}, - } - - d.taskRepository.On("FindAllByUserID", context.Background(), entity.UserID("user-xxxxx")). - Return(tasks, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - taskRepository: &mocks.TaskRepository{}, - } - t.setup(d) - - usecase := New(d.taskRepository) - output, err := usecase.GetAll(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - }) - } -} - -func (s *TaskUsecaseTestSuite) TestRemove() { - type args struct { - ctx context.Context - payload *dto.TaskRemoveIn - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when task repository FindByID return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskRemoveIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - }, - }, - expected: expected{ - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error ErrTaskAuthorization when task not own by the user", - args: args{ - ctx: context.Background(), - payload: &dto.TaskRemoveIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - }, - }, - expected: expected{ - err: domain.ErrTaskAuthorization, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{UserID: "user-yyyyy"}, nil) - }, - }, - { - name: "it should return error when task repository DeleteByID return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskRemoveIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - }, - }, - expected: expected{ - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{UserID: "user-xxxxx"}, nil) - - d.taskRepository.On("DeleteByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error nil when success delete task", - args: args{ - ctx: context.Background(), - payload: &dto.TaskRemoveIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - }, - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{UserID: "user-xxxxx"}, nil) - - d.taskRepository.On("DeleteByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(nil) - }, - }, - } - - for _, t := range tests { - d := &dependency{ - taskRepository: &mocks.TaskRepository{}, - } - t.setup(d) - - usecase := New(d.taskRepository) - err := usecase.Remove(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - } -} - -func (s *TaskUsecaseTestSuite) TestGetByID() { - type args struct { - ctx context.Context - payload *dto.TaskGetByIDIn - } - type expected struct { - output dto.TaskGetByIDOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when task repository FindByID return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskGetByIDIn{}, - }, - expected: expected{ - output: dto.TaskGetByIDOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("")). - Return(entity.Task{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error ErrTaskAuthorization when task not own by the user", - args: args{ - ctx: context.Background(), - payload: &dto.TaskGetByIDIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - }, - }, - expected: expected{ - output: dto.TaskGetByIDOut{}, - err: domain.ErrTaskAuthorization, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{UserID: "user-yyyyy"}, nil) - }, - }, - { - name: "it should return error nil when success get task", - args: args{ - ctx: context.Background(), - payload: &dto.TaskGetByIDIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - }, - }, - expected: expected{ - output: dto.TaskGetByIDOut{ - ID: "task-xxxxx", - Content: "task_content", - Description: "task_description", - IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, - err: nil, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{ - ID: "task-xxxxx", - UserID: "user-xxxxx", - Content: "task_content", - Description: "task_description", - IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, nil) - }, - }, - } - - for _, t := range tests { - d := &dependency{ - taskRepository: &mocks.TaskRepository{}, - } - t.setup(d) - - usecase := New(d.taskRepository) - output, err := usecase.GetByID(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - } -} - -func (s *TaskUsecaseTestSuite) TestUpdate() { - type args struct { - ctx context.Context - payload *dto.TaskUpdateIn - } - type expected struct { - output dto.TaskUpdateOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when task repository FindByID return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskUpdateIn{TaskID: "task-xxxxx"}, - }, - expected: expected{ - output: dto.TaskUpdateOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{}, test.ErrUnexpected) - }, - }, - { - name: "it should return error ErrTaskAuthorization when task is not own by the user", - args: args{ - ctx: context.Background(), - payload: &dto.TaskUpdateIn{TaskID: "task-xxxxx", UserID: "user-xxxxx"}, - }, - expected: expected{ - output: dto.TaskUpdateOut{}, - err: domain.ErrTaskAuthorization, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{UserID: "user-yyyyy"}, nil) - }, - }, - { - name: "it should return error when task repository Update return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.TaskUpdateIn{TaskID: "task-xxxxx", UserID: "user-xxxxx"}, - }, - expected: expected{ - output: dto.TaskUpdateOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{UserID: "user-xxxxx"}, nil) - - d.taskRepository.On("Update", context.Background(), &entity.Task{UserID: "user-xxxxx"}). - Return(entity.TaskID(""), test.ErrUnexpected) - }, - }, - { - name: "it should return error nil when success update", - args: args{ - ctx: context.Background(), - payload: &dto.TaskUpdateIn{ - TaskID: "task-xxxxx", - UserID: "user-xxxxx", - Content: "new_content", - Description: "new_description", - IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - }, - }, - expected: expected{ - output: dto.TaskUpdateOut{ - ID: "task-xxxxx", - }, - err: nil, - }, - setup: func(d *dependency) { - d.taskRepository.On("FindByID", context.Background(), entity.TaskID("task-xxxxx")). - Return(entity.Task{ - ID: "task-xxxxx", - UserID: "user-xxxxx", - Content: "task_content", - Description: "task_description", - IsCompleted: false, - DueDate: entity.NullTime{NullTime: sql.NullTime{Valid: false}}, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, nil) - - d.taskRepository.On("Update", context.Background(), &entity.Task{ - ID: "task-xxxxx", - UserID: "user-xxxxx", - Content: "new_content", - Description: "new_description", - IsCompleted: true, - DueDate: entity.NullTime{NullTime: sql.NullTime{Time: test.TimeAfterNow, Valid: true}}, - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }).Return(entity.TaskID("task-xxxxx"), nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - taskRepository: &mocks.TaskRepository{}, - } - t.setup(d) - - usecase := New(d.taskRepository) - output, err := usecase.Update(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - }) - } -} diff --git a/internal/user/delivery/http/handler.go b/internal/user/delivery/http/handler.go deleted file mode 100644 index 26583b7..0000000 --- a/internal/user/delivery/http/handler.go +++ /dev/null @@ -1,50 +0,0 @@ -package http - -import ( - "encoding/json" - "net/http" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/pkg/errorx" -) - -type HTTPHandler struct { - userUsecase domain.UserUsecase - validator domain.ValidatorProvider -} - -// New creates a new user handler. -func New(validator domain.ValidatorProvider, userUsecase domain.UserUsecase) HTTPHandler { - return HTTPHandler{validator: validator, userUsecase: userUsecase} -} - -// POST /users to create new user. -func (h *HTTPHandler) Post(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - encoder := json.NewEncoder(w) - - var payload dto.UserCreateIn - if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { - w.WriteHeader(http.StatusBadRequest) - encoder.Encode(domain.NewErrorResponse(http.StatusBadRequest, "Invalid request body")) - return - } - if err := h.validator.Validate(&payload); err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - output, err := h.userUsecase.Create(r.Context(), &payload) - if err != nil { - code, msg := errorx.HTTPErrorTranslator(err) - w.WriteHeader(code) - encoder.Encode(domain.NewErrorResponse(code, msg)) - return - } - - w.WriteHeader(http.StatusCreated) - encoder.Encode(domain.NewSuccessResponse(http.StatusCreated, "Successfully registered user", output)) -} diff --git a/internal/user/delivery/http/handler_test.go b/internal/user/delivery/http/handler_test.go deleted file mode 100644 index fe23ea3..0000000 --- a/internal/user/delivery/http/handler_test.go +++ /dev/null @@ -1,165 +0,0 @@ -package http - -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/pkg/errorx" - "github.com/edwintantawi/taskit/test" -) - -type UserHTTPHandlerTestSuite struct { - suite.Suite -} - -func TestUserHTTPHandlerSuite(t *testing.T) { - suite.Run(t, new(UserHTTPHandlerTestSuite)) -} - -type dependency struct { - req *http.Request - validator *mocks.ValidatorProvider - userUsecase *mocks.UserUsecase -} - -func (s *UserHTTPHandlerTestSuite) TestPost() { - type args struct { - requestBody []byte - } - type expected struct { - contentType string - statusCode int - message string - error string - payload map[string]any - } - tests := []struct { - name string - isError bool - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should response with error when request body is invalid or not provided", - isError: true, - args: args{ - requestBody: []byte(`{`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusBadRequest, - message: http.StatusText(http.StatusBadRequest), - error: "Invalid request body", - }, - setup: func(d *dependency) {}, - }, - { - name: "it should response with error when payload is not valid", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.UserCreateIn{}). - Return(test.ErrValidator) - }, - }, - { - name: "it should response with error when user usecase Create return unexpected error", - isError: true, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusInternalServerError, - message: http.StatusText(http.StatusInternalServerError), - error: errorx.InternalServerErrorMessage, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.UserCreateIn{}). - Return(nil) - - d.userUsecase.On("Create", mock.Anything, &dto.UserCreateIn{}). - Return(dto.UserCreateOut{}, test.ErrUnexpected) - }, - }, - { - name: "it should response with success when success", - isError: false, - args: args{ - requestBody: []byte(`{}`), - }, - expected: expected{ - contentType: "application/json", - statusCode: http.StatusCreated, - message: "Successfully registered user", - payload: map[string]any{ - "id": "user-xxxxx", - "email": "gopher@go.dev", - }, - }, - setup: func(d *dependency) { - d.validator.On("Validate", &dto.UserCreateIn{}). - Return(nil) - - d.userUsecase.On("Create", mock.Anything, &dto.UserCreateIn{}). - Return(dto.UserCreateOut{ID: "user-xxxxx", Email: "gopher@go.dev"}, nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - reqBody := bytes.NewReader(t.args.requestBody) - rr := httptest.NewRecorder() - req := httptest.NewRequest("POST", "/", reqBody) - - d := &dependency{ - validator: &mocks.ValidatorProvider{}, - userUsecase: &mocks.UserUsecase{}, - req: req, - } - t.setup(d) - - handler := New(d.validator, d.userUsecase) - handler.Post(rr, d.req) - - s.Equal(t.expected.contentType, rr.Header().Get("Content-Type")) - s.Equal(t.expected.statusCode, rr.Code) - - if t.isError { - var resBody domain.ErrorResponse - json.NewDecoder(rr.Body).Decode(&resBody) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.error, resBody.Error) - } else { - var resBody domain.SuccessResponse - json.NewDecoder(rr.Body).Decode(&resBody) - payloadMap := resBody.Payload.(map[string]any) - - s.Equal(t.expected.statusCode, resBody.StatusCode) - s.Equal(t.expected.message, resBody.Message) - s.Equal(t.expected.payload, payloadMap) - } - }) - } -} diff --git a/internal/user/repository/postgres.go b/internal/user/repository/postgres.go new file mode 100644 index 0000000..c83ea05 --- /dev/null +++ b/internal/user/repository/postgres.go @@ -0,0 +1,61 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "time" + + "github.com/google/uuid" + + "github.com/edwintantawi/taskit/internal/entity" +) + +var ( + ErrUserNotFound = errors.New("user not found") +) + +// Postgres represents a User postgres repository. +type Postgres struct { + db *sql.DB +} + +func NewPostgres(db *sql.DB) Postgres { + return Postgres{db} +} + +// Save saves a new user to the database. +func (p Postgres) Save(ctx context.Context, newUser entity.NewUser) (entity.AddedUser, error) { + id := uuid.NewString() + createdTime := time.Now().UTC() + + q := `INSERT INTO users (id, name, email, password, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $5)` + + _, err := p.db.ExecContext(ctx, q, id, newUser.Name, newUser.Email, newUser.Password, createdTime) + if err != nil { + return entity.AddedUser{}, err + } + + return entity.AddedUser{ID: id, Email: newUser.Email}, nil +} + +// FindByEmail find user by email. +func (p Postgres) FindByEmail(ctx context.Context, email string) (entity.User, error) { + var user entity.User + q := `SELECT id, name, email, password, created_at, updated_at + FROM users WHERE email = $1` + + row := p.db.QueryRowContext(ctx, q, email) + err := row.Scan(&user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt) + if errors.Is(err, sql.ErrNoRows) { + return entity.User{}, ErrUserNotFound + } else if err != nil { + return entity.User{}, err + } + + user.CreatedAt = user.CreatedAt.UTC() + user.UpdatedAt = user.UpdatedAt.UTC() + + return user, nil +} diff --git a/internal/user/repository/postgres_test.go b/internal/user/repository/postgres_test.go new file mode 100644 index 0000000..9ff5756 --- /dev/null +++ b/internal/user/repository/postgres_test.go @@ -0,0 +1,137 @@ +package repository + +import ( + "context" + "database/sql" + "log" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/edwintantawi/taskit/internal/entity" + "github.com/edwintantawi/taskit/pkg/database" + "github.com/edwintantawi/taskit/test" +) + +type UserRepositoryPostgresTestSuite struct { + suite.Suite + db *sql.DB + userTableHelper test.UserTableHelper + cleanUpSuite func() error + cleanUpTest func() +} + +func TestUserRepositoryPostgresSuite(t *testing.T) { + suite.Run(t, new(UserRepositoryPostgresTestSuite)) +} + +func (s *UserRepositoryPostgresTestSuite) SetupSuite() { + resource := test.NewPostgresResource() + if err := database.Migration(resource.DSN, "../../../migrations"); err != nil { + s.Fail("Could not migrate database", err) + } + + s.userTableHelper = test.NewUserTableHelper(resource.DB) + + s.db = resource.DB + s.cleanUpSuite = resource.CleanUp + s.cleanUpTest = test.TruncateTables(resource.DB) +} + +func (s *UserRepositoryPostgresTestSuite) TearDownSuite() { + if err := s.cleanUpSuite(); err != nil { + log.Fatalf("Could not cleanup resource: %s", err) + } +} + +func (s *UserRepositoryPostgresTestSuite) TestSave() { + s.Run("it should return an error if database fail or operation canceled", func() { + ctx := context.Background() + repository := NewPostgres(s.db) + ctx, cancel := context.WithCancel(ctx) + cancel() + + _, err := repository.Save(ctx, entity.NewUser{}) + + s.EqualError(err, context.Canceled.Error()) + }) + + s.Run("it should save a new user to the database", func() { + defer s.cleanUpTest() + + ctx := context.Background() + repository := NewPostgres(s.db) + newUser := entity.NewUser{ + Name: "Gopher", + Email: "gopher@go.dev", + Password: "secret_password", + } + + addedUser, err := repository.Save(ctx, newUser) + + s.NoError(err) + s.NotEmpty(addedUser.ID) + s.Equal(newUser.Email, addedUser.Email) + + userInDB := s.userTableHelper.GetByID(addedUser.ID) + s.Equal(addedUser.ID, userInDB.ID) + s.Equal(newUser.Name, userInDB.Name) + s.Equal(newUser.Email, userInDB.Email) + s.Equal(newUser.Password, userInDB.Password) + s.NotEmpty(userInDB.CreatedAt) + s.Equal(userInDB.CreatedAt, userInDB.UpdatedAt) + }) +} + +func (s *UserRepositoryPostgresTestSuite) TestFindByEmail() { + s.Run("it should return an error if database fail or operation canceled", func() { + ctx := context.Background() + repository := NewPostgres(s.db) + ctx, cancel := context.WithCancel(ctx) + cancel() + + user, err := repository.FindByEmail(ctx, "gopher@gmail.com") + + s.EqualError(err, context.Canceled.Error()) + s.Empty(user) + }) + + s.Run("it should return error when user not found", func() { + ctx := context.Background() + repository := NewPostgres(s.db) + email := "gopher@go.dev" + + user, err := repository.FindByEmail(ctx, email) + + s.Equal(ErrUserNotFound, err) + s.Empty(user) + }) + + s.Run("it should return user when user found", func() { + defer s.cleanUpTest() + + ctx := context.Background() + repository := NewPostgres(s.db) + email := "gopher@go.dev" + + existingUserInDB := test.User{ + ID: "user-xxxxx", + Name: "Gopher", + Email: email, + Password: "secret_password", + CreatedAt: test.TimeBeforeNow, + UpdatedAt: test.TimeAfterNow, + } + s.userTableHelper.Add(existingUserInDB) + + user, err := repository.FindByEmail(ctx, email) + + s.NoError(err) + s.Equal(existingUserInDB.ID, user.ID) + s.Equal(existingUserInDB.Name, user.Name) + s.Equal(existingUserInDB.Email, user.Email) + s.Equal(existingUserInDB.Password, user.Password) + s.Equal(existingUserInDB.CreatedAt, user.CreatedAt) + s.Equal(existingUserInDB.UpdatedAt, user.UpdatedAt) + }) +} diff --git a/internal/user/repository/repository.go b/internal/user/repository/repository.go deleted file mode 100644 index f1eced7..0000000 --- a/internal/user/repository/repository.go +++ /dev/null @@ -1,70 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "errors" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -type Repository struct { - db *sql.DB - idProvider domain.IDProvider -} - -// New create a new user repository. -func New(db *sql.DB, idProvider domain.IDProvider) Repository { - return Repository{db: db, idProvider: idProvider} -} - -// Store save a new user to database. -func (r *Repository) Store(ctx context.Context, u *entity.User) (entity.UserID, error) { - id := entity.UserID(r.idProvider.Generate()) - q := `INSERT INTO users (id, name, email, password) VALUES ($1, $2, $3, $4)` - _, err := r.db.ExecContext(ctx, q, id, u.Name, u.Email, u.Password) - if err != nil { - return "", err - } - return id, nil -} - -// VerifyAvailableEmail check if the email is available. -func (r *Repository) VerifyAvailableEmail(ctx context.Context, email string) error { - var id entity.UserID - q := `SELECT id FROM users WHERE email = $1` - err := r.db.QueryRowContext(ctx, q, email).Scan(&id) - if errors.Is(err, sql.ErrNoRows) { - return nil - } else if err != nil { - return err - } - return domain.ErrEmailNotAvailable -} - -// FindByEmail find a user by email. -func (r *Repository) FindByEmail(ctx context.Context, email string) (entity.User, error) { - var u entity.User - q := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE email = $1` - err := r.db.QueryRowContext(ctx, q, email).Scan(&u.ID, &u.Name, &u.Email, &u.Password, &u.CreatedAt, &u.UpdatedAt) - if errors.Is(err, sql.ErrNoRows) { - return u, domain.ErrUserNotFound - } else if err != nil { - return u, err - } - return u, nil -} - -// FindByID find a user by id. -func (r *Repository) FindByID(ctx context.Context, id entity.UserID) (entity.User, error) { - var u entity.User - q := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1` - err := r.db.QueryRowContext(ctx, q, id).Scan(&u.ID, &u.Name, &u.Email, &u.Password, &u.CreatedAt, &u.UpdatedAt) - if errors.Is(err, sql.ErrNoRows) { - return u, domain.ErrUserNotFound - } else if err != nil { - return u, err - } - return u, nil -} diff --git a/internal/user/repository/repository_test.go b/internal/user/repository/repository_test.go deleted file mode 100644 index 7a065a0..0000000 --- a/internal/user/repository/repository_test.go +++ /dev/null @@ -1,376 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "regexp" - "testing" - - "github.com/DATA-DOG/go-sqlmock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/test" -) - -type UserRepositoryTestSuite struct { - suite.Suite -} - -func TestUserRepositorySuite(t *testing.T) { - suite.Run(t, new(UserRepositoryTestSuite)) -} - -type dependency struct { - mockDB sqlmock.Sqlmock - idProvider *mocks.IDProvider -} - -func (s *UserRepositoryTestSuite) TestCreate() { - type args struct { - ctx context.Context - user *entity.User - } - type expected struct { - userID entity.UserID - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to store", - args: args{ - ctx: context.Background(), - user: &entity.User{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_password"}, - }, - expected: expected{ - userID: "", - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.idProvider.On("Generate").Return("user-xxxxx") - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO users (id, name, email, password) VALUES ($1, $2, $3, $4)`)). - WithArgs("user-xxxxx", "Gopher", "gopher@go.dev", "secret_password"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error nil and user id when successfully store", - args: args{ - ctx: context.Background(), - user: &entity.User{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_password"}, - }, - expected: expected{ - userID: "user-xxxxx", - err: nil, - }, - setup: func(d *dependency) { - d.idProvider.On("Generate").Return("user-xxxxx") - d.mockDB.ExpectExec(regexp.QuoteMeta(`INSERT INTO users (id, name, email, password) VALUES ($1, $2, $3, $4)`)). - WithArgs("user-xxxxx", "Gopher", "gopher@go.dev", "secret_password"). - WillReturnResult(sqlmock.NewResult(1, 1)) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - idProvider: &mocks.IDProvider{}, - } - t.setup(d) - - repository := New(db, d.idProvider) - userID, err := repository.Store(t.args.ctx, t.args.user) - - s.Equal(t.expected.userID, userID) - s.Equal(t.expected.err, err) - }) - } -} - -func (s *UserRepositoryTestSuite) TestVerifyAvailableEmail() { - type args struct { - ctx context.Context - email string - } - type expected struct { - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - email: "gopher@go.dev", - }, - expected: expected{ - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id FROM users WHERE email = $1")). - WithArgs("gopher@go.dev"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error ErrEmailNotAvailable when email is already exist in database", - args: args{ - ctx: context.Background(), - email: "gopher@go.dev", - }, - expected: expected{ - err: domain.ErrEmailNotAvailable, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id"}).AddRow("user-xxxxx") - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id FROM users WHERE email = $1")). - WithArgs("gopher@go.dev"). - WillReturnRows(mockRow) - }, - }, - { - name: "it should return error nil when email is not exist in database", - args: args{ - ctx: context.Background(), - email: "gopher@go.dev", - }, - expected: expected{ - err: nil, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id FROM users WHERE email = $1")). - WithArgs("gopher@go.dev"). - WillReturnError(sql.ErrNoRows) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - err = repository.VerifyAvailableEmail(t.args.ctx, t.args.email) - - s.Equal(t.expected.err, err) - }) - } -} - -func (s *UserRepositoryTestSuite) TestFindByEmail() { - type args struct { - ctx context.Context - email string - } - type expected struct { - user entity.User - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - email: "gopher@go.dev", - }, - expected: expected{ - user: entity.User{}, - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email, password, created_at, updated_at FROM users WHERE email = $1")). - WithArgs("gopher@go.dev"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error ErrUserNotExist when user is not exist", - args: args{ - ctx: context.Background(), - email: "gopher@go.dev", - }, - expected: expected{ - user: entity.User{}, - err: domain.ErrUserNotFound, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email, password, created_at, updated_at FROM users WHERE email = $1")). - WithArgs("gopher@go.dev"). - WillReturnError(sql.ErrNoRows) - }, - }, - { - name: "it should return error nil and user when user is exist", - args: args{ - ctx: context.Background(), - email: "gopher@go.dev", - }, - expected: expected{ - user: entity.User{ - ID: "user-xxxxx", - Name: "Gopher", - Email: "gopher@go.dev", - Password: "secret_password", - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "name", "email", "password", "created_at", "updated_at"}). - AddRow("user-xxxxx", "Gopher", "gopher@go.dev", "secret_password", test.TimeBeforeNow, test.TimeBeforeNow) - - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email, password, created_at, updated_at FROM users WHERE email = $1")). - WithArgs("gopher@go.dev"). - WillReturnRows(mockRow) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - user, err := repository.FindByEmail(t.args.ctx, t.args.email) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.user, user) - }) - } -} - -func (s *UserRepositoryTestSuite) TestFindByID() { - type args struct { - ctx context.Context - userID entity.UserID - } - type expected struct { - user entity.User - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when database fail to query", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - user: entity.User{}, - err: test.ErrDatabase, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1")). - WithArgs("user-xxxxx"). - WillReturnError(test.ErrDatabase) - }, - }, - { - name: "it should return error ErrUserNotFound when user is not found", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - user: entity.User{}, - err: domain.ErrUserNotFound, - }, - setup: func(d *dependency) { - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1")). - WithArgs("user-xxxxx"). - WillReturnError(sql.ErrNoRows) - }, - }, - { - name: "it should return error nil and user when user is found", - args: args{ - ctx: context.Background(), - userID: "user-xxxxx", - }, - expected: expected{ - user: entity.User{ - ID: "user-xxxxx", - Name: "Gopher", - Email: "gopher@go.dev", - Password: "secret_password", - CreatedAt: test.TimeBeforeNow, - UpdatedAt: test.TimeBeforeNow, - }, - err: nil, - }, - setup: func(d *dependency) { - mockRow := sqlmock.NewRows([]string{"id", "name", "email", "password", "created_at", "updated_at"}). - AddRow("user-xxxxx", "Gopher", "gopher@go.dev", "secret_password", test.TimeBeforeNow, test.TimeBeforeNow) - - d.mockDB.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1")). - WithArgs("user-xxxxx"). - WillReturnRows(mockRow) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - db, mockDB, err := sqlmock.New() - if err != nil { - s.FailNow("an error '%s' was not expected when opening a database mock connection", err) - } - - d := &dependency{ - mockDB: mockDB, - } - t.setup(d) - - repository := New(db, nil) - user, err := repository.FindByID(t.args.ctx, t.args.userID) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.user, user) - }) - } -} diff --git a/internal/user/usecase/mocks/PasswordHasher.go b/internal/user/usecase/mocks/PasswordHasher.go new file mode 100644 index 0000000..a595c09 --- /dev/null +++ b/internal/user/usecase/mocks/PasswordHasher.go @@ -0,0 +1,48 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import mock "github.com/stretchr/testify/mock" + +// PasswordHasher is an autogenerated mock type for the PasswordHasher type +type PasswordHasher struct { + mock.Mock +} + +// Hash provides a mock function with given fields: password +func (_m *PasswordHasher) Hash(password string) ([]byte, error) { + ret := _m.Called(password) + + var r0 []byte + if rf, ok := ret.Get(0).(func(string) []byte); ok { + r0 = rf(password) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(password) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewPasswordHasher interface { + mock.TestingT + Cleanup(func()) +} + +// NewPasswordHasher creates a new instance of PasswordHasher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewPasswordHasher(t mockConstructorTestingTNewPasswordHasher) *PasswordHasher { + mock := &PasswordHasher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/user/usecase/mocks/UserRepositoryFindSaver.go b/internal/user/usecase/mocks/UserRepositoryFindSaver.go new file mode 100644 index 0000000..967128d --- /dev/null +++ b/internal/user/usecase/mocks/UserRepositoryFindSaver.go @@ -0,0 +1,72 @@ +// Code generated by mockery v2.15.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + entity "github.com/edwintantawi/taskit/internal/entity" + mock "github.com/stretchr/testify/mock" +) + +// UserRepositoryFindSaver is an autogenerated mock type for the UserRepositoryFindSaver type +type UserRepositoryFindSaver struct { + mock.Mock +} + +// FindByEmail provides a mock function with given fields: ctx, email +func (_m *UserRepositoryFindSaver) FindByEmail(ctx context.Context, email string) (entity.User, error) { + ret := _m.Called(ctx, email) + + var r0 entity.User + if rf, ok := ret.Get(0).(func(context.Context, string) entity.User); ok { + r0 = rf(ctx, email) + } else { + r0 = ret.Get(0).(entity.User) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, email) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Save provides a mock function with given fields: ctx, newUser +func (_m *UserRepositoryFindSaver) Save(ctx context.Context, newUser entity.NewUser) (entity.AddedUser, error) { + ret := _m.Called(ctx, newUser) + + var r0 entity.AddedUser + if rf, ok := ret.Get(0).(func(context.Context, entity.NewUser) entity.AddedUser); ok { + r0 = rf(ctx, newUser) + } else { + r0 = ret.Get(0).(entity.AddedUser) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, entity.NewUser) error); ok { + r1 = rf(ctx, newUser) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +type mockConstructorTestingTNewUserRepositoryFindSaver interface { + mock.TestingT + Cleanup(func()) +} + +// NewUserRepositoryFindSaver creates a new instance of UserRepositoryFindSaver. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewUserRepositoryFindSaver(t mockConstructorTestingTNewUserRepositoryFindSaver) *UserRepositoryFindSaver { + mock := &UserRepositoryFindSaver{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/user/usecase/register.go b/internal/user/usecase/register.go new file mode 100644 index 0000000..78a27db --- /dev/null +++ b/internal/user/usecase/register.go @@ -0,0 +1,48 @@ +package usecase + +import ( + "context" + "errors" + + "github.com/edwintantawi/taskit/internal/entity" +) + +var ( + ErrEmailAlreadyExists = errors.New("email already exists") +) + +type RegisterPayload struct { + Name string + Email string + Password string +} + +type Register struct { + userRepository UserRepositoryFindSaver + passwordHasher PasswordHasher +} + +func NewRegister(userRepository UserRepositoryFindSaver, passwordHasher PasswordHasher) Register { + return Register{userRepository: userRepository, passwordHasher: passwordHasher} +} + +// Execute execute the register usecase. +func (r Register) Execute(ctx context.Context, payload RegisterPayload) (entity.AddedUser, error) { + newUser := entity.NewUser(payload) + if err := newUser.Validate(); err != nil { + return entity.AddedUser{}, err + } + + _, err := r.userRepository.FindByEmail(ctx, newUser.Email) + if err == nil { + return entity.AddedUser{}, ErrEmailAlreadyExists + } + + hashedPassword, err := r.passwordHasher.Hash(newUser.Password) + if err != nil { + return entity.AddedUser{}, err + } + newUser.Password = string(hashedPassword) + + return r.userRepository.Save(ctx, newUser) +} diff --git a/internal/user/usecase/register_test.go b/internal/user/usecase/register_test.go new file mode 100644 index 0000000..21b1e1a --- /dev/null +++ b/internal/user/usecase/register_test.go @@ -0,0 +1,101 @@ +package usecase + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/edwintantawi/taskit/internal/entity" + "github.com/edwintantawi/taskit/internal/user/usecase/mocks" +) + +type UserUsecaseRegisterTestSuite struct { + suite.Suite +} + +func TestUserUsecaseRegisterSuite(t *testing.T) { + suite.Run(t, new(UserUsecaseRegisterTestSuite)) +} + +func (s *UserUsecaseRegisterTestSuite) TestExecute() { + s.Run("it should return error when entity validation fail", func() { + ctx := context.Background() + payload := RegisterPayload{} + + usecase := NewRegister(nil, nil) + output, err := usecase.Execute(ctx, payload) + + s.Error(err) + s.Empty(output) + }) + + s.Run("it should return error ErrEmailAlreadyExists when user with that email is already exists", func() { + ctx := context.Background() + payload := RegisterPayload{ + Name: "Gopher", + Email: "gopher@go.dev", + Password: "secret_password", + } + + mockUserRepository := &mocks.UserRepositoryFindSaver{} + mockUserRepository.On("FindByEmail", ctx, payload.Email).Return(entity.User{}, nil) + + usecase := NewRegister(mockUserRepository, nil) + output, err := usecase.Execute(ctx, payload) + + s.Equal(ErrEmailAlreadyExists, err) + s.Empty(output) + }) + + s.Run("it should return error when fail to hash the password", func() { + errOccurred := errors.New("failed hashing password") + + ctx := context.Background() + payload := RegisterPayload{ + Name: "Gopher", + Email: "gopher@go.dev", + Password: "secret_password", + } + + mockUserRepository := &mocks.UserRepositoryFindSaver{} + mockUserRepository.On("FindByEmail", ctx, payload.Email).Return(entity.User{}, errors.New("user not found")) + + mockPasswordHasher := &mocks.PasswordHasher{} + mockPasswordHasher.On("Hash", payload.Password).Return(nil, errOccurred) + + usecase := NewRegister(mockUserRepository, mockPasswordHasher) + output, err := usecase.Execute(ctx, payload) + + s.Equal(errOccurred, err) + s.Empty(output) + }) + + s.Run("it should return added user when success register new user", func() { + ctx := context.Background() + payload := RegisterPayload{ + Name: "Gopher", + Email: "gopher@go.dev", + Password: "secret_password", + } + + mockUserRepository := &mocks.UserRepositoryFindSaver{} + mockUserRepository.On("FindByEmail", ctx, payload.Email).Return(entity.User{}, errors.New("user not found")) + mockUserRepository.On("Save", ctx, entity.NewUser{ + Name: payload.Name, + Email: payload.Email, + Password: "hashed_password", + }).Return(entity.AddedUser{ID: "user-xxxxx", Email: payload.Email}, nil) + + mockPasswordHasher := &mocks.PasswordHasher{} + mockPasswordHasher.On("Hash", payload.Password).Return([]byte("hashed_password"), nil) + + usecase := NewRegister(mockUserRepository, mockPasswordHasher) + output, err := usecase.Execute(ctx, payload) + + s.NoError(err) + s.Equal("user-xxxxx", output.ID) + s.Equal(payload.Email, output.Email) + }) +} diff --git a/internal/user/usecase/usecase.go b/internal/user/usecase/usecase.go index a2481ce..3418999 100644 --- a/internal/user/usecase/usecase.go +++ b/internal/user/usecase/usecase.go @@ -3,42 +3,26 @@ package usecase import ( "context" - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" + "github.com/edwintantawi/taskit/internal/entity" ) -type Usecase struct { - validator domain.ValidatorProvider - userRepository domain.UserRepository - hashProvider domain.HashProvider +// UserRepositoryFindSaver is the interface that groups the basic find and save methods. +type UserRepositoryFindSaver interface { + UserRepositoryFinder + UserRepositorySaver } -// New create a new user usecase. -func New(validator domain.ValidatorProvider, userRepository domain.UserRepository, hashProvider domain.HashProvider) Usecase { - return Usecase{validator: validator, userRepository: userRepository, hashProvider: hashProvider} +// UserRepositorySaver is the interface that wraps the basic save method. +type UserRepositorySaver interface { + Save(ctx context.Context, newUser entity.NewUser) (entity.AddedUser, error) } -// Create create a new user. -func (u *Usecase) Create(ctx context.Context, payload *dto.UserCreateIn) (dto.UserCreateOut, error) { - user := &entity.User{Name: payload.Name, Email: payload.Email, Password: payload.Password} - if err := u.validator.Validate(user); err != nil { - return dto.UserCreateOut{}, err - } - - if err := u.userRepository.VerifyAvailableEmail(ctx, user.Email); err != nil { - return dto.UserCreateOut{}, err - } - - securePassword, err := u.hashProvider.Hash(user.Password) - if err != nil { - return dto.UserCreateOut{}, err - } - user.Password = string(securePassword) +// UserRepositoryFinder is the interface that wraps the basic find method. +type UserRepositoryFinder interface { + FindByEmail(ctx context.Context, email string) (entity.User, error) +} - id, err := u.userRepository.Store(ctx, user) - if err != nil { - return dto.UserCreateOut{}, err - } - return dto.UserCreateOut{ID: id, Email: user.Email}, nil +// PasswordHasher is an interface for hashing passwords. +type PasswordHasher interface { + Hash(password string) ([]byte, error) } diff --git a/internal/user/usecase/usecase_test.go b/internal/user/usecase/usecase_test.go deleted file mode 100644 index b73f0bc..0000000 --- a/internal/user/usecase/usecase_test.go +++ /dev/null @@ -1,160 +0,0 @@ -package usecase - -import ( - "context" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/internal/domain/mocks" - "github.com/edwintantawi/taskit/test" -) - -type UserUsecaseTestSuite struct { - suite.Suite -} - -func TestUserUsecaseSuite(t *testing.T) { - suite.Run(t, new(UserUsecaseTestSuite)) -} - -type dependency struct { - validator *mocks.ValidatorProvider - userRepository *mocks.UserRepository - hashProvider *mocks.HashProvider -} - -func (s *UserUsecaseTestSuite) TestCreate() { - type args struct { - ctx context.Context - payload *dto.UserCreateIn - } - type expected struct { - output dto.UserCreateOut - err error - } - tests := []struct { - name string - args args - expected expected - setup func(d *dependency) - }{ - { - name: "it should return error when user validation error", - args: args{ - ctx: context.Background(), - payload: &dto.UserCreateIn{}, - }, - expected: expected{ - output: dto.UserCreateOut{}, - err: test.ErrValidator, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything).Return(test.ErrValidator) - }, - }, - { - name: "it should return error when user repository VerifyAvailableEmail return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.UserCreateIn{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_password"}, - }, - expected: expected{ - output: dto.UserCreateOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything).Return(nil) - - d.userRepository.On("VerifyAvailableEmail", context.Background(), "gopher@go.dev"). - Return(test.ErrUnexpected) - }, - }, - { - name: "it should return error when hash provider Hash return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.UserCreateIn{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_password"}, - }, - expected: expected{ - output: dto.UserCreateOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything).Return(nil) - - d.userRepository.On("VerifyAvailableEmail", context.Background(), "gopher@go.dev"). - Return(nil) - - d.hashProvider.On("Hash", "secret_password"). - Return(nil, test.ErrUnexpected) - }, - }, - { - name: "it should return error when user repository Store return unexpected error", - args: args{ - ctx: context.Background(), - payload: &dto.UserCreateIn{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_password"}, - }, - expected: expected{ - output: dto.UserCreateOut{}, - err: test.ErrUnexpected, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything).Return(nil) - - d.userRepository.On("VerifyAvailableEmail", context.Background(), "gopher@go.dev"). - Return(nil) - - d.hashProvider.On("Hash", "secret_password"). - Return([]byte("secret_hashed_password"), nil) - - d.userRepository.On("Store", context.Background(), &entity.User{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_hashed_password"}). - Return(entity.UserID(""), test.ErrUnexpected) - }, - }, - { - name: "it should return error nil and output when success", - args: args{ - ctx: context.Background(), - payload: &dto.UserCreateIn{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_password"}, - }, - expected: expected{ - output: dto.UserCreateOut{ID: "user-xxxxx", Email: "gopher@go.dev"}, - err: nil, - }, - setup: func(d *dependency) { - d.validator.On("Validate", mock.Anything).Return(nil) - - d.userRepository.On("VerifyAvailableEmail", context.Background(), "gopher@go.dev"). - Return(nil) - - d.hashProvider.On("Hash", "secret_password"). - Return([]byte("secret_hashed_password"), nil) - - d.userRepository.On("Store", context.Background(), &entity.User{Name: "Gopher", Email: "gopher@go.dev", Password: "secret_hashed_password"}). - Return(entity.UserID("user-xxxxx"), nil) - }, - }, - } - - for _, t := range tests { - s.Run(t.name, func() { - d := &dependency{ - validator: &mocks.ValidatorProvider{}, - userRepository: &mocks.UserRepository{}, - hashProvider: &mocks.HashProvider{}, - } - t.setup(d) - - usecase := New(d.validator, d.userRepository, d.hashProvider) - output, err := usecase.Create(t.args.ctx, t.args.payload) - - s.Equal(t.expected.err, err) - s.Equal(t.expected.output, output) - }) - } -} diff --git a/migrations/000002_create_authentications_table.down.sql b/migrations/000002_create_authentications_table.down.sql deleted file mode 100644 index c489193..0000000 --- a/migrations/000002_create_authentications_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE authentications; \ No newline at end of file diff --git a/migrations/000002_create_authentications_table.up.sql b/migrations/000002_create_authentications_table.up.sql deleted file mode 100644 index 66a50f9..0000000 --- a/migrations/000002_create_authentications_table.up.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE authentications ( - id VARCHAR(64) PRIMARY KEY, - user_id VARCHAR(255) NOT NULL, - token TEXT NOT NULL, - expires_at TIMESTAMP NOT NULL, - - CONSTRAINT fk_authentications_users FOREIGN KEY(user_id) REFERENCES users(id) -); \ No newline at end of file diff --git a/migrations/000003_create_tasks_table.down.sql b/migrations/000003_create_tasks_table.down.sql deleted file mode 100644 index 3518cd0..0000000 --- a/migrations/000003_create_tasks_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE tasks; \ No newline at end of file diff --git a/migrations/000003_create_tasks_table.up.sql b/migrations/000003_create_tasks_table.up.sql deleted file mode 100644 index 5f097dd..0000000 --- a/migrations/000003_create_tasks_table.up.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE tasks ( - id VARCHAR(64) PRIMARY KEY, - user_id VARCHAR(64) NOT NULL, - content VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - is_completed BOOLEAN NOT NULL DEFAULT FALSE, - due_date TIMESTAMP, - created_at TIMESTAMP NOT NULL DEFAULT NOW(), - updated_at TIMESTAMP NOT NULL DEFAULT NOW(), - - CONSTRAINT fk_tasks_users FOREIGN KEY(user_id) REFERENCES users(id) -) \ No newline at end of file diff --git a/pkg/database/migration.go b/pkg/database/migration.go new file mode 100644 index 0000000..53776a8 --- /dev/null +++ b/pkg/database/migration.go @@ -0,0 +1,31 @@ +package database + +import ( + "errors" + "os" + + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + "github.com/golang-migrate/migrate/v4/source/iofs" +) + +// Migration runs the database migrations. +// The dsn is the database connection string. +// The migrationDir is the path to the directory containing the migrations. +func Migration(dsn, migrationDir string) error { + iofsDriver, err := iofs.New(os.DirFS(migrationDir), ".") + if err != nil { + return err + } + migrator, err := migrate.NewWithSourceInstance("iofs", iofsDriver, dsn) + if err != nil { + return err + } + err = migrator.Up() + if errors.Is(err, migrate.ErrNoChange) { + return nil + } else if err != nil { + return err + } + return nil +} diff --git a/pkg/errorx/http_translator.go b/pkg/errorx/http_translator.go deleted file mode 100644 index 224084d..0000000 --- a/pkg/errorx/http_translator.go +++ /dev/null @@ -1,70 +0,0 @@ -package errorx - -import ( - "fmt" - "log" - "net/http" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/pkg/security" -) - -// HTTPError message -var ( - InternalServerErrorMessage = "Something went wrong" -) - -// HTTPErrorTranslator translates error to http status code and human readable error message. -func HTTPErrorTranslator(err error) (code int, msg string) { - log.Println("[ERROR]", err) - switch err { - // User entity - case entity.ErrEmailInvalid: - return http.StatusBadRequest, "Email must be a valid email address" - case entity.ErrPasswordTooShort: - return http.StatusBadRequest, fmt.Sprintf("Password must be greater then %d character in length", entity.MinPasswordLength) - // User repository - case domain.ErrEmailNotAvailable: - return http.StatusBadRequest, "Email is not available" - case domain.ErrUserNotFound: - return http.StatusNotFound, "User not found" - // Auth entity - case entity.ErrAuthTokenExpired: - return http.StatusBadRequest, "Refresh token is expired" - // Auth repository - case domain.ErrAuthNotFound: - return http.StatusNotFound, "Authentication not found" - case domain.ErrEmailNotExist: - return http.StatusBadRequest, "Email is not exist" - // Auth usecase - case domain.ErrPasswordIncorrect: - return http.StatusBadRequest, "Password is incorrect" - // Task repository - case domain.ErrTaskNotFound: - return http.StatusNotFound, "Task not found" - // Task usecase - case domain.ErrTaskAuthorization: - return http.StatusForbidden, "Not have access to this task" - // DTO - case dto.ErrEmailEmpty: - return http.StatusBadRequest, "Email is required field" - case dto.ErrPasswordEmpty: - return http.StatusBadRequest, "Password is required field" - case dto.ErrNameEmpty: - return http.StatusBadRequest, "Name is required field" - case dto.ErrRefreshTokenEmpty: - return http.StatusBadRequest, "Refresh token is required field" - case dto.ErrContentEmpty: - return http.StatusBadRequest, "Content is required field" - // Security JWT - case security.ErrAccessTokenExpired: - return http.StatusUnauthorized, "Access token is expired" - case security.ErrAccessTokenInvalid: - return http.StatusUnauthorized, "Access token is invalid" - // Other - default: - return http.StatusInternalServerError, InternalServerErrorMessage - } -} diff --git a/pkg/errorx/http_translator_test.go b/pkg/errorx/http_translator_test.go deleted file mode 100644 index 5a333f3..0000000 --- a/pkg/errorx/http_translator_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package errorx - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/suite" - - "github.com/edwintantawi/taskit/internal/domain" - "github.com/edwintantawi/taskit/internal/domain/dto" - "github.com/edwintantawi/taskit/internal/domain/entity" - "github.com/edwintantawi/taskit/pkg/security" -) - -type HTTPErrorTranslatorTestSuite struct { - suite.Suite -} - -func TestHTTPErrorTranslatorSuite(t *testing.T) { - suite.Run(t, new(HTTPErrorTranslatorTestSuite)) -} - -func (s *HTTPErrorTranslatorTestSuite) TestErrorTranslator() { - tests := []struct { - err error - expectedCode int - expectedMsg string - }{ - // User entity - {entity.ErrEmailInvalid, 400, "Email must be a valid email address"}, - {entity.ErrPasswordTooShort, 400, fmt.Sprintf("Password must be greater then %d character in length", entity.MinPasswordLength)}, - // User repository - {domain.ErrEmailNotAvailable, 400, "Email is not available"}, - {domain.ErrUserNotFound, 404, "User not found"}, - // Auth entity - {entity.ErrAuthTokenExpired, 400, "Refresh token is expired"}, - // Auth repository - {domain.ErrAuthNotFound, 404, "Authentication not found"}, - // Auth usecase - {domain.ErrPasswordIncorrect, 400, "Password is incorrect"}, - {domain.ErrEmailNotExist, 400, "Email is not exist"}, - // Task repository - {domain.ErrTaskNotFound, 404, "Task not found"}, - // Task usecase - {domain.ErrTaskAuthorization, 403, "Not have access to this task"}, - // DTO - {dto.ErrEmailEmpty, 400, "Email is required field"}, - {dto.ErrPasswordEmpty, 400, "Password is required field"}, - {dto.ErrNameEmpty, 400, "Name is required field"}, - {dto.ErrRefreshTokenEmpty, 400, "Refresh token is required field"}, - {dto.ErrContentEmpty, 400, "Content is required field"}, - // Security JWT - {security.ErrAccessTokenExpired, 401, "Access token is expired"}, - {security.ErrAccessTokenInvalid, 401, "Access token is invalid"}, - // Other - {errors.New("other error"), 500, "Something went wrong"}, - } - - for _, test := range tests { - code, msg := HTTPErrorTranslator(test.err) - s.Equal(test.expectedCode, code) - s.Equal(test.expectedMsg, msg) - } -} diff --git a/pkg/httpsvr/httpsvr.go b/pkg/httpsvr/httpsvr.go deleted file mode 100644 index 4920b0e..0000000 --- a/pkg/httpsvr/httpsvr.go +++ /dev/null @@ -1,52 +0,0 @@ -package httpsvr - -import ( - "context" - "errors" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/go-chi/chi/v5" -) - -type Server struct { - Addr string - Router *chi.Mux -} - -// New creates a new HTTP server. -func New(addr string, r *chi.Mux) Server { - return Server{Addr: addr, Router: r} -} - -// Run starts the HTTP server with gracefully shutdown. -func (s *Server) Run() error { - svr := &http.Server{ - Addr: s.Addr, - Handler: s.Router, - } - - shutdownChan := make(chan error) - go func() { - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - - log.Println("Shutting down the server") - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - shutdownChan <- svr.Shutdown(ctx) - }() - - err := svr.ListenAndServe() - if !errors.Is(err, http.ErrServerClosed) { - return err - } - - return <-shutdownChan -} diff --git a/pkg/idgen/uuid.go b/pkg/idgen/uuid.go deleted file mode 100644 index aebdb8c..0000000 --- a/pkg/idgen/uuid.go +++ /dev/null @@ -1,15 +0,0 @@ -package idgen - -import "github.com/google/uuid" - -type UUID struct{} - -// NewUUID creates a new UUID generator. -func NewUUID() UUID { - return UUID{} -} - -// Generate generates a new UUID string. -func (p *UUID) Generate() string { - return uuid.NewString() -} diff --git a/pkg/postgres/postgres.go b/pkg/postgres/postgres.go deleted file mode 100644 index 7a2f8ef..0000000 --- a/pkg/postgres/postgres.go +++ /dev/null @@ -1,71 +0,0 @@ -package postgres - -import ( - "database/sql" - "errors" - "fmt" - "log" - "os" - - "github.com/golang-migrate/migrate/v4" - _ "github.com/golang-migrate/migrate/v4/database/postgres" - "github.com/golang-migrate/migrate/v4/source/iofs" - _ "github.com/lib/pq" -) - -type Config struct { - Port string - Host string - DB string - User string - Password string - SSLMode string -} - -const Driver = "postgres" - -// New create new postgres connection. -func New(cfg Config) (*sql.DB, func(autoMigrate bool) error) { - dsn := fmt.Sprintf( - "postgres://%s:%s@%s:%s/%s?sslmode=%s", - cfg.User, - cfg.Password, - cfg.Host, - cfg.Port, - cfg.DB, - cfg.SSLMode, - ) - - db, err := sql.Open(Driver, dsn) - if err != nil { - log.Fatalf("Failed open postgres connection: %v", err) - } - if err := db.Ping(); err != nil { - log.Fatalf("Failed to ping postgres database: %v", err) - } - - migrate := func(autoMigrate bool) error { - if !autoMigrate { - return nil - } - iofsDriver, err := iofs.New(os.DirFS("migrations"), ".") - if err != nil { - return err - } - migrator, err := migrate.NewWithSourceInstance("iofs", iofsDriver, dsn) - if err != nil { - return err - } - err = migrator.Up() - if errors.Is(err, migrate.ErrNoChange) { - log.Println("Migration no change, nothing to migrate") - return nil - } else if err != nil { - return err - } - log.Println("Successfully migrate database") - return nil - } - - return db, migrate -} diff --git a/pkg/security/bcrypt.go b/pkg/security/bcrypt.go deleted file mode 100644 index aa737f5..0000000 --- a/pkg/security/bcrypt.go +++ /dev/null @@ -1,19 +0,0 @@ -package security - -import "golang.org/x/crypto/bcrypt" - -type Bcrypt struct{} - -func NewBcrypt() Bcrypt { - return Bcrypt{} -} - -// Hash hashes a raw string. -func (b *Bcrypt) Hash(raw string) ([]byte, error) { - return bcrypt.GenerateFromPassword([]byte(raw), bcrypt.DefaultCost) -} - -// Compare compares a raw string with a hashed string. -func (b *Bcrypt) Compare(raw string, hashed string) error { - return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(raw)) -} diff --git a/pkg/security/jwt.go b/pkg/security/jwt.go deleted file mode 100644 index ddf05b5..0000000 --- a/pkg/security/jwt.go +++ /dev/null @@ -1,98 +0,0 @@ -package security - -import ( - "errors" - "time" - - "github.com/golang-jwt/jwt/v4" - - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -var ( - ErrAccessTokenExpired = errors.New("security.jwt.access_token_expired") - ErrAccessTokenInvalid = errors.New("security.jwt.access_token_invalid") -) - -type jwtClaims struct { - jwt.RegisteredClaims - UserID entity.UserID `json:"user_id"` -} - -type JWTTokenConfig struct { - Key string - Exp int -} - -type JWT struct { - accessTokenKey string - refreshTokenKey string - accessTokenExpires int - refreshTokenExpires int -} - -func NewJWT(accessToken, refreshToken JWTTokenConfig) JWT { - return JWT{ - accessTokenKey: accessToken.Key, - refreshTokenKey: refreshToken.Key, - accessTokenExpires: accessToken.Exp, - refreshTokenExpires: refreshToken.Exp, - } -} - -func (j *JWT) GenerateAccessToken(userID entity.UserID) (string, time.Time, error) { - expiresTime := time.Now().Add(time.Duration(j.accessTokenExpires) * time.Second) - claims := jwtClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(expiresTime), - }, - UserID: userID, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signedToken, err := token.SignedString([]byte(j.accessTokenKey)) - if err != nil { - return "", time.Time{}, err - } - return signedToken, expiresTime, nil -} - -func (j *JWT) GenerateRefreshToken(userID entity.UserID) (string, time.Time, error) { - expiresTime := time.Now().Add(time.Duration(j.refreshTokenExpires) * time.Second) - claims := jwtClaims{ - RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(expiresTime), - }, - UserID: userID, - } - - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signedToken, err := token.SignedString([]byte(j.refreshTokenKey)) - if err != nil { - return "", time.Time{}, err - } - return signedToken, expiresTime, nil -} - -func (j *JWT) VerifyAccessToken(rawToken string) (entity.UserID, error) { - token, err := jwt.Parse(rawToken, func(t *jwt.Token) (interface{}, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, errors.New("invalid token alg") - } - return []byte(j.accessTokenKey), nil - }) - - if errors.Is(err, jwt.ErrTokenExpired) { - return "", ErrAccessTokenExpired - } else if err != nil { - return "", ErrAccessTokenInvalid - } - - claims, ok := token.Claims.(jwt.MapClaims) - if !ok || !token.Valid { - return "", ErrAccessTokenInvalid - } - - userID := entity.UserID(claims["user_id"].(string)) - return userID, nil -} diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go deleted file mode 100644 index 9e5a240..0000000 --- a/pkg/validator/validator.go +++ /dev/null @@ -1,13 +0,0 @@ -package validator - -import "github.com/edwintantawi/taskit/internal/domain" - -type Validator struct{} - -func New() Validator { - return Validator{} -} - -func (v *Validator) Validate(validater domain.Validater) error { - return validater.Validate() -} diff --git a/test/constant.go b/test/constant.go new file mode 100644 index 0000000..0deb49d --- /dev/null +++ b/test/constant.go @@ -0,0 +1,8 @@ +package test + +import "time" + +var ( + TimeBeforeNow = time.Now().Add(-1 * time.Hour).UTC().Round(time.Microsecond) + TimeAfterNow = time.Now().Add(1 * time.Hour).UTC().Round(time.Microsecond) +) diff --git a/test/dockertest.go b/test/dockertest.go new file mode 100644 index 0000000..c13fc68 --- /dev/null +++ b/test/dockertest.go @@ -0,0 +1,78 @@ +package test + +import ( + "database/sql" + "fmt" + "log" + "time" + + _ "github.com/lib/pq" + "github.com/ory/dockertest/v3" + "github.com/ory/dockertest/v3/docker" +) + +const ( + postgresImageTag = "15.1" + postgresUser = "postgres" + postgresPassword = "secret" + postgresDB = "taskit" +) + +type PostgresResource struct { + DB *sql.DB + DSN string + CleanUp func() error +} + +func NewPostgresResource() PostgresResource { + var db *sql.DB + + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("Could not construct pool: %s", err) + } + + err = pool.Client.Ping() + if err != nil { + log.Fatalf("Could not connect to Docker: %s", err) + } + + resource, err := pool.RunWithOptions(&dockertest.RunOptions{ + Repository: "postgres", + Tag: postgresImageTag, + Env: []string{ + fmt.Sprintf("POSTGRES_USER=%s", postgresUser), + fmt.Sprintf("POSTGRES_PASSWORD=%s", postgresPassword), + fmt.Sprintf("POSTGRES_DB=%s", postgresDB), + "listen_addresses = '*'", + }, + }, func(config *docker.HostConfig) { + config.AutoRemove = true + config.RestartPolicy = docker.RestartPolicy{Name: "no"} + }) + if err != nil { + log.Fatalf("Could not start resource: %s", err) + } + + hostAndPort := resource.GetHostPort("5432/tcp") + dsn := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable", postgresUser, postgresPassword, hostAndPort, postgresDB) + + resource.Expire(120) + + pool.MaxWait = 120 * time.Second + if err = pool.Retry(func() error { + db, err = sql.Open("postgres", dsn) + if err != nil { + return err + } + return db.Ping() + }); err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + + cleanUp := func() error { + return pool.Purge(resource) + } + + return PostgresResource{db, dsn, cleanUp} +} diff --git a/test/helper.go b/test/helper.go deleted file mode 100644 index 2de5880..0000000 --- a/test/helper.go +++ /dev/null @@ -1,38 +0,0 @@ -package test - -import ( - "context" - "errors" - "net/http" - "time" - - "github.com/go-chi/chi/v5" - - "github.com/edwintantawi/taskit/internal/domain/entity" -) - -var ( - ErrUnexpected = errors.New("test.unexpected") - ErrRowAffected = errors.New("test.row_affected") - ErrDatabase = errors.New("test.database") - ErrRowScan = errors.New("test.rowscan") - ErrRows = errors.New("test.rows") - ErrValidator = errors.New("test.validator") - - TimeAfterNow = time.Now().Add(time.Hour) - TimeBeforeNow = time.Now().Add(-time.Hour) -) - -// InjectAuthContext injects the user ID into the request context. -func InjectAuthContext(r *http.Request, userID entity.UserID) *http.Request { - return r.WithContext(context.WithValue(r.Context(), entity.AuthUserIDKey, userID)) -} - -// InjectChiRouterParams injects the chi router params into the request context. -func InjectChiRouterParams(r *http.Request, params map[string]string) *http.Request { - rctx := chi.NewRouteContext() - for key, value := range params { - rctx.URLParams.Add(key, value) - } - return r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx)) -} diff --git a/test/table_helper.go b/test/table_helper.go new file mode 100644 index 0000000..858088e --- /dev/null +++ b/test/table_helper.go @@ -0,0 +1,19 @@ +package test + +import ( + "database/sql" + "fmt" + "log" + "strings" +) + +var Tables = []string{"users"} + +func TruncateTables(db *sql.DB) func() { + return func() { + q := fmt.Sprintf("TRUNCATE TABLE %s", strings.Join(Tables, ", ")) + if _, err := db.Exec(q); err != nil { + log.Fatalf("Could not truncate tables: %s", err) + } + } +} diff --git a/test/user_table_helper.go b/test/user_table_helper.go new file mode 100644 index 0000000..626ab92 --- /dev/null +++ b/test/user_table_helper.go @@ -0,0 +1,42 @@ +package test + +import ( + "database/sql" + "log" + "time" +) + +type User struct { + ID string + Name string + Email string + Password string + CreatedAt time.Time + UpdatedAt time.Time +} + +type UserTableHelper struct { + db *sql.DB +} + +func NewUserTableHelper(db *sql.DB) UserTableHelper { + return UserTableHelper{db} +} + +func (uth UserTableHelper) GetByID(id string) User { + var user User + q := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1` + if err := uth.db.QueryRow(q, id).Scan(&user.ID, &user.Name, &user.Email, &user.Password, &user.CreatedAt, &user.UpdatedAt); err != nil { + log.Fatalf("Could not get user: %s", err) + } + return user +} + +func (uth UserTableHelper) Add(user User) { + q := `INSERT INTO users (id, name, email, password, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6)` + + if _, err := uth.db.Exec(q, user.ID, user.Name, user.Email, user.Password, user.CreatedAt, user.UpdatedAt); err != nil { + log.Fatalf("Could not add user: %s", err) + } +}