From 84e4fe29861f07f4e2c12d05ab820ad51fcf1544 Mon Sep 17 00:00:00 2001 From: alexferl Date: Wed, 13 Mar 2024 21:28:56 -0400 Subject: [PATCH 1/2] get the user roles/status from the db instead of token Signed-off-by: alexferl --- casbin/policy.csv | 12 +- handlers/auth.go | 10 +- handlers/auth_test.go | 33 +- handlers/personal_access_token.go | 11 +- handlers/personal_access_token_test.go | 93 ++- handlers/root_test.go | 5 +- handlers/setup_test.go | 34 ++ handlers/task.go | 88 ++- handlers/task_test.go | 528 +++++++----------- handlers/user.go | 281 +++++++--- handlers/user_test.go | 516 ++++++++++------- models/error.go | 36 ++ models/personal_access_token.go | 8 +- models/personal_access_token_test.go | 11 +- models/user.go | 230 +++++++- models/user_test.go | 283 +++++++++- openapi/components/schemas/auth/Token.yaml | 20 +- openapi/openapi.yaml | 12 +- openapi/paths/tasks/{id}.yaml | 2 +- openapi/paths/users/me.yaml | 2 +- .../{{id}.yaml => {id_or_username}.yaml} | 6 +- openapi/paths/users/{id_or_username}_ban.yaml | 50 ++ .../paths/users/{id_or_username}_lock.yaml | 50 ++ .../users/{id_or_username}_roles_{role}.yaml | 64 +++ ...atus.yaml => {id_or_username}_status.yaml} | 2 +- server/server.go | 59 +- services/error.go | 4 +- services/personal_access_token_test.go | 21 +- services/user.go | 5 +- util/jwt/jwt.go | 49 +- util/jwt/jwt_test.go | 55 -- 31 files changed, 1647 insertions(+), 933 deletions(-) create mode 100644 handlers/setup_test.go create mode 100644 models/error.go rename openapi/paths/users/{{id}.yaml => {id_or_username}.yaml} (95%) create mode 100644 openapi/paths/users/{id_or_username}_ban.yaml create mode 100644 openapi/paths/users/{id_or_username}_lock.yaml create mode 100644 openapi/paths/users/{id_or_username}_roles_{role}.yaml rename openapi/paths/users/{{id}_status.yaml => {id_or_username}_status.yaml} (97%) diff --git a/casbin/policy.csv b/casbin/policy.csv index a11fecd..f08ad63 100644 --- a/casbin/policy.csv +++ b/casbin/policy.csv @@ -13,17 +13,19 @@ p, any, /google, GET p, any, /oauth2/*/login, GET p, any, /oauth2/*/callback, GET -p, user, /me, (GET)|(PUT) +p, user, /me, (GET)|(PATCH) p, user, /me/personal_access_tokens, (GET)|(POST) p, user, /me/personal_access_tokens/:id, (GET)|(DELETE) p, user, /tasks, (GET)|(POST) -p, user, /tasks/:id, (GET)|(PUT)|(DELETE) +p, user, /tasks/:id, (GET)|(PATCH)|(DELETE) p, user, /tasks/:id/transition, PUT -p, user, /users/:id, GET +p, user, /users/:id_or_username, GET p, admin, /users, GET -p, admin, /users/:id, PUT -p, admin, /users/:id/status, PUT +p, admin, /users/:id_or_username, PATCH +p, admin, /users/:id_or_username/ban, (PUT)|(DELETE) +p, admin, /users/:id_or_username/lock, (PUT)|(DELETE) +p, admin, /users/:id_or_username/roles/:role, (PUT)|(DELETE) g, *, any g, user, any diff --git a/handlers/auth.go b/handlers/auth.go index c5eb438..61906b1 100644 --- a/handlers/auth.go +++ b/handlers/auth.go @@ -116,13 +116,13 @@ type LogoutRequest struct { } func (h *AuthHandler) logout(c echo.Context) error { - token := c.Get("refresh_token").(jwx.Token) + currentUser := c.Get("user").(*models.User) encodedToken := c.Get("refresh_token_encoded").(string) ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() - user, err := h.svc.Read(ctx, token.Subject()) + user, err := h.svc.Read(ctx, currentUser.Id) if err != nil { var se *services.Error if errors.As(err, &se) { @@ -237,8 +237,9 @@ func (h *AuthHandler) signup(c echo.Context) error { if se.Kind == services.Exist { return h.Validate(c, http.StatusConflict, echo.Map{"message": se.Message}) } + } else { + log.Error().Err(err).Msg("failed getting user") } - log.Error().Err(err).Msg("failed getting user") } user := models.NewUser(body.Email, body.Username) @@ -259,8 +260,9 @@ func (h *AuthHandler) signup(c echo.Context) error { if se.Kind == services.Exist { return h.Validate(c, http.StatusConflict, echo.Map{"message": se.Message}) } + } else { + log.Error().Err(err).Msg("failed inserting new user") } - log.Error().Err(err).Msg("failed inserting new user") return err } diff --git a/handlers/auth_test.go b/handlers/auth_test.go index ba69db6..c5963ef 100644 --- a/handlers/auth_test.go +++ b/handlers/auth_test.go @@ -20,7 +20,6 @@ import ( "github.com/alexferl/echo-boilerplate/config" "github.com/alexferl/echo-boilerplate/handlers" "github.com/alexferl/echo-boilerplate/models" - "github.com/alexferl/echo-boilerplate/server" "github.com/alexferl/echo-boilerplate/services" "github.com/alexferl/echo-boilerplate/util/cookie" "github.com/alexferl/echo-boilerplate/util/jwt" @@ -34,9 +33,10 @@ type AuthHandlerTestSuite struct { func (s *AuthHandlerTestSuite) SetupTest() { svc := handlers.NewMockUserService(s.T()) + patSvc := handlers.NewMockPersonalAccessTokenService(s.T()) h := handlers.NewAuthHandler(openapi.NewHandler(), svc) s.svc = svc - s.server = server.NewTestServer(h) + s.server = getServer(svc, patSvc, h) } func TestAuthHandlerTestSuite(t *testing.T) { @@ -158,19 +158,6 @@ func (s *AuthHandlerTestSuite) TestAuthHandler_Login_400() { assert.Equal(s.T(), http.StatusBadRequest, resp.Code) } -// TODO: fix -//func (s *AuthHandlerTestSuite) TestAuthHandler_Login_422() { -// b, _ := json.Marshal(`{"username":"foo","password":"bar","derp":"dep"}`) -// -// req := httptest.NewRequest(http.MethodPost, "/auth/login", bytes.NewBuffer(b)) -// req.Header.Set("Content-Type", "application/json") -// resp := httptest.NewRecorder() -// -// s.server.ServeHTTP(resp, req) -// -// assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) -//} - func (s *AuthHandlerTestSuite) TestAuthHandler_Logout_204_Cookie() { user := models.NewUser("test@example.com", "test") _, refresh, _ := user.Login() @@ -600,7 +587,6 @@ func (s *AuthHandlerTestSuite) TestAuthHandler_Signup_422() { func (s *AuthHandlerTestSuite) TestAuthHandler_Token_200() { user := models.NewUser("test@example.com", "test") access, _, _ := user.Login() - token, _ := jwt.ParseEncoded(access) req := httptest.NewRequest(http.MethodGet, "/auth/token", nil) @@ -608,12 +594,16 @@ func (s *AuthHandlerTestSuite) TestAuthHandler_Token_200() { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", access)) resp := httptest.NewRecorder() + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(user, nil).Once() + s.server.ServeHTTP(resp, req) var result handlers.TokenResponse _ = json.Unmarshal(resp.Body.Bytes(), &result) - roles, _ := token.Get("roles") typ, _ := token.Get("type") assert.Equal(s.T(), http.StatusOK, resp.Code) @@ -621,7 +611,6 @@ func (s *AuthHandlerTestSuite) TestAuthHandler_Token_200() { assert.Equal(s.T(), token.IssuedAt(), result.Iat) assert.Equal(s.T(), token.Issuer(), result.Iss) assert.Equal(s.T(), token.NotBefore(), result.Nbf) - assert.ElementsMatch(s.T(), roles, user.Roles) assert.Equal(s.T(), token.Subject(), result.Sub) assert.Equal(s.T(), typ, result.Type) } @@ -643,7 +632,6 @@ func (s *AuthHandlerTestSuite) TestAuthHandler_Token_401() { func (s *AuthHandlerTestSuite) TestAuthHandler_Cookie_200() { user := models.NewUser("test@example.com", "test") access, _, _ := user.Login() - token, _ := jwt.ParseEncoded(access) req := httptest.NewRequest(http.MethodGet, "/auth/token", nil) @@ -651,12 +639,16 @@ func (s *AuthHandlerTestSuite) TestAuthHandler_Cookie_200() { req.AddCookie(cookie.NewAccessToken(access)) resp := httptest.NewRecorder() + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(user, nil).Once() + s.server.ServeHTTP(resp, req) var result handlers.TokenResponse _ = json.Unmarshal(resp.Body.Bytes(), &result) - roles, _ := token.Get("roles") typ, _ := token.Get("type") assert.Equal(s.T(), http.StatusOK, resp.Code) @@ -664,7 +656,6 @@ func (s *AuthHandlerTestSuite) TestAuthHandler_Cookie_200() { assert.Equal(s.T(), token.IssuedAt(), result.Iat) assert.Equal(s.T(), token.Issuer(), result.Iss) assert.Equal(s.T(), token.NotBefore(), result.Nbf) - assert.ElementsMatch(s.T(), roles, user.Roles) assert.Equal(s.T(), token.Subject(), result.Sub) assert.Equal(s.T(), typ, result.Type) } diff --git a/handlers/personal_access_token.go b/handlers/personal_access_token.go index 95d262a..bd280b4 100644 --- a/handlers/personal_access_token.go +++ b/handlers/personal_access_token.go @@ -9,7 +9,6 @@ import ( "github.com/alexferl/echo-openapi" "github.com/alexferl/golib/http/api/server" "github.com/labstack/echo/v4" - jwx "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rs/zerolog/log" "github.com/alexferl/echo-boilerplate/models" @@ -49,7 +48,7 @@ type CreatePersonalAccessTokenRequest struct { } func (h *PersonalAccessTokenHandler) create(c echo.Context) error { - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) body := &CreatePersonalAccessTokenRequest{} if err := c.Bind(body); err != nil { @@ -60,7 +59,7 @@ func (h *PersonalAccessTokenHandler) create(c echo.Context) error { ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() - res, err := h.svc.FindOne(ctx, token.Subject(), body.Name) + res, err := h.svc.FindOne(ctx, currentUser.Id, body.Name) if err != nil { var se *services.Error if !errors.As(err, &se) { @@ -73,7 +72,7 @@ func (h *PersonalAccessTokenHandler) create(c echo.Context) error { return h.Validate(c, http.StatusConflict, echo.Map{"message": "token name already in-use"}) } - newPAT, err := models.NewPersonalAccessToken(token, body.Name, body.ExpiresAt) + newPAT, err := models.NewPersonalAccessToken(currentUser.Id, body.Name, body.ExpiresAt) if err != nil { if errors.Is(err, models.ErrExpiresAtPast) { m := echo.Map{ @@ -104,12 +103,12 @@ func (h *PersonalAccessTokenHandler) create(c echo.Context) error { } func (h *PersonalAccessTokenHandler) list(c echo.Context) error { - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() - pats, err := h.svc.Find(ctx, token.Subject()) + pats, err := h.svc.Find(ctx, currentUser.Id) if err != nil { log.Error().Err(err).Msg("failed getting personal access token") return err diff --git a/handlers/personal_access_token_test.go b/handlers/personal_access_token_test.go index 109f4cf..5230191 100644 --- a/handlers/personal_access_token_test.go +++ b/handlers/personal_access_token_test.go @@ -12,21 +12,19 @@ import ( "github.com/alexferl/echo-openapi" api "github.com/alexferl/golib/http/api/server" "github.com/labstack/echo/v4" - jwx "github.com/lestrrat-go/jwx/v2/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" "github.com/alexferl/echo-boilerplate/handlers" "github.com/alexferl/echo-boilerplate/models" - "github.com/alexferl/echo-boilerplate/server" "github.com/alexferl/echo-boilerplate/services" - "github.com/alexferl/echo-boilerplate/util/jwt" ) type PersonalAccessTokenHandlerTestSuite struct { suite.Suite svc *handlers.MockPersonalAccessTokenService + userSvc *handlers.MockUserService server *api.Server user *models.User accessToken []byte @@ -35,15 +33,15 @@ type PersonalAccessTokenHandlerTestSuite struct { } func (s *PersonalAccessTokenHandlerTestSuite) SetupTest() { + userSvc := handlers.NewMockUserService(s.T()) svc := handlers.NewMockPersonalAccessTokenService(s.T()) h := handlers.NewPersonalAccessTokenHandler(openapi.NewHandler(), svc) - user := models.NewUser("test@example.com", "test") - user.Id = "100" - user.Create(user.Id) + user := getUser() access, _, _ := user.Login() s.svc = svc - s.server = server.NewTestServer(h) + s.userSvc = userSvc + s.server = getServer(userSvc, svc, h) s.user = user s.accessToken = access } @@ -52,12 +50,12 @@ func TestPersonalAccessTokenHandlerTestSuite(t *testing.T) { suite.Run(t, new(PersonalAccessTokenHandlerTestSuite)) } -func createTokens(token jwx.Token, num int) models.PersonalAccessTokens { +func createTokens(userId string, num int) models.PersonalAccessTokens { result := make(models.PersonalAccessTokens, 0) for i := range num { pat, _ := models.NewPersonalAccessToken( - token, + userId, fmt.Sprintf("my_token%d", i), time.Now().Add((7*24)*time.Hour).Format("2006-01-02"), ) @@ -68,21 +66,24 @@ func createTokens(token jwx.Token, num int) models.PersonalAccessTokens { } func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Create_200() { - token, _ := jwt.ParseEncoded(s.accessToken) - payload := &handlers.CreatePersonalAccessTokenRequest{ Name: "My Token", ExpiresAt: time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02"), } b, _ := json.Marshal(payload) - newPAT, _ := models.NewPersonalAccessToken(token, payload.Name, payload.ExpiresAt) + newPAT, _ := models.NewPersonalAccessToken(s.user.Id, payload.Name, payload.ExpiresAt) req := httptest.NewRequest(http.MethodPost, "/me/personal_access_tokens", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). FindOne(mock.Anything, mock.Anything, mock.Anything). Return(nil, nil) @@ -110,21 +111,24 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Cre } func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Create_409() { - token, _ := jwt.ParseEncoded(s.accessToken) - payload := &handlers.CreatePersonalAccessTokenRequest{ Name: "My Token", ExpiresAt: time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02"), } b, _ := json.Marshal(payload) - newPAT, _ := models.NewPersonalAccessToken(token, payload.Name, payload.ExpiresAt) + newPAT, _ := models.NewPersonalAccessToken(s.user.Id, payload.Name, payload.ExpiresAt) req := httptest.NewRequest(http.MethodPost, "/me/personal_access_tokens", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). FindOne(mock.Anything, mock.Anything, mock.Anything). Return(newPAT, nil) @@ -146,27 +150,35 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Cre req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.server.ServeHTTP(resp, req) assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) } func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Create_422_Exp() { - token, _ := jwt.ParseEncoded(s.accessToken) - payload := &handlers.CreatePersonalAccessTokenRequest{ Name: "My Token", ExpiresAt: time.Now().Add(-(7 * 24) * time.Hour).Format("2006-01-02"), } b, _ := json.Marshal(payload) - newPAT, _ := models.NewPersonalAccessToken(token, payload.Name, payload.ExpiresAt) + newPAT, _ := models.NewPersonalAccessToken(s.user.Id, payload.Name, payload.ExpiresAt) req := httptest.NewRequest(http.MethodPost, "/me/personal_access_tokens", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). FindOne(mock.Anything, mock.Anything, mock.Anything). Return(newPAT, nil) @@ -177,15 +189,19 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Cre } func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_List_200() { - token, _ := jwt.ParseEncoded(s.accessToken) num := 10 - pats := createTokens(token, num) + pats := createTokens(s.user.Id, num) req := httptest.NewRequest(http.MethodGet, "/me/personal_access_tokens", nil) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Find(mock.Anything, mock.Anything). Return(pats, nil) @@ -210,10 +226,8 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Lis } func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Get_200() { - token, _ := jwt.ParseEncoded(s.accessToken) - newPAT, _ := models.NewPersonalAccessToken( - token, + s.user.Id, fmt.Sprintf("my_token"), time.Now().Add((7*24)*time.Hour).Format("2006-01-02"), ) @@ -223,6 +237,11 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Get req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). Return(newPAT, nil) @@ -241,6 +260,11 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Get req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). Return(nil, &services.Error{ @@ -258,10 +282,8 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Get } func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Revoke_204() { - token, _ := jwt.ParseEncoded(s.accessToken) - newPAT, _ := models.NewPersonalAccessToken( - token, + s.user.Id, fmt.Sprintf("my_token"), time.Now().Add((7*24)*time.Hour).Format("2006-01-02"), ) @@ -271,6 +293,11 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Rev req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). Return(newPAT, nil) @@ -300,6 +327,11 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Rev req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). Return(nil, &services.Error{ @@ -317,10 +349,8 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Rev } func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Revoke_409() { - token, _ := jwt.ParseEncoded(s.accessToken) - newPAT, _ := models.NewPersonalAccessToken( - token, + s.user.Id, fmt.Sprintf("my_token"), time.Now().Add((7*24)*time.Hour).Format("2006-01-02"), ) @@ -331,6 +361,11 @@ func (s *PersonalAccessTokenHandlerTestSuite) TestPersonalAccessTokenHandler_Rev req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). Return(newPAT, nil) diff --git a/handlers/root_test.go b/handlers/root_test.go index f5acdb9..6275501 100644 --- a/handlers/root_test.go +++ b/handlers/root_test.go @@ -9,13 +9,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/alexferl/echo-boilerplate/handlers" - "github.com/alexferl/echo-boilerplate/server" _ "github.com/alexferl/echo-boilerplate/testing" ) func TestHandler_Root(t *testing.T) { h := handlers.NewRootHandler(openapi.NewHandler()) - s := server.NewTestServer(h) + userSvc := handlers.NewMockUserService(t) + patSvc := handlers.NewMockPersonalAccessTokenService(t) + s := getServer(userSvc, patSvc, h) req := httptest.NewRequest(http.MethodGet, "/", nil) resp := httptest.NewRecorder() diff --git a/handlers/setup_test.go b/handlers/setup_test.go new file mode 100644 index 0000000..cb323e0 --- /dev/null +++ b/handlers/setup_test.go @@ -0,0 +1,34 @@ +package handlers_test + +import ( + "github.com/alexferl/echo-boilerplate/handlers" + "github.com/alexferl/echo-boilerplate/models" + "github.com/alexferl/echo-boilerplate/server" + _ "github.com/alexferl/echo-boilerplate/testing" + api "github.com/alexferl/golib/http/api/server" +) + +func getUser() *models.User { + user := models.NewUser("test@example.com", "test") + user.Id = "1000" + user.Create(user.Id) + return user +} + +func getAdmin() *models.User { + admin := models.NewUserWithRole("admin@example.com", "admin", models.AdminRole) + admin.Id = "2000" + admin.Create(admin.Id) + return admin +} + +func getSuper() *models.User { + super := models.NewUserWithRole("super@example.com", "super", models.SuperRole) + super.Id = "3000" + super.Create(super.Id) + return super +} + +func getServer(userSvc handlers.UserService, patSvc handlers.PersonalAccessTokenService, handler ...handlers.Handler) *api.Server { + return server.NewTestServer(userSvc, patSvc, handler...) +} diff --git a/handlers/task.go b/handlers/task.go index cd906a7..840b220 100644 --- a/handlers/task.go +++ b/handlers/task.go @@ -9,13 +9,10 @@ import ( "github.com/alexferl/echo-openapi" "github.com/alexferl/golib/http/api/server" "github.com/labstack/echo/v4" - jwx "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rs/zerolog/log" - "github.com/alexferl/echo-boilerplate/data" "github.com/alexferl/echo-boilerplate/models" "github.com/alexferl/echo-boilerplate/services" - "github.com/alexferl/echo-boilerplate/util/jwt" "github.com/alexferl/echo-boilerplate/util/pagination" ) @@ -43,7 +40,7 @@ func (h *TaskHandler) Register(s *server.Server) { s.Add(http.MethodPost, "/tasks", h.create) s.Add(http.MethodGet, "/tasks", h.list) s.Add(http.MethodGet, "/tasks/:id", h.get) - s.Add(http.MethodPut, "/tasks/:id", h.update) + s.Add(http.MethodPatch, "/tasks/:id", h.update) s.Add(http.MethodPut, "/tasks/:id/transition", h.transition) s.Add(http.MethodDelete, "/tasks/:id", h.delete) } @@ -53,7 +50,7 @@ type CreateTaskRequest struct { } func (h *TaskHandler) create(c echo.Context) error { - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) body := &CreateTaskRequest{} if err := c.Bind(body); err != nil { @@ -67,7 +64,7 @@ func (h *TaskHandler) create(c echo.Context) error { model := models.NewTask() model.Title = body.Title - task, err := h.svc.Create(ctx, token.Subject(), model) + task, err := h.svc.Create(ctx, currentUser.Id, model) if err != nil { log.Error().Err(err).Msg("failed creating task") return err @@ -109,16 +106,10 @@ func (h *TaskHandler) get(c echo.Context) error { task, err := h.svc.Read(ctx, id) if err != nil { - var se *services.Error - if errors.As(err, &se) { - if se.Kind == services.NotExist { - return h.Validate(c, http.StatusNotFound, echo.Map{"message": se.Message}) - } else if se.Kind == services.Deleted { - return h.Validate(c, http.StatusGone, echo.Map{"message": se.Message}) - } + sErr := h.readTask(c, err) + if sErr != nil { + return sErr() } - log.Error().Err(err).Msg("failed getting task") - return err } return h.Validate(c, http.StatusOK, task.Response()) @@ -130,7 +121,7 @@ type UpdateTaskRequest struct { func (h *TaskHandler) update(c echo.Context) error { id := c.Param("id") - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) body := &UpdateTaskRequest{} if err := c.Bind(body); err != nil { @@ -143,19 +134,13 @@ func (h *TaskHandler) update(c echo.Context) error { task, err := h.svc.Read(ctx, id) if err != nil { - var se *services.Error - if errors.As(err, &se) { - if se.Kind == services.NotExist { - return h.Validate(c, http.StatusNotFound, echo.Map{"message": se.Message}) - } else if se.Kind == services.Deleted { - return h.Validate(c, http.StatusGone, echo.Map{"message": se.Message}) - } + sErr := h.readTask(c, err) + if sErr != nil { + return sErr() } - log.Error().Err(err).Msg("failed getting task") - return err } - if token.Subject() != task.CreatedBy.(*models.User).Id && !jwt.HasRoles(token, models.AdminRole.String(), models.SuperRole.String()) { + if currentUser.Id != task.CreatedBy.(*models.User).Id && !currentUser.HasRoleOrHigher(models.AdminRole) { return h.Validate(c, http.StatusForbidden, echo.Map{"message": "you don't have access"}) } @@ -163,7 +148,7 @@ func (h *TaskHandler) update(c echo.Context) error { task.Title = *body.Title } - res, err := h.svc.Update(ctx, token.Subject(), task) + res, err := h.svc.Update(ctx, currentUser.Id, task) if err != nil { log.Error().Err(err).Msg("failed updating task") return err @@ -178,7 +163,7 @@ type TransitionTaskRequest struct { func (h *TaskHandler) transition(c echo.Context) error { id := c.Param("id") - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) body := &TransitionTaskRequest{} if err := c.Bind(body); err != nil { @@ -191,27 +176,21 @@ func (h *TaskHandler) transition(c echo.Context) error { task, err := h.svc.Read(ctx, id) if err != nil { - var se *services.Error - if errors.As(err, &se) { - if se.Kind == services.NotExist { - return h.Validate(c, http.StatusNotFound, echo.Map{"message": se.Message}) - } else if se.Kind == services.Deleted { - return h.Validate(c, http.StatusGone, echo.Map{"message": se.Message}) - } + sErr := h.readTask(c, err) + if sErr != nil { + return sErr() } - log.Error().Err(err).Msg("failed getting task") - return err } if *body.Completed != task.Completed { if *body.Completed { - task.Complete(token.Subject()) + task.Complete(currentUser.Id) } else { task.Incomplete() } } - res, err := h.svc.Update(ctx, token.Subject(), task) + res, err := h.svc.Update(ctx, currentUser.Id, task) if err != nil { log.Error().Err(err).Msg("failed updating task") return err @@ -222,29 +201,24 @@ func (h *TaskHandler) transition(c echo.Context) error { func (h *TaskHandler) delete(c echo.Context) error { id := c.Param("id") - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() task, err := h.svc.Read(ctx, id) if err != nil { - if errors.Is(err, data.ErrNoDocuments) { - return h.Validate(c, http.StatusNotFound, echo.Map{"message": "task not found"}) + sErr := h.readTask(c, err) + if sErr != nil { + return sErr() } - log.Error().Err(err).Msg("failed getting task") - return err - } - - if task.DeletedAt != nil { - return h.Validate(c, http.StatusGone, echo.Map{"message": "task was deleted"}) } - if token.Subject() != task.CreatedBy.(*models.User).Id && !jwt.HasRoles(token, models.AdminRole.String(), models.SuperRole.String()) { + if currentUser.Id != task.CreatedBy.(*models.User).Id && !currentUser.HasRoleOrHigher(models.AdminRole) { return h.Validate(c, http.StatusForbidden, echo.Map{"message": "you don't have access"}) } - err = h.svc.Delete(ctx, token.Subject(), task) + err = h.svc.Delete(ctx, currentUser.Id, task) if err != nil { log.Error().Err(err).Msg("failed deleting task") return err @@ -252,3 +226,17 @@ func (h *TaskHandler) delete(c echo.Context) error { return h.Validate(c, http.StatusNoContent, nil) } + +func (h *TaskHandler) readTask(c echo.Context, err error) func() error { + var se *services.Error + if errors.As(err, &se) { + msg := echo.Map{"message": se.Message} + if se.Kind == services.NotExist { + return func() error { return h.Validate(c, http.StatusNotFound, msg) } + } else if se.Kind == services.Deleted { + return func() error { return h.Validate(c, http.StatusGone, msg) } + } + } + log.Error().Err(err).Msg("failed getting task") + return func() error { return err } +} diff --git a/handlers/task_test.go b/handlers/task_test.go index 56c9b18..08e643c 100644 --- a/handlers/task_test.go +++ b/handlers/task_test.go @@ -15,16 +15,15 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" - "github.com/alexferl/echo-boilerplate/data" "github.com/alexferl/echo-boilerplate/handlers" "github.com/alexferl/echo-boilerplate/models" - "github.com/alexferl/echo-boilerplate/server" "github.com/alexferl/echo-boilerplate/services" ) type TaskHandlerTestSuite struct { suite.Suite svc *handlers.MockTaskService + userSvc *handlers.MockUserService server *api.Server user *models.User accessToken []byte @@ -32,14 +31,15 @@ type TaskHandlerTestSuite struct { func (s *TaskHandlerTestSuite) SetupTest() { svc := handlers.NewMockTaskService(s.T()) + userSvc := handlers.NewMockUserService(s.T()) + patSvc := handlers.NewMockPersonalAccessTokenService(s.T()) h := handlers.NewTaskHandler(openapi.NewHandler(), svc) - user := models.NewUser("test@example.com", "test") - user.Id = "1" - user.Create(user.Id) + user := getUser() access, _, _ := user.Login() s.svc = svc - s.server = server.NewTestServer(h) + s.userSvc = userSvc + s.server = getServer(userSvc, patSvc, h) s.user = user s.accessToken = access } @@ -58,9 +58,14 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Get_200() { task.Create(s.user.Id) task.CreatedBy = s.user + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(task, nil) + Return(task, nil).Once() s.server.ServeHTTP(resp, req) @@ -71,64 +76,167 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Get_200() { assert.Equal(s.T(), s.user.Id, result.CreatedBy.Id) } -func (s *TaskHandlerTestSuite) TestTaskHandler_Get_401() { - req := httptest.NewRequest(http.MethodGet, "/tasks/1", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() +func (s *TaskHandlerTestSuite) TestTaskHandler_401() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodPost, "/tasks"}, + {http.MethodGet, "/tasks"}, + {http.MethodGet, "/tasks/1"}, + {http.MethodPatch, "/tasks/1"}, + {http.MethodPut, "/tasks/1/transition"}, + {http.MethodDelete, "/tasks/1"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() - s.server.ServeHTTP(resp, req) + s.server.ServeHTTP(resp, req) - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) -func (s *TaskHandlerTestSuite) TestTaskHandler_Get_404() { - req := httptest.NewRequest(http.MethodGet, "/tasks/1", nil) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.NotExist, - Message: services.ErrTaskNotFound.Error(), + assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) + assert.Equal(s.T(), "token invalid", result.Message) }) + } +} - s.server.ServeHTTP(resp, req) +func (s *TaskHandlerTestSuite) TestTaskHandler_404() { + title := "My Edited Task" + updateBody := &handlers.UpdateTaskRequest{ + Title: &title, + } - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) + t := true + transitionBody := &handlers.TransitionTaskRequest{ + Completed: &t, + } - assert.Equal(s.T(), http.StatusNotFound, resp.Code) - assert.Equal(s.T(), services.ErrTaskNotFound.Error(), result.Message) + testCases := []struct { + method string + endpoint string + body any + }{ + {http.MethodGet, "/tasks/1", nil}, + {http.MethodPatch, "/tasks/1", updateBody}, + {http.MethodPut, "/tasks/1/transition", transitionBody}, + {http.MethodDelete, "/tasks/1", nil}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + b, _ := json.Marshal(tc.body) + req := httptest.NewRequest(tc.method, tc.endpoint, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) + resp := httptest.NewRecorder() + + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(nil, &services.Error{ + Kind: services.NotExist, + Message: services.ErrTaskNotFound.Error(), + }).Once() + + s.server.ServeHTTP(resp, req) + + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) + + assert.Equal(s.T(), http.StatusNotFound, resp.Code) + assert.Equal(s.T(), services.ErrTaskNotFound.Error(), result.Message) + }) + } } -func (s *TaskHandlerTestSuite) TestTaskHandler_Get_410() { - req := httptest.NewRequest(http.MethodGet, "/tasks/1", nil) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() +func (s *TaskHandlerTestSuite) TestTaskHandler_410() { + title := "My Edited Task" + updateBody := &handlers.UpdateTaskRequest{ + Title: &title, + } - task := models.NewTask() - task.Create(s.user.Id) - task.Delete(s.user.Id) - task.CreatedBy = s.user - task.DeletedBy = s.user + t := true + transitionBody := &handlers.TransitionTaskRequest{ + Completed: &t, + } - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.Deleted, - Message: services.ErrTaskDeleted.Error(), + testCases := []struct { + method string + endpoint string + body any + }{ + {http.MethodGet, "/tasks/1", nil}, + {http.MethodPatch, "/tasks/1", updateBody}, + {http.MethodPut, "/tasks/1/transition", transitionBody}, + {http.MethodDelete, "/tasks/1", nil}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + b, _ := json.Marshal(tc.body) + req := httptest.NewRequest(tc.method, tc.endpoint, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) + resp := httptest.NewRecorder() + + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(nil, &services.Error{ + Kind: services.Deleted, + Message: services.ErrTaskDeleted.Error(), + }).Once() + + s.server.ServeHTTP(resp, req) + + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) + + assert.Equal(s.T(), http.StatusGone, resp.Code) + assert.Equal(s.T(), services.ErrTaskDeleted.Error(), result.Message) }) + } +} - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusGone, resp.Code) - assert.Equal(s.T(), services.ErrTaskDeleted.Error(), result.Message) +func (s *TaskHandlerTestSuite) TestTaskHandler_422() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodPost, "/tasks"}, + {http.MethodPatch, "/tasks/1"}, + {http.MethodPut, "/tasks/1/transition"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + body := &handlers.UpdateTaskRequest{} + b, _ := json.Marshal(body) + req := httptest.NewRequest(tc.method, tc.endpoint, bytes.NewBuffer(b)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) + resp := httptest.NewRecorder() + + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + + s.server.ServeHTTP(resp, req) + + assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) + }) + } } func (s *TaskHandlerTestSuite) TestTaskHandler_Update_200() { @@ -138,7 +246,7 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Update_200() { } b, _ := json.Marshal(payload) - req := httptest.NewRequest(http.MethodPut, "/tasks/1", bytes.NewBuffer(b)) + req := httptest.NewRequest(http.MethodPatch, "/tasks/1", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() @@ -147,16 +255,21 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Update_200() { task.Create(s.user.Id) task.CreatedBy = s.user + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(task, nil) + Return(task, nil).Once() task.Update(s.user.Id) task.UpdatedBy = s.user s.svc.EXPECT(). Update(mock.Anything, mock.Anything, mock.Anything). - Return(task, nil) + Return(task, nil).Once() s.server.ServeHTTP(resp, req) @@ -168,16 +281,6 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Update_200() { assert.Equal(s.T(), s.user.Id, result.UpdatedBy.Id) } -func (s *TaskHandlerTestSuite) TestTaskHandler_Update_401() { - req := httptest.NewRequest(http.MethodPut, "/tasks/1", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} - func (s *TaskHandlerTestSuite) TestTaskHandler_Update_403() { title := "My Edited Task" payload := &handlers.UpdateTaskRequest{ @@ -185,7 +288,7 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Update_403() { } b, _ := json.Marshal(payload) - req := httptest.NewRequest(http.MethodPut, "/tasks/1", bytes.NewBuffer(b)) + req := httptest.NewRequest(http.MethodPatch, "/tasks/1", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() @@ -195,89 +298,18 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Update_403() { task.Create(user.Id) task.CreatedBy = user - s.svc.EXPECT(). + // middleware + s.userSvc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(task, nil) - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusForbidden, resp.Code) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Update_404() { - title := "My Edited Task" - payload := &handlers.UpdateTaskRequest{ - Title: &title, - } - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPut, "/tasks/1", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.NotExist, - Message: services.ErrTaskNotFound.Error(), - }) - - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusNotFound, resp.Code) - assert.Equal(s.T(), services.ErrTaskNotFound.Error(), result.Message) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Update_410() { - title := "My Edited Task" - payload := &handlers.UpdateTaskRequest{ - Title: &title, - } - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPut, "/tasks/1", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - task := models.NewTask() - task.Create(s.user.Id) - task.Delete(s.user.Id) - task.CreatedBy = s.user - task.DeletedBy = s.user + Return(s.user, nil).Once() s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.Deleted, - Message: services.ErrTaskDeleted.Error(), - }) - - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusGone, resp.Code) - assert.Equal(s.T(), services.ErrTaskDeleted.Error(), result.Message) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Update_422() { - payload := &handlers.UpdateTaskRequest{} - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPut, "/tasks/1", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() + Return(task, nil).Once() s.server.ServeHTTP(resp, req) - assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) + assert.Equal(s.T(), http.StatusForbidden, resp.Code) } func (s *TaskHandlerTestSuite) TestTaskHandler_Transition_200() { @@ -296,16 +328,21 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Transition_200() { task.Create(s.user.Id) task.CreatedBy = s.user + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(task, nil) + Return(task, nil).Once() task.Complete(s.user.Id) task.CompletedBy = s.user s.svc.EXPECT(). Update(mock.Anything, mock.Anything, mock.Anything). - Return(task, nil) + Return(task, nil).Once() s.server.ServeHTTP(resp, req) @@ -317,92 +354,6 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Transition_200() { assert.True(s.T(), result.Completed) } -func (s *TaskHandlerTestSuite) TestTaskHandler_Transition_401() { - req := httptest.NewRequest(http.MethodPut, "/tasks/1/transition", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Transition_404() { - t := true - payload := &handlers.TransitionTaskRequest{ - Completed: &t, - } - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPut, "/tasks/1/transition", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.NotExist, - Message: services.ErrTaskNotFound.Error(), - }) - - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusNotFound, resp.Code) - assert.Equal(s.T(), services.ErrTaskNotFound.Error(), result.Message) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Transition_410() { - t := true - payload := &handlers.TransitionTaskRequest{ - Completed: &t, - } - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPut, "/tasks/1/transition", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - task := models.NewTask() - task.Create(s.user.Id) - task.Delete(s.user.Id) - task.CreatedBy = s.user - task.DeletedBy = s.user - - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.Deleted, - Message: services.ErrTaskDeleted.Error(), - }) - - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusGone, resp.Code) - assert.Equal(s.T(), services.ErrTaskDeleted.Error(), result.Message) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Transition_422() { - payload := &handlers.TransitionTaskRequest{} - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPut, "/tasks/1/transition", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) -} - func (s *TaskHandlerTestSuite) TestTaskHandler_Delete_200() { req := httptest.NewRequest(http.MethodDelete, "/tasks/1", nil) req.Header.Set("Content-Type", "application/json") @@ -413,12 +364,18 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Delete_200() { task.Create(s.user.Id) task.CreatedBy = s.user + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(task, nil) + Return(task, nil).Once() + s.svc.EXPECT(). Delete(mock.Anything, mock.Anything, mock.Anything). - Return(nil) + Return(nil).Once() s.server.ServeHTTP(resp, req) @@ -436,61 +393,18 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Delete_403() { task.Create(user.Id) task.CreatedBy = user - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(task, nil) - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusForbidden, resp.Code) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Delete_404() { - req := httptest.NewRequest(http.MethodDelete, "/tasks/1", nil) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - task := models.NewTask() - task.Create(s.user.Id) - task.CreatedBy = s.user - - s.svc.EXPECT(). + // middleware + s.userSvc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(nil, data.ErrNoDocuments) - - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusNotFound, resp.Code) - assert.Equal(s.T(), "task not found", result.Message) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Delete_410() { - req := httptest.NewRequest(http.MethodDelete, "/tasks/1", nil) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - task := models.NewTask() - task.Create(s.user.Id) - task.CreatedBy = s.user - task.Delete(s.user.Id) - task.DeletedBy = s.user + Return(s.user, nil).Once() s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(task, nil) + Return(task, nil).Once() s.server.ServeHTTP(resp, req) - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusGone, resp.Code) - assert.Equal(s.T(), "task was deleted", result.Message) + assert.Equal(s.T(), http.StatusForbidden, resp.Code) } func createTasks(num int, user *models.User) models.Tasks { @@ -515,9 +429,14 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_List_200() { num := 10 tasks := createTasks(num, s.user) + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Find(mock.Anything, mock.Anything). - Return(int64(num), tasks, nil) + Return(int64(num), tasks, nil).Once() s.server.ServeHTTP(resp, req) @@ -541,16 +460,6 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_List_200() { assert.Equal(s.T(), link, h.Get("Link")) } -func (s *TaskHandlerTestSuite) TestTaskHandler_List_401() { - req := httptest.NewRequest(http.MethodGet, "/tasks", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} - func (s *TaskHandlerTestSuite) TestTaskHandler_Create_200() { payload := &handlers.CreateTaskRequest{Title: "Test"} b, _ := json.Marshal(payload) @@ -560,9 +469,14 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Create_200() { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.userSvc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.svc.EXPECT(). Create(mock.Anything, mock.Anything, mock.Anything). - Return(&models.Task{Model: &models.Model{CreatedBy: s.user}, Title: payload.Title}, nil) + Return(&models.Task{Model: &models.Model{CreatedBy: s.user}, Title: payload.Title}, nil).Once() s.server.ServeHTTP(resp, req) @@ -573,33 +487,3 @@ func (s *TaskHandlerTestSuite) TestTaskHandler_Create_200() { assert.Equal(s.T(), payload.Title, result.Title) assert.Equal(s.T(), s.user.Id, result.CreatedBy.Id) } - -func (s *TaskHandlerTestSuite) TestTaskHandler_Create_401() { - payload := &handlers.CreateTaskRequest{Title: "Test"} - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/tasks", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} - -func (s *TaskHandlerTestSuite) TestTaskHandler_Create_422() { - payload := &handlers.CreateTaskRequest{} - b, _ := json.Marshal(payload) - - req := httptest.NewRequest(http.MethodPost, "/tasks", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - var result models.TaskResponse - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) -} diff --git a/handlers/user.go b/handlers/user.go index c565f48..3c370d3 100644 --- a/handlers/user.go +++ b/handlers/user.go @@ -9,7 +9,6 @@ import ( "github.com/alexferl/echo-openapi" "github.com/alexferl/golib/http/api/server" "github.com/labstack/echo/v4" - jwx "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rs/zerolog/log" "github.com/alexferl/echo-boilerplate/models" @@ -40,20 +39,25 @@ func NewUserHandler(openapi *openapi.Handler, svc UserService) *UserHandler { func (h *UserHandler) Register(s *server.Server) { s.Add(http.MethodGet, "/me", h.getCurrentUser) - s.Add(http.MethodPut, "/me", h.updateCurrentUser) - s.Add(http.MethodGet, "/users/:id", h.get) - s.Add(http.MethodPut, "/users/:id", h.update) - s.Add(http.MethodPut, "/users/:id/status", h.updateStatus) + s.Add(http.MethodPatch, "/me", h.updateCurrentUser) + s.Add(http.MethodGet, "/users/:id_or_username", h.get) + s.Add(http.MethodPatch, "/users/:id_or_username", h.update) + s.Add(http.MethodPut, "/users/:id_or_username/ban", h.ban) + s.Add(http.MethodDelete, "/users/:id_or_username/ban", h.unban) + s.Add(http.MethodPut, "/users/:id_or_username/lock", h.lock) + s.Add(http.MethodDelete, "/users/:id_or_username/lock", h.unlock) + s.Add(http.MethodPut, "/users/:id_or_username/roles/:role", h.addRole) + s.Add(http.MethodDelete, "/users/:id_or_username/roles/:role", h.removeRole) s.Add(http.MethodGet, "/users", h.list) } func (h *UserHandler) getCurrentUser(c echo.Context) error { - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() - user, err := h.svc.Read(ctx, token.Subject()) + user, err := h.svc.Read(ctx, currentUser.Id) if err != nil { log.Error().Err(err).Msg("failed getting user") return err @@ -68,7 +72,7 @@ type UpdateCurrentUserRequest struct { } func (h *UserHandler) updateCurrentUser(c echo.Context) error { - token := c.Get("token").(jwx.Token) + currentUser := c.Get("user").(*models.User) body := &UpdateCurrentUserRequest{} if err := c.Bind(body); err != nil { @@ -79,7 +83,7 @@ func (h *UserHandler) updateCurrentUser(c echo.Context) error { ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() - user, err := h.svc.Read(ctx, token.Subject()) + user, err := h.svc.Read(ctx, currentUser.Id) if err != nil { log.Error().Err(err).Msg("failed getting user") return err @@ -93,7 +97,7 @@ func (h *UserHandler) updateCurrentUser(c echo.Context) error { user.Bio = *body.Bio } - res, err := h.svc.Update(ctx, token.Subject(), user) + res, err := h.svc.Update(ctx, currentUser.Id, user) if err != nil { log.Error().Err(err).Msg("failed updating user") return err @@ -103,23 +107,17 @@ func (h *UserHandler) updateCurrentUser(c echo.Context) error { } func (h *UserHandler) get(c echo.Context) error { - id := c.Param("id") + id := c.Param("id_or_username") ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() user, err := h.svc.Read(ctx, id) if err != nil { - var se *services.Error - if errors.As(err, &se) { - if se.Kind == services.NotExist { - return h.Validate(c, http.StatusNotFound, echo.Map{"message": se.Message}) - } else if se.Kind == services.Deleted { - return h.Validate(c, http.StatusGone, echo.Map{"message": se.Message}) - } + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() } - log.Error().Err(err).Msg("failed getting user") - return err } return h.Validate(c, http.StatusOK, user.Response()) @@ -131,8 +129,8 @@ type UpdateUserRequest struct { } func (h *UserHandler) update(c echo.Context) error { - id := c.Param("id") - token := c.Get("token").(jwx.Token) + id := c.Param("id_or_username") + currentUser := c.Get("user").(*models.User) body := &UpdateUserRequest{} if err := c.Bind(body); err != nil { @@ -145,16 +143,10 @@ func (h *UserHandler) update(c echo.Context) error { user, err := h.svc.Read(ctx, id) if err != nil { - var se *services.Error - if errors.As(err, &se) { - if se.Kind == services.NotExist { - return h.Validate(c, http.StatusNotFound, echo.Map{"message": se.Message}) - } else if se.Kind == services.Deleted { - return h.Validate(c, http.StatusGone, echo.Map{"message": se.Message}) - } + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() } - log.Error().Err(err).Msg("failed getting user") - return err } if body.Name != nil { @@ -165,7 +157,7 @@ func (h *UserHandler) update(c echo.Context) error { user.Bio = *body.Bio } - res, err := h.svc.Update(ctx, token.Subject(), user) + res, err := h.svc.Update(ctx, currentUser.Id, user) if err != nil { log.Error().Err(err).Msg("failed updating user") return err @@ -174,67 +166,198 @@ func (h *UserHandler) update(c echo.Context) error { return h.Validate(c, http.StatusOK, res.Response()) } -type UpdateUserStatusRequest struct { - IsBanned *bool `json:"is_banned,omitempty"` - IsLocked *bool `json:"is_locked,omitempty"` +func (h *UserHandler) ban(c echo.Context) error { + id := c.Param("id_or_username") + currentUser := c.Get("user").(*models.User) + + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) + defer cancel() + + user, err := h.svc.Read(ctx, id) + if err != nil { + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() + } + } + + err = user.Ban(currentUser) + if err != nil { + mErr := h.checkModelErr(c, err, "banning") + if mErr != nil { + return mErr() + } + } + + _, err = h.svc.Update(ctx, currentUser.Id, user) + if err != nil { + log.Error().Err(err).Msg("failed updating user") + return err + } + + return h.Validate(c, http.StatusNoContent, nil) } -func (h *UserHandler) updateStatus(c echo.Context) error { - id := c.Param("id") - token := c.Get("token").(jwx.Token) +func (h *UserHandler) unban(c echo.Context) error { + id := c.Param("id_or_username") + currentUser := c.Get("user").(*models.User) - body := &UpdateUserStatusRequest{} - if err := c.Bind(body); err != nil { - log.Error().Err(err).Msg("failed binding body") + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) + defer cancel() + + user, err := h.svc.Read(ctx, id) + if err != nil { + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() + } + } + + err = user.Unban(currentUser) + if err != nil { + mErr := h.checkModelErr(c, err, "unbanning") + if mErr != nil { + return mErr() + } + } + + _, err = h.svc.Update(ctx, currentUser.Id, user) + if err != nil { + log.Error().Err(err).Msg("failed updating user") return err } + return h.Validate(c, http.StatusNoContent, nil) +} + +func (h *UserHandler) lock(c echo.Context) error { + id := c.Param("id_or_username") + currentUser := c.Get("user").(*models.User) + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) defer cancel() user, err := h.svc.Read(ctx, id) if err != nil { - var se *services.Error - if errors.As(err, &se) { - if se.Kind == services.NotExist { - return h.Validate(c, http.StatusNotFound, echo.Map{"message": se.Message}) - } else if se.Kind == services.Deleted { - return h.Validate(c, http.StatusGone, echo.Map{"message": se.Message}) - } + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() } - log.Error().Err(err).Msg("failed getting user") + } + + err = user.Lock(currentUser) + if err != nil { + mErr := h.checkModelErr(c, err, "locking") + if mErr != nil { + return mErr() + } + } + + _, err = h.svc.Update(ctx, currentUser.Id, user) + if err != nil { + log.Error().Err(err).Msg("failed updating user") + return err + } + + return h.Validate(c, http.StatusNoContent, nil) +} + +func (h *UserHandler) unlock(c echo.Context) error { + id := c.Param("id_or_username") + currentUser := c.Get("user").(*models.User) + + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) + defer cancel() + + user, err := h.svc.Read(ctx, id) + if err != nil { + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() + } + } + + err = user.Unlock(currentUser) + if err != nil { + mErr := h.checkModelErr(c, err, "locking") + if mErr != nil { + return mErr() + } + } + + _, err = h.svc.Update(ctx, currentUser.Id, user) + if err != nil { + log.Error().Err(err).Msg("failed updating user") return err } - if user.Id == token.Subject() { - return c.JSON(http.StatusConflict, echo.Map{"message": "you cannot update your own status"}) + return h.Validate(c, http.StatusNoContent, nil) +} + +func (h *UserHandler) addRole(c echo.Context) error { + id := c.Param("id_or_username") + role := c.Param("role") + currentUser := c.Get("user").(*models.User) + + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) + defer cancel() + + user, err := h.svc.Read(ctx, id) + if err != nil { + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() + } } - if body.IsBanned != nil { - banned := *body.IsBanned - if banned { - user.Ban(token.Subject()) - } else { - user.Unban(token.Subject()) + err = user.AddRole(currentUser, models.RolesMap[role]) + if err != nil { + mErr := h.checkModelErr(c, err, "locking") + if mErr != nil { + return mErr() } } - if body.IsLocked != nil { - locked := *body.IsLocked - if locked { - user.Lock(token.Subject()) - } else { - user.Unlock(token.Subject()) + _, err = h.svc.Update(ctx, currentUser.Id, user) + if err != nil { + log.Error().Err(err).Msg("failed updating user") + return err + } + + return h.Validate(c, http.StatusNoContent, nil) +} + +func (h *UserHandler) removeRole(c echo.Context) error { + id := c.Param("id_or_username") + role := c.Param("role") + currentUser := c.Get("user").(*models.User) + + ctx, cancel := context.WithTimeout(c.Request().Context(), time.Second*10) + defer cancel() + + user, err := h.svc.Read(ctx, id) + if err != nil { + sErr := h.readUser(c, err) + if sErr != nil { + return sErr() } } - res, err := h.svc.Update(ctx, token.Subject(), user) + err = user.RemoveRole(currentUser, models.RolesMap[role]) + if err != nil { + mErr := h.checkModelErr(c, err, "locking") + if mErr != nil { + return mErr() + } + } + + _, err = h.svc.Update(ctx, currentUser.Id, user) if err != nil { log.Error().Err(err).Msg("failed updating user") return err } - return h.Validate(c, http.StatusOK, res.Response()) + return h.Validate(c, http.StatusNoContent, nil) } func (h *UserHandler) list(c echo.Context) error { @@ -257,3 +380,31 @@ func (h *UserHandler) list(c echo.Context) error { return h.Validate(c, http.StatusOK, users.Public()) } + +func (h *UserHandler) readUser(c echo.Context, err error) func() error { + var se *services.Error + if errors.As(err, &se) { + msg := echo.Map{"message": se.Message} + if se.Kind == services.NotExist { + return func() error { return h.Validate(c, http.StatusNotFound, msg) } + } else if se.Kind == services.Deleted { + return func() error { return h.Validate(c, http.StatusGone, msg) } + } + } + log.Error().Err(err).Msg("failed getting user") + return func() error { return err } +} + +func (h *UserHandler) checkModelErr(c echo.Context, err error, action string) func() error { + var me *models.Error + if errors.As(err, &me) { + msg := echo.Map{"message": me.Message} + if me.Kind == models.Conflict { + return func() error { return h.Validate(c, http.StatusConflict, msg) } + } else if me.Kind == models.Permission { + return func() error { return h.Validate(c, http.StatusForbidden, msg) } + } + } + log.Error().Err(err).Msgf("failed %s user", action) + return func() error { return err } +} diff --git a/handlers/user_test.go b/handlers/user_test.go index 37d85b6..ac69723 100644 --- a/handlers/user_test.go +++ b/handlers/user_test.go @@ -17,7 +17,6 @@ import ( "github.com/alexferl/echo-boilerplate/handlers" "github.com/alexferl/echo-boilerplate/models" - "github.com/alexferl/echo-boilerplate/server" "github.com/alexferl/echo-boilerplate/services" ) @@ -29,27 +28,29 @@ type UserHandlerTestSuite struct { accessToken []byte admin *models.User adminAccessToken []byte + super *models.User } func (s *UserHandlerTestSuite) SetupTest() { svc := handlers.NewMockUserService(s.T()) + patSvc := handlers.NewMockPersonalAccessTokenService(s.T()) h := handlers.NewUserHandler(openapi.NewHandler(), svc) - user := models.NewUser("test@example.com", "test") - user.Id = "100" - user.Create(user.Id) + + user := getUser() access, _, _ := user.Login() - admin := models.NewUserWithRole("admin@example.com", "admin", models.AdminRole) - admin.Id = "200" - admin.Create(admin.Id) + admin := getAdmin() adminAccess, _, _ := admin.Login() + super := getSuper() + s.svc = svc - s.server = server.NewTestServer(h) + s.server = getServer(svc, patSvc, h) s.user = user s.accessToken = access s.admin = admin s.adminAccessToken = adminAccess + s.super = super } func TestUserHandlerTestSuite(t *testing.T) { @@ -62,9 +63,14 @@ func (s *UserHandlerTestSuite) TestUserHandler_GetCurrentUser_200() { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(s.user, nil) + Return(s.user, nil).Once() + + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() s.server.ServeHTTP(resp, req) @@ -75,20 +81,6 @@ func (s *UserHandlerTestSuite) TestUserHandler_GetCurrentUser_200() { assert.Equal(s.T(), s.user.Id, result.Id) } -func (s *UserHandlerTestSuite) TestUserHandler_GetCurrentUser_401() { - req := httptest.NewRequest(http.MethodGet, "/me", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) - assert.Equal(s.T(), "token invalid", result.Message) -} - func (s *UserHandlerTestSuite) TestUserHandler_UpdateCurrentUser_200() { updatedUser := s.user updatedUser.Name = "updated name" @@ -96,18 +88,23 @@ func (s *UserHandlerTestSuite) TestUserHandler_UpdateCurrentUser_200() { Name: &updatedUser.Name, }) - req := httptest.NewRequest(http.MethodPut, "/me", bytes.NewBuffer(b)) + req := httptest.NewRequest(http.MethodPatch, "/me", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(s.user, nil) + Return(s.user, nil).Once() + + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() s.svc.EXPECT(). Update(mock.Anything, mock.Anything, mock.Anything). - Return(updatedUser, nil) + Return(updatedUser, nil).Once() s.server.ServeHTTP(resp, req) @@ -118,32 +115,16 @@ func (s *UserHandlerTestSuite) TestUserHandler_UpdateCurrentUser_200() { assert.Equal(s.T(), updatedUser.Name, result.Name) } -func (s *UserHandlerTestSuite) TestUserHandler_UpdateCurrentUser_401() { - req := httptest.NewRequest(http.MethodPut, "/me", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} - func (s *UserHandlerTestSuite) TestUserHandler_UpdateCurrentUser_422() { - req := httptest.NewRequest(http.MethodPut, "/me", bytes.NewBuffer([]byte(`{"invalid": "key"}`))) + req := httptest.NewRequest(http.MethodPatch, "/me", bytes.NewBuffer([]byte(`{"invalid": "key"}`))) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) -} - -func (s *UserHandlerTestSuite) TestUserHandler_Get_200() { - req := httptest.NewRequest(http.MethodGet, "/users/1", nil) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() s.svc.EXPECT(). Read(mock.Anything, mock.Anything). @@ -151,58 +132,31 @@ func (s *UserHandlerTestSuite) TestUserHandler_Get_200() { s.server.ServeHTTP(resp, req) - var result models.UserResponse - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusOK, resp.Code) - assert.Equal(s.T(), s.user.Id, result.Id) + assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) } -func (s *UserHandlerTestSuite) TestUserHandler_Get_404() { - req := httptest.NewRequest(http.MethodGet, "/users/404", nil) +func (s *UserHandlerTestSuite) TestUserHandler_Get_200() { + req := httptest.NewRequest(http.MethodGet, "/users/1", nil) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.NotExist, - Message: services.ErrTaskNotFound.Error(), - }) - - s.server.ServeHTTP(resp, req) - - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) - - assert.Equal(s.T(), http.StatusNotFound, resp.Code) - assert.Equal(s.T(), services.ErrTaskNotFound.Error(), result.Message) -} - -func (s *UserHandlerTestSuite) TestUserHandler_Get_410() { - user := models.NewUser("deleted@example.com", "deleted") - user.Delete(user.Id) - - req := httptest.NewRequest(http.MethodGet, "/users/1", nil) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) - resp := httptest.NewRecorder() + Return(s.user, nil).Once() s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.Deleted, - Message: services.ErrUserDeleted.Error(), - }) + Return(s.user, nil).Once() s.server.ServeHTTP(resp, req) - var result echo.HTTPError + var result models.UserResponse _ = json.Unmarshal(resp.Body.Bytes(), &result) - assert.Equal(s.T(), http.StatusGone, resp.Code) - assert.Equal(s.T(), services.ErrUserDeleted.Error(), result.Message) + assert.Equal(s.T(), http.StatusOK, resp.Code) + assert.Equal(s.T(), s.user.Id, result.Id) } func (s *UserHandlerTestSuite) TestUserHandler_Update_200() { @@ -212,18 +166,23 @@ func (s *UserHandlerTestSuite) TestUserHandler_Update_200() { Name: &updatedUser.Name, }) - req := httptest.NewRequest(http.MethodPut, "/users/1", bytes.NewBuffer(b)) + req := httptest.NewRequest(http.MethodPatch, "/users/1", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) resp := httptest.NewRecorder() + // middleware s.svc.EXPECT(). Read(mock.Anything, mock.Anything). - Return(s.user, nil) + Return(s.admin, nil).Once() + + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() s.svc.EXPECT(). Update(mock.Anything, mock.Anything, mock.Anything). - Return(updatedUser, nil) + Return(updatedUser, nil).Once() s.server.ServeHTTP(resp, req) @@ -234,16 +193,6 @@ func (s *UserHandlerTestSuite) TestUserHandler_Update_200() { assert.Equal(s.T(), updatedUser.Name, result.Name) } -func (s *UserHandlerTestSuite) TestUserHandler_Update_401() { - req := httptest.NewRequest(http.MethodPut, "/users/1", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} - func (s *UserHandlerTestSuite) TestUserHandler_Update_404() { updatedUser := s.user updatedUser.Name = "updated name" @@ -251,17 +200,22 @@ func (s *UserHandlerTestSuite) TestUserHandler_Update_404() { Name: &updatedUser.Name, }) - req := httptest.NewRequest(http.MethodPut, "/users/1", bytes.NewBuffer(b)) + req := httptest.NewRequest(http.MethodPatch, "/users/1", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) resp := httptest.NewRecorder() + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). Return(nil, &services.Error{ Kind: services.NotExist, - Message: services.ErrTaskNotFound.Error(), - }) + Message: services.ErrUserNotFound.Error(), + }).Once() s.server.ServeHTTP(resp, req) @@ -269,7 +223,7 @@ func (s *UserHandlerTestSuite) TestUserHandler_Update_404() { _ = json.Unmarshal(resp.Body.Bytes(), &result) assert.Equal(s.T(), http.StatusNotFound, resp.Code) - assert.Equal(s.T(), services.ErrTaskNotFound.Error(), result.Message) + assert.Equal(s.T(), services.ErrUserNotFound.Error(), result.Message) } func (s *UserHandlerTestSuite) TestUserHandler_Update_410() { @@ -280,17 +234,22 @@ func (s *UserHandlerTestSuite) TestUserHandler_Update_410() { }) updatedUser.Delete(s.admin.Id) - req := httptest.NewRequest(http.MethodPut, "/users/1", bytes.NewBuffer(b)) + req := httptest.NewRequest(http.MethodPatch, "/users/1", bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) resp := httptest.NewRecorder() + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() + s.svc.EXPECT(). Read(mock.Anything, mock.Anything). Return(nil, &services.Error{ Kind: services.Deleted, Message: services.ErrUserDeleted.Error(), - }) + }).Once() s.server.ServeHTTP(resp, req) @@ -301,125 +260,276 @@ func (s *UserHandlerTestSuite) TestUserHandler_Update_410() { assert.Equal(s.T(), services.ErrUserDeleted.Error(), result.Message) } -func (s *UserHandlerTestSuite) TestUserHandler_UpdateStatus_200() { - updatedUser := s.user - updatedUser.Name = "updated name" - t := true - b, _ := json.Marshal(&handlers.UpdateUserStatusRequest{ - IsLocked: &t, - }) - - req := httptest.NewRequest(http.MethodPut, "/users/1/status", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) - resp := httptest.NewRecorder() +func (s *UserHandlerTestSuite) TestUserHandler_204() { + bannedUser := models.NewUser("banned@example.com", "banned") + _ = bannedUser.Ban(s.admin) + + lockedUser := models.NewUser("locked@example.com", "locked") + _ = lockedUser.Lock(s.admin) + + testCases := []struct { + method string + endpoint string + target *models.User + }{ + {http.MethodPut, "/users/1/ban", s.user}, + {http.MethodDelete, "/users/1/ban", bannedUser}, + {http.MethodPut, "/users/1/lock", s.user}, + {http.MethodDelete, "/users/1/lock", lockedUser}, + {http.MethodPut, "/users/1/roles/admin", s.user}, + {http.MethodDelete, "/users/1/roles/user", s.user}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) + resp := httptest.NewRecorder() - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(s.user, nil) + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() - s.svc.EXPECT(). - Update(mock.Anything, mock.Anything, mock.Anything). - Return(updatedUser, nil) + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(tc.target, nil).Once() - s.server.ServeHTTP(resp, req) + s.svc.EXPECT(). + Update(mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil).Once() - var result models.UserResponse - _ = json.Unmarshal(resp.Body.Bytes(), &result) + s.server.ServeHTTP(resp, req) - assert.Equal(s.T(), http.StatusOK, resp.Code) - assert.Equal(s.T(), updatedUser.Name, result.Name) + assert.Equal(s.T(), http.StatusNoContent, resp.Code) + }) + } } -func (s *UserHandlerTestSuite) TestUserHandler_UpdateStatus_401() { - req := httptest.NewRequest(http.MethodPut, "/users/1/status", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() +func (s *UserHandlerTestSuite) TestUserHandler_401() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodGet, "/me"}, + {http.MethodPatch, "/me"}, + {http.MethodGet, "/users/1"}, + {http.MethodPatch, "/users/1"}, + {http.MethodPut, "/users/1/ban"}, + {http.MethodDelete, "/users/1/ban"}, + {http.MethodPut, "/users/1/lock"}, + {http.MethodDelete, "/users/1/lock"}, + {http.MethodPut, "/users/1/roles/user"}, + {http.MethodDelete, "/users/1/roles/user"}, + {http.MethodGet, "/users"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + resp := httptest.NewRecorder() - s.server.ServeHTTP(resp, req) + s.server.ServeHTTP(resp, req) + + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) + assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) + assert.Equal(s.T(), "token invalid", result.Message) + }) + } } -func (s *UserHandlerTestSuite) TestUserHandler_UpdateStatus_404() { - updatedUser := s.user - updatedUser.Name = "updated name" - t := true - b, _ := json.Marshal(&handlers.UpdateUserStatusRequest{ - IsLocked: &t, - }) +func (s *UserHandlerTestSuite) TestUserHandler_403() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodPut, "/users/1/ban"}, + {http.MethodDelete, "/users/1/ban"}, + {http.MethodPut, "/users/1/lock"}, + {http.MethodDelete, "/users/1/lock"}, + {http.MethodPut, "/users/1/roles/super"}, + {http.MethodDelete, "/users/1/roles/super"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) + resp := httptest.NewRecorder() - req := httptest.NewRequest(http.MethodPut, "/users/1/status", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) - resp := httptest.NewRecorder() + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.NotExist, - Message: services.ErrTaskNotFound.Error(), - }) + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.super, nil).Once() - s.server.ServeHTTP(resp, req) + s.server.ServeHTTP(resp, req) - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) - assert.Equal(s.T(), http.StatusNotFound, resp.Code) - assert.Equal(s.T(), services.ErrTaskNotFound.Error(), result.Message) + assert.Equal(s.T(), http.StatusForbidden, resp.Code) + }) + } } -func (s *UserHandlerTestSuite) TestUserHandler_UpdateStatus_409() { - updatedUser := s.admin - updatedUser.Name = "updated name" - t := true - b, _ := json.Marshal(&handlers.UpdateUserStatusRequest{ - IsLocked: &t, - }) +func (s *UserHandlerTestSuite) TestUserHandler_404() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodGet, "/users/1"}, + {http.MethodPut, "/users/1/ban"}, + {http.MethodDelete, "/users/1/ban"}, + {http.MethodPut, "/users/1/lock"}, + {http.MethodDelete, "/users/1/lock"}, + {http.MethodPut, "/users/1/roles/user"}, + {http.MethodDelete, "/users/1/roles/user"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) + resp := httptest.NewRecorder() + + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() + + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(nil, &services.Error{ + Kind: services.NotExist, + Message: services.ErrUserNotFound.Error(), + }).Once() + + s.server.ServeHTTP(resp, req) + + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) + + assert.Equal(s.T(), http.StatusNotFound, resp.Code) + assert.Equal(s.T(), services.ErrUserNotFound.Error(), result.Message) + }) + } +} - req := httptest.NewRequest(http.MethodPut, "/users/1/status", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) - resp := httptest.NewRecorder() +func (s *UserHandlerTestSuite) TestUserHandler_409() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodPut, "/users/1/ban"}, + {http.MethodDelete, "/users/1/ban"}, + {http.MethodPut, "/users/1/lock"}, + {http.MethodDelete, "/users/1/lock"}, + {http.MethodPut, "/users/1/roles/user"}, + {http.MethodDelete, "/users/1/roles/user"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) + resp := httptest.NewRecorder() - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(updatedUser, nil) + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() - s.server.ServeHTTP(resp, req) + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() - assert.Equal(s.T(), http.StatusConflict, resp.Code) -} + s.server.ServeHTTP(resp, req) -func (s *UserHandlerTestSuite) TestUserHandler_UpdateStatus_410() { - updatedUser := s.user - updatedUser.Name = "updated name" - t := true - b, _ := json.Marshal(&handlers.UpdateUserStatusRequest{ - IsLocked: &t, - }) - updatedUser.Delete(s.admin.Id) + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) - req := httptest.NewRequest(http.MethodPut, "/users/1/status", bytes.NewBuffer(b)) - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) - resp := httptest.NewRecorder() + assert.Equal(s.T(), http.StatusConflict, resp.Code) + }) + } +} - s.svc.EXPECT(). - Read(mock.Anything, mock.Anything). - Return(nil, &services.Error{ - Kind: services.Deleted, - Message: services.ErrUserDeleted.Error(), +func (s *UserHandlerTestSuite) TestUserHandler_410() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodGet, "/users/1"}, + {http.MethodPut, "/users/1/ban"}, + {http.MethodDelete, "/users/1/ban"}, + {http.MethodPut, "/users/1/lock"}, + {http.MethodDelete, "/users/1/lock"}, + {http.MethodPut, "/users/1/roles/user"}, + {http.MethodDelete, "/users/1/roles/user"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) + resp := httptest.NewRecorder() + + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() + + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(nil, &services.Error{ + Kind: services.Deleted, + Message: services.ErrUserDeleted.Error(), + }).Once() + + s.server.ServeHTTP(resp, req) + + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) + + assert.Equal(s.T(), http.StatusGone, resp.Code) + assert.Equal(s.T(), services.ErrUserDeleted.Error(), result.Message) }) + } +} - s.server.ServeHTTP(resp, req) +func (s *UserHandlerTestSuite) TestUserHandler_Roles_422() { + testCases := []struct { + method string + endpoint string + }{ + {http.MethodPut, "/users/1/roles/wrong"}, + {http.MethodDelete, "/users/1/roles/wrong"}, + } + for _, tc := range testCases { + s.T().Run(fmt.Sprintf("%s_%s", tc.method, tc.endpoint), func(t *testing.T) { + req := httptest.NewRequest(tc.method, tc.endpoint, nil) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.adminAccessToken)) + resp := httptest.NewRecorder() - var result echo.HTTPError - _ = json.Unmarshal(resp.Body.Bytes(), &result) + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() - assert.Equal(s.T(), http.StatusGone, resp.Code) - assert.Equal(s.T(), services.ErrUserDeleted.Error(), result.Message) + s.server.ServeHTTP(resp, req) + + var result echo.HTTPError + _ = json.Unmarshal(resp.Body.Bytes(), &result) + + assert.Equal(s.T(), http.StatusUnprocessableEntity, resp.Code) + }) + } } func createUsers(num int) models.Users { @@ -442,9 +552,14 @@ func (s *UserHandlerTestSuite) TestUserHandler_List_200() { num := 10 users := createUsers(num) + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.admin, nil).Once() + s.svc.EXPECT(). Find(mock.Anything, mock.Anything). - Return(int64(num), users, nil) + Return(int64(num), users, nil).Once() s.server.ServeHTTP(resp, req) @@ -468,22 +583,17 @@ func (s *UserHandlerTestSuite) TestUserHandler_List_200() { assert.Equal(s.T(), link, h.Get("Link")) } -func (s *UserHandlerTestSuite) TestUserHandler_List_401() { - req := httptest.NewRequest(http.MethodGet, "/users", nil) - req.Header.Set("Content-Type", "application/json") - resp := httptest.NewRecorder() - - s.server.ServeHTTP(resp, req) - - assert.Equal(s.T(), http.StatusUnauthorized, resp.Code) -} - func (s *UserHandlerTestSuite) TestUserHandler_List_403() { req := httptest.NewRequest(http.MethodGet, "/users", nil) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.accessToken)) resp := httptest.NewRecorder() + // middleware + s.svc.EXPECT(). + Read(mock.Anything, mock.Anything). + Return(s.user, nil).Once() + s.server.ServeHTTP(resp, req) assert.Equal(s.T(), http.StatusForbidden, resp.Code) diff --git a/models/error.go b/models/error.go new file mode 100644 index 0000000..0384ee2 --- /dev/null +++ b/models/error.go @@ -0,0 +1,36 @@ +package models + +import "fmt" + +// Error represents a model error +type Error struct { + Kind Kind + Message string +} + +// Kind defines supported error types. +type Kind uint8 + +const ( + Other Kind = iota + 1 // Unclassified error. + Conflict + Permission +) + +func (k Kind) String() string { + return [...]string{"other", "conflict", "permission"}[k-1] +} + +// NewError instantiates a new error. +func NewError(err error, kind Kind) error { + e := &Error{ + Kind: kind, + Message: err.Error(), + } + return e +} + +// Error returns the message. +func (e *Error) Error() string { + return fmt.Sprintf("kind=%s, message=%v", e.Kind, e.Message) +} diff --git a/models/personal_access_token.go b/models/personal_access_token.go index f47b970..c17bb08 100644 --- a/models/personal_access_token.go +++ b/models/personal_access_token.go @@ -4,7 +4,6 @@ import ( "errors" "time" - jwx "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rs/xid" "github.com/alexferl/echo-boilerplate/util/jwt" @@ -72,7 +71,7 @@ type PersonalAccessTokenResponse struct { UserId string `json:"user_id" bson:"user_id"` } -func NewPersonalAccessToken(token jwx.Token, name string, expiresAt string) (*PersonalAccessToken, error) { +func NewPersonalAccessToken(userId string, name string, expiresAt string) (*PersonalAccessToken, error) { t, err := time.Parse("2006-01-02", expiresAt) if err != nil { return nil, err @@ -83,8 +82,7 @@ func NewPersonalAccessToken(token jwx.Token, name string, expiresAt string) (*Pe return nil, ErrExpiresAtPast } - roles := jwt.GetRoles(token) - pat, err := jwt.GeneratePersonalToken(token.Subject(), t.Sub(now), map[string]any{"roles": roles}) + pat, err := jwt.GeneratePersonalToken(userId, t.Sub(now), nil) if err != nil { return nil, err } @@ -95,6 +93,6 @@ func NewPersonalAccessToken(token jwx.Token, name string, expiresAt string) (*Pe ExpiresAt: &t, Name: name, Token: string(pat), - UserId: token.Subject(), + UserId: userId, }, nil } diff --git a/models/personal_access_token_test.go b/models/personal_access_token_test.go index 539fc73..4d90203 100644 --- a/models/personal_access_token_test.go +++ b/models/personal_access_token_test.go @@ -5,23 +5,16 @@ import ( "time" "github.com/stretchr/testify/assert" - - "github.com/alexferl/echo-boilerplate/util/jwt" ) func TestPersonalAccessToken(t *testing.T) { user := NewUser("test@email.com", "test") - access, _, err := user.Login() - assert.NoError(t, err) - - token, err := jwt.ParseEncoded(access) - assert.NoError(t, err) // expires_at not in future - _, err = NewPersonalAccessToken(token, "My Token", time.Now().Format("2006-01-02")) + _, err := NewPersonalAccessToken(user.Id, "My Token", time.Now().Format("2006-01-02")) assert.Error(t, err) - pat, err := NewPersonalAccessToken(token, "My Token", time.Now().Add((7*24)*time.Hour).Format("2006-01-02")) + pat, err := NewPersonalAccessToken(user.Id, "My Token", time.Now().Add((7*24)*time.Hour).Format("2006-01-02")) assert.NoError(t, err) err = pat.Encrypt() diff --git a/models/user.go b/models/user.go index 3439f86..d52f5e5 100644 --- a/models/user.go +++ b/models/user.go @@ -1,6 +1,7 @@ package models import ( + "errors" "slices" "time" @@ -8,7 +9,7 @@ import ( "github.com/alexferl/echo-boilerplate/util/password" ) -type Role int +type Role int8 const ( UserRole Role = iota + 1 @@ -20,6 +21,40 @@ func (r Role) String() string { return [...]string{"user", "admin", "super"}[r-1] } +var RolesMap = map[string]Role{ + "user": UserRole, + "admin": AdminRole, + "super": SuperRole, +} + +var ( + ErrAdminRoleRequired = errors.New("admin or greater role required") + + ErrBanSelf = errors.New("cannot ban self") + ErrBanExist = errors.New("user already banned") + ErrBanMorePrivileged = errors.New("cannot ban user with higher permissions") + + ErrUnbanSelf = errors.New("cannot unban self") + ErrUnbanExist = errors.New("user already unbanned") + ErrUnbanMorePrivileged = errors.New("cannot unban user with higher permissions") + + ErrLockSelf = errors.New("cannot lock self") + ErrLockExist = errors.New("user already locked") + ErrLockMorePrivileged = errors.New("cannot lock user with higher permissions") + + ErrUnlockSelf = errors.New("cannot unlock self") + ErrUnlockExist = errors.New("user already unlocked") + ErrUnlockMorePrivileged = errors.New("cannot unlock user with higher permissions") + + ErrRoleSelf = errors.New("cannot modify own roles") + + ErrRoleAddExist = errors.New("user already has role") + ErrRoleAddMorePrivileged = errors.New("cannot add a more privileged role") + + ErrRoleRemoveNotExist = errors.New("user doesn't have role") + ErrRoleRemoveMorePrivileged = errors.New("cannot remove a more privileged role") +) + type User struct { *Model `bson:",inline"` BannedAt *time.Time `json:"banned_at" bson:"banned_at"` @@ -125,7 +160,7 @@ func NewUser(email string, username string) *User { func NewUserWithRole(email string, username string, role Role) *User { user := NewUser(email, username) - user.AddRole(role) + user.addRole(role) return user } @@ -176,46 +211,210 @@ func (u *User) ValidatePassword(s string) error { return password.Verify([]byte(u.Password), []byte(s)) } -func (u *User) AddRole(role Role) { +func (u *User) HasRoleOrHigher(role Role) bool { + if slices.Max(stringSliceToRolesSlice(u.Roles)) >= role { + return true + } + return false +} + +func stringSliceToRolesSlice(roles []string) []Role { + var rs []Role + for _, r := range roles { + rs = append(rs, RolesMap[r]) + } + return rs +} + +// compare checks if u has a higher role than user +// and returns true if it does. +// It also returns true if both highest roles equals the SuperRole +// as users with the SuperRole are allowed to interact (ban, lock etc.) +// with each others. +func (u *User) compare(user *User) bool { + roles := stringSliceToRolesSlice(u.Roles) + otherRoles := stringSliceToRolesSlice(user.Roles) + highestRole := slices.Max(roles) + otherHighestRole := slices.Max(otherRoles) + + if highestRole == SuperRole && otherHighestRole == SuperRole { + return false + } + + if highestRole >= otherHighestRole { + return true + } + + return false +} + +// hasRoleOrHigher check if user as at least role and returns true if it does. +func hasRoleOrHigher(user *User, role Role) bool { + if slices.Max(stringSliceToRolesSlice(user.Roles)) >= role { + return true + } + return false +} + +func (u *User) addRole(role Role) { + u.Roles = append(u.Roles, role.String()) +} + +func (u *User) AddRole(user *User, role Role) error { + if slices.Max(stringSliceToRolesSlice(user.Roles)) < AdminRole { + return NewError(ErrAdminRoleRequired, Permission) + } + + if user.Id == u.Id { + return NewError(ErrRoleSelf, Conflict) + } + + if !hasRoleOrHigher(user, role) { + return NewError(ErrRoleAddMorePrivileged, Permission) + } + + if slices.Contains(u.Roles, role.String()) { + return NewError(ErrRoleAddExist, Conflict) + } + + u.addRole(role) + + return nil +} + +func (u *User) removeRole(role Role) { + idx := slices.Index(u.Roles, role.String()) + u.Roles = slices.Delete(u.Roles, idx, idx+1) +} + +func (u *User) RemoveRole(user *User, role Role) error { + if slices.Max(stringSliceToRolesSlice(user.Roles)) < AdminRole { + return NewError(ErrAdminRoleRequired, Permission) + } + + if user.Id == u.Id { + return NewError(ErrRoleSelf, Conflict) + } + + if !hasRoleOrHigher(user, role) { + return NewError(ErrRoleRemoveMorePrivileged, Permission) + } + if !slices.Contains(u.Roles, role.String()) { - u.Roles = append(u.Roles, role.String()) + return NewError(ErrRoleRemoveNotExist, Conflict) } + + u.removeRole(role) + + return nil } -func (u *User) Ban(id string) { +func (u *User) Ban(user *User) error { + if slices.Max(stringSliceToRolesSlice(user.Roles)) < AdminRole { + return NewError(ErrAdminRoleRequired, Permission) + } + + if user.Id == u.Id { + return NewError(ErrBanSelf, Conflict) + } + + if u.compare(user) { + return NewError(ErrBanMorePrivileged, Permission) + } + + if u.IsBanned { + return NewError(ErrBanExist, Conflict) + } + u.IsBanned = true t := time.Now() u.BannedAt = &t - u.BannedBy = &Ref{Id: id} + u.BannedBy = &Ref{Id: user.Id} u.UnbannedAt = nil u.UnbannedBy = nil + + return nil } -func (u *User) Unban(id string) { +func (u *User) Unban(user *User) error { + if slices.Max(stringSliceToRolesSlice(user.Roles)) < AdminRole { + return NewError(ErrAdminRoleRequired, Permission) + } + + if user.Id == u.Id { + return NewError(ErrUnbanSelf, Conflict) + } + + if u.compare(user) { + return NewError(ErrUnbanMorePrivileged, Permission) + } + + if !u.IsBanned { + return NewError(ErrUnbanExist, Conflict) + } + u.IsBanned = false t := time.Now() u.BannedAt = nil u.BannedBy = nil u.UnbannedAt = &t - u.UnbannedBy = &Ref{Id: id} + u.UnbannedBy = &Ref{Id: user.Id} + + return nil } -func (u *User) Lock(id string) { +func (u *User) Lock(user *User) error { + if slices.Max(stringSliceToRolesSlice(user.Roles)) < AdminRole { + return NewError(ErrAdminRoleRequired, Permission) + } + + if user.Id == u.Id { + return NewError(ErrLockSelf, Conflict) + } + + if u.compare(user) { + return NewError(ErrLockMorePrivileged, Permission) + } + + if u.IsLocked { + return NewError(ErrLockExist, Conflict) + } + u.IsLocked = true t := time.Now() u.LockedAt = &t - u.LockedBy = &Ref{Id: id} + u.LockedBy = &Ref{Id: user.Id} u.UnlockedAt = nil u.UnlockedBy = nil + + return nil } -func (u *User) Unlock(id string) { +func (u *User) Unlock(user *User) error { + if slices.Max(stringSliceToRolesSlice(user.Roles)) < AdminRole { + return NewError(ErrAdminRoleRequired, Permission) + } + + if user.Id == u.Id { + return NewError(ErrUnlockSelf, Conflict) + } + + if u.compare(user) { + return NewError(ErrUnlockMorePrivileged, Permission) + } + + if !u.IsLocked { + return NewError(ErrUnlockExist, Conflict) + } + u.IsLocked = false t := time.Now() u.LockedAt = nil u.LockedBy = nil u.UnlockedAt = &t - u.UnlockedBy = &Ref{Id: id} + u.UnlockedBy = &Ref{Id: user.Id} + + return nil } func (u *User) Login() ([]byte, []byte, error) { @@ -263,12 +462,7 @@ func (u *User) ValidateRefreshToken(s string) error { } func (u *User) getTokens() ([]byte, []byte, error) { - claims := map[string]any{ - "roles": u.Roles, - "is_banned": u.IsBanned, - "is_locked": u.IsLocked, - } - access, refresh, err := jwt.GenerateTokens(u.Id, claims) + access, refresh, err := jwt.GenerateTokens(u.Id, nil) if err != nil { return nil, nil, err } diff --git a/models/user_test.go b/models/user_test.go index 9b4cb14..5a07d85 100644 --- a/models/user_test.go +++ b/models/user_test.go @@ -1,6 +1,7 @@ package models import ( + "errors" "slices" "testing" @@ -9,12 +10,12 @@ import ( func TestUser(t *testing.T) { email := "test@example.com" - username := "text" + username := "test" id := "1" user := NewUserWithRole(email, username, AdminRole) assert.NotEqual(t, "", user.Id) - user.AddRole(SuperRole) + user.addRole(SuperRole) assert.True(t, slices.Contains(user.Roles, SuperRole.String())) pwd := "abcdefghijlk" @@ -36,26 +37,6 @@ func TestUser(t *testing.T) { assert.Equal(t, id, user.DeletedBy.(*Ref).Id) assert.NotNil(t, user.DeletedAt) - user.Ban(id) - assert.Equal(t, id, user.BannedBy.(*Ref).Id) - assert.NotNil(t, user.BannedAt) - - user.Unban(id) - assert.Equal(t, id, user.UnbannedBy.(*Ref).Id) - assert.NotNil(t, user.UnbannedAt) - assert.Nil(t, user.BannedBy) - assert.Nil(t, user.BannedAt) - - user.Lock(id) - assert.Equal(t, id, user.LockedBy.(*Ref).Id) - assert.NotNil(t, user.LockedAt) - - user.Unlock(id) - assert.Equal(t, id, user.UnlockedBy.(*Ref).Id) - assert.NotNil(t, user.UnlockedAt) - assert.Nil(t, user.LockedBy) - assert.Nil(t, user.LockedAt) - access, refresh, err := user.Login() assert.NoError(t, err) assert.NotEqual(t, "", string(access)) @@ -75,3 +56,261 @@ func TestUser(t *testing.T) { assert.Equal(t, "", user.RefreshToken) assert.NotNil(t, user.LastLogoutAt) } + +func TestBan(t *testing.T) { + user := NewUser("test@example.com", "test") + bannedUser := NewUser("banned@example.com", "banned") + bannedUser.IsBanned = true + admin := NewUserWithRole("admin@example.com", "admin", AdminRole) + admin1 := NewUserWithRole("admin1@example.com", "admin1", AdminRole) + super := NewUserWithRole("super@example.com", "super", SuperRole) + super1 := NewUserWithRole("super1@example.com", "super1", SuperRole) + + testCases := []struct { + name string + user *User + target *User + err error + kind Kind + }{ + {"need admin or higher role", user, user, ErrAdminRoleRequired, Permission}, + {"cannot ban self", admin, admin, ErrBanSelf, Conflict}, + {"target already banned", admin, bannedUser, ErrBanExist, Conflict}, + {"target cannot be more privileged", admin, super, ErrBanMorePrivileged, Permission}, + {"admin cannot ban admin", admin, admin1, ErrBanMorePrivileged, Permission}, + {"super can ban super", super, super1, nil, 0}, + {"success", admin, user, nil, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.target.Ban(tc.user) + if tc.err != nil { + assert.Error(t, err) + var e *Error + assert.ErrorAs(t, err, &e) + if errors.As(err, &e) { + assert.Equal(t, tc.err.Error(), e.Message) + assert.Equal(t, tc.kind, e.Kind) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, tc.target.BannedAt) + assert.Equal(t, tc.user.Id, tc.target.BannedBy.(*Ref).Id) + } + }) + } +} + +func TestUnban(t *testing.T) { + user := NewUser("test@example.com", "test") + user.IsBanned = true + unbannedUser := NewUser("unbanned@example.com", "unbanned") + admin := NewUserWithRole("admin@example.com", "admin", AdminRole) + admin1 := NewUserWithRole("admin1@example.com", "admin1", AdminRole) + super := NewUserWithRole("super@example.com", "super", SuperRole) + super1 := NewUserWithRole("super1@example.com", "super1", SuperRole) + super1.IsBanned = true + + testCases := []struct { + name string + user *User + target *User + err error + kind Kind + }{ + {"need admin or higher role", user, user, ErrAdminRoleRequired, Permission}, + {"cannot unban self", admin, admin, ErrUnbanSelf, Conflict}, + {"target already unbanned", admin, unbannedUser, ErrUnbanExist, Conflict}, + {"target cannot be more privileged", admin, super, ErrUnbanMorePrivileged, Permission}, + {"admin cannot unban admin", admin, admin1, ErrUnbanMorePrivileged, Permission}, + {"super can unban super", super, super1, nil, 0}, + {"success", admin, user, nil, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.target.Unban(tc.user) + if tc.err != nil { + assert.Error(t, err) + var e *Error + assert.ErrorAs(t, err, &e) + if errors.As(err, &e) { + assert.Equal(t, tc.err.Error(), e.Message) + assert.Equal(t, tc.kind, e.Kind) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, tc.target.UnbannedAt) + assert.Equal(t, tc.user.Id, tc.target.UnbannedBy.(*Ref).Id) + } + }) + } +} + +func TestLock(t *testing.T) { + user := NewUser("test@example.com", "test") + lockedUser := NewUser("locked@example.com", "locked") + lockedUser.IsLocked = true + admin := NewUserWithRole("admin@example.com", "admin", AdminRole) + admin1 := NewUserWithRole("admin1@example.com", "admin1", AdminRole) + super := NewUserWithRole("super@example.com", "super", SuperRole) + super1 := NewUserWithRole("super1@example.com", "super1", SuperRole) + + testCases := []struct { + name string + user *User + target *User + err error + kind Kind + }{ + {"need admin or higher role", user, user, ErrAdminRoleRequired, Permission}, + {"cannot lock self", admin, admin, ErrLockSelf, Conflict}, + {"target already locked", admin, lockedUser, ErrLockExist, Conflict}, + {"target cannot be more privileged", admin, super, ErrLockMorePrivileged, Permission}, + {"admin cannot lock admin", admin, admin1, ErrLockMorePrivileged, Permission}, + {"super can lock super", super, super1, nil, 0}, + {"success", admin, user, nil, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.target.Lock(tc.user) + if tc.err != nil { + assert.Error(t, err) + var e *Error + assert.ErrorAs(t, err, &e) + if errors.As(err, &e) { + assert.Equal(t, tc.err.Error(), e.Message) + assert.Equal(t, tc.kind, e.Kind) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, tc.target.LockedAt) + assert.Equal(t, tc.user.Id, tc.target.LockedBy.(*Ref).Id) + } + }) + } +} + +func TestUnlock(t *testing.T) { + user := NewUser("test@example.com", "test") + user.IsLocked = true + unlockedUser := NewUser("unlocked@example.com", "unlocked") + admin := NewUserWithRole("admin@example.com", "admin", AdminRole) + admin1 := NewUserWithRole("admin1@example.com", "admin1", AdminRole) + super := NewUserWithRole("super@example.com", "super", SuperRole) + super1 := NewUserWithRole("super1@example.com", "super1", SuperRole) + super1.IsLocked = true + + testCases := []struct { + name string + user *User + target *User + err error + kind Kind + }{ + {"need admin or higher role", user, user, ErrAdminRoleRequired, Permission}, + {"cannot unlock self", admin, admin, ErrUnlockSelf, Conflict}, + {"target already unlocked", admin, unlockedUser, ErrUnlockExist, Conflict}, + {"target cannot be more privileged", admin, super, ErrUnlockMorePrivileged, Permission}, + {"admin cannot unlock admin", admin, admin1, ErrUnlockMorePrivileged, Permission}, + {"super can unlock super", super, super1, nil, 0}, + {"success", admin, user, nil, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.target.Unlock(tc.user) + if tc.err != nil { + assert.Error(t, err) + var e *Error + assert.ErrorAs(t, err, &e) + if errors.As(err, &e) { + assert.Equal(t, tc.err.Error(), e.Message) + assert.Equal(t, tc.kind, e.Kind) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, tc.target.UnlockedAt) + assert.Equal(t, tc.user.Id, tc.target.UnlockedBy.(*Ref).Id) + } + }) + } +} + +func TestAddRole(t *testing.T) { + user := NewUser("test@example.com", "test") + admin := NewUserWithRole("admin@example.com", "admin", AdminRole) + + testCases := []struct { + name string + user *User + target *User + role Role + err error + kind Kind + }{ + {"need admin or higher role", user, user, UserRole, ErrAdminRoleRequired, Permission}, + {"cannot modify self", admin, admin, AdminRole, ErrRoleSelf, Conflict}, + {"cannot add more privileged role", admin, user, SuperRole, ErrRoleAddMorePrivileged, Permission}, + {"cannot add existing role", admin, user, UserRole, ErrRoleAddExist, Conflict}, + {"success", admin, user, AdminRole, nil, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.target.AddRole(tc.user, tc.role) + if tc.err != nil { + assert.Error(t, err) + var e *Error + assert.ErrorAs(t, err, &e) + if errors.As(err, &e) { + assert.Equal(t, tc.err.Error(), e.Message) + assert.Equal(t, tc.kind, e.Kind) + } + } else { + assert.NoError(t, err) + assert.Contains(t, tc.target.Roles, tc.role.String()) + } + }) + } +} + +func TestRemoveRole(t *testing.T) { + user := NewUser("test@example.com", "test") + admin := NewUserWithRole("admin@example.com", "admin", AdminRole) + + testCases := []struct { + name string + user *User + target *User + role Role + err error + kind Kind + }{ + {"need admin or higher role", user, user, UserRole, ErrAdminRoleRequired, Permission}, + {"cannot modify self", admin, admin, AdminRole, ErrRoleSelf, Conflict}, + {"cannot remove more privileged role", admin, user, SuperRole, ErrRoleRemoveMorePrivileged, Permission}, + {"cannot remove non existing role", admin, user, AdminRole, ErrRoleRemoveNotExist, Conflict}, + {"success", admin, user, UserRole, nil, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.target.RemoveRole(tc.user, tc.role) + if tc.err != nil { + assert.Error(t, err) + var e *Error + assert.ErrorAs(t, err, &e) + if errors.As(err, &e) { + assert.Equal(t, tc.err.Error(), e.Message) + assert.Equal(t, tc.kind, e.Kind) + } + } else { + assert.NoError(t, err) + assert.NotContains(t, tc.target.Roles, tc.role.String()) + } + }) + } +} diff --git a/openapi/components/schemas/auth/Token.yaml b/openapi/components/schemas/auth/Token.yaml index aff067b..422f523 100644 --- a/openapi/components/schemas/auth/Token.yaml +++ b/openapi/components/schemas/auth/Token.yaml @@ -6,11 +6,8 @@ required: - iat - iss - nbf - - roles - sub - type - - is_banned - - is_locked properties: exp: type: string @@ -28,26 +25,11 @@ properties: type: string description: Token not before date example: '2024-01-23T19:35:48Z' - roles: - type: array - items: - type: string - minItems: 1 - description: Token roles - example: ['user'] sub: type: string description: Token subject example: cmfh5i4i016kiqso7gr0 type: type: string - description: Token type + description: Token kind example: access - is_banned: - type: boolean - description: True if account is banned - example: true - is_locked: - type: boolean - description: True if account is locked - example: false diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index cfa22e5..a79aceb 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -53,10 +53,14 @@ paths: $ref: './paths/tasks/{id}_transition.yaml' /users: $ref: './paths/users/users.yaml' - /users/{id}: - $ref: './paths/users/{id}.yaml' - /users/{id}/status: - $ref: './paths/users/{id}_status.yaml' + /users/{id_or_username}: + $ref: './paths/users/{id_or_username}.yaml' + /users/{id_or_username}/ban: + $ref: './paths/users/{id_or_username}_ban.yaml' + /users/{id_or_username}/lock: + $ref: './paths/users/{id_or_username}_lock.yaml' + /users/{id_or_username}/roles/{role}: + $ref: './paths/users/{id_or_username}_roles_{role}.yaml' components: securitySchemes: cookieAuth: diff --git a/openapi/paths/tasks/{id}.yaml b/openapi/paths/tasks/{id}.yaml index 15c8f17..53fdf2e 100644 --- a/openapi/paths/tasks/{id}.yaml +++ b/openapi/paths/tasks/{id}.yaml @@ -24,7 +24,7 @@ get: $ref: '../../components/responses/Unauthorized.yaml' '410': $ref: '../../components/responses/Gone.yaml' -put: +patch: summary: Update a task description: Returns the updated task. operationId: updateTask diff --git a/openapi/paths/users/me.yaml b/openapi/paths/users/me.yaml index 96205b7..a0cced5 100644 --- a/openapi/paths/users/me.yaml +++ b/openapi/paths/users/me.yaml @@ -16,7 +16,7 @@ get: $ref: '../../components/schemas/users/me/CurrentUser.yaml' '401': $ref: '../../components/responses/Unauthorized.yaml' -put: +patch: summary: Update current user description: Returns the updated current user. operationId: updateCurrentUser diff --git a/openapi/paths/users/{id}.yaml b/openapi/paths/users/{id_or_username}.yaml similarity index 95% rename from openapi/paths/users/{id}.yaml rename to openapi/paths/users/{id_or_username}.yaml index 3556fda..ee6032f 100644 --- a/openapi/paths/users/{id}.yaml +++ b/openapi/paths/users/{id_or_username}.yaml @@ -8,7 +8,7 @@ get: tags: - users parameters: - - name: id + - name: id_or_username in: path required: true schema: @@ -24,7 +24,7 @@ get: $ref: '../../components/responses/Unauthorized.yaml' '410': $ref: '../../components/responses/Gone.yaml' -put: +patch: summary: Update user description: Returns the updated user. operationId: updateUser @@ -34,7 +34,7 @@ put: tags: - users parameters: - - name: id + - name: id_or_username in: path required: true schema: diff --git a/openapi/paths/users/{id_or_username}_ban.yaml b/openapi/paths/users/{id_or_username}_ban.yaml new file mode 100644 index 0000000..c1d5d14 --- /dev/null +++ b/openapi/paths/users/{id_or_username}_ban.yaml @@ -0,0 +1,50 @@ +put: + summary: Ban a user + description: Bans a user. + operationId: banUser + security: + - cookieAuth: [] + - bearerAuth: [] + tags: + - users + parameters: + - name: id_or_username + in: path + required: true + schema: + type: string + responses: + '204': + description: Successfully banned user + '401': + $ref: '../../components/responses/Unauthorized.yaml' + '403': + $ref: '../../components/responses/Forbidden.yaml' + '409': + $ref: '../../components/responses/Conflict.yaml' + '410': + $ref: '../../components/responses/Gone.yaml' +delete: + summary: Unban a user + description: Unbans a user. + operationId: unbanUser + security: + - cookieAuth: [] + - bearerAuth: [] + tags: + - users + parameters: + - name: id_or_username + in: path + required: true + schema: + type: string + responses: + '204': + description: Successfully unbanned user + '401': + $ref: '../../components/responses/Unauthorized.yaml' + '403': + $ref: '../../components/responses/Forbidden.yaml' + '410': + $ref: '../../components/responses/Gone.yaml' diff --git a/openapi/paths/users/{id_or_username}_lock.yaml b/openapi/paths/users/{id_or_username}_lock.yaml new file mode 100644 index 0000000..44c3578 --- /dev/null +++ b/openapi/paths/users/{id_or_username}_lock.yaml @@ -0,0 +1,50 @@ +put: + summary: Lock a user + description: Locks a user. + operationId: lockUser + security: + - cookieAuth: [] + - bearerAuth: [] + tags: + - users + parameters: + - name: id_or_username + in: path + required: true + schema: + type: string + responses: + '204': + description: Successfully locked user + '401': + $ref: '../../components/responses/Unauthorized.yaml' + '403': + $ref: '../../components/responses/Forbidden.yaml' + '409': + $ref: '../../components/responses/Conflict.yaml' + '410': + $ref: '../../components/responses/Gone.yaml' +delete: + summary: Unlock a user + description: Unlocks a user. + operationId: unlockUser + security: + - cookieAuth: [] + - bearerAuth: [] + tags: + - users + parameters: + - name: id_or_username + in: path + required: true + schema: + type: string + responses: + '204': + description: Successfully unlocked user + '401': + $ref: '../../components/responses/Unauthorized.yaml' + '403': + $ref: '../../components/responses/Forbidden.yaml' + '410': + $ref: '../../components/responses/Gone.yaml' diff --git a/openapi/paths/users/{id_or_username}_roles_{role}.yaml b/openapi/paths/users/{id_or_username}_roles_{role}.yaml new file mode 100644 index 0000000..6a6f00a --- /dev/null +++ b/openapi/paths/users/{id_or_username}_roles_{role}.yaml @@ -0,0 +1,64 @@ +put: + summary: Add user role + description: Adds a role to the user. + operationId: addRole + security: + - cookieAuth: [] + - bearerAuth: [] + tags: + - users + parameters: + - name: id_or_username + in: path + required: true + schema: + type: string + - name: role + in: path + required: true + schema: + type: string + enum: ['user', 'admin', 'super'] + responses: + '204': + description: Successfully added role + '401': + $ref: '../../components/responses/Unauthorized.yaml' + '403': + $ref: '../../components/responses/Forbidden.yaml' + '409': + $ref: '../../components/responses/Conflict.yaml' + '410': + $ref: '../../components/responses/Gone.yaml' + '422': + $ref: '../../components/responses/UnprocessableEntity.yaml' +delete: + summary: Remove user role + description: Removes a role from the user. + operationId: removeRole + security: + - cookieAuth: [] + - bearerAuth: [] + tags: + - users + parameters: + - name: id_or_username + in: path + required: true + schema: + type: string + - name: role + in: path + required: true + schema: + type: string + enum: ['user', 'admin', 'super'] + responses: + '204': + description: Successfully removed role + '401': + $ref: '../../components/responses/Unauthorized.yaml' + '403': + $ref: '../../components/responses/Forbidden.yaml' + '410': + $ref: '../../components/responses/Gone.yaml' diff --git a/openapi/paths/users/{id}_status.yaml b/openapi/paths/users/{id_or_username}_status.yaml similarity index 97% rename from openapi/paths/users/{id}_status.yaml rename to openapi/paths/users/{id_or_username}_status.yaml index 8cf7999..5163879 100644 --- a/openapi/paths/users/{id}_status.yaml +++ b/openapi/paths/users/{id_or_username}_status.yaml @@ -8,7 +8,7 @@ put: tags: - users parameters: - - name: id + - name: id_or_username in: path required: true schema: diff --git a/server/server.go b/server/server.go index fab225d..cb7a5d0 100644 --- a/server/server.go +++ b/server/server.go @@ -9,21 +9,18 @@ import ( casbinMw "github.com/alexferl/echo-casbin" jwtMw "github.com/alexferl/echo-jwt" openapiMw "github.com/alexferl/echo-openapi" - "github.com/alexferl/golib/database/mongodb" "github.com/alexferl/golib/http/api/server" "github.com/casbin/casbin/v2" "github.com/labstack/echo/v4" jwx "github.com/lestrrat-go/jwx/v2/jwt" "github.com/rs/zerolog/log" "github.com/spf13/viper" - "go.mongodb.org/mongo-driver/bson" _ "go.uber.org/automaxprocs" "github.com/alexferl/echo-boilerplate/config" "github.com/alexferl/echo-boilerplate/data" "github.com/alexferl/echo-boilerplate/handlers" "github.com/alexferl/echo-boilerplate/mappers" - "github.com/alexferl/echo-boilerplate/models" "github.com/alexferl/echo-boilerplate/services" "github.com/alexferl/echo-boilerplate/util/hash" "github.com/alexferl/echo-boilerplate/util/jwt" @@ -46,7 +43,7 @@ func New() *server.Server { userMapper := mappers.NewUser(client) userSvc := services.NewUser(userMapper) - return newServer([]handlers.Handler{ + return newServer(userSvc, patSvc, []handlers.Handler{ handlers.NewRootHandler(openapi), handlers.NewAuthHandler(openapi, userSvc), handlers.NewPersonalAccessTokenHandler(openapi, patSvc), @@ -55,28 +52,22 @@ func New() *server.Server { }...) } -func NewTestServer(handler ...handlers.Handler) *server.Server { +func NewTestServer(userSvc handlers.UserService, patSvc handlers.PersonalAccessTokenService, handler ...handlers.Handler) *server.Server { c := config.New() c.BindFlags() viper.Set(config.CookiesEnabled, true) viper.Set(config.CSRFEnabled, true) - return newServer(handler...) + return newServer(userSvc, patSvc, handler...) } -func newServer(handler ...handlers.Handler) *server.Server { +func newServer(userSvc handlers.UserService, patSvc handlers.PersonalAccessTokenService, handler ...handlers.Handler) *server.Server { key, err := jwt.LoadPrivateKey() if err != nil { log.Panic().Err(err).Msg("failed loading private key") } - client, err := mongodb.New() - if err != nil { - log.Panic().Err(err).Msg("failed creating mongo client") - } - mapper := data.NewMapper(client, viper.GetString(config.AppName), "personal_access_tokens") - jwtConfig := jwtMw.Config{ Key: key, UseRefreshToken: true, @@ -93,21 +84,24 @@ func newServer(handler ...handlers.Handler) *server.Server { "/oauth2/google/login": {http.MethodGet}, }, AfterParseFunc: func(c echo.Context, t jwx.Token, encodedToken string, src jwtMw.TokenSource) *echo.HTTPError { + ctx, cancel := context.WithTimeout(c.Request().Context(), 10*time.Second) + defer cancel() + + user, err := userSvc.Read(ctx, t.Subject()) + if err != nil { + log.Error().Err(err).Msg("failed getting user") + return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error") + } + + c.Set("user", user) // set roles for casbin - claims := t.PrivateClaims() - c.Set("roles", claims["roles"]) - isBanned := claims["is_banned"] - isLocked := claims["is_locked"] + c.Set("roles", user.Roles) - if val, ok := isBanned.(bool); ok { - if val { - return echo.NewHTTPError(http.StatusForbidden, "account banned") - } + if user.IsBanned { + return echo.NewHTTPError(http.StatusForbidden, "account banned") } - if val, ok := isLocked.(bool); ok { - if val { - return echo.NewHTTPError(http.StatusForbidden, "account locked") - } + if user.IsLocked { + return echo.NewHTTPError(http.StatusForbidden, "account locked") } // CSRF @@ -134,21 +128,20 @@ func newServer(handler ...handlers.Handler) *server.Server { } // Personal Access Tokens + claims := t.PrivateClaims() typ := claims["type"] if typ == jwt.PersonalToken.String() { - ctx, cancel := context.WithTimeout(c.Request().Context(), 10*time.Second) - defer cancel() - - filter := bson.D{{"user_id", t.Subject()}} - result, err := mapper.FindOne(ctx, filter, &models.PersonalAccessToken{}) + pat, err := patSvc.FindOne(ctx, t.Subject(), "") if err != nil { - if errors.Is(err, data.ErrNoDocuments) { - return echo.NewHTTPError(http.StatusUnauthorized, "token invalid") + var se *services.Error + if errors.As(err, &se) { + if se.Kind == services.NotExist { + return echo.NewHTTPError(http.StatusUnauthorized, "token invalid") + } } return echo.NewHTTPError(http.StatusInternalServerError, "Internal Server Error") } - pat := result.(*models.PersonalAccessToken) if err = pat.Validate(encodedToken); err != nil { return echo.NewHTTPError(http.StatusUnauthorized, "token mismatch") } diff --git a/services/error.go b/services/error.go index f8b5d04..f86c64a 100644 --- a/services/error.go +++ b/services/error.go @@ -17,10 +17,12 @@ const ( Exist // Item already exist. NotExist // Item does not exist. Deleted // Item was deleted. + Conflict + Permission ) func (k Kind) String() string { - return [...]string{"other", "exist", "not_exist", "deleted"}[k-1] + return [...]string{"other", "exist", "not_exist", "deleted", "conflict", "permission"}[k-1] } // NewError instantiates a new error. diff --git a/services/personal_access_token_test.go b/services/personal_access_token_test.go index b8937f2..a9551be 100644 --- a/services/personal_access_token_test.go +++ b/services/personal_access_token_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - jwx "github.com/lestrrat-go/jwx/v2/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" @@ -14,23 +13,21 @@ import ( "github.com/alexferl/echo-boilerplate/data" "github.com/alexferl/echo-boilerplate/models" "github.com/alexferl/echo-boilerplate/services" - "github.com/alexferl/echo-boilerplate/util/jwt" ) type PersonalAccessTokenTestSuite struct { suite.Suite mapper *services.MockPersonalAccessTokenMapper svc *services.PersonalAccessToken - token jwx.Token + user *models.User } func (s *PersonalAccessTokenTestSuite) SetupTest() { s.mapper = services.NewMockPersonalAccessTokenMapper(s.T()) s.svc = services.NewPersonalAccessToken(s.mapper) user := models.NewUser("test@email.com", "test") - access, _, _ := user.Login() - token, _ := jwt.ParseEncoded(access) - s.token = token + user.Id = "100" + s.user = user } func TestPersonalAccessToken(t *testing.T) { @@ -40,7 +37,7 @@ func TestPersonalAccessToken(t *testing.T) { func (s *PersonalAccessTokenTestSuite) TestPersonalAccessToken_Create() { name := "my_token" expiresAt := time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02") - m, err := models.NewPersonalAccessToken(s.token, name, expiresAt) + m, err := models.NewPersonalAccessToken(s.user.Id, name, expiresAt) assert.NoError(s.T(), err) s.mapper.EXPECT(). @@ -56,7 +53,7 @@ func (s *PersonalAccessTokenTestSuite) TestPersonalAccessToken_Create() { func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_Read() { name := "my_token" expiresAt := time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02") - m, err := models.NewPersonalAccessToken(s.token, name, expiresAt) + m, err := models.NewPersonalAccessToken(s.user.Id, name, expiresAt) assert.NoError(s.T(), err) id := "123" m.Id = id @@ -75,7 +72,7 @@ func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_Read() { func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_Read_Err() { name := "my_token" expiresAt := time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02") - m, err := models.NewPersonalAccessToken(s.token, name, expiresAt) + m, err := models.NewPersonalAccessToken(s.user.Id, name, expiresAt) assert.NoError(s.T(), err) id := "123" m.Id = id @@ -96,7 +93,7 @@ func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_Read_Err func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_Revoke() { name := "my_token" expiresAt := time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02") - m, err := models.NewPersonalAccessToken(s.token, name, expiresAt) + m, err := models.NewPersonalAccessToken(s.user.Id, name, expiresAt) assert.NoError(s.T(), err) id := "123" m.Id = id @@ -133,7 +130,7 @@ func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_Find() { func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_FindOne() { name := "my_token" expiresAt := time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02") - m, err := models.NewPersonalAccessToken(s.token, name, expiresAt) + m, err := models.NewPersonalAccessToken(s.user.Id, name, expiresAt) assert.NoError(s.T(), err) id := "123" userId := "456" @@ -155,7 +152,7 @@ func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_FindOne( func (s *PersonalAccessTokenTestSuite) TestPersonalAccessTokenTestSuite_FindOne_Err() { name := "my_token" expiresAt := time.Now().Add((7 * 24) * time.Hour).Format("2006-01-02") - m, err := models.NewPersonalAccessToken(s.token, name, expiresAt) + m, err := models.NewPersonalAccessToken(s.user.Id, name, expiresAt) assert.NoError(s.T(), err) id := "123" userId := "456" diff --git a/services/user.go b/services/user.go index 2a93512..bbe8f92 100644 --- a/services/user.go +++ b/services/user.go @@ -48,7 +48,10 @@ func (u *User) Create(ctx context.Context, model *models.User) (*models.User, er } func (u *User) Read(ctx context.Context, id string) (*models.User, error) { - filter := bson.D{{"id", id}} + filter := bson.D{{"$or", bson.A{ + bson.D{{"id", id}}, + bson.D{{"username", id}}, + }}} user, err := u.mapper.FindOne(ctx, filter) if err != nil { if errors.Is(err, data.ErrNoDocuments) { diff --git a/util/jwt/jwt.go b/util/jwt/jwt.go index af2ce21..8ea409e 100644 --- a/util/jwt/jwt.go +++ b/util/jwt/jwt.go @@ -7,26 +7,24 @@ import ( "fmt" "io" "os" - "slices" "time" "github.com/lestrrat-go/jwx/v2/jwa" jwx "github.com/lestrrat-go/jwx/v2/jwt" - "github.com/rs/zerolog/log" "github.com/spf13/viper" "github.com/alexferl/echo-boilerplate/config" ) -type TokenType int64 +type Type int8 const ( - AccessToken TokenType = iota + 1 + AccessToken Type = iota + 1 RefreshToken PersonalToken ) -func (t TokenType) String() string { +func (t Type) String() string { return [...]string{"access", "refresh", "personal"}[t-1] } @@ -58,7 +56,7 @@ func GeneratePersonalToken(sub string, expiry time.Duration, claims map[string]a return generateToken(PersonalToken, expiry, sub, claims) } -func generateToken(typ TokenType, expiry time.Duration, sub string, claims map[string]any) ([]byte, error) { +func generateToken(typ Type, expiry time.Duration, sub string, claims map[string]any) ([]byte, error) { key, err := LoadPrivateKey() if err != nil { return nil, err @@ -72,8 +70,10 @@ func generateToken(typ TokenType, expiry time.Duration, sub string, claims map[s Expiration(time.Now().Add(expiry)). Claim("type", typ.String()) - for k, v := range claims { - builder.Claim(k, v) + if claims != nil { + for k, v := range claims { + builder.Claim(k, v) + } } token, err := builder.Build() @@ -103,39 +103,6 @@ func ParseEncoded(encodedToken []byte) (jwx.Token, error) { return token, nil } -func GetRoles(token jwx.Token) []string { - val, _ := token.Get("roles") - roles := val.([]interface{}) - - var res []string - for _, role := range roles { - res = append(res, role.(string)) - } - - return res -} - -func HasRoles(token jwx.Token, roles ...string) bool { - if val, ok := token.Get("roles"); !ok { - log.Error().Msgf("failed getting roles for token: %s", token.Subject()) - return false - } else { - jwtRoles, ok := val.([]interface{}) - if !ok { - log.Error().Msgf("failed converting roles to slice for token: %s", token.Subject()) - return false - } - - for _, r := range jwtRoles { - if slices.Contains(roles, r.(string)) { - return true - } - } - } - - return false -} - func LoadPrivateKey() (*rsa.PrivateKey, error) { f, err := os.Open(viper.GetString(config.JWTPrivateKey)) if err != nil { diff --git a/util/jwt/jwt_test.go b/util/jwt/jwt_test.go index 8755e1a..084baa4 100644 --- a/util/jwt/jwt_test.go +++ b/util/jwt/jwt_test.go @@ -39,58 +39,3 @@ func TestParseEncoded(t *testing.T) { _, ok = refreshToken.Get("claim") assert.False(t, ok) } - -func TestHasRoles(t *testing.T) { - c := config.New() - c.BindFlags() - - testCases := []struct { - name string - claims map[string]any - role string - hasRole bool - }{ - {"has role", map[string]any{"roles": []string{"user"}}, "user", true}, - {"invalid role", map[string]any{"roles": []string{"user"}}, "invalid", false}, - {"invalid roles key", map[string]any{"invalid": []string{"user"}}, "invalid", false}, - {"roles key not slice", map[string]any{"roles": "user"}, "user", false}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - access, _, err := GenerateTokens("123", tc.claims) - assert.NoError(t, err) - - token, err := ParseEncoded(access) - assert.NoError(t, err) - - assert.Equal(t, tc.hasRole, HasRoles(token, tc.role)) - }) - } -} - -func TestGetRoles(t *testing.T) { - c := config.New() - c.BindFlags() - - testCases := []struct { - name string - roles []string - }{ - {"no role", []string{""}}, - {"user role", []string{"user"}}, - {"many roles", []string{"user", "admin", "super"}}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - access, _, err := GenerateTokens("123", map[string]any{"roles": tc.roles}) - assert.NoError(t, err) - - token, err := ParseEncoded(access) - assert.NoError(t, err) - - assert.Equal(t, tc.roles, GetRoles(token)) - }) - } -} From 0fb49768e9b2750193882f96df1e174075590238 Mon Sep 17 00:00:00 2001 From: alexferl Date: Wed, 13 Mar 2024 21:33:09 -0400 Subject: [PATCH 2/2] upgrade deps Signed-off-by: alexferl --- go.mod | 16 ++++++++-------- go.sum | 36 +++++++++++++++++------------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index fd9e4fc..3e0bebe 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/alexferl/golib/http/api v0.0.0-20240228040247-93f62184757c github.com/alexferl/golib/log v0.0.0-20240228040247-93f62184757c github.com/alexferl/httplink v0.1.0 - github.com/casbin/casbin/v2 v2.82.0 + github.com/casbin/casbin/v2 v2.84.1 github.com/labstack/echo/v4 v4.11.4 github.com/lestrrat-go/jwx/v2 v2.0.21 github.com/matthewhartstonge/argon2 v1.0.0 @@ -22,22 +22,22 @@ require ( github.com/stretchr/testify v1.9.0 go.mongodb.org/mongo-driver v1.14.0 go.uber.org/automaxprocs v1.5.3 - golang.org/x/oauth2 v0.15.0 + golang.org/x/oauth2 v0.18.0 ) require ( - cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute v1.25.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/casbin/govaluate v1.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/getkin/kin-openapi v0.123.0 // indirect - github.com/go-openapi/jsonpointer v0.20.3 // indirect - github.com/go-openapi/swag v0.22.10 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -82,8 +82,8 @@ require ( golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a4006a0..f3e6f8a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU= +cloud.google.com/go/compute v1.25.0/go.mod h1:GR7F0ZPZH8EhChlMo9FkLd7eUTwEymjqQagxzilIxIE= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= github.com/alexferl/echo-casbin v1.0.0 h1:5EV1DpVvpVtygtKK50s0Sk8YLZKmKJCaoUnOSu9D/LA= @@ -18,8 +18,8 @@ github.com/alexferl/golib/log v0.0.0-20240228040247-93f62184757c h1:EGHN+74BItaX github.com/alexferl/golib/log v0.0.0-20240228040247-93f62184757c/go.mod h1:fX5j3IQXCTT7IXVzZxzv4rR1Umt/vVNXwSjr7M9kiiI= github.com/alexferl/httplink v0.1.0 h1:2Wps+hbWSFEz1cOCddmE3BJg7YoKuFU/XPFBjHFFGYg= github.com/alexferl/httplink v0.1.0/go.mod h1:fNi0VlNX8Dro/6KZKTV3huWTdevbIeIuNZJykIxe1MQ= -github.com/casbin/casbin/v2 v2.82.0 h1:2CgvunqQQoepcbGRnMc9vEcDhuqh3B5yWKoj+kKSxf8= -github.com/casbin/casbin/v2 v2.82.0/go.mod h1:jX8uoN4veP85O/n2674r2qtfSXI6myvxW85f6TH50fw= +github.com/casbin/casbin/v2 v2.84.1 h1:pmIo88Os4cL7rrjwe+/8N8yBPIMxTC+LiKKzY5z+Xdo= +github.com/casbin/casbin/v2 v2.84.1/go.mod h1:jX8uoN4veP85O/n2674r2qtfSXI6myvxW85f6TH50fw= github.com/casbin/govaluate v1.1.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/casbin/govaluate v1.1.1 h1:J1rFKIBhiC5xr0APd5HP6rDL+xt+BRoyq1pa4o2i/5c= github.com/casbin/govaluate v1.1.1/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= @@ -36,10 +36,10 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= -github.com/go-openapi/jsonpointer v0.20.3 h1:jykzYWS/kyGtsHfRt6aV8JTB9pcQAXPIA7qlZ5aRlyk= -github.com/go-openapi/jsonpointer v0.20.3/go.mod h1:c7l0rjoouAuIxCm8v/JWKRgMjDG/+/7UBWsXMrv6PsM= -github.com/go-openapi/swag v0.22.10 h1:4y86NVn7Z2yYd6pfS4Z+Nyh3aAUL3Nul+LMbhFKy0gA= -github.com/go-openapi/swag v0.22.10/go.mod h1:Cnn8BYtRlx6BNE3DPN86f/xkapGIcLWzh3CLEb4C1jI= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= @@ -49,10 +49,10 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -182,14 +182,13 @@ golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= @@ -208,7 +207,6 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= @@ -222,12 +220,12 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=