From 1fe9ffe204c0315ee9e6478d9dba5ee61e65770a Mon Sep 17 00:00:00 2001 From: Thuan Nguyen Date: Mon, 25 Jul 2022 23:58:46 +0700 Subject: [PATCH 1/9] Todo app with built with Gorilla Mux --- .env.example | 9 + .gitignore | 2 +- Dockerfile | 10 + app/app.go | 60 +++ app/handler/handler.go | 34 ++ app/handler/task.go | 156 ++++++ app/handler/user.go | 88 ++++ app/middleware/cors_middleware.go | 21 + app/middleware/jwt_authentication.go | 65 +++ app/middleware/logging_middleware.go | 18 + app/middleware/prometheus_middleware.go | 49 ++ app/middleware/rate_limiting_middleware.go | 136 ++++++ app/middleware/recovery_middleware.go | 31 ++ app/middleware/validate_middleware.go | 19 + app/model/model.go | 58 +++ app/model/task.go | 127 +++++ app/model/token.go | 8 + app/model/user.go | 101 ++++ app/render/error.go | 53 +++ app/render/json.go | 22 + app/render/no_content.go | 9 + app/request/auth.go | 39 ++ app/request/task.go | 39 ++ app/router/api.go | 30 ++ app/router/router.go | 35 ++ app/transformer/resource.go | 125 +++++ app/transformer/serializer.go | 90 ++++ app/transformer/task.go | 33 ++ app/transformer/token.go | 23 + app/transformer/transformer.go | 35 ++ app/transformer/user.go | 31 ++ app/utils/common.go | 24 + app/utils/error.go | 51 ++ app/validator/date_validation.go | 15 + app/validator/validator.go | 109 +++++ cmd/main.go | 12 + cmd/migrate.go | 31 ++ config/config.go | 118 +++++ docker-compose.yml | 47 ++ go.mod | 39 ++ go.sum | 527 +++++++++++++++++++++ migration/20220724000001_init_schema.go | 66 +++ migration/migration.go | 270 +++++++++++ test/task_test.go | 464 ++++++++++++++++++ 44 files changed, 3328 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 app/app.go create mode 100644 app/handler/handler.go create mode 100644 app/handler/task.go create mode 100644 app/handler/user.go create mode 100644 app/middleware/cors_middleware.go create mode 100644 app/middleware/jwt_authentication.go create mode 100644 app/middleware/logging_middleware.go create mode 100644 app/middleware/prometheus_middleware.go create mode 100644 app/middleware/rate_limiting_middleware.go create mode 100644 app/middleware/recovery_middleware.go create mode 100644 app/middleware/validate_middleware.go create mode 100644 app/model/model.go create mode 100644 app/model/task.go create mode 100644 app/model/token.go create mode 100644 app/model/user.go create mode 100644 app/render/error.go create mode 100644 app/render/json.go create mode 100644 app/render/no_content.go create mode 100644 app/request/auth.go create mode 100644 app/request/task.go create mode 100644 app/router/api.go create mode 100644 app/router/router.go create mode 100644 app/transformer/resource.go create mode 100644 app/transformer/serializer.go create mode 100644 app/transformer/task.go create mode 100644 app/transformer/token.go create mode 100644 app/transformer/transformer.go create mode 100644 app/transformer/user.go create mode 100644 app/utils/common.go create mode 100644 app/utils/error.go create mode 100644 app/validator/date_validation.go create mode 100644 app/validator/validator.go create mode 100644 cmd/main.go create mode 100644 cmd/migrate.go create mode 100644 config/config.go create mode 100644 docker-compose.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 migration/20220724000001_init_schema.go create mode 100644 migration/migration.go create mode 100644 test/task_test.go diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..a39eb6ff1 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +APP_HOST= +APP_PORT=8080 +APP_SECRET=8c6c19688d9f94b3900042923aa94a99 + +DB_HOST=localhost +DB_PORT=5432 +DB_NAME=manabie +DB_USERNAME=manabie +DB_PASSWORD=12345 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a512c8b38..ad300f4f3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,7 @@ *.out # Dependency directories (remove the comment below to include it) -# vendor/ +vendor/ out/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..70c72eac9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:1.18.4 +WORKDIR /go/src +COPY . /go/src +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -o start cmd/main.go + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /root/ +COPY --from=0 /go/src/start . +CMD ["./start"] \ No newline at end of file diff --git a/app/app.go b/app/app.go new file mode 100644 index 000000000..e66253000 --- /dev/null +++ b/app/app.go @@ -0,0 +1,60 @@ +package app + +import ( + "context" + "fmt" + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/handler" + "github.com/huuthuan-nguyen/manabie/app/router" + "github.com/huuthuan-nguyen/manabie/config" + "github.com/huuthuan-nguyen/manabie/migration" + "log" + "net/http" + "time" +) + +type App struct { + router *mux.Router + handler *handler.Handler + ctx context.Context + server *http.Server + config *config.Config +} + +func (app *App) Run() { + + if err := app.Migrate(); err != nil { + log.Fatalf("Migrating fail:%s\n", err) + return + } + + app.server = &http.Server{ + Addr: fmt.Sprintf("%s:%s", app.config.Server.Host, app.config.Server.Port), + Handler: app.router, + WriteTimeout: 30 * time.Second, + ReadTimeout: 30 * time.Second, + } + defer app.server.Close() + + log.Printf("Listening on %s:%v...\n", app.config.Server.Host, app.config.Server.Port) + log.Fatalln(app.server.ListenAndServe()) +} + +func NewApp(config *config.Config) *App { + c := context.Background() + h := handler.New(c, config) + r := router.NewRouter(config, h) + + return &App{ + router: r, + handler: h, + ctx: c, + config: config, + } +} + +// Migrate /** +func (app *App) Migrate() error { + migrateEngine := migration.NewEngine(app.config) + return migrateEngine.Migrate() +} diff --git a/app/handler/handler.go b/app/handler/handler.go new file mode 100644 index 000000000..d359a313b --- /dev/null +++ b/app/handler/handler.go @@ -0,0 +1,34 @@ +package handler + +import ( + "context" + "github.com/huuthuan-nguyen/manabie/app/model" + "github.com/huuthuan-nguyen/manabie/config" + "github.com/uptrace/bun" + "log" +) + +type Handler struct { + db *bun.DB + ctx context.Context + config *config.Config +} + +func (handler *Handler) GetDB() *bun.DB { + return handler.db +} + +// New /** +func New(ctx context.Context, config *config.Config) *Handler { + + connection, err := model.NewConnection(config) + if err != nil { + log.Fatalf("Error connecting to database...") + } + + return &Handler{ + db: connection, + ctx: ctx, + config: config, + } +} diff --git a/app/handler/task.go b/app/handler/task.go new file mode 100644 index 000000000..788749806 --- /dev/null +++ b/app/handler/task.go @@ -0,0 +1,156 @@ +package handler + +import ( + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/model" + "github.com/huuthuan-nguyen/manabie/app/render" + "github.com/huuthuan-nguyen/manabie/app/request" + "github.com/huuthuan-nguyen/manabie/app/transformer" + "github.com/huuthuan-nguyen/manabie/app/utils" + "log" + "net/http" + "strconv" +) + +// TaskStore /** +func (handler *Handler) TaskStore(w http.ResponseWriter, r *http.Request) { + taskRequest := request.Task{} + taskModel := model.Task{} + + if err := taskRequest.Bind(r, &taskModel); err != nil { + render.Error(w, r, err) + return + } + + if err := taskModel.Create(r.Context(), handler.db); err != nil { + log.Println(err) + if _, ok := err.(*model.ErrExceedDailyLimit); ok { + utils.PanicBadRequest(err) + return + } + utils.PanicInternalServerError(err) + return + } + + taskTransformer := &transformer.TaskTransformer{} + taskItem := transformer.NewItem(taskModel, taskTransformer) + + // render task item to JSON + render.JSON(w, r, taskItem) +} + +// TaskUpdate /** +func (handler *Handler) TaskUpdate(w http.ResponseWriter, r *http.Request) { + taskRequest := request.Task{} + taskModel := model.Task{} + vars := mux.Vars(r) + + id, ok := vars["id"] + if !ok { + utils.PanicNotFound() + } + + taskID, err := strconv.Atoi(id) + if err != nil { + utils.PanicInternalServerError(err) + return + } + + _, err = model.FindOneTaskByID(r.Context(), taskID, handler.db) + if err != nil { + utils.PanicNotFound() + return + } + + taskModel.ID = taskID + if err := taskRequest.Bind(r, &taskModel); err != nil { + render.Error(w, r, err) + return + } + + if err := taskModel.Update(r.Context(), handler.db); err != nil { + utils.PanicInternalServerError(err) + return + } + + taskTransformer := &transformer.TaskTransformer{} + taskItem := transformer.NewItem(taskModel, taskTransformer) + + // render task item to JSON + render.JSON(w, r, taskItem) +} + +// TaskDestroy /** +func (handler *Handler) TaskDestroy(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + + id, ok := vars["id"] + if !ok { + utils.PanicNotFound() + } + + taskID, err := strconv.Atoi(id) + if err != nil { + utils.PanicInternalServerError(err) + return + } + + _, err = model.FindOneTaskByID(r.Context(), taskID, handler.db) + if err != nil { + utils.PanicNotFound() + return + } + + taskModel := model.Task{} + taskModel.ID = taskID + + if err := taskModel.Delete(r.Context(), handler.db); err != nil { + utils.PanicInternalServerError(err) + return + } + + render.NoContent(w, r) +} + +// TaskIndex /** +func (handler *Handler) TaskIndex(w http.ResponseWriter, r *http.Request) { + // prepare data response + tasks, err := model.FindTasks(r.Context(), handler.db, r.URL.Query()) + if err != nil { + render.Error(w, r, err) + return + } + + taskTransformer := &transformer.TaskTransformer{} + taskCollection := transformer.NewCollection(tasks, taskTransformer) + + // render Collection to JSON + render.JSON(w, r, taskCollection) +} + +// TaskShow /** +func (handler *Handler) TaskShow(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, ok := vars["id"] + if !ok { + utils.PanicNotFound() + } + + taskID, err := strconv.Atoi(id) + if err != nil { + utils.PanicInternalServerError(err) + return + } + + task, err := model.FindOneTaskByID(r.Context(), taskID, handler.db) + if err != nil { + utils.PanicNotFound() + return + } + + taskTransformer := &transformer.TaskTransformer{} + taskItem := transformer.NewItem(task, taskTransformer) + + // render task item to JSON + render.JSON(w, r, taskItem) +} diff --git a/app/handler/user.go b/app/handler/user.go new file mode 100644 index 000000000..2ddd7001f --- /dev/null +++ b/app/handler/user.go @@ -0,0 +1,88 @@ +package handler + +import ( + "github.com/golang-jwt/jwt" + "github.com/huuthuan-nguyen/manabie/app/middleware" + "github.com/huuthuan-nguyen/manabie/app/model" + "github.com/huuthuan-nguyen/manabie/app/render" + "github.com/huuthuan-nguyen/manabie/app/request" + "github.com/huuthuan-nguyen/manabie/app/transformer" + "github.com/huuthuan-nguyen/manabie/app/utils" + "net/http" + "time" +) + +// UserRegister /** +func (handler *Handler) UserRegister(w http.ResponseWriter, r *http.Request) { + credentialsRequest := request.Credentials{} + userModel := model.User{} + + if err := credentialsRequest.Bind(r, &userModel); err != nil { + render.Error(w, r, err) + return + } + + if err := userModel.Create(r.Context(), handler.db); err != nil { + utils.PanicInternalServerError(err) + return + } + + userTransformer := &transformer.UserTransformer{} + userItem := transformer.NewItem(userModel, userTransformer) + + // render user item to JSON + render.JSON(w, r, userItem) +} + +// UserLogin /** +func (handler *Handler) UserLogin(w http.ResponseWriter, r *http.Request) { + credentialsRequest := request.Credentials{} + userModel := model.User{} + + if err := credentialsRequest.Bind(r, &userModel); err != nil { + render.Error(w, r, err) + return + } + + // find user by email in database + currentUser, err := model.FindOneUserByEmail(r.Context(), userModel.Email, handler.db) + if err != nil { + render.Error(w, r, err) + } + + // compare password + if !model.CheckPasswordHash(userModel.Password, currentUser.Password) { + // throw error + utils.PanicUnauthorized() + return + } + + // expired at + expirationTime := time.Now().Add(1440 * time.Minute) // one day expiration + // create JWT claims with expiration and email + claims := &middleware.Payload{ + Email: userModel.Email, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: expirationTime.Unix(), // expired at in unix milliseconds format + }, + } + // create token with HS256 algorithm and claims + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + // token string + tokenString, err := token.SignedString([]byte(handler.config.Server.Secret)) + if err != nil { + utils.PanicInternalServerError(err) + return + } + + // return token + tokenModel := model.Token{ + AccessToken: tokenString, + ExpiredAt: expirationTime, + } + tokenTransformer := &transformer.TokenTransformer{} + tokenItem := transformer.NewItem(tokenModel, tokenTransformer) + + // render token item to JSON + render.JSON(w, r, tokenItem) +} diff --git a/app/middleware/cors_middleware.go b/app/middleware/cors_middleware.go new file mode 100644 index 000000000..63c80550d --- /dev/null +++ b/app/middleware/cors_middleware.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "github.com/gorilla/mux" + "net/http" +) + +// CORSMiddleware wrap the gorilla Logging Handler +func CORSMiddleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, HEAD") + if r.Method == http.MethodOptions { + return + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/app/middleware/jwt_authentication.go b/app/middleware/jwt_authentication.go new file mode 100644 index 000000000..d54c3fdfc --- /dev/null +++ b/app/middleware/jwt_authentication.go @@ -0,0 +1,65 @@ +package middleware + +import ( + "context" + "fmt" + "github.com/golang-jwt/jwt" + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/model" + "github.com/huuthuan-nguyen/manabie/app/utils" + "github.com/huuthuan-nguyen/manabie/config" + "github.com/uptrace/bun" + "log" + "net/http" + "strings" +) + +// JWTAuthenticateMiddleware protect resources +func JWTAuthenticateMiddleware(config *config.Config, db bun.IDB) mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bearerToken := r.Header.Get("Authorization") + if len(bearerToken) > 0 { + bearerToken = strings.TrimPrefix(bearerToken, "Bearer ") + } else { + utils.PanicUnauthorized() + return + } + + payload := &Payload{} + tkn, err := jwt.ParseWithClaims(bearerToken, payload, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method %v", token.Header["alg"]) + } + return []byte(config.Server.Secret), nil + }) + + if err != nil || tkn == nil || !tkn.Valid { + log.Println(err) + utils.PanicUnauthorized() + return + } + + payload, ok := tkn.Claims.(*Payload) + if !ok { + utils.PanicUnauthorized() + return + } + + // prepare user + user, err := model.FindOneUserByEmail(r.Context(), payload.Email, db) + if err != nil { + utils.PanicUnauthorized() + return + } + + ctx := context.WithValue(r.Context(), "user", &user) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + +type Payload struct { + Email string `json:"email,omitempty"` + jwt.StandardClaims +} diff --git a/app/middleware/logging_middleware.go b/app/middleware/logging_middleware.go new file mode 100644 index 000000000..d15205a55 --- /dev/null +++ b/app/middleware/logging_middleware.go @@ -0,0 +1,18 @@ +package middleware + +import ( + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "net/http" + "os" +) + +// LoggingMiddleware wrap the gorilla Logging Handler +func LoggingMiddleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next = handlers.LoggingHandler(os.Stdout, next) // run logger before execute the logic + next.ServeHTTP(w, r) + }) + } +} diff --git a/app/middleware/prometheus_middleware.go b/app/middleware/prometheus_middleware.go new file mode 100644 index 000000000..4fd0dd879 --- /dev/null +++ b/app/middleware/prometheus_middleware.go @@ -0,0 +1,49 @@ +package middleware + +import ( + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" + "net/http" +) + +// PrometheusMiddleware /** +func PrometheusMiddleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + timer := prometheus.NewTimer(httpDuration.WithLabelValues(r.URL.Path)) + totalRequests.WithLabelValues(r.URL.Path).Inc() + totalHTTPMethods.WithLabelValues(r.Method).Inc() + next.ServeHTTP(w, r) + timer.ObserveDuration() + }) + } +} + +func init() { + _ = prometheus.Register(totalRequests) + _ = prometheus.Register(totalHTTPMethods) + _ = prometheus.Register(httpDuration) +} + +var totalRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Number of incoming requests", + }, + []string{"path"}, +) + +var totalHTTPMethods = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_methods_total", + Help: "Number of requests per HTTP method", + }, []string{"method"}, +) + +var httpDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_response_time_seconds", + Help: "Duration of HTTP requests", + }, + []string{"path"}, +) diff --git a/app/middleware/rate_limiting_middleware.go b/app/middleware/rate_limiting_middleware.go new file mode 100644 index 000000000..b0b1d981a --- /dev/null +++ b/app/middleware/rate_limiting_middleware.go @@ -0,0 +1,136 @@ +package middleware + +import ( + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/utils" + "golang.org/x/time/rate" + "net" + "net/http" + "strings" + "sync" + "time" +) + +var limiter = NewIPRateLimiter(1, 500) // rate is 1, token bucket size is 500 + +func init() { + go limiter.CleanUpVisitors() +} + +// RateLimitingMiddleware /** +func RateLimitingMiddleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + visitor := limiter.GetVisitor(GetIP(r)) + if !visitor.limiter.Allow() { + utils.PanicTooManyRequests() + } + next.ServeHTTP(w, r) + }) + } +} + +// Visitor struct +type Visitor struct { + limiter *rate.Limiter + lastSeen time.Time +} + +// IPRateLimiter /** +type IPRateLimiter struct { + visitors map[string]*Visitor + mu *sync.RWMutex + r rate.Limit + b int +} + +// NewIPRateLimiter /** +func NewIPRateLimiter(r rate.Limit, b int) *IPRateLimiter { + return &IPRateLimiter{ + visitors: make(map[string]*Visitor), + mu: &sync.RWMutex{}, + r: r, + b: b, + } +} + +// AddVisitor creates a new rate limiter and adds it to the ips map, +// using the IP address as the key +func (i *IPRateLimiter) AddVisitor(ip string) *Visitor { + i.mu.Lock() + defer i.mu.Unlock() + + limiter := rate.NewLimiter(i.r, i.b) + + i.visitors[ip] = &Visitor{ + limiter: limiter, + lastSeen: time.Now(), + } + return i.visitors[ip] +} + +// GetVisitor /** +// GetLimiter returns the rate limiter for the provided IP address if it exists +// Otherwise call AddVisitor to add IP address to the map +func (i *IPRateLimiter) GetVisitor(ip string) *Visitor { + i.mu.Lock() + visitor, ok := i.visitors[ip] + + if !ok { + i.mu.Unlock() + return i.AddVisitor(ip) + } + + i.mu.Unlock() + return visitor +} + +// CleanUpVisitors /** +// Every minute check the map for IPs that haven't been seen for +// more than 3 minutes and delete the entries +func (i *IPRateLimiter) CleanUpVisitors() { + for { + time.Sleep(time.Minute) + + i.mu.Lock() + + for ip, v := range i.visitors { + if time.Since(v.lastSeen) > 3*time.Minute { + delete(i.visitors, ip) + } + } + + i.mu.Unlock() + } +} + +// GetIP /** +func GetIP(r *http.Request) string { + // get IP from the X-REAL-IP header + ip := r.Header.Get("X-REAL-IP") + netIP := net.ParseIP(ip) + if netIP != nil { + return ip + } + // get IP from X-FORWARDED-FOR header + ips := r.Header.Get("X-FORWARDED-FOR") + splitIps := strings.Split(ips, ",") + for _, ip := range splitIps { + netIP := net.ParseIP(ip) + if netIP != nil { + return ip + } + } + + // get IP from RemoteAddr + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return "" + } + netIP = net.ParseIP(ip) + if netIP != nil { + return ip + } + + return "" +} diff --git a/app/middleware/recovery_middleware.go b/app/middleware/recovery_middleware.go new file mode 100644 index 000000000..15bbe6c79 --- /dev/null +++ b/app/middleware/recovery_middleware.go @@ -0,0 +1,31 @@ +package middleware + +import ( + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/utils" + "net/http" +) + +// RecoveringMiddleware wrap the gorilla Recovery handler +func RecoveringMiddleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // recover when panic happening + defer func() { + if err := recover(); err != nil { + // Handling the error + if e, ok := err.(utils.Error); ok { + statusCode := e.StatusCode + w.WriteHeader(statusCode) + _, _ = w.Write([]byte(e.Error())) + } else { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("Something went wrong.")) + } + } + }() + + next.ServeHTTP(w, r) + }) + } +} diff --git a/app/middleware/validate_middleware.go b/app/middleware/validate_middleware.go new file mode 100644 index 000000000..3536726a9 --- /dev/null +++ b/app/middleware/validate_middleware.go @@ -0,0 +1,19 @@ +package middleware + +import ( + "context" + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/validator" + "net/http" +) + +// ValidateMiddleware add validator to request context +func ValidateMiddleware() mux.MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + validate, _ := validator.NewValidator() + ctx := context.WithValue(r.Context(), "validate", validate) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} diff --git a/app/model/model.go b/app/model/model.go new file mode 100644 index 000000000..48741f5d5 --- /dev/null +++ b/app/model/model.go @@ -0,0 +1,58 @@ +package model + +import ( + "context" + "database/sql" + "fmt" + "github.com/huuthuan-nguyen/manabie/config" + "github.com/uptrace/bun" + "github.com/uptrace/bun/dialect/pgdialect" + "github.com/uptrace/bun/driver/pgdriver" +) + +// A TxFn is a function that will be called with an initialized `Transaction` object +// that can be used for executing statements and queries against a database. +type TxFn func(db bun.Tx) error + +// WithTransaction creates a new transaction and handles rollback/commit based on the +// error object returned by the `TxFn` +func WithTransaction(ctx context.Context, db *bun.DB, fn TxFn) (err error) { + tx, err := db.BeginTx(ctx, &sql.TxOptions{}) + if err != nil { + return err + } + + defer func() { + if p := recover(); p != nil { + // a panic occurred, rollback and re panic + _ = tx.Rollback() + panic(p) + } else if err != nil { + // something went wrong, rollback + _ = tx.Rollback() + } else { + // all good, commit + err = tx.Commit() + } + }() + + err = fn(tx) + return err +} + +// NewConnection /** +func NewConnection(c *config.Config) (*bun.DB, error) { + dns := "postgres://%s:%s@%s:%s/%s?sslmode=disable" + + dnsConnectionString := fmt.Sprintf(dns, + c.Database.Username, + c.Database.Password, + c.Database.Host, + c.Database.Port, + c.Database.Name, + ) + + sqlDB := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dnsConnectionString))) + + return bun.NewDB(sqlDB, pgdialect.New()), nil +} diff --git a/app/model/task.go b/app/model/task.go new file mode 100644 index 000000000..d3cb0b49d --- /dev/null +++ b/app/model/task.go @@ -0,0 +1,127 @@ +package model + +import ( + "context" + "github.com/uptrace/bun" + "net/url" + "time" +) + +type Task struct { + bun.BaseModel `bun:"table:tasks,alias:task"` + ID int `bun:"id,pk,autoincrement"` + Content string `bun:"content"` + PublishedDate time.Time `bun:"published_date"` + Status int `bun:"status"` + CreatedBy int `bun:"created_by,pk"` + Owner *User `bun:"rel:belongs-to,join:created_by=id"` + CreatedAt time.Time `bun:"created_at,default:current_timestamp"` + UpdatedAt time.Time `bun:"updated_at,default:current_timestamp"` +} + +type ErrExceedDailyLimit struct{} + +func (e ErrExceedDailyLimit) Error() string { + return "user is exceeded daily limit of tasks" +} + +type ErrInvalidUser struct{} + +func (e ErrInvalidUser) Error() string { + return "invalid user" +} + +// FindTasks /** +func FindTasks(ctx context.Context, db bun.IDB, params url.Values) ([]Task, error) { + var tasks = make([]Task, 0) + user, ok := ctx.Value("user").(*User) + if !ok { + return nil, &ErrInvalidUser{} + } + query := db.NewSelect().Model(&tasks) + + for k := range params { + v := params.Get(k) + + if k == "published_date" { + query.Where("published_date=?", v) + continue + } + } + + query.Where("created_by=?", user.ID) + + err := query.Scan(ctx) + + return tasks, err +} + +// FindOneTaskByID /** +func FindOneTaskByID(ctx context.Context, id int, db bun.IDB) (Task, error) { + user, ok := ctx.Value("user").(*User) + if !ok { + return Task{}, &ErrInvalidUser{} + } + + task := Task{ + ID: id, + } + + err := db.NewSelect(). + Model(&task). + Where("id=?", id). + Where("created_by=?", user.ID). + Scan(ctx) + return task, err +} + +// Create /** +func (task *Task) Create(ctx context.Context, db bun.IDB) error { + user, ok := ctx.Value("user").(*User) + if !ok { + return &ErrInvalidUser{} + } + + if user.IsExceededDailyLimit(ctx, db) { + return &ErrExceedDailyLimit{} + } + + task.PublishedDate = time.Now().UTC() + task.CreatedAt = time.Now().UTC() + task.UpdatedAt = time.Now().UTC() + task.CreatedBy = user.ID + + if _, err := db.NewInsert().Model(task).Exec(ctx); err != nil { + return err + } + return nil + +} + +// Update /** +func (task *Task) Update(ctx context.Context, db bun.IDB) error { + // force current time on updated_at column + task.UpdatedAt = time.Now().UTC() + if _, err := db.NewUpdate(). + Model(task). + ExcludeColumn("created_at", "id", "created_by", "published_date"). + Where("id=?", task.ID). + Returning("*"). + Exec(ctx); err != nil { + return err + } + + return nil +} + +// Delete /** +func (task *Task) Delete(ctx context.Context, db bun.IDB) error { + + if _, err := db.NewDelete(). + Model(task). + Where("id=?", task.ID). + Exec(ctx); err != nil { + return err + } + return nil +} diff --git a/app/model/token.go b/app/model/token.go new file mode 100644 index 000000000..42d43739d --- /dev/null +++ b/app/model/token.go @@ -0,0 +1,8 @@ +package model + +import "time" + +type Token struct { + AccessToken string + ExpiredAt time.Time +} diff --git a/app/model/user.go b/app/model/user.go new file mode 100644 index 000000000..7d11d614c --- /dev/null +++ b/app/model/user.go @@ -0,0 +1,101 @@ +package model + +import ( + "context" + "github.com/uptrace/bun" + "golang.org/x/crypto/bcrypt" + "log" + "time" +) + +type User struct { + bun.BaseModel `bun:"table:users,alias:user"` + ID int `bun:"id,pk,autoincrement"` + Email string `bun:"email"` + Password string `bun:"password"` + IsActive bool `bun:"is_active,default:false"` + DailyLimit int `bun:"daily_limit,default:3"` + Tasks []*Task `bun:"rel:has-many,join:id=created_by"` + CreatedAt time.Time `bun:"created_at,default:current_timestamp"` + UpdatedAt time.Time `bun:"updated_at,default:current_timestamp"` +} + +// FindOneUserByID /** +func FindOneUserByID(ctx context.Context, id int, db bun.IDB) (User, error) { + user := User{ + ID: id, + } + + err := db.NewSelect().Model(&user).WherePK().Scan(ctx) + return user, err +} + +// FindOneUserByEmail /** +func FindOneUserByEmail(ctx context.Context, email string, db bun.IDB) (User, error) { + user := User{ + Email: email, + } + + err := db.NewSelect().Model(&user).Where("email=?", user.Email).Scan(ctx) + return user, err +} + +// Create /** +func (user *User) Create(ctx context.Context, db bun.IDB) error { + // hash password using bcrypt before saving + if hashedPassword, err := HashPassword(user.Password); err != nil { + return err + } else { + user.Password = hashedPassword + } + user.IsActive = true // default is true when user register + user.DailyLimit = 3 // default daily limit is 3 + user.CreatedAt = time.Now().UTC() + user.UpdatedAt = time.Now().UTC() + if _, err := db.NewInsert().Model(user).Exec(ctx); err != nil { + return err + } + return nil +} + +// HashPassword /** +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 10) + return string(bytes), err +} + +// CheckPasswordHash /** +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// IsExceededDailyLimit /** +func (user *User) IsExceededDailyLimit(ctx context.Context, db bun.IDB) bool { + if user.countTasksCurrentDate(ctx, db) < user.DailyLimit { + return false + } + return true +} + +// countTasksByDate /** +func (user *User) countTasksByDate(date string, ctx context.Context, db bun.IDB) int { + count, err := db.NewSelect(). + Model((*Task)(nil)). + Where("published_date=?", date). + Where("created_by=?", user.ID). + Count(ctx) + if err != nil { + log.Println(err) + return 0 + } + + return count +} + +// countTasksCurrentDate /** +func (user *User) countTasksCurrentDate(ctx context.Context, db bun.IDB) int { + currentTime := time.Now() + currentDate := currentTime.Format("2006-01-02") + return user.countTasksByDate(currentDate, ctx, db) +} diff --git a/app/render/error.go b/app/render/error.go new file mode 100644 index 000000000..9784e4b2b --- /dev/null +++ b/app/render/error.go @@ -0,0 +1,53 @@ +package render + +import ( + "encoding/json" + "github.com/go-playground/validator/v10" + "github.com/huuthuan-nguyen/manabie/app/transformer" + "github.com/huuthuan-nguyen/manabie/app/utils" + validation "github.com/huuthuan-nguyen/manabie/app/validator" + "net/http" +) + +// Error /** +func Error(w http.ResponseWriter, r *http.Request, payload any) { + + var statusCode = http.StatusOK + var data any + jsonSerializer := transformer.NewJSONSerializer() + + switch e := payload.(type) { + case validator.ValidationErrors: // bad request + statusCode = http.StatusBadRequest + data = validation.Translate(e) + jsonSerializer.Messages = []string{http.StatusText(http.StatusNotFound)} + break + case utils.Error: // common error + statusCode = http.StatusBadRequest + jsonSerializer.Messages = []string{http.StatusText(e.StatusCode)} + break + case error: // internal error + statusCode = http.StatusInternalServerError + jsonSerializer.Messages = []string{e.Error()} + break + default: // default + statusCode = http.StatusInternalServerError + jsonSerializer.Messages = []string{http.StatusText(http.StatusInternalServerError)} + } + + transformerManager := transformer.Manager{ + Serializer: jsonSerializer, + } + errorItem := transformer.NewError(data) + + payloadStruct := transformerManager.CreateData(errorItem) + payloadResponse, err := json.Marshal(payloadStruct) + if err != nil { + utils.PanicInternalServerError(err) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + _, _ = w.Write(payloadResponse) + return +} diff --git a/app/render/json.go b/app/render/json.go new file mode 100644 index 000000000..74f19fc4e --- /dev/null +++ b/app/render/json.go @@ -0,0 +1,22 @@ +package render + +import ( + "encoding/json" + "github.com/huuthuan-nguyen/manabie/app/transformer" + "github.com/huuthuan-nguyen/manabie/app/utils" + "net/http" +) + +// JSON /** +func JSON(w http.ResponseWriter, r *http.Request, payload interface{}) { + transformerManager := transformer.NewManager() + + payloadStruct := transformerManager.CreateData(payload) + payloadResponse, err := json.Marshal(payloadStruct) + if err != nil { + utils.PanicInternalServerError(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(payloadResponse) +} diff --git a/app/render/no_content.go b/app/render/no_content.go new file mode 100644 index 000000000..c749997d2 --- /dev/null +++ b/app/render/no_content.go @@ -0,0 +1,9 @@ +package render + +import "net/http" + +// NoContent /** +func NoContent(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNoContent) +} diff --git a/app/request/auth.go b/app/request/auth.go new file mode 100644 index 000000000..81fc324d5 --- /dev/null +++ b/app/request/auth.go @@ -0,0 +1,39 @@ +package request + +import ( + "encoding/json" + "github.com/go-playground/validator/v10" + "github.com/huuthuan-nguyen/manabie/app/model" + "net/http" +) + +type Credentials struct { + Email string `json:"email" validate:"required"` + Password string `json:"password" validate:"required"` +} + +// Validate /** +func (credentials *Credentials) Validate(r *http.Request) error { + if validate, ok := r.Context().Value("validate").(*validator.Validate); ok { + return validate.Struct(credentials) + } + + return nil +} + +// Bind /** +func (credentials *Credentials) Bind(r *http.Request, userModel *model.User) error { + defer r.Body.Close() + + if err := json.NewDecoder(r.Body).Decode(credentials); err != nil { + return err + } + + if err := credentials.Validate(r); err != nil { + return err + } + + userModel.Email = credentials.Email + userModel.Password = credentials.Password + return nil +} diff --git a/app/request/task.go b/app/request/task.go new file mode 100644 index 000000000..99a2d0baa --- /dev/null +++ b/app/request/task.go @@ -0,0 +1,39 @@ +package request + +import ( + "encoding/json" + "github.com/go-playground/validator/v10" + "github.com/huuthuan-nguyen/manabie/app/model" + "net/http" +) + +type Task struct { + Content string `json:"content" validate:"required"` + Status int `json:"status" validate:"omitempty"` +} + +// Validate /** +func (task *Task) Validate(r *http.Request) error { + if validate, ok := r.Context().Value("validate").(*validator.Validate); ok { + return validate.Struct(task) + } + + return nil +} + +// Bind /** +func (task *Task) Bind(r *http.Request, taskModel *model.Task) error { + defer r.Body.Close() + + if err := json.NewDecoder(r.Body).Decode(task); err != nil { + return err + } + + if err := task.Validate(r); err != nil { + return err + } + + taskModel.Content = task.Content + taskModel.Status = task.Status + return nil +} diff --git a/app/router/api.go b/app/router/api.go new file mode 100644 index 000000000..87776753d --- /dev/null +++ b/app/router/api.go @@ -0,0 +1,30 @@ +package router + +import ( + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/handler" + "github.com/huuthuan-nguyen/manabie/app/middleware" + "github.com/huuthuan-nguyen/manabie/config" + "net/http" +) + +func SetAPIRoutes(router *mux.Router, config *config.Config, handler *handler.Handler) *mux.Router { + apiRouter := router.PathPrefix("/api").Subrouter() + + // authentication + apiRouter.HandleFunc("/register", handler.UserRegister).Methods(http.MethodPost, http.MethodOptions) + apiRouter.HandleFunc("/auth/login", handler.UserLogin).Methods(http.MethodPost, http.MethodOptions) + + // tasks + productRouter := apiRouter.PathPrefix("/").Subrouter() + productRouter.HandleFunc("/tasks", handler.TaskStore).Methods(http.MethodPost, http.MethodOptions) + productRouter.HandleFunc("/tasks/{id:[0-9]+}", handler.TaskUpdate).Methods(http.MethodPut, http.MethodOptions) + productRouter.HandleFunc("/tasks/{id:[0-9]+}", handler.TaskDestroy).Methods(http.MethodDelete, http.MethodOptions) + productRouter.HandleFunc("/tasks", handler.TaskIndex).Methods(http.MethodGet, http.MethodOptions) + productRouter.HandleFunc("/tasks/{id:[0-9]+}", handler.TaskShow).Methods(http.MethodGet, http.MethodOptions) + + jwtAuthenticationMiddleware := middleware.JWTAuthenticateMiddleware(config, handler.GetDB()) + productRouter.Use(jwtAuthenticationMiddleware) + + return router +} diff --git a/app/router/router.go b/app/router/router.go new file mode 100644 index 000000000..505d76308 --- /dev/null +++ b/app/router/router.go @@ -0,0 +1,35 @@ +package router + +import ( + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/handler" + "github.com/huuthuan-nguyen/manabie/app/middleware" + "github.com/huuthuan-nguyen/manabie/config" +) + +func NewRouter(config *config.Config, handler *handler.Handler) *mux.Router { + router := mux.NewRouter().StrictSlash(false) + + // Routes for api + router = SetAPIRoutes(router, config, handler) + + recoveryMiddleware := middleware.RecoveringMiddleware() + loggingMiddleware := middleware.LoggingMiddleware() + compressMiddleware := handlers.CompressHandler + rateLimitingMiddleware := middleware.RateLimitingMiddleware() + corsMiddleware := middleware.CORSMiddleware() + prometheusMiddleware := middleware.PrometheusMiddleware() + validateMiddleware := middleware.ValidateMiddleware() + router.Use( + compressMiddleware, + loggingMiddleware, + recoveryMiddleware, + prometheusMiddleware, + corsMiddleware, + rateLimitingMiddleware, + validateMiddleware, + ) + + return router +} diff --git a/app/transformer/resource.go b/app/transformer/resource.go new file mode 100644 index 000000000..f889f79fa --- /dev/null +++ b/app/transformer/resource.go @@ -0,0 +1,125 @@ +package transformer + +import ( + "errors" + "github.com/huuthuan-nguyen/manabie/app/utils" + "reflect" +) + +type Resource struct { + Transformer Transformer +} + +type Resourcer interface { + GetTransformer() any + SetTransformer(transformer Transformer) +} + +func (r *Resource) GetTransformer() any { + return r.Transformer +} + +func (r *Resource) SetTransformer(transformer Transformer) { + r.Transformer = transformer +} + +type Item struct { + Resource + Data any +} + +type Collection struct { + Resource + Total int + Page int + ItemsPerPage int + Data []any +} + +type Error struct { + Resource + Data any +} + +func (i *Item) GetData() any { + return i.Data +} + +func (i *Item) SetData(data any) { + i.Data = data +} + +func (c *Collection) GetData() []any { + return c.Data +} + +func (c *Collection) SetData(data []any) { + c.Data = data +} + +func (c *Collection) GetTotal() int { + return c.Total +} + +func (c *Collection) SetTotal(total int) { + c.Total = total +} + +func (c *Collection) GetPage() int { + return c.Page +} + +func (c *Collection) SetPage(page int) { + c.Page = page +} + +func (c *Collection) GetItemsPerPage() int { + return c.ItemsPerPage +} + +func (c *Collection) SetItemsPerPage(itemsPerPage int) { + c.ItemsPerPage = itemsPerPage +} + +func (e *Error) GetData() any { + return e.Data +} + +func (e *Error) SetData(data any) { + e.Data = data +} + +// NewItem /** +func NewItem(data any, transformer Transformer) Item { + i := Item{} + i.SetData(data) + i.SetTransformer(transformer) + return i +} + +// NewCollection /** +func NewCollection(data any, transformer Transformer) Collection { + c := Collection{} + var r = make([]any, 0) + switch reflect.TypeOf(data).Kind() { + case reflect.Slice, reflect.Array: + s := reflect.ValueOf(data) + for i := 0; i < s.Len(); i++ { + e := s.Index(i).Interface() + r = append(r, e) + } + default: + utils.PanicInternalServerError(errors.New("can not reflect value type")) + } + c.SetData(r) + c.SetTransformer(transformer) + return c +} + +// NewError /** +func NewError(data any) Error { + e := Error{} + e.SetData(data) + e.SetTransformer(nil) + return e +} diff --git a/app/transformer/serializer.go b/app/transformer/serializer.go new file mode 100644 index 000000000..cca9479c6 --- /dev/null +++ b/app/transformer/serializer.go @@ -0,0 +1,90 @@ +package transformer + +import "math" + +type Serializer interface { + SerializeItem(resource Item) any + SerializeCollection(resource Collection) any + SerializeError(resource Error) any + SerializeNil() any +} + +// NewJSONSerializer /** +func NewJSONSerializer() *JSONSerializer { + return &JSONSerializer{ + Status: 1, + Messages: []string{"Successful."}, + Data: nil, + } +} + +type JSONSerializer struct { + Status int `json:"status"` + Messages []string `json:"messages"` + Data any `json:"data"` +} + +type CollectionJson struct { + Items []any `json:"items"` + Pagination PaginationJson `json:"-"` +} + +type PaginationJson struct { + Current int `json:"current"` + Prev int `json:"prev"` + Next int `json:"next"` + TotalItems int `json:"total_items"` + First int `json:"first"` + Last int `json:"last"` + ItemsPerPage int `json:"items_per_page"` +} + +func (j JSONSerializer) SerializeItem(resource Item) interface{} { + formattedItem := resource.Transformer.Transform(resource.Data) + j.Data = formattedItem + + return j +} + +func (j JSONSerializer) SerializeCollection(resource Collection) interface{} { + var formattedCollection = make([]any, 0) + for _, v := range resource.GetData() { + formattedCollection = append(formattedCollection, resource.Transformer.Transform(v)) + } + + collection := CollectionJson{} + total := resource.GetTotal() // total + page := resource.GetPage() // page from request + itemsPerPage := resource.GetItemsPerPage() // items_per_page from request + last := 1 // last default is 1 + if itemsPerPage != 0 { + last = int(math.Ceil(float64(total / itemsPerPage))) // calculate the last + } else { + itemsPerPage = total + } + collection.Items = formattedCollection + collection.Pagination = PaginationJson{ + Current: page, + Prev: 1, + Next: 1, + First: 1, + Last: last, + TotalItems: total, + ItemsPerPage: itemsPerPage, + } + + j.Data = collection + + return j +} + +func (j JSONSerializer) SerializeNil() any { + return j +} + +func (j JSONSerializer) SerializeError(resource Error) any { + payload := resource.GetData() + j.Status = 0 // default 0 for all error + j.Data = payload + return j +} diff --git a/app/transformer/task.go b/app/transformer/task.go new file mode 100644 index 000000000..eec6d9bed --- /dev/null +++ b/app/transformer/task.go @@ -0,0 +1,33 @@ +package transformer + +import ( + "github.com/huuthuan-nguyen/manabie/app/model" + "time" +) + +type TaskTransformer struct { + ID int `json:"id"` + Content string `json:"content"` + PublishedDate string `json:"published_date"` + Status int `json:"status"` + CreatedBy int `json:"created_by"` + CreateAt time.Time `json:"create_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Transform /** +func (task *TaskTransformer) Transform(e any) any { + taskModel, ok := e.(model.Task) + if !ok { + return e + } + + task.ID = taskModel.ID + task.Content = taskModel.Content + task.PublishedDate = taskModel.PublishedDate.Format("2006-01-02") + task.Status = taskModel.Status + task.CreatedBy = taskModel.CreatedBy + task.CreateAt = taskModel.CreatedAt + task.UpdatedAt = taskModel.UpdatedAt + return *task +} diff --git a/app/transformer/token.go b/app/transformer/token.go new file mode 100644 index 000000000..0be95ae23 --- /dev/null +++ b/app/transformer/token.go @@ -0,0 +1,23 @@ +package transformer + +import ( + "github.com/huuthuan-nguyen/manabie/app/model" + "time" +) + +type TokenTransformer struct { + AccessToken string `json:"access_token"` + ExpiredAt time.Time `json:"expired_at"` +} + +// Transform /** +func (token *TokenTransformer) Transform(e any) any { + tokenModel, ok := e.(model.Token) + if !ok { + return e + } + + token.AccessToken = tokenModel.AccessToken + token.ExpiredAt = tokenModel.ExpiredAt + return *token +} diff --git a/app/transformer/transformer.go b/app/transformer/transformer.go new file mode 100644 index 000000000..7e04e5f68 --- /dev/null +++ b/app/transformer/transformer.go @@ -0,0 +1,35 @@ +package transformer + +type Transformer interface { + Transform(model any) any +} + +type Manager struct { + Serializer Serializer +} + +// NewManager /** +func NewManager() *Manager { + return &Manager{ + Serializer: NewJSONSerializer(), + } +} + +// CreateData /** +func (m *Manager) CreateData(data any) any { + return m.serializeResource(data) +} + +// SerializeResource /** +func (m *Manager) serializeResource(resource any) any { + switch v := resource.(type) { + case Collection: + return m.Serializer.SerializeCollection(v) + case Item: + return m.Serializer.SerializeItem(v) + case Error: + return m.Serializer.SerializeError(v) + default: + return m.Serializer.SerializeNil() + } +} diff --git a/app/transformer/user.go b/app/transformer/user.go new file mode 100644 index 000000000..cccf6aecc --- /dev/null +++ b/app/transformer/user.go @@ -0,0 +1,31 @@ +package transformer + +import ( + "github.com/huuthuan-nguyen/manabie/app/model" + "time" +) + +type UserTransformer struct { + ID int `json:"id"` + Email string `json:"email"` + IsActive bool `json:"is_active"` + DailyLimit int `json:"daily_limit"` + CreateAt time.Time `json:"create_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +// Transform /** +func (user *UserTransformer) Transform(e any) any { + userModel, ok := e.(model.User) + if !ok { + return e + } + + user.ID = userModel.ID + user.Email = userModel.Email + user.IsActive = userModel.IsActive + user.DailyLimit = userModel.DailyLimit + user.CreateAt = userModel.CreatedAt + user.UpdatedAt = userModel.UpdatedAt + return *user +} diff --git a/app/utils/common.go b/app/utils/common.go new file mode 100644 index 000000000..945d91190 --- /dev/null +++ b/app/utils/common.go @@ -0,0 +1,24 @@ +package utils + +import ( + "github.com/huuthuan-nguyen/manabie/config" + "log" +) + +// ReadConfig /** +func ReadConfig() *config.Config { + c := &config.Config{} + // get the config path from command arguments + configPath, err := config.ParseConfigFlags() + if err != nil { + log.Printf("Config File path is can not be loaded: %v\n", err) + } + + // parse config from yml + err = config.ReadENVFile(c, configPath) + if err != nil { + log.Fatalf("Fail to load config from env: %v", err) + } + + return c +} diff --git a/app/utils/error.go b/app/utils/error.go new file mode 100644 index 000000000..bb5e1b585 --- /dev/null +++ b/app/utils/error.go @@ -0,0 +1,51 @@ +package utils + +import "net/http" + +type Error struct { + StatusCode int + Message string +} + +// Custom error +func (e *Error) Error() string { + return e.Message +} + +// NewError /** +func NewError(c int, s string) Error { + return Error{c, s} +} + +// PanicInternalServerError /** +func PanicInternalServerError(err ...error) { + message := "Something went wrong." + if len(err) > 0 { + message = err[0].Error() + } + panic(NewError(http.StatusInternalServerError, message)) +} + +// PanicBadRequest /** +func PanicBadRequest(err ...error) { + message := "Bad request." + if len(err) > 0 { + message = err[0].Error() + } + panic(NewError(http.StatusBadRequest, message)) +} + +// PanicTooManyRequests /** +func PanicTooManyRequests() { + panic(NewError(http.StatusTooManyRequests, "Too many requests.")) +} + +// PanicNotFound /** +func PanicNotFound() { + panic(NewError(http.StatusNotFound, "Not found.")) +} + +// PanicUnauthorized /** +func PanicUnauthorized() { + panic(NewError(http.StatusUnauthorized, "Unauthorized.")) +} diff --git a/app/validator/date_validation.go b/app/validator/date_validation.go new file mode 100644 index 000000000..e39b50726 --- /dev/null +++ b/app/validator/date_validation.go @@ -0,0 +1,15 @@ +package validator + +import ( + "github.com/go-playground/validator/v10" + "time" +) + +// DateValidation /** +func DateValidation(fl validator.FieldLevel) bool { + _, err := time.Parse("2006-01-02", fl.Field().String()) + if err != nil { + return false + } + return true +} diff --git a/app/validator/validator.go b/app/validator/validator.go new file mode 100644 index 000000000..c05fe48e2 --- /dev/null +++ b/app/validator/validator.go @@ -0,0 +1,109 @@ +package validator + +import ( + "github.com/go-playground/locales/en" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + enTranslations "github.com/go-playground/validator/v10/translations/en" + "reflect" + "strings" +) + +var ( + translator ut.Translator + translations = map[string]string{ + "required_with": "{0} is required with {1}", + } + validations = map[string]func(fl validator.FieldLevel) bool{ + "date": DateValidation, + } +) + +// NewValidator /** +func NewValidator() (*validator.Validate, error) { + + v := validator.New() + // name func registration + registerTagNameFunc(v) + // translations registration + if err := registerTranslations(v); err != nil { + return nil, err + } + // validations registration + if err := registerValidations(v); err != nil { + return nil, err + } + + return v, nil +} + +// Translate /** +func Translate(err error) map[string][]string { + result := make(map[string][]string) + + errs := err.(validator.ValidationErrors) + + for _, err := range errs { + result[err.Field()] = append(result[err.Field()], err.Translate(translator)) + } + + return result +} + +// registerTranslations /** +func registerTranslations(v *validator.Validate) error { + + // default local is english + enLocal := en.New() + uni := ut.New(enLocal) + translator, _ = uni.GetTranslator("en") + // register translations + if err := enTranslations.RegisterDefaultTranslations(v, translator); err != nil { + return err + } + + // register custom translations + for tag, message := range translations { + registerFn := func(ut ut.Translator) error { + return ut.Add(tag, message, false) + } + translateFn := func(ut ut.Translator, fe validator.FieldError) string { + param := fe.Param() + tag := fe.Tag() + t, err := ut.T(tag, fe.Field(), param) + if err != nil { + return fe.(error).Error() + } + return t + } + if err := v.RegisterTranslation(tag, translator, registerFn, translateFn); err != nil { + return err + } + } + + return nil +} + +// registerTagNameFunc /** +func registerTagNameFunc(v *validator.Validate) { + // register tag name func + v.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] + if name == "-" { + return "" + } + return name + }) +} + +// registerValidations /** +func registerValidations(v *validator.Validate) error { + // register custom validations + for tag, customFunc := range validations { + if err := v.RegisterValidation(tag, customFunc); err != nil { + return err + } + } + + return nil +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 000000000..496ad7a71 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,12 @@ +package main + +import ( + "github.com/huuthuan-nguyen/manabie/app" + "github.com/huuthuan-nguyen/manabie/app/utils" +) + +func main() { + config := utils.ReadConfig() // read config from env + application := app.NewApp(config) + application.Run() +} diff --git a/cmd/migrate.go b/cmd/migrate.go new file mode 100644 index 000000000..b0d1f81d9 --- /dev/null +++ b/cmd/migrate.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "github.com/huuthuan-nguyen/manabie/app/utils" + "github.com/huuthuan-nguyen/manabie/migration" + "log" +) + +var rollback = flag.Bool("rollback", false, "Rollback to previous migration.") +var reset = flag.Bool("reset", false, "Reset the database and run all migrations.") + +func main() { + flag.Parse() + var err error + + config := utils.ReadConfig() + engine := migration.NewEngine(config) + // if rollback + if *rollback { + err = engine.Rollback() + } else if *reset { + err = engine.Reset() + } else { // default is migrate + err = engine.Migrate() + } + + if err != nil { + log.Fatalf("Error migrating database:%v\n", err) + } +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 000000000..3b91402af --- /dev/null +++ b/config/config.go @@ -0,0 +1,118 @@ +package config + +import ( + "flag" + "fmt" + "github.com/joho/godotenv" + "os" + "reflect" + "strconv" +) + +// Config server configuration +type Config struct { + Server struct { + // Host is the local machine IP Address to bind the HTTP Server to + Host string `yaml:"host" env:"APP_HOST"` + // Port is the local machine TCP Port to bind the HTTP Server to + Port string `yaml:"port" env:"APP_PORT"` + // Secret for JWT + Secret string `yaml:"secret" env:"APP_SECRET"` + } + Database struct { + Host string `env:"DB_HOST"` // DB Host + Port string `env:"DB_PORT"` // DB Port + Name string `env:"DB_NAME"` // DB Name + Username string `env:"DB_USERNAME"` // DB UserName + Password string `env:"DB_PASSWORD"` // DB Password + } +} + +// ReadENVFile /** +func ReadENVFile(c *Config, configPath string) error { + if len(configPath) > 0 { + err := godotenv.Load() + if err != nil { + return err + } + } + + Decode(c) + return nil +} + +// ValidateConfigPath /** +func ValidateConfigPath(configPath string) error { + s, err := os.Stat(configPath) + if err != nil { + return err + } + + if s.IsDir() { + return fmt.Errorf("'%s' is a directory, not a file", configPath) + } + + return nil +} + +// ParseConfigFlags /** +func ParseConfigFlags() (configPath string, err error) { + + flag.StringVar(&configPath, "config", ".env", "Path to the config file") + + flag.Parse() + // parse the flags + if err := ValidateConfigPath(configPath); err != nil { + return "", err + } + + // return the config path + return configPath, nil +} + +// Decode /** +func Decode(i interface{}) { + var t reflect.Type + var v reflect.Value + var ok bool + + if v, ok = i.(reflect.Value); !ok { + v = reflect.ValueOf(i).Elem() + } + + t = v.Type() + + for i := 0; i < v.NumField(); i++ { + if v.CanInterface() { + if v.Field(i).Kind() != reflect.Struct { + if eTag, ok := t.Field(i).Tag.Lookup("env"); ok { + var eRVal interface{} + eString, exist := os.LookupEnv(eTag) // get string value from environment + // try to cast to real value + if t.Field(i).Type.String() == "int" { + if v, err := strconv.Atoi(eString); err == nil { + eRVal = v + } else { + if exist { + panic(eTag + " must be int type") + } else { + eRVal = 0 + } + } + } else { + eRVal = eString + } + + eVal := reflect.ValueOf(eRVal) + if eVal.IsValid() { + v.Field(i).Set(eVal.Convert(t.Field(i).Type)) + } + } + } else if v.Field(i).Kind() == reflect.Struct { + Decode(v.Field(i)) + } else { + panic("can not reflect value:" + v.Field(i).Kind().String()) + } + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..e1f5060d1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,47 @@ +version: '3.5' + +services: + # App Service + app: + # Configuration for building the docker image for the service + build: + context: ./ # Use an image built from the specified dockerfile in the current directory. + dockerfile: Dockerfile + ports: + - "8080:8080" # Forward the exposed port 8080 on the container to port 8080 on the host machine + depends_on: + - db + restart: unless-stopped + networks: # Networks to join (Services on the same network can communicate with each other using their name) + - manabie + environment: + APP_HOST: 0.0.0.0 + APP_PORT: 8080 + APP_SECRET: 8c6c19688d9f94b3900042923aa94a99 + DB_HOST: db + DB_PORT: 5432 + DB_NAME: manabie + DB_USERNAME: manabie + DB_PASSWORD: 12345 + db: + image: postgres:14.2 + environment: + POSTGRES_USER: manabie + POSTGRES_PASSWORD: 12345 + POSTGRES_DB: manabie + PGDATA: /data/postgres + volumes: + - db:/data/postgres + ports: + - "5432:5432" + networks: + - manabie # Join manabie network + restart: unless-stopped + +networks: + manabie: + driver: bridge + +volumes: + db: + driver: local \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..7f3690ea8 --- /dev/null +++ b/go.mod @@ -0,0 +1,39 @@ +module github.com/huuthuan-nguyen/manabie + +go 1.18 + +require ( + github.com/go-playground/locales v0.14.0 + github.com/go-playground/universal-translator v0.18.0 + github.com/go-playground/validator/v10 v10.11.0 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/gorilla/handlers v1.5.1 + github.com/gorilla/mux v1.8.0 + github.com/joho/godotenv v1.4.0 + github.com/prometheus/client_golang v1.12.2 + github.com/uptrace/bun v1.1.6 + github.com/uptrace/bun/dialect/pgdialect v1.1.6 + github.com/uptrace/bun/driver/pgdriver v1.1.6 + golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa + golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.32.1 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/protobuf v1.26.0 // indirect + mellium.im/sasl v0.2.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..a72ccc1af --- /dev/null +++ b/go.sum @@ -0,0 +1,527 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= +github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= +github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +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.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +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/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +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= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34= +github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= +github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= +github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +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/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= +github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= +github.com/uptrace/bun v1.1.6 h1:vDJ1Qs6fXock5+q/PSOZZ7vZVZABmWkGlgZDUkJwbfc= +github.com/uptrace/bun v1.1.6/go.mod h1:Z2Pd3cRvNKbrYuL6Gp1XGjA9QEYz+rDz5KkEi9MZLnQ= +github.com/uptrace/bun/dialect/pgdialect v1.1.6 h1:mDGOPm9FkWaM2kQLddzcRW6juUdITJ1XPUr1vt2anYw= +github.com/uptrace/bun/dialect/pgdialect v1.1.6/go.mod h1:vSyJq5nr4EP/QcVZEOctDnbHQ4OKebZbXGRd1bTBIFI= +github.com/uptrace/bun/driver/pgdriver v1.1.6 h1:t5fzjXasWt3uwXS5h5m0BhfdJEZp1Z9rzTgIO3mnq80= +github.com/uptrace/bun/driver/pgdriver v1.1.6/go.mod h1:Qb/QgyqAWYQPGk/o6eA6A8EIkOyl0c007NK8//wKJGw= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d h1:/m5NbqQelATgoSPVC2Z23sR4kVNokFwDDyWh/3rGY+I= +golang.org/x/sys v0.0.0-20220708085239-5a0f0661e09d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= +golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +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= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/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/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-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w= +mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/migration/20220724000001_init_schema.go b/migration/20220724000001_init_schema.go new file mode 100644 index 000000000..2252a4edb --- /dev/null +++ b/migration/20220724000001_init_schema.go @@ -0,0 +1,66 @@ +package migration + +import ( + "context" + "github.com/huuthuan-nguyen/manabie/app/model" + "github.com/uptrace/bun" +) + +type InitSchema struct { + Migration + Version string +} + +func (m *InitSchema) Up() (err error) { + db := m.GetDB() + ctx := context.Background() + return model.WithTransaction(ctx, db, func(tx bun.Tx) error { + + // create "users" table + const users = `CREATE TABLE IF NOT EXISTS users( + id SERIAL PRIMARY KEY, + email VARCHAR(255) NOT NULL UNIQUE, + password TEXT NOT NULL, + is_active BOOLEAN DEFAULT false NOT NULL, + daily_limit INT NOT NULL DEFAULT 3, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL + )` + if _, err = tx.Exec(users); err != nil { + return err + } + + // create "tasks" table + const tasks = `CREATE TABLE IF NOT EXISTS tasks( + id SERIAL PRIMARY KEY, + content TEXT NOT NULL, + published_date DATE NOT NULL, + status SMALLINT NOT NULL DEFAULT 0, + created_by INT NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + CONSTRAINT fk_owner + FOREIGN KEY(created_by) + REFERENCES users(id) + ON DELETE RESTRICT + )` + if _, err = tx.Exec(tasks); err != nil { + return err + } + + return nil + }) +} + +func (m *InitSchema) Down() (err error) { + db := m.GetDB() + ctx := context.Background() + return model.WithTransaction(ctx, db, func(tx bun.Tx) error { + _, err := tx.Exec(`DROP TABLE IF EXISTS tasks, users`) + return err + }) +} + +func (m *InitSchema) GetVersion() string { + return "20220724000001" +} diff --git a/migration/migration.go b/migration/migration.go new file mode 100644 index 000000000..b7d88b76c --- /dev/null +++ b/migration/migration.go @@ -0,0 +1,270 @@ +package migration + +import ( + "context" + "fmt" + "github.com/huuthuan-nguyen/manabie/app/model" + "github.com/huuthuan-nguyen/manabie/config" + "github.com/uptrace/bun" + "log" + "strings" +) + +type Migrator interface { + Up() (err error) + Down() (err error) + GetVersion() string + SetDB(db *bun.DB) + GetDB() *bun.DB +} + +var Migrators = []Migrator{ + &InitSchema{}, +} + +// Migration /** +type Migration struct { + bun.BaseModel `bun:"table:migrations,alias:migration"` + ID int `bun:"id,pk,autoincrement"` + Migration string `bun:"migration"` + Batch int `bun:"batch"` + DB *bun.DB `bun:"-"` +} + +// SetDB /** +func (m *Migration) SetDB(db *bun.DB) { + m.DB = db +} + +// GetDB /** +func (m *Migration) GetDB() *bun.DB { + return m.DB +} + +// Engine /** +type Engine struct { + DB *bun.DB +} + +// NewEngine /** +func NewEngine(c *config.Config) *Engine { + const migration = `CREATE TABLE IF NOT EXISTS migrations( + id SERIAL PRIMARY KEY, + migration VARCHAR(255) NOT NULL, + batch INT NOT NULL)` + + db, err := model.NewConnection(c) + if err != nil { + log.Fatalln("Error connecting to database...") + } + + // init the "migration" table + _, err = db.Exec(migration) + + if err != nil { + log.Fatalf("Error while creating migrations table:%v\n", err) + } + + return &Engine{ + DB: db, + } +} + +// Migrate /** +func (engine *Engine) Migrate() (err error) { + lastBatch, err := engine.getLastBatch() + + if err != nil { + return err + } + + successMigration := 0 + + log.Println("Begin migrating...") + + for _, migrator := range Migrators { + migrator.SetDB(engine.DB) + + version := migrator.GetVersion() + + migration, err := engine.getMigrationByVersion(version) + if err != nil { // not migrated yet + migration = Migration{ + Migration: version, + Batch: lastBatch + 1, + DB: engine.DB, + } + } else { // already migrated + continue + } + + // "up" the migration changes + log.Println("Migrating ", migrator.GetVersion()) + err = migrator.Up() + + if err != nil { + log.Printf("Error when migrating %s: %v\n", migrator.GetVersion(), err) + panic(err) + } + + // insert a new record to migration table + err = migration.Create() + if err != nil { + panic(err) + } + successMigration++ + log.Println("Migrated ", migrator.GetVersion()) + } + + if successMigration > 0 { + log.Println("Migrate Done.") + } else { + log.Println("Nothing to migrate.") + } + + return err +} + +// Rollback /** +func (engine *Engine) Rollback() (err error) { + lastBatch, err := engine.getLastBatch() + migrations, err := engine.getMigrationByBatch(lastBatch) + + successRollback := 0 + + log.Println("Begin rolling back...") + for _, m := range migrations { + m.SetDB(engine.DB) + log.Println("Rolling back ", m.Migration) + err = m.Rollback() + if err != nil { + return err + } + err = m.Delete() + if err != nil { + return err + } + log.Println("Rolled back ", m.Migration) + successRollback++ + } + + if successRollback > 0 { + log.Println("Rollback Done.") + } else { + log.Println("Nothing to rollback.") + } + return +} + +/** +Get the latest batch on migration table. +*/ +func (engine *Engine) getLastBatch() (maxBatch int, err error) { + statement := "SELECT COALESCE(MAX(batch), 0) AS b FROM migrations" + err = engine.DB.QueryRow(statement).Scan(&maxBatch) + return maxBatch, err +} + +// Create /** +func (m *Migration) Create() (err error) { + statement := "INSERT INTO migrations(migration, batch) VALUES(?, ?)" + + _, err = m.DB.Exec(statement, m.Migration, m.Batch) + return err +} + +// Delete /** +func (m *Migration) Delete() (err error) { + _, err = m.DB.Exec("DELETE FROM migrations WHERE id=?", m.ID) + return err +} + +// Rollback /** +func (m *Migration) Rollback() (err error) { + for _, migrator := range Migrators { + migrator.SetDB(m.DB) + if m.Migration == migrator.GetVersion() { + err = migrator.Down() + } + } + return err +} + +/** +Get migrations by batch number +*/ +func (engine *Engine) getMigrationByBatch(batch int) (migrations []Migration, err error) { + err = engine.DB.NewSelect(). + Model(&migrations). + Column("id", "migration", "batch"). + Where("batch=?", batch). + Order("id DESC"). + Scan(context.Background()) + return migrations, err +} + +/** +Get specific migration by version +*/ +func (engine *Engine) getMigrationByVersion(version string) (migration Migration, err error) { + err = engine.DB.NewSelect(). + Model(&migration). + Column("id", "migration", "batch"). + Where("migration=?", version). + Scan(context.Background()) + migration.SetDB(engine.DB) + return migration, err +} + +// Reset /** +func (engine *Engine) Reset() (err error) { + ctx := context.Background() + err = model.WithTransaction(ctx, engine.DB, func(tx bun.Tx) error { + log.Println("Begin drop all tables...") + if _, err := tx.ExecContext(ctx, "SET session_replication_role = 'replica'"); err != nil { + return err + } + + rows, err := tx.Query(`SELECT tablename from pg_tables WHERE schemaname = 'public' AND tablename <> 'migrations'`) + if err != nil { + return err + } + + defer rows.Close() + + tableNames := make([]string, 0) + for rows.Next() { + var tableName string + if err := rows.Scan(&tableName); err != nil { + return err + } else { + tableNames = append(tableNames, tableName) + } + } + if len(tableNames) > 0 { + if _, err := tx.Exec(fmt.Sprintf("DROP TABLE IF EXISTS %s CASCADE", strings.Join(tableNames, ", "))); err != nil { + return err + } + } + + log.Println("Drop all tables done.") + + // reset the "migrations" table + if _, err := tx.Exec("TRUNCATE migrations"); err != nil { + return err + } + + if _, err := engine.DB.Exec("SET session_replication_role = 'origin'"); err != nil { + return err + } + + return nil + }) + + if err != nil { + return err + } + + err = engine.Migrate() + return err +} diff --git a/test/task_test.go b/test/task_test.go new file mode 100644 index 000000000..66aa8bcbe --- /dev/null +++ b/test/task_test.go @@ -0,0 +1,464 @@ +package test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/gorilla/mux" + "github.com/huuthuan-nguyen/manabie/app/handler" + "github.com/huuthuan-nguyen/manabie/app/model" + "github.com/huuthuan-nguyen/manabie/app/router" + "github.com/huuthuan-nguyen/manabie/app/utils" + "github.com/uptrace/bun" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +var token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg0NTYwOX0.2dCHBNe0A1kbAriNUG-vevC71Sj6PKY9qthIh6DAR9Y" +var server *mux.Router +var db bun.IDB + +// metaResponse /** +type metaResponse struct { + Status int `json:"status"` + Messages []string `json:"messages"` +} + +// taskResponse /** +type taskResponse struct { + metaResponse + Data struct { + ID int `json:"id"` + Content string `json:"content"` + PublishedDate string `json:"published_date"` + Status int `json:"status"` + CreatedBy int `json:"created_by"` + CreateAt time.Time `json:"create_at"` + UpdatedAt time.Time `json:"updated_at"` + } `json:"data"` +} + +// listResponse /** +type listResponse struct { + metaResponse + Data struct { + Items []struct { + ID int `json:"id"` + Content string `json:"content"` + PublishedDate string `json:"published_date"` + Status int `json:"status"` + CreatedBy int `json:"created_by"` + CreateAt time.Time `json:"create_at"` + UpdatedAt time.Time `json:"updated_at"` + } `json:"items"` + } `json:"data"` +} + +func truncateDB(ctx context.Context) error { + _, err := db.ExecContext(ctx, "TRUNCATE TABLE users CASCADE") + return err +} + +func truncateTaskRecords(ctx context.Context) error { + _, err := db.ExecContext(ctx, "TRUNCATE TABLE tasks") + return err +} + +func populateTasks(ctx context.Context) error { + tasks := []struct { + id int + content string + publishedDate time.Time + status int + createdBy int + createdAt time.Time + updatedAt time.Time + }{ + { + id: 1, + content: "Do homework", + publishedDate: time.Now().UTC(), + status: 0, + createdBy: 1, + createdAt: time.Now().UTC(), + updatedAt: time.Now().UTC(), + }, + { + id: 2, + content: "Do laundry", + publishedDate: time.Now().UTC(), + status: 0, + createdBy: 1, + createdAt: time.Now().UTC(), + updatedAt: time.Now().UTC(), + }, + { + id: 3, + content: "Do housework", + publishedDate: time.Now().UTC(), + status: 0, + createdBy: 1, + createdAt: time.Now().UTC(), + updatedAt: time.Now().UTC(), + }, + } + for _, task := range tasks { + if _, err := db.ExecContext(ctx, "INSERT INTO tasks(id, content, published_date, status, created_by, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?)", + task.id, + task.content, + task.publishedDate, + task.status, + task.createdBy, + task.createdAt, + task.updatedAt, + ); err != nil { + return err + } + } + return nil +} + +func initUser(ctx context.Context) error { + password, err := model.HashPassword("123@pass!word") + if err != nil { + return err + } + _, err = db.ExecContext(ctx, + "INSERT INTO users(id, email, password, is_active, daily_limit, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, ?)", + 1, + "huuthuan.nguyen@hotmail.com", + password, + true, + 3, + time.Now().UTC(), + time.Now().UTC(), + ) + return err +} + +func TestMain(m *testing.M) { // life cycle testing + + config := utils.ReadConfig() // read config from env + c := context.Background() + h := handler.New(c, config) + server = router.NewRouter(config, h) + + ctx := context.Background() + db = h.GetDB() + // truncate + if err := truncateDB(ctx); err != nil { + log.Printf("truncate DB failed:%v\n", err) + return + } + // init user + if err := initUser(ctx); err != nil { + log.Printf("init user failed:%v\n", err) + return + } + + code := m.Run() + os.Exit(code) +} + +// TestCreateTask /** +func TestCreateTask(t *testing.T) { + + var jsonStr = []byte(`{"content":"Do homework"}`) + request, _ := http.NewRequest("POST", "/api/tasks", bytes.NewBuffer(jsonStr)) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusOK != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } + + var data taskResponse + err := json.Unmarshal(responseRecorder.Body.Bytes(), &data) + if err != nil { + t.Errorf("Unable to unmarshal json response\n") + } + + // assert Status on json body + if data.Status != 1 { + t.Errorf("Expected status is '%d'. Got '%d'\n", 1, data.Status) + } + + // assert ID on json body + if data.Data.ID == 0 { + t.Errorf("Expected ID of task is greater than '0'. Got '%v'\n", data.Data.ID) + } + + // assert Content on json body + if data.Data.Content != "Do homework" { + t.Errorf("Expected Content of Task to be 'Do homework'. Got '%v'\n", data.Data.Content) + } +} + +// TestCreateTaskReturnFailWhenUserExceededDailyLimit /** +func TestCreateTaskReturnFailWhenUserExceededDailyLimit(t *testing.T) { + + ctx := context.Background() + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records") + } + + testData := []struct { + input string + statusCode int + }{ + { + input: `{"content":"Do homework"}`, + statusCode: http.StatusOK, + }, + { + input: `{"content":"Do laundry"}`, + statusCode: http.StatusOK, + }, + { + input: `{"content":"Do housework"}`, + statusCode: http.StatusOK, + }, + { + input: `{"content":"Get sleeping"}`, + statusCode: http.StatusBadRequest, + }, + } + + for _, data := range testData { + request, _ := http.NewRequest("POST", "/api/tasks", bytes.NewBuffer([]byte(data.input))) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if data.statusCode != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } + } +} + +// TestListTasks /** +func TestListTasks(t *testing.T) { + + ctx := context.Background() + // truncate all tasks + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + // populate test tasks + if err := populateTasks(ctx); err != nil { + t.Errorf("Unable to populate task records\n") + } + + request, _ := http.NewRequest("GET", "/api/tasks", nil) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusOK != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } + + var data listResponse + err := json.Unmarshal(responseRecorder.Body.Bytes(), &data) + if err != nil { + t.Errorf("Unable to unmarshal json response\n") + } + + // count number of tasks + if len(data.Data.Items) != 3 { + t.Errorf("Expected number of tasks to be '3'. Got '%d'\n", len(data.Data.Items)) + } +} + +// TestListTasks /** +func TestShowSpecificTask(t *testing.T) { + + ctx := context.Background() + // truncate all tasks + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + // populate test tasks + if err := populateTasks(ctx); err != nil { + t.Errorf("Unable to populate task records\n") + } + + request, _ := http.NewRequest("GET", "/api/tasks/1", nil) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusOK != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } + + var data taskResponse + err := json.Unmarshal(responseRecorder.Body.Bytes(), &data) + if err != nil { + t.Errorf("Unable to unmarshal json response\n") + } + + // count number of tasks + if data.Data.Content != "Do homework" { + t.Errorf("Expected content of task to be 'Do homework'. Got '%v'\n", data.Data.Content) + } +} + +// TestShowSpecificTaskReturnNotFoundWhenTaskDoesNotExist /** +func TestShowSpecificTaskReturnNotFoundWhenTaskDoesNotExist(t *testing.T) { + + ctx := context.Background() + // truncate all tasks + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + // populate test tasks + if err := populateTasks(ctx); err != nil { + t.Errorf("Unable to populate task records\n") + } + + request, _ := http.NewRequest("GET", "/api/tasks/4", nil) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusNotFound != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } +} + +// TestUpdateSpecificTask /** +func TestUpdateSpecificTask(t *testing.T) { + + ctx := context.Background() + // truncate all tasks + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + // populate test tasks + if err := populateTasks(ctx); err != nil { + t.Errorf("Unable to populate task records\n") + } + + request, _ := http.NewRequest("PUT", "/api/tasks/1", bytes.NewBuffer([]byte(`{"content":"Do homework later"}`))) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusOK != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } + + var data taskResponse + err := json.Unmarshal(responseRecorder.Body.Bytes(), &data) + if err != nil { + t.Errorf("Unable to unmarshal json response\n") + } + + // count number of tasks + if data.Data.Content != "Do homework later" { + t.Errorf("Expected content of task to be 'Do homework later'. Got '%v'\n", data.Data.Content) + } +} + +// TestUpdateSpecificTaskReturnNotFoundWhenTaskDoesNotExist /** +func TestUpdateSpecificTaskReturnNotFoundWhenTaskDoesNotExist(t *testing.T) { + + ctx := context.Background() + // truncate all tasks + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + // populate test tasks + if err := populateTasks(ctx); err != nil { + t.Errorf("Unable to populate task records\n") + } + + request, _ := http.NewRequest("PUT", "/api/tasks/4", bytes.NewBuffer([]byte(`{"content":"Do homework later"}`))) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusNotFound != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } +} + +// TestDeleteSpecificTask /** +func TestDeleteSpecificTask(t *testing.T) { + + ctx := context.Background() + // truncate all tasks + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + // populate test tasks + if err := populateTasks(ctx); err != nil { + t.Errorf("Unable to populate task records\n") + } + + request, _ := http.NewRequest("DELETE", "/api/tasks/1", bytes.NewBuffer([]byte(`{"content":"Do homework later"}`))) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusNoContent != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } +} + +// TestDeleteSpecificTaskReturnNotFoundWhenTaskDoesNotExist /** +func TestDeleteSpecificTaskReturnNotFoundWhenTaskDoesNotExist(t *testing.T) { + + ctx := context.Background() + // truncate all tasks + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + // populate test tasks + if err := populateTasks(ctx); err != nil { + t.Errorf("Unable to populate task records\n") + } + + request, _ := http.NewRequest("DELETE", "/api/tasks/4", nil) + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + responseRecorder := httptest.NewRecorder() + server.ServeHTTP(responseRecorder, request) + + // assert status code + if http.StatusNotFound != responseRecorder.Code { + t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + } +} From 476b3cbf432f4d3e3dc4d59c0f585ab4119f59eb Mon Sep 17 00:00:00 2001 From: Thuan Nguyen <33919309+huuthuan-nguyen@users.noreply.github.com> Date: Tue, 26 Jul 2022 01:16:20 +0700 Subject: [PATCH 2/9] Update Guide for using the app --- README.md | 234 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 210 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 6398b5c09..7e3436d58 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,218 @@ -### Requirements +### About the app +This app is built in Golang and PostgreSQL (for storing data). Gorilla Mux is the default router which is using in the app and for integration test too. -- Implement one single API which accepts a todo task and records it - - There is a maximum **limit of N tasks per user** that can be added **per day**. - - Different users can have **different** maximum daily limit. -- Write integration (functional) tests -- Write unit tests -- Choose a suitable architecture to make your code simple, organizable, and maintainable -- Using Docker to run locally - - Using Docker for database (if used) is mandatory. -- Write a concise README - - How to run your code locally? - - A sample “curl” command to call your API - - How to run your unit tests locally? - - What do you love about your solution? - - What else do you want us to know about however you do not have enough time to complete? +The app is inspired heavily by Laravel, so you would feel familiar if you had developed with Laravel. I also update JWT Authentication and full CRUD for this app. -### Notes +### How to run the app +```shell +docker-compose up +``` -- We're using Golang at Manabie. **However**, we encourage you to use the programming language that you are most comfortable with because we want you to **shine** with all your skills and knowledge. +Default app port is `8080`, if you your `8080` port is not available, do not hesitate to change to another one in `docker-compose.yml` -### How to submit your solution? +### How to use the app +1. Register new account +Method: `POST` -- Fork this repo and show us your development progress via a PR +URL: `/api/register` -### Interesting facts about Manabie +cURL: `curl --location --request POST 'http://localhost:8080/api/register' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "email": "huuthuan.nguyen@hotmail.com", + "password": "123@pass!word" +}'` -- Monthly there are about 2 million lines of code changes (inserted/updated/deleted) committed into our GitHub repositories. To avoid **regression bugs**, we write different kinds of **automated tests** (unit/integration (functionality)/end2end) as parts of the definition of done of our assigned tasks. -- We nurture the cultural values: **knowledge sharing** and **good communication**, therefore good written documents and readable, organizable, and maintainable code are in our blood when we build any features to grow our products. -- We have **collaborative** culture at Manabie. Feel free to ask trieu@manabie.com any questions. We are very happy to answer all of them. +Payload: +```json +{ + "email": "huuthuan.nguyen@hotmail.com", + "password": "123@pass!word" +} +``` +Response: +```json +{ + "status": 1, + "messages": [ + "Successful." + ], + "data": { + "id": 1, + "email": "huuthuan.nguyen@hotmail.com", + "is_active": true, + "daily_limit": 3, + "create_at": "2022-07-25T18:02:49.291850383Z", + "updated_at": "2022-07-25T18:02:49.291852258Z" + } +} +``` -Thank you for spending time to read and attempt our take-home assessment. We are looking forward to your submission. +Default quota for your tasks is 3 per day, you can not change this. + +2. Login with your email and password +Method: `POST` + +URL: `/api/auth/login` + +cURL: `curl --location --request POST 'http://localhost:8080/api/auth/login' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "email": "huuthuan.nguyen@hotmail.com", + "password": "123@pass!word" +}'` + +Payload: +```json +{ + "email": "huuthuan.nguyen@hotmail.com", + "password": "123@pass!word" +} +``` +Response: +```json +{ + "status": 1, + "messages": [ + "Successful." + ], + "data": { + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ", + "expired_at": "2022-07-26T18:04:04.060592793Z" + } +} +``` + +3. Add new task +Method: `POST` + +URL: `/api/tasks` + +cURL: `curl --location --request POST 'http://localhost:8080/api/tasks' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "content": "Do homework" +}'` + +Payload: +```json +{ + "content": "Do homework" +} +``` +Response +```json +{ + "status": 1, + "messages": [ + "Successful." + ], + "data": { + "id": 1, + "content": "Do homework", + "published_date": "2022-07-25", + "status": 0, + "created_by": 1, + "create_at": "2022-07-25T18:04:30.128784138Z", + "updated_at": "2022-07-25T18:04:30.128785013Z" + } +} +``` + +4. Update your task by ID +Method: `PUT` + +URL: `/api/tasks/:id` + +cURL: `curl --location --request PUT 'http://localhost:8080/api/tasks/1' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "content": "Do homework later" +}'` + +Payload: +```json +{ + "content": "Do homework later" +} +``` +Response: +```json +{ + "status": 1, + "messages": [ + "Successful." + ], + "data": { + "id": 1, + "content": "Do homework later", + "published_date": "2022-07-25", + "status": 0, + "created_by": 1, + "create_at": "2022-07-25T18:04:30.128784138Z", + "updated_at": "2022-07-25T18:07:45.866218Z" + } +} +``` + +5. Remove your task by ID +Method: `DELETE` + +URL: `/api/tasks/:id` + +cURL: `curl --location --request DELETE 'http://localhost:8080/api/tasks/1' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ'` + +Payload: +```json + +``` +Response +```json +``` + +6. List your tasks +Method: `GET` + +URL: `/api/tasks` + +cURL: `curl --location --request GET 'http://localhost:8080/api/tasks' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ'` + +Payload: +```json + +``` +Response: +```json +{ + "status": 1, + "messages": [ + "Successful." + ], + "data": { + "items": [ + { + "id": 1, + "content": "Do homework later", + "published_date": "2022-07-25", + "status": 0, + "created_by": 1, + "create_at": "2022-07-25T18:04:30.128784138Z", + "updated_at": "2022-07-25T18:07:45.866218Z" + }, + { + "id": 1, + "content": "Do laundry", + "published_date": "2022-07-25", + "status": 0, + "created_by": 1, + "create_at": "2022-07-25T18:04:30.128784138Z", + "updated_at": "2022-07-25T18:07:45.866218Z" + } + ] + } +} +``` \ No newline at end of file From 5eb29cbafa48f17c8a3992a460f361995acdbcc8 Mon Sep 17 00:00:00 2001 From: Thuan Nguyen <33919309+huuthuan-nguyen@users.noreply.github.com> Date: Tue, 26 Jul 2022 01:18:36 +0700 Subject: [PATCH 3/9] beautify guide --- README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 7e3436d58..1fd00e74c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ docker-compose up Default app port is `8080`, if you your `8080` port is not available, do not hesitate to change to another one in `docker-compose.yml` ### How to use the app -1. Register new account +1. #### Register new account + Method: `POST` URL: `/api/register` @@ -30,6 +31,7 @@ Payload: "password": "123@pass!word" } ``` + Response: ```json { @@ -50,7 +52,8 @@ Response: Default quota for your tasks is 3 per day, you can not change this. -2. Login with your email and password +2. #### Login with your email and password + Method: `POST` URL: `/api/auth/login` @@ -69,6 +72,7 @@ Payload: "password": "123@pass!word" } ``` + Response: ```json { @@ -83,7 +87,8 @@ Response: } ``` -3. Add new task +3. #### Add new task + Method: `POST` URL: `/api/tasks` @@ -101,6 +106,7 @@ Payload: "content": "Do homework" } ``` + Response ```json { @@ -120,7 +126,8 @@ Response } ``` -4. Update your task by ID +4. #### Update your task by ID + Method: `PUT` URL: `/api/tasks/:id` @@ -138,6 +145,7 @@ Payload: "content": "Do homework later" } ``` + Response: ```json { @@ -157,7 +165,8 @@ Response: } ``` -5. Remove your task by ID +5. #### Remove your task by ID + Method: `DELETE` URL: `/api/tasks/:id` @@ -173,7 +182,8 @@ Response ```json ``` -6. List your tasks +6. #### List your tasks + Method: `GET` URL: `/api/tasks` @@ -185,6 +195,7 @@ Payload: ```json ``` + Response: ```json { From 19686723e8e16c13bb1acea50b9831b0b85c0698 Mon Sep 17 00:00:00 2001 From: Thuan Nguyen <33919309+huuthuan-nguyen@users.noreply.github.com> Date: Tue, 26 Jul 2022 01:20:26 +0700 Subject: [PATCH 4/9] beautify cURL markdown --- README.md | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1fd00e74c..5c552481d 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,14 @@ Method: `POST` URL: `/api/register` -cURL: `curl --location --request POST 'http://localhost:8080/api/register' \ +cURL: ```shell +curl --location --request POST 'http://localhost:8080/api/register' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "huuthuan.nguyen@hotmail.com", "password": "123@pass!word" -}'` +}' +``` Payload: ```json @@ -58,12 +60,14 @@ Method: `POST` URL: `/api/auth/login` -cURL: `curl --location --request POST 'http://localhost:8080/api/auth/login' \ +cURL: ```shell +curl --location --request POST 'http://localhost:8080/api/auth/login' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "huuthuan.nguyen@hotmail.com", "password": "123@pass!word" -}'` +}' +``` Payload: ```json @@ -93,12 +97,14 @@ Method: `POST` URL: `/api/tasks` -cURL: `curl --location --request POST 'http://localhost:8080/api/tasks' \ +cURL: ```shell +curl --location --request POST 'http://localhost:8080/api/tasks' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' \ --header 'Content-Type: application/json' \ --data-raw '{ "content": "Do homework" -}'` +}' +``` Payload: ```json @@ -132,12 +138,14 @@ Method: `PUT` URL: `/api/tasks/:id` -cURL: `curl --location --request PUT 'http://localhost:8080/api/tasks/1' \ +cURL: ```shell +curl --location --request PUT 'http://localhost:8080/api/tasks/1' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' \ --header 'Content-Type: application/json' \ --data-raw '{ "content": "Do homework later" -}'` +}' +``` Payload: ```json @@ -171,8 +179,10 @@ Method: `DELETE` URL: `/api/tasks/:id` -cURL: `curl --location --request DELETE 'http://localhost:8080/api/tasks/1' \ ---header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ'` +cURL: ```shell +curl --location --request DELETE 'http://localhost:8080/api/tasks/1' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' +``` Payload: ```json @@ -188,8 +198,10 @@ Method: `GET` URL: `/api/tasks` -cURL: `curl --location --request GET 'http://localhost:8080/api/tasks' \ ---header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ'` +cURL: ```shell +curl --location --request GET 'http://localhost:8080/api/tasks' \ +--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' +``` Payload: ```json From 2d23ddb0af082bf32beba69e20e520b686fdb845 Mon Sep 17 00:00:00 2001 From: Thuan Nguyen <33919309+huuthuan-nguyen@users.noreply.github.com> Date: Tue, 26 Jul 2022 01:21:24 +0700 Subject: [PATCH 5/9] beautify cURL markdown --- README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5c552481d..b499aa4b1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Method: `POST` URL: `/api/register` -cURL: ```shell +cURL: +```shell curl --location --request POST 'http://localhost:8080/api/register' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -60,7 +61,8 @@ Method: `POST` URL: `/api/auth/login` -cURL: ```shell +cURL: +```shell curl --location --request POST 'http://localhost:8080/api/auth/login' \ --header 'Content-Type: application/json' \ --data-raw '{ @@ -97,7 +99,8 @@ Method: `POST` URL: `/api/tasks` -cURL: ```shell +cURL: +```shell curl --location --request POST 'http://localhost:8080/api/tasks' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' \ --header 'Content-Type: application/json' \ @@ -138,7 +141,8 @@ Method: `PUT` URL: `/api/tasks/:id` -cURL: ```shell +cURL: +```shell curl --location --request PUT 'http://localhost:8080/api/tasks/1' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' \ --header 'Content-Type: application/json' \ @@ -179,7 +183,8 @@ Method: `DELETE` URL: `/api/tasks/:id` -cURL: ```shell +cURL: +```shell curl --location --request DELETE 'http://localhost:8080/api/tasks/1' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' ``` @@ -198,7 +203,8 @@ Method: `GET` URL: `/api/tasks` -cURL: ```shell +cURL: +```shell curl --location --request GET 'http://localhost:8080/api/tasks' \ --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1dXRodWFuLm5ndXllbkBob3RtYWlsLmNvbSIsImV4cCI6MTY1ODg1ODY0NH0.vfglN4fOZ7NDat1hxznivNk9T4znCgw4l8K6XFemAYQ' ``` From fdc1af3720a388b2a87ede4c845ce6cc18c35ab6 Mon Sep 17 00:00:00 2001 From: Thuan Nguyen Date: Tue, 26 Jul 2022 01:40:24 +0700 Subject: [PATCH 6/9] Update Integration test --- test/task_test.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/task_test.go b/test/task_test.go index 66aa8bcbe..e21684450 100644 --- a/test/task_test.go +++ b/test/task_test.go @@ -168,6 +168,11 @@ func TestMain(m *testing.M) { // life cycle testing // TestCreateTask /** func TestCreateTask(t *testing.T) { + ctx := context.Background() + if err := truncateTaskRecords(ctx); err != nil { + t.Errorf("Unable to truncate task records\n") + } + var jsonStr = []byte(`{"content":"Do homework"}`) request, _ := http.NewRequest("POST", "/api/tasks", bytes.NewBuffer(jsonStr)) request.Header.Set("Content-Type", "application/json") @@ -208,7 +213,7 @@ func TestCreateTaskReturnFailWhenUserExceededDailyLimit(t *testing.T) { ctx := context.Background() if err := truncateTaskRecords(ctx); err != nil { - t.Errorf("Unable to truncate task records") + t.Errorf("Unable to truncate task records\n") } testData := []struct { @@ -243,7 +248,7 @@ func TestCreateTaskReturnFailWhenUserExceededDailyLimit(t *testing.T) { // assert status code if data.statusCode != responseRecorder.Code { - t.Errorf("Expected response code %d. Got %d\n", http.StatusOK, responseRecorder.Code) + t.Errorf("Expected response code %d. Got %d\n", data.statusCode, responseRecorder.Code) } } } From 6ea7983ba92d1df482481635800688a2df0993b3 Mon Sep 17 00:00:00 2001 From: Thuan Nguyen Date: Tue, 26 Jul 2022 01:44:30 +0700 Subject: [PATCH 7/9] fixed: UTC timezone for check exceed limit by date --- app/model/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/model/user.go b/app/model/user.go index 7d11d614c..14c3ccd32 100644 --- a/app/model/user.go +++ b/app/model/user.go @@ -95,7 +95,7 @@ func (user *User) countTasksByDate(date string, ctx context.Context, db bun.IDB) // countTasksCurrentDate /** func (user *User) countTasksCurrentDate(ctx context.Context, db bun.IDB) int { - currentTime := time.Now() + currentTime := time.Now().UTC() currentDate := currentTime.Format("2006-01-02") return user.countTasksByDate(currentDate, ctx, db) } From fcccdf24b9615bb261f6e8e71fea0f1b9bb710f5 Mon Sep 17 00:00:00 2001 From: Thuan Nguyen <33919309+huuthuan-nguyen@users.noreply.github.com> Date: Tue, 26 Jul 2022 01:49:19 +0700 Subject: [PATCH 8/9] Adding final section to README.md --- README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b499aa4b1..a4d0848ca 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ This app is built in Golang and PostgreSQL (for storing data). Gorilla Mux is th The app is inspired heavily by Laravel, so you would feel familiar if you had developed with Laravel. I also update JWT Authentication and full CRUD for this app. -### How to run the app +### How to run the app? ```shell docker-compose up ``` Default app port is `8080`, if you your `8080` port is not available, do not hesitate to change to another one in `docker-compose.yml` -### How to use the app +### How to use the app? 1. #### Register new account Method: `POST` @@ -244,4 +244,17 @@ Response: ] } } -``` \ No newline at end of file +``` + +### How to run test locally? + +Requisite: start your app with docker +```shell +APP_HOST= APP_PORT=8080 APP_SECRET=8c6c19688d9f94b3900042923aa94a99 DB_HOST=localhost DB_PORT=5432 DB_NAME=manabie DB_USERNAME=manabie DB_PASSWORD=12345 go test -v -count=1 ./test +``` + +### Final + +This app need to be optimized the database, use the aggregated column for checking exceeded daily limit instead of `count` query, because `count` query is so expensive when the records grow up. + +We need to move the testing bash script to docker-compose.yml too \ No newline at end of file From 23b76149a20041ca7ff3843ce39c8a40c4464cf2 Mon Sep 17 00:00:00 2001 From: Thuan Nguyen <33919309+huuthuan-nguyen@users.noreply.github.com> Date: Tue, 26 Jul 2022 01:50:37 +0700 Subject: [PATCH 9/9] Add postman collection URL for import --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a4d0848ca..a8f4bd9ad 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,8 @@ APP_HOST= APP_PORT=8080 APP_SECRET=8c6c19688d9f94b3900042923aa94a99 DB_HOST=loca ### Final -This app need to be optimized the database, use the aggregated column for checking exceeded daily limit instead of `count` query, because `count` query is so expensive when the records grow up. +- This app need to be optimized the database, use the aggregated column for checking exceeded daily limit instead of `count` query, because `count` query is so expensive when the records grow up. +- We need to move the testing bash script to docker-compose.yml too +- Add swagger instead of using Postman -We need to move the testing bash script to docker-compose.yml too \ No newline at end of file +Postman Collection: `https://www.getpostman.com/collections/2cdc08395f26e9047e4f` \ No newline at end of file