From ccde5a9ae97605043a4928b58fc7b80bb21e5a18 Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Sun, 6 Jul 2025 19:35:15 -0300 Subject: [PATCH 1/9] feature: add the groups model --- .../migrations/0006_add_groups_table.down.sql | 1 + .../migrations/0006_add_groups_table.up.sql | 41 +++++++++++++ internal/models/groups.go | 20 +++++++ internal/models/groups_test.go | 57 +++++++++++++++++++ internal/repository/factory.go | 4 ++ internal/repository/group.go | 16 ++++++ internal/repository/manager.go | 4 ++ internal/repository/manager_test.go | 1 + test/utils/test_migrator.go | 4 +- 9 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 configs/migrations/0006_add_groups_table.down.sql create mode 100644 configs/migrations/0006_add_groups_table.up.sql create mode 100644 internal/models/groups.go create mode 100644 internal/models/groups_test.go create mode 100644 internal/repository/group.go diff --git a/configs/migrations/0006_add_groups_table.down.sql b/configs/migrations/0006_add_groups_table.down.sql new file mode 100644 index 0000000..865ebf9 --- /dev/null +++ b/configs/migrations/0006_add_groups_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS public.groups CASCADE; diff --git a/configs/migrations/0006_add_groups_table.up.sql b/configs/migrations/0006_add_groups_table.up.sql new file mode 100644 index 0000000..12c4b24 --- /dev/null +++ b/configs/migrations/0006_add_groups_table.up.sql @@ -0,0 +1,41 @@ +CREATE TABLE public.groups ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone, + user_id bigint NOT NULL, + client_id bigint NOT NULL, + tags text[] NOT NULL +); + + +CREATE SEQUENCE public.groups_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +ALTER SEQUENCE public.groups_id_seq OWNED BY public.groups.id; + + +ALTER TABLE ONLY public.groups ALTER COLUMN id SET DEFAULT nextval('public.groups_id_seq'::regclass); + + +ALTER TABLE ONLY public.groups + ADD CONSTRAINT groups_user_client_key UNIQUE (user_id, client_id); + + +ALTER TABLE ONLY public.groups + ADD CONSTRAINT groups_pkey PRIMARY KEY (id); + + +ALTER TABLE ONLY public.groups + ADD CONSTRAINT fk_groups_user FOREIGN KEY (user_id) REFERENCES public.users(id); + + +ALTER TABLE ONLY public.groups + ADD CONSTRAINT fk_groups_client FOREIGN KEY (client_id) REFERENCES public.clients(id); + + +CREATE INDEX idx_groups_user_client ON public.groups USING btree (user_id, client_id); diff --git a/internal/models/groups.go b/internal/models/groups.go new file mode 100644 index 0000000..076b717 --- /dev/null +++ b/internal/models/groups.go @@ -0,0 +1,20 @@ +package models + +import ( + "github.com/lib/pq" + "gorm.io/gorm" +) + +type Group struct { + Model + User User `gorm:"not null;foreignKey:UserID" validate:"required" json:"-"` + UserID uint `gorm:"not null" json:"-"` + Client Client `gorm:"not null;foreignKey:ClientID" validate:"required" json:"-"` + ClientID uint `gorm:"not null" json:"-"` + Tags pq.StringArray `gorm:"type:text[];not null" validate:"required" json:"groups"` +} + +// BeforeSave Language model/struct hook +func (group *Group) BeforeSave(tx *gorm.DB) error { + return validateModel("validate", group) +} diff --git a/internal/models/groups_test.go b/internal/models/groups_test.go new file mode 100644 index 0000000..6b1bccb --- /dev/null +++ b/internal/models/groups_test.go @@ -0,0 +1,57 @@ +package models + +import ( + "fmt" + "testing" + + "github.com/brianvoe/gofakeit/v7" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidGroupModel(t *testing.T) { + group := Group{} + assert.False(t, IsValid("validate", group)) + val := validateModel("validate", group) + assert.NotNil(t, val, fmt.Sprintf("%v", val)) + err := group.BeforeSave(nil) + assert.NotNil(t, err, fmt.Sprintf("%v", err)) + + client := Client{ + Name: "internal", + Secret: GenerateRandomString(64), + CanonicalURI: []string{"localhost"}, + RedirectURI: []string{"/"}, + Scopes: PublicScope, + Type: PublicClient, + } + err = client.BeforeSave(nil) + require.Nil(t, err, fmt.Sprintf("%s", err)) + user := User{ + FirstName: gofakeit.FirstName(), + LastName: gofakeit.LastName(), + Username: gofakeit.Username(), + Email: gofakeit.Email(), + Passphrase: gofakeit.Password(true, true, true, true, false, 10), + CodeSecret: gofakeit.Password(true, true, true, true, false, 64), + RecoverSecret: gofakeit.Password(true, true, true, true, false, 64), + } + user.Client = client + user.Language = Language{ + Name: "English", + IsoCode: "en-US", + } + err = user.BeforeSave(nil) + require.Nil(t, err, fmt.Sprintf("%s", err)) + group = Group{ + User: user, + Client: client, + Tags: []string{"testing"}, + } + + assert.True(t, IsValid("validate", group)) + val = validateModel("validate", group) + assert.Nil(t, val, fmt.Sprintf("%v", val)) + err = group.BeforeSave(nil) + assert.Nil(t, err, fmt.Sprintf("%v", err)) +} diff --git a/internal/repository/factory.go b/internal/repository/factory.go index e79f632..3f27cac 100644 --- a/internal/repository/factory.go +++ b/internal/repository/factory.go @@ -52,3 +52,7 @@ func (f *RepositoryFactory) NewEmailRepository() *EmailRepository { func (f *RepositoryFactory) NewSettingRepository() *SettingRepository { return NewSettingRepository(f.db) } + +func (f *RepositoryFactory) NewGroupRepository() *GroupRepository { + return NewGroupRepository(f.db) +} diff --git a/internal/repository/group.go b/internal/repository/group.go new file mode 100644 index 0000000..d4b4cc3 --- /dev/null +++ b/internal/repository/group.go @@ -0,0 +1,16 @@ +package repository + +import ( + "github.com/earaujoassis/space/internal/gateways/database" + "github.com/earaujoassis/space/internal/models" +) + +type GroupRepository struct { + *BaseRepository[models.Group] +} + +func NewGroupRepository(db *database.DatabaseService) *GroupRepository { + return &GroupRepository{ + BaseRepository: NewBaseRepository[models.Group](db), + } +} diff --git a/internal/repository/manager.go b/internal/repository/manager.go index 23044c6..a2a601e 100644 --- a/internal/repository/manager.go +++ b/internal/repository/manager.go @@ -50,3 +50,7 @@ func (rm *RepositoryManager) Emails() *EmailRepository { func (rm *RepositoryManager) Settings() *SettingRepository { return rm.factory.NewSettingRepository() } + +func (rm *RepositoryManager) Groups() *GroupRepository { + return rm.factory.NewGroupRepository() +} diff --git a/internal/repository/manager_test.go b/internal/repository/manager_test.go index b51dd17..4811c69 100644 --- a/internal/repository/manager_test.go +++ b/internal/repository/manager_test.go @@ -11,4 +11,5 @@ func (s *RepositoryTestSuite) TestRepositoryManager() { s.Require().NotNil(manager.Users()) s.Require().NotNil(manager.Emails()) s.Require().NotNil(manager.Settings()) + s.Require().NotNil(manager.Groups()) } diff --git a/test/utils/test_migrator.go b/test/utils/test_migrator.go index 2ac7873..a67aaf7 100644 --- a/test/utils/test_migrator.go +++ b/test/utils/test_migrator.go @@ -13,5 +13,7 @@ func RunUnitTestMigrator(db *gorm.DB) error { &models.Session{}, &models.User{}, &models.Email{}, - &models.Setting{}) + &models.Setting{}, + &models.Group{}, + ) } From d940c7e6d2ca58358c4ec54841aa343e18e623a4 Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Sun, 6 Jul 2025 21:28:59 -0300 Subject: [PATCH 2/9] chore: update cleanup and auto-migrate for tests --- test/unit/base_test_suite.go | 3 +++ test/utils/test_migrator.go | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/test/unit/base_test_suite.go b/test/unit/base_test_suite.go index e06b4d1..54fff0c 100644 --- a/test/unit/base_test_suite.go +++ b/test/unit/base_test_suite.go @@ -56,6 +56,9 @@ func (s *BaseTestSuite) SetupTest() { func (s *BaseTestSuite) cleanupDatabase() { db, err := s.AppCtx.DB.GetDB().DB() s.Require().NoError(err) + db.Exec("DELETE FROM groups") + db.Exec("DELETE FROM settings") + db.Exec("DELETE FROM emails") db.Exec("DELETE FROM sessions") db.Exec("DELETE FROM users") db.Exec("DELETE FROM services") diff --git a/test/utils/test_migrator.go b/test/utils/test_migrator.go index a67aaf7..d6907ef 100644 --- a/test/utils/test_migrator.go +++ b/test/utils/test_migrator.go @@ -7,11 +7,12 @@ import ( ) func RunUnitTestMigrator(db *gorm.DB) error { - return db.AutoMigrate(&models.Language{}, + return db.AutoMigrate( + &models.Language{}, &models.Client{}, &models.Service{}, - &models.Session{}, &models.User{}, + &models.Session{}, &models.Email{}, &models.Setting{}, &models.Group{}, From e42fb4edb65b0e0df9de57fe207a47cbcb697f61 Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Sun, 6 Jul 2025 21:29:50 -0300 Subject: [PATCH 3/9] chore: fix naming for emails migration --- ...users_emails_table.down.sql => 0004_add_emails_table.down.sql} | 0 ...add_users_emails_table.up.sql => 0004_add_emails_table.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename configs/migrations/{0004_add_users_emails_table.down.sql => 0004_add_emails_table.down.sql} (100%) rename configs/migrations/{0004_add_users_emails_table.up.sql => 0004_add_emails_table.up.sql} (100%) diff --git a/configs/migrations/0004_add_users_emails_table.down.sql b/configs/migrations/0004_add_emails_table.down.sql similarity index 100% rename from configs/migrations/0004_add_users_emails_table.down.sql rename to configs/migrations/0004_add_emails_table.down.sql diff --git a/configs/migrations/0004_add_users_emails_table.up.sql b/configs/migrations/0004_add_emails_table.up.sql similarity index 100% rename from configs/migrations/0004_add_users_emails_table.up.sql rename to configs/migrations/0004_add_emails_table.up.sql From c91e4e918807091aaa2fd5bcf7549a25c2846d56 Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Sun, 6 Jul 2025 21:42:59 -0300 Subject: [PATCH 4/9] hotfix: fix index name on settings table --- configs/migrations/0007_rename_index_for_settings.down.sql | 1 + configs/migrations/0007_rename_index_for_settings.up.sql | 1 + 2 files changed, 2 insertions(+) create mode 100644 configs/migrations/0007_rename_index_for_settings.down.sql create mode 100644 configs/migrations/0007_rename_index_for_settings.up.sql diff --git a/configs/migrations/0007_rename_index_for_settings.down.sql b/configs/migrations/0007_rename_index_for_settings.down.sql new file mode 100644 index 0000000..9101b5a --- /dev/null +++ b/configs/migrations/0007_rename_index_for_settings.down.sql @@ -0,0 +1 @@ +ALTER INDEX public.idx_settings_user_realm_category_property RENAME TO idx_settings_address; diff --git a/configs/migrations/0007_rename_index_for_settings.up.sql b/configs/migrations/0007_rename_index_for_settings.up.sql new file mode 100644 index 0000000..02fb291 --- /dev/null +++ b/configs/migrations/0007_rename_index_for_settings.up.sql @@ -0,0 +1 @@ +ALTER INDEX public.idx_settings_address RENAME TO idx_settings_user_realm_category_property; From 5835958d01cb842c0e125f770301517b18653308 Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Sun, 6 Jul 2025 23:20:27 -0300 Subject: [PATCH 5/9] chore: organize api subpackage --- internal/api/api_test.go | 91 +++-------------- internal/api/clients/clients_test.go | 32 ++++++ .../create_handler.go} | 4 +- .../create_handler_test.go} | 18 ++-- .../credentials_handler.go} | 4 +- .../credentials_handler_test.go} | 10 +- .../endpoints.go} | 24 ++--- .../list_handler.go} | 4 +- .../list_handler_test.go} | 18 ++-- .../profile_handler.go} | 4 +- .../profile_handler_test.go} | 22 ++--- internal/api/endpoints.go | 18 +++- internal/api/endpoints_health_check.go | 12 --- internal/api/endpoints_services.go | 24 ----- internal/api/endpoints_sessions.go | 20 ---- internal/api/endpoints_test.go | 52 ++++++++++ internal/api/endpoints_users.go | 97 ------------------- internal/api/health_check/endpoints.go | 9 ++ .../handler.go} | 4 +- .../handler_test.go} | 4 +- .../api/health_check/health_check_test.go | 32 ++++++ internal/api/helpers/helpers_test.go | 30 ++++++ internal/api/{ => helpers}/security.go | 8 +- internal/api/helpers/security_test.go | 35 +++++++ internal/api/security_test.go | 27 ------ .../admin_handler.go} | 4 +- .../admin_handler_test.go} | 38 ++++---- .../emails_create_handler.go} | 4 +- .../emails_create_handler_test.go} | 18 ++-- .../emails_list_handler.go} | 4 +- .../emails_list_handler_test.go} | 14 +-- internal/api/self/endpoints.go | 58 +++++++++++ .../password_handler.go} | 4 +- .../password_handler_test.go} | 14 +-- .../requests_handler.go} | 4 +- .../requests_handler_test.go} | 14 +-- internal/api/self/self_test.go | 32 ++++++ .../settings_list_handler.go} | 4 +- .../settings_list_handler_test.go} | 14 +-- .../settings_patch_handler.go} | 4 +- .../settings_patch_handler_test.go} | 30 +++--- .../workspace.go} | 4 +- .../workspace_test.go} | 6 +- .../create_handler.go} | 4 +- .../create_handler_test.go} | 22 ++--- internal/api/services/endpoints.go | 26 +++++ .../list_handler.go} | 4 +- .../list_handler_test.go} | 18 ++-- internal/api/services/services_test.go | 32 ++++++ .../create_handler.go} | 4 +- .../create_handler_test.go} | 8 +- internal/api/sessions/endpoints.go | 22 +++++ .../requests_handler.go} | 4 +- .../requests_handler_test.go} | 14 +-- internal/api/sessions/sessions_test.go | 32 ++++++ .../clients_list_handler.go} | 4 +- .../clients_list_handler_test.go} | 22 ++--- .../clients_revoke_handler.go} | 4 +- .../clients_revoke_handler_test.go} | 22 ++--- .../create_handler.go} | 4 +- .../create_handler_test.go} | 10 +- internal/api/users/endpoints.go | 54 +++++++++++ .../profile_handler.go} | 4 +- .../profile_handler_test.go} | 22 ++--- .../sessions_list_handler.go} | 4 +- .../sessions_list_handler_test.go} | 22 ++--- .../sessions_revoke_handler.go} | 4 +- .../sessions_revoke_handler_test.go} | 26 ++--- internal/api/users/users_test.go | 32 ++++++ test/unit/api_base_test_suite.go | 92 ++++++++++++++++++ 70 files changed, 860 insertions(+), 529 deletions(-) create mode 100644 internal/api/clients/clients_test.go rename internal/api/{clients_create_handler.go => clients/create_handler.go} (95%) rename internal/api/{clients_create_handler_test.go => clients/create_handler_test.go} (85%) rename internal/api/{clients_credentials_handler.go => clients/credentials_handler.go} (96%) rename internal/api/{clients_credentials_handler_test.go => clients/credentials_handler_test.go} (91%) rename internal/api/{endpoints_clients.go => clients/endpoints.go} (60%) rename internal/api/{clients_list_handler.go => clients/list_handler.go} (93%) rename internal/api/{clients_list_handler_test.go => clients/list_handler_test.go} (82%) rename internal/api/{clients_profile_handler.go => clients/profile_handler.go} (96%) rename internal/api/{clients_profile_handler_test.go => clients/profile_handler_test.go} (88%) delete mode 100644 internal/api/endpoints_health_check.go delete mode 100644 internal/api/endpoints_services.go delete mode 100644 internal/api/endpoints_sessions.go create mode 100644 internal/api/endpoints_test.go delete mode 100644 internal/api/endpoints_users.go create mode 100644 internal/api/health_check/endpoints.go rename internal/api/{health_check_handler.go => health_check/handler.go} (85%) rename internal/api/{health_check_handler_test.go => health_check/handler_test.go} (77%) create mode 100644 internal/api/health_check/health_check_test.go create mode 100644 internal/api/helpers/helpers_test.go rename internal/api/{ => helpers}/security.go (93%) create mode 100644 internal/api/helpers/security_test.go delete mode 100644 internal/api/security_test.go rename internal/api/{users_me_admin_handler.go => self/admin_handler.go} (96%) rename internal/api/{users_me_admin_handler_test.go => self/admin_handler_test.go} (85%) rename internal/api/{users_me_emails_create_handler.go => self/emails_create_handler.go} (94%) rename internal/api/{users_me_emails_create_handler_test.go => self/emails_create_handler_test.go} (80%) rename internal/api/{users_me_emails_list_handler.go => self/emails_list_handler.go} (93%) rename internal/api/{users_me_emails_list_handler_test.go => self/emails_list_handler_test.go} (81%) create mode 100644 internal/api/self/endpoints.go rename internal/api/{users_me_password_handler.go => self/password_handler.go} (96%) rename internal/api/{users_me_password_handler_test.go => self/password_handler_test.go} (90%) rename internal/api/{users_me_requests_handler.go => self/requests_handler.go} (98%) rename internal/api/{users_me_requests_handler_test.go => self/requests_handler_test.go} (89%) create mode 100644 internal/api/self/self_test.go rename internal/api/{users_me_settings_list_handler.go => self/settings_list_handler.go} (93%) rename internal/api/{users_me_settings_list_handler_test.go => self/settings_list_handler_test.go} (79%) rename internal/api/{users_me_settings_patch_handler.go => self/settings_patch_handler.go} (97%) rename internal/api/{users_me_settings_patch_handler_test.go => self/settings_patch_handler_test.go} (89%) rename internal/api/{users_me_workspace.go => self/workspace.go} (94%) rename internal/api/{users_me_workspace_test.go => self/workspace_test.go} (88%) rename internal/api/{services_create_handler.go => services/create_handler.go} (95%) rename internal/api/{services_create_handler_test.go => services/create_handler_test.go} (85%) create mode 100644 internal/api/services/endpoints.go rename internal/api/{services_list_handler.go => services/list_handler.go} (93%) rename internal/api/{services_list_handler_test.go => services/list_handler_test.go} (82%) create mode 100644 internal/api/services/services_test.go rename internal/api/{sessions_create_handler.go => sessions/create_handler.go} (97%) rename internal/api/{sessions_create_handler_test.go => sessions/create_handler_test.go} (92%) create mode 100644 internal/api/sessions/endpoints.go rename internal/api/{sessions_requests_handler.go => sessions/requests_handler.go} (97%) rename internal/api/{sessions_requests_handler_test.go => sessions/requests_handler_test.go} (84%) create mode 100644 internal/api/sessions/sessions_test.go rename internal/api/{users_clients_list_handler.go => users/clients_list_handler.go} (95%) rename internal/api/{users_clients_list_handler_test.go => users/clients_list_handler_test.go} (83%) rename internal/api/{users_clients_revoke_handler.go => users/clients_revoke_handler.go} (95%) rename internal/api/{users_clients_revoke_handler_test.go => users/clients_revoke_handler_test.go} (86%) rename internal/api/{users_create_handler.go => users/create_handler.go} (97%) rename internal/api/{users_create_handler_test.go => users/create_handler_test.go} (86%) create mode 100644 internal/api/users/endpoints.go rename internal/api/{users_profile_handler.go => users/profile_handler.go} (96%) rename internal/api/{users_profile_handler_test.go => users/profile_handler_test.go} (83%) rename internal/api/{users_sessions_list_handler.go => users/sessions_list_handler.go} (95%) rename internal/api/{users_sessions_list_handler_test.go => users/sessions_list_handler_test.go} (83%) rename internal/api/{users_sessions_revoke_handler.go => users/sessions_revoke_handler.go} (95%) rename internal/api/{users_sessions_revoke_handler_test.go => users/sessions_revoke_handler_test.go} (85%) create mode 100644 internal/api/users/users_test.go create mode 100644 test/unit/api_base_test_suite.go diff --git a/internal/api/api_test.go b/internal/api/api_test.go index e804da7..80cb39e 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -1,100 +1,31 @@ package api import ( - "net/http" "testing" - "github.com/brianvoe/gofakeit/v7" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/stretchr/testify/suite" - "github.com/earaujoassis/space/internal/ioc" - "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/security" - "github.com/earaujoassis/space/internal/shared" - "github.com/earaujoassis/space/test/factory" "github.com/earaujoassis/space/test/unit" - "github.com/earaujoassis/space/test/utils" ) -type ApiHandlerTestSuite struct { - unit.BaseTestSuite +type ApiTestSuite struct { + unit.ApiBaseTestSuite Router *gin.Engine } -func (s *ApiHandlerTestSuite) SetupSuite() { - s.BaseTestSuite.SetupSuite() +func (s *ApiTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() gin.SetMode(gin.TestMode) - s.Router = s.setupRouter() + s.Router = s.SetupRouter() } -func (s *ApiHandlerTestSuite) SetupTest() { - s.BaseTestSuite.SetupTest() - s.Router = s.setupRouter() +func (s *ApiTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() + ExposeRoutes(s.Router) } -func (s *ApiHandlerTestSuite) setupRouter() *gin.Engine { - router := gin.New() - security.SetTrustedProxies(router) - store := cookie.NewStore([]byte(s.Config.SessionSecret)) - store.Options(sessions.Options{Secure: false, HttpOnly: true}) - router.Use(sessions.Sessions("space.session", store)) - router.Use(ioc.InjectAppContext(s.BaseTestSuite.AppCtx)) - router.GET("/set-session", func(c *gin.Context) { - var user models.User - - admin := c.Query("admin") == "true" - session := sessions.Default(c) - repositories := ioc.GetRepositories(c) - if admin { - user = s.Factory.NewUserWithOption(factory.UserOptions{Admin: true}).Model - } else { - user = s.Factory.NewUserWithOption(factory.UserOptions{Admin: false}).Model - } - client := repositories.Clients().FindOrCreate(models.DefaultClient) - applicationSession := models.Session{ - User: user, - Client: client, - IP: gofakeit.IPv4Address(), - UserAgent: gofakeit.UserAgent(), - Scopes: models.PublicScope, - TokenType: models.ApplicationToken, - } - err := repositories.Sessions().Create(&applicationSession) - s.Require().NoError(err) - session.Set(shared.CookieSessionKey, applicationSession.Token) - session.Save() - c.String(200, "Session set") - }) - router.RedirectTrailingSlash = false - ExposeRoutes(router) - return router -} - -func (s *ApiHandlerTestSuite) createSessionCookie(admin bool) *http.Cookie { - var path string - - if admin { - path = "/set-session?admin=true" - } else { - path = "/set-session?admin=false" - } - w := s.PerformRequest(s.Router, "GET", path, nil, nil, nil) - r := utils.ParseResponse(w.Result(), nil) - s.Equal(200, w.Code) - s.Contains(r.Body, "Session set") - - for _, cookie := range w.Result().Cookies() { - if cookie.Name == "space.session" { - return cookie - } - } - - return nil -} - -func TestApiSuite(t *testing.T) { - suite.Run(t, new(ApiHandlerTestSuite)) +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(ApiTestSuite)) } diff --git a/internal/api/clients/clients_test.go b/internal/api/clients/clients_test.go new file mode 100644 index 0000000..02a1fd5 --- /dev/null +++ b/internal/api/clients/clients_test.go @@ -0,0 +1,32 @@ +package clients + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + + "github.com/earaujoassis/space/test/unit" +) + +type ClientsTestSuite struct { + unit.ApiBaseTestSuite + Router *gin.Engine +} + +func (s *ClientsTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *ClientsTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() + group := s.Router.Group("/api") + ExposeRoutes(group) +} + +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(ClientsTestSuite)) +} diff --git a/internal/api/clients_create_handler.go b/internal/api/clients/create_handler.go similarity index 95% rename from internal/api/clients_create_handler.go rename to internal/api/clients/create_handler.go index 4169d82..c5d1779 100644 --- a/internal/api/clients_create_handler.go +++ b/internal/api/clients/create_handler.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -12,7 +12,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func clientsCreateHandler(c *gin.Context) { +func createHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/clients_create_handler_test.go b/internal/api/clients/create_handler_test.go similarity index 85% rename from internal/api/clients_create_handler_test.go rename to internal/api/clients/create_handler_test.go index 0cdfd4c..fd96263 100644 --- a/internal/api/clients_create_handler_test.go +++ b/internal/api/clients/create_handler_test.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -11,14 +11,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestClientsCreateHandlerWithoutHeader() { +func (s *ClientsTestSuite) TestCreateHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "POST", "/api/clients", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestClientsCreateHandlerByUnauthenticatedUser() { +func (s *ClientsTestSuite) TestCreateHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -29,12 +29,12 @@ func (s *ApiHandlerTestSuite) TestClientsCreateHandlerByUnauthenticatedUser() { s.Contains(r.Body, "User must be authenticated") } -func (s *ApiHandlerTestSuite) TestClientsCreateHandlerWithoutActionGrant() { +func (s *ClientsTestSuite) TestCreateHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "POST", "/api/clients", header, cookie, nil) @@ -44,8 +44,8 @@ func (s *ApiHandlerTestSuite) TestClientsCreateHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestClientsCreateHandlerByAdminUser() { - cookie := s.createSessionCookie(true) +func (s *ClientsTestSuite) TestCreateHandlerByAdminUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -82,8 +82,8 @@ func (s *ApiHandlerTestSuite) TestClientsCreateHandlerByAdminUser() { s.Require().Equal(204, w.Code) } -func (s *ApiHandlerTestSuite) TestClientsCreateHandlerByCommonUser() { - cookie := s.createSessionCookie(false) +func (s *ClientsTestSuite) TestCreateHandlerByCommonUser() { + cookie := s.CreateSessionCookie(false) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/clients_credentials_handler.go b/internal/api/clients/credentials_handler.go similarity index 96% rename from internal/api/clients_credentials_handler.go rename to internal/api/clients/credentials_handler.go index 09d2418..45bba97 100644 --- a/internal/api/clients_credentials_handler.go +++ b/internal/api/clients/credentials_handler.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -14,7 +14,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func clientsCredentialsHandler(c *gin.Context) { +func credentialsHandler(c *gin.Context) { clientUUID := c.Param("client_id") repositories := ioc.GetRepositories(c) user := c.MustGet("User").(models.User) diff --git a/internal/api/clients_credentials_handler_test.go b/internal/api/clients/credentials_handler_test.go similarity index 91% rename from internal/api/clients_credentials_handler_test.go rename to internal/api/clients/credentials_handler_test.go index 687b22e..3a2fb08 100644 --- a/internal/api/clients_credentials_handler_test.go +++ b/internal/api/clients/credentials_handler_test.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -7,12 +7,12 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestClientsCredentialsHandlerByAdminUser() { +func (s *ClientsTestSuite) TestCredentialsHandlerByAdminUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -63,8 +63,8 @@ func (s *ApiHandlerTestSuite) TestClientsCredentialsHandlerByAdminUser() { s.Equal("Client credentials are not available", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestClientsCredentialsHandlerByCommonUser() { - cookie := s.createSessionCookie(false) +func (s *ClientsTestSuite) TestCredentialsHandlerByCommonUser() { + cookie := s.CreateSessionCookie(false) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/endpoints_clients.go b/internal/api/clients/endpoints.go similarity index 60% rename from internal/api/endpoints_clients.go rename to internal/api/clients/endpoints.go index 6ea5153..797b2d9 100644 --- a/internal/api/endpoints_clients.go +++ b/internal/api/clients/endpoints.go @@ -1,35 +1,37 @@ -package api +package clients import ( "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/api/helpers" ) -// exposeClientsRoutes defines and exposes HTTP routes for a given gin.RouterGroup +// ExposeRoutes defines and exposes HTTP routes for a given gin.RouterGroup // // in the REST API scope, for the clients resource -func exposeClientsRoutes(router *gin.RouterGroup) { +func ExposeRoutes(router *gin.RouterGroup) { // In order to avoid an overhead in this endpoint, it relies only on the cookies session data to guarantee security // TODO Improve security for this endpoint avoiding any overhead router.GET("/clients/:client_id/credentials", - requiresApplicationSession(), - clientsCredentialsHandler) + helpers.RequiresApplicationSession(), + credentialsHandler) clientsRoutes := router.Group("/clients") - clientsRoutes.Use(requiresConformance()) - clientsRoutes.Use(requiresApplicationSession()) - clientsRoutes.Use(actionTokenBearerAuthorization()) + clientsRoutes.Use(helpers.RequiresConformance()) + clientsRoutes.Use(helpers.RequiresApplicationSession()) + clientsRoutes.Use(helpers.ActionTokenBearerAuthorization()) { // Requires X-Requested-By and Origin (same-origin policy) // Authorization type: action token / Bearer (for web use) - clientsRoutes.GET("", clientsListHandler) + clientsRoutes.GET("", listHandler) // Requires X-Requested-By and Origin (same-origin policy) // Authorization type: action token / Bearer (for web use) - clientsRoutes.POST("", clientsCreateHandler) + clientsRoutes.POST("", createHandler) // In order to avoid an overhead in this endpoint, it relies only on the cookies session data to guarantee security // Authorization type: action token / Bearer (for web use) // TODO Improve security for this endpoint avoiding any overhead - clientsRoutes.PATCH("/:client_id/profile", clientsProfileHandler) + clientsRoutes.PATCH("/:client_id/profile", profileHandler) } } diff --git a/internal/api/clients_list_handler.go b/internal/api/clients/list_handler.go similarity index 93% rename from internal/api/clients_list_handler.go rename to internal/api/clients/list_handler.go index a1e0aaf..17f9217 100644 --- a/internal/api/clients_list_handler.go +++ b/internal/api/clients/list_handler.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -12,7 +12,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func clientsListHandler(c *gin.Context) { +func listHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/clients_list_handler_test.go b/internal/api/clients/list_handler_test.go similarity index 82% rename from internal/api/clients_list_handler_test.go rename to internal/api/clients/list_handler_test.go index 6efcaaa..4a207c9 100644 --- a/internal/api/clients_list_handler_test.go +++ b/internal/api/clients/list_handler_test.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -7,14 +7,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestClientsListHandlerWithoutHeader() { +func (s *ClientsTestSuite) TestListHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "GET", "/api/clients", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestClientsListHandlerByUnauthenticatedUser() { +func (s *ClientsTestSuite) TestListHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -26,12 +26,12 @@ func (s *ApiHandlerTestSuite) TestClientsListHandlerByUnauthenticatedUser() { s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestClientsListHandlerWithoutActionGrant() { +func (s *ClientsTestSuite) TestListHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "GET", "/api/clients", header, cookie, nil) @@ -41,8 +41,8 @@ func (s *ApiHandlerTestSuite) TestClientsListHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestClientsListHandlerByAdminUser() { - cookie := s.createSessionCookie(true) +func (s *ClientsTestSuite) TestListHandlerByAdminUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -64,8 +64,8 @@ func (s *ApiHandlerTestSuite) TestClientsListHandlerByAdminUser() { s.NotEmpty(client["id"]) } -func (s *ApiHandlerTestSuite) TestClientsListHandlerByCommonUser() { - cookie := s.createSessionCookie(false) +func (s *ClientsTestSuite) TestListHandlerByCommonUser() { + cookie := s.CreateSessionCookie(false) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/clients_profile_handler.go b/internal/api/clients/profile_handler.go similarity index 96% rename from internal/api/clients_profile_handler.go rename to internal/api/clients/profile_handler.go index 7c60622..a2cfffa 100644 --- a/internal/api/clients_profile_handler.go +++ b/internal/api/clients/profile_handler.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -14,7 +14,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func clientsProfileHandler(c *gin.Context) { +func profileHandler(c *gin.Context) { clientUUID := c.Param("client_id") repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) diff --git a/internal/api/clients_profile_handler_test.go b/internal/api/clients/profile_handler_test.go similarity index 88% rename from internal/api/clients_profile_handler_test.go rename to internal/api/clients/profile_handler_test.go index 81964ac..b986b90 100644 --- a/internal/api/clients_profile_handler_test.go +++ b/internal/api/clients/profile_handler_test.go @@ -1,4 +1,4 @@ -package api +package clients import ( "fmt" @@ -9,7 +9,7 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestClientsProfileHandlerWithoutHeader() { +func (s *ClientsTestSuite) TestProfileHandlerWithoutHeader() { client := s.Factory.NewClient().Model path := fmt.Sprintf("/api/clients/%s/profile", client.UUID) w := s.PerformRequest(s.Router, "PATCH", path, nil, nil, nil) @@ -18,7 +18,7 @@ func (s *ApiHandlerTestSuite) TestClientsProfileHandlerWithoutHeader() { s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestClientsProfileHandlerByUnauthenticatedUser() { +func (s *ClientsTestSuite) TestProfileHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -33,14 +33,14 @@ func (s *ApiHandlerTestSuite) TestClientsProfileHandlerByUnauthenticatedUser() { s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestClientsProfileHandlerWithoutActionGrant() { +func (s *ClientsTestSuite) TestProfileHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } client := s.Factory.NewClient().Model path := fmt.Sprintf("/api/clients/%s/profile", client.UUID) - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) @@ -50,8 +50,8 @@ func (s *ApiHandlerTestSuite) TestClientsProfileHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestClientsProfileHandlerInvalidId() { - cookie := s.createSessionCookie(true) +func (s *ClientsTestSuite) TestProfileHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -81,8 +81,8 @@ func (s *ApiHandlerTestSuite) TestClientsProfileHandlerInvalidId() { s.Require().Equal(400, w.Code) } -func (s *ApiHandlerTestSuite) TestClientsProfileHandlerByAdminUser() { - cookie := s.createSessionCookie(true) +func (s *ClientsTestSuite) TestProfileHandlerByAdminUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -122,8 +122,8 @@ func (s *ApiHandlerTestSuite) TestClientsProfileHandlerByAdminUser() { s.Require().Equal(204, w.Code) } -func (s *ApiHandlerTestSuite) TestClientsProfileHandlerByCommonUser() { - cookie := s.createSessionCookie(false) +func (s *ClientsTestSuite) TestProfileHandlerByCommonUser() { + cookie := s.CreateSessionCookie(false) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/endpoints.go b/internal/api/endpoints.go index 0767196..5cbc9d0 100644 --- a/internal/api/endpoints.go +++ b/internal/api/endpoints.go @@ -2,6 +2,13 @@ package api import ( "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/api/clients" + "github.com/earaujoassis/space/internal/api/health_check" + "github.com/earaujoassis/space/internal/api/self" + "github.com/earaujoassis/space/internal/api/services" + "github.com/earaujoassis/space/internal/api/sessions" + "github.com/earaujoassis/space/internal/api/users" ) // ExposeRoutes defines and exposes HTTP routes for a given gin.RouterGroup @@ -10,9 +17,10 @@ import ( func ExposeRoutes(router *gin.Engine) { restAPI := router.Group("/api") - exposeUsersRoutes(restAPI) - exposeSessionsRoutes(restAPI) - exposeClientsRoutes(restAPI) - exposeServicesRoutes(restAPI) - exposeHealthCheck(restAPI) + health_check.ExposeRoutes(restAPI) + users.ExposeRoutes(restAPI) + self.ExposeRoutes(restAPI) + sessions.ExposeRoutes(restAPI) + clients.ExposeRoutes(restAPI) + services.ExposeRoutes(restAPI) } diff --git a/internal/api/endpoints_health_check.go b/internal/api/endpoints_health_check.go deleted file mode 100644 index dc01799..0000000 --- a/internal/api/endpoints_health_check.go +++ /dev/null @@ -1,12 +0,0 @@ -package api - -import ( - "github.com/gin-gonic/gin" -) - -func exposeHealthCheck(router *gin.RouterGroup) { - healthCheckRoutes := router.Group("/health-check") - { - healthCheckRoutes.GET("", healthCheckHandler) - } -} diff --git a/internal/api/endpoints_services.go b/internal/api/endpoints_services.go deleted file mode 100644 index 44ac86b..0000000 --- a/internal/api/endpoints_services.go +++ /dev/null @@ -1,24 +0,0 @@ -package api - -import ( - "github.com/gin-gonic/gin" -) - -// exposeServicesRoutes defines and exposes HTTP routes for a given gin.RouterGroup -// -// in the REST API scope, for the services resource -func exposeServicesRoutes(router *gin.RouterGroup) { - servicesRoutes := router.Group("/services") - servicesRoutes.Use(requiresConformance()) - servicesRoutes.Use(requiresApplicationSession()) - servicesRoutes.Use(actionTokenBearerAuthorization()) - { - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - servicesRoutes.GET("", servicesListHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - servicesRoutes.POST("", servicesCreateHandler) - } -} diff --git a/internal/api/endpoints_sessions.go b/internal/api/endpoints_sessions.go deleted file mode 100644 index 777d9c6..0000000 --- a/internal/api/endpoints_sessions.go +++ /dev/null @@ -1,20 +0,0 @@ -package api - -import ( - "github.com/gin-gonic/gin" -) - -// exposeSessionsRoutes defines and exposes HTTP routes for a given gin.RouterGroup -// -// in the REST API scope, for the sessions resource -func exposeSessionsRoutes(router *gin.RouterGroup) { - sessionsRoutes := router.Group("/sessions") - sessionsRoutes.Use(requiresConformance()) - { - // Requires X-Requested-By and Origin (same-origin policy) - sessionsRoutes.POST("", sessionsCreateHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - sessionsRoutes.POST("/requests", sessionsRequestsHandler) - } -} diff --git a/internal/api/endpoints_test.go b/internal/api/endpoints_test.go new file mode 100644 index 0000000..f241142 --- /dev/null +++ b/internal/api/endpoints_test.go @@ -0,0 +1,52 @@ +package api + +func (s *ApiTestSuite) TestEndpoints() { + w := s.PerformRequest(s.Router, "GET", "/api/health-check", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/clients/1/credentials", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/clients", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/clients", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "PATCH", "/api/clients/1/profile", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/users/me/requests", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/users/me/requests", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/users/me/workspace", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "PATCH", "/api/users/me/password", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "PATCH", "/api/users/me/admin", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/users/me/emails", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/users/me/emails", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/users/me/settings", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "PATCH", "/api/users/me/settings", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/services", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/services", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/sessions/requests", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/sessions", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "POST", "/api/users", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/users/1/profile", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/users/1/clients", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "DELETE", "/api/users/1/clients/1/revoke", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "GET", "/api/users/1/sessions", nil, nil, nil) + s.Require().NotEqual(404, w.Code) + w = s.PerformRequest(s.Router, "DELETE", "/api/users/1/sessions/1/revoke", nil, nil, nil) + s.Require().NotEqual(404, w.Code) +} diff --git a/internal/api/endpoints_users.go b/internal/api/endpoints_users.go deleted file mode 100644 index 749775b..0000000 --- a/internal/api/endpoints_users.go +++ /dev/null @@ -1,97 +0,0 @@ -package api - -import ( - "github.com/gin-gonic/gin" -) - -// exposeUsersRoutes defines and exposes HTTP routes for a given gin.RouterGroup -// -// in the REST API scope, for the users resource -func exposeUsersRoutes(router *gin.RouterGroup) { - usersRoutes := router.Group("/users") - usersRoutes.Use(requiresConformance()) - { - // Requires X-Requested-By and Origin (same-origin policy) - usersRoutes.POST("", usersCreateHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - usersRoutes.GET("/:user_id/profile", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersProfileHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - usersRoutes.GET("/:user_id/clients", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersClientsListHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - usersRoutes.DELETE("/:user_id/clients/:client_id/revoke", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersClientsRevokeHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - usersRoutes.GET("/:user_id/sessions", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersSessionsListHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - usersRoutes.DELETE("/:user_id/sessions/:session_id/revoke", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersSessionsRevokeHandler) - } - usersMeRoutes := usersRoutes.Group("/me") - usersMeRoutes.Use(requiresConformance()) - { - // Requires X-Requested-By and Origin (same-origin policy) - usersMeRoutes.POST("/requests", usersMeRequestsHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - usersMeRoutes.GET("/workspace", - requiresApplicationSession(), - usersMeWorkspaceHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - usersMeRoutes.PATCH("/password", usersMePasswordHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - // Authorization type: action token / Bearer (for web use) - usersMeRoutes.PATCH("/admin", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersMeAdminHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - usersMeRoutes.GET("/emails", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersMeEmailsListHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - usersMeRoutes.POST("/emails", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersMeEmailsCreateHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - usersMeRoutes.GET("/settings", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersMeSettingsListHandler) - - // Requires X-Requested-By and Origin (same-origin policy) - usersMeRoutes.PATCH("/settings", - requiresApplicationSession(), - actionTokenBearerAuthorization(), - usersMeSettingsPatchHandler) - } -} diff --git a/internal/api/health_check/endpoints.go b/internal/api/health_check/endpoints.go new file mode 100644 index 0000000..406dc1b --- /dev/null +++ b/internal/api/health_check/endpoints.go @@ -0,0 +1,9 @@ +package health_check + +import ( + "github.com/gin-gonic/gin" +) + +func ExposeRoutes(router *gin.RouterGroup) { + router.GET("/health-check", handler) +} diff --git a/internal/api/health_check_handler.go b/internal/api/health_check/handler.go similarity index 85% rename from internal/api/health_check_handler.go rename to internal/api/health_check/handler.go index c978fbe..fa010d3 100644 --- a/internal/api/health_check_handler.go +++ b/internal/api/health_check/handler.go @@ -1,4 +1,4 @@ -package api +package health_check import ( "net/http" @@ -8,7 +8,7 @@ import ( "github.com/earaujoassis/space/internal/ioc" ) -func healthCheckHandler(c *gin.Context) { +func handler(c *gin.Context) { db, err := ioc.GetDB(c).DB() if err != nil { c.String(http.StatusOK, "unhealthy") diff --git a/internal/api/health_check_handler_test.go b/internal/api/health_check/handler_test.go similarity index 77% rename from internal/api/health_check_handler_test.go rename to internal/api/health_check/handler_test.go index 9897e0f..1a231d2 100644 --- a/internal/api/health_check_handler_test.go +++ b/internal/api/health_check/handler_test.go @@ -1,10 +1,10 @@ -package api +package health_check import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestHealthCheckHandler() { +func (s *HealthCheckTestSuite) TestHandler() { w := s.PerformRequest(s.Router, "GET", "/api/health-check", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(200, w.Code) diff --git a/internal/api/health_check/health_check_test.go b/internal/api/health_check/health_check_test.go new file mode 100644 index 0000000..4021e21 --- /dev/null +++ b/internal/api/health_check/health_check_test.go @@ -0,0 +1,32 @@ +package health_check + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + + "github.com/earaujoassis/space/test/unit" +) + +type HealthCheckTestSuite struct { + unit.ApiBaseTestSuite + Router *gin.Engine +} + +func (s *HealthCheckTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *HealthCheckTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() + group := s.Router.Group("/api") + ExposeRoutes(group) +} + +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(HealthCheckTestSuite)) +} diff --git a/internal/api/helpers/helpers_test.go b/internal/api/helpers/helpers_test.go new file mode 100644 index 0000000..107a32a --- /dev/null +++ b/internal/api/helpers/helpers_test.go @@ -0,0 +1,30 @@ +package helpers + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + + "github.com/earaujoassis/space/test/unit" +) + +type HelpersTestSuite struct { + unit.ApiBaseTestSuite + Router *gin.Engine +} + +func (s *HelpersTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *HelpersTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() +} + +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(HelpersTestSuite)) +} diff --git a/internal/api/security.go b/internal/api/helpers/security.go similarity index 93% rename from internal/api/security.go rename to internal/api/helpers/security.go index 90ccdd4..ce73728 100644 --- a/internal/api/security.go +++ b/internal/api/helpers/security.go @@ -1,4 +1,4 @@ -package api +package helpers import ( "fmt" @@ -15,7 +15,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func requiresConformance() gin.HandlerFunc { +func RequiresConformance() gin.HandlerFunc { return func(c *gin.Context) { host := fmt.Sprintf("%s://%s", shared.Scheme(c.Request), c.Request.Host) correctXRequestedBy := c.Request.Header.Get("X-Requested-By") == "SpaceApi" @@ -32,7 +32,7 @@ func requiresConformance() gin.HandlerFunc { } } -func requiresApplicationSession() gin.HandlerFunc { +func RequiresApplicationSession() gin.HandlerFunc { return func(c *gin.Context) { var applicationSession models.Session @@ -61,7 +61,7 @@ func requiresApplicationSession() gin.HandlerFunc { } // The following Authorization method is used by the web client, with an action token -func actionTokenBearerAuthorization() gin.HandlerFunc { +func ActionTokenBearerAuthorization() gin.HandlerFunc { return func(c *gin.Context) { authorizationBearer := strings.Replace(c.Request.Header.Get("Authorization"), "Bearer ", "", 1) if !security.ValidToken(authorizationBearer) { diff --git a/internal/api/helpers/security_test.go b/internal/api/helpers/security_test.go new file mode 100644 index 0000000..fb941d7 --- /dev/null +++ b/internal/api/helpers/security_test.go @@ -0,0 +1,35 @@ +package helpers + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func defaultRoute(c *gin.Context) { + c.String(http.StatusOK, "All good") +} + +func (s *HelpersTestSuite) TestRequiresConformance() { + router := s.Router + router.Use(RequiresConformance()) + router.GET("/", defaultRoute) + w := s.PerformRequest(router, "GET", "/", nil, nil, nil) + s.Equal(400, w.Code) +} + +func (s *HelpersTestSuite) TestActionTokenBearerAuthorization() { + router := s.Router + router.Use(ActionTokenBearerAuthorization()) + router.GET("/", defaultRoute) + w := s.PerformRequest(router, "GET", "/", nil, nil, nil) + s.Equal(400, w.Code) +} + +func (s *HelpersTestSuite) TestRequiresApplicationSession() { + router := s.Router + router.Use(RequiresApplicationSession()) + router.GET("/", defaultRoute) + w := s.PerformRequest(router, "GET", "/", nil, nil, nil) + s.Equal(401, w.Code) +} diff --git a/internal/api/security_test.go b/internal/api/security_test.go deleted file mode 100644 index a2a47bf..0000000 --- a/internal/api/security_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package api - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func defaultRoute(c *gin.Context) { - c.String(http.StatusOK, "All good") -} - -func (s *ApiHandlerTestSuite) TestRequiresConformance() { - router := gin.New() - router.Use(requiresConformance()) - router.GET("/", defaultRoute) - w := s.PerformRequest(router, "GET", "/", nil, nil, nil) - s.Equal(w.Code, 400) -} - -func (s *ApiHandlerTestSuite) TestActionTokenBearerAuthorization() { - router := gin.New() - router.Use(actionTokenBearerAuthorization()) - router.GET("/", defaultRoute) - w := s.PerformRequest(router, "GET", "/", nil, nil, nil) - s.Equal(w.Code, 400) -} diff --git a/internal/api/users_me_admin_handler.go b/internal/api/self/admin_handler.go similarity index 96% rename from internal/api/users_me_admin_handler.go rename to internal/api/self/admin_handler.go index 0dc6ae2..25f1235 100644 --- a/internal/api/users_me_admin_handler.go +++ b/internal/api/self/admin_handler.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -13,7 +13,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersMeAdminHandler(c *gin.Context) { +func adminHandler(c *gin.Context) { var uuid = c.PostForm("user_id") var providedApplicationKey = c.PostForm("application_key") diff --git a/internal/api/users_me_admin_handler_test.go b/internal/api/self/admin_handler_test.go similarity index 85% rename from internal/api/users_me_admin_handler_test.go rename to internal/api/self/admin_handler_test.go index 2dd1631..0ec522e 100644 --- a/internal/api/users_me_admin_handler_test.go +++ b/internal/api/self/admin_handler_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -9,14 +9,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutHeader() { +func (s *SelfTestSuite) TestAdminHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "PATCH", "/api/users/me/admin", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerByUnauthenticatedUser() { +func (s *SelfTestSuite) TestAdminHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -28,12 +28,12 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerByUnauthenticatedUser() { s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutActionGrant() { +func (s *SelfTestSuite) TestAdminHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "PATCH", "/api/users/me/admin", header, cookie, nil) @@ -43,8 +43,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWhenFeatureIsDisabled() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestAdminHandlerWhenFeatureIsDisabled() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -63,8 +63,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWhenFeatureIsDisabled() { s.Equal("feature is not available at this time", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutKey() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestAdminHandlerWithoutKey() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -83,8 +83,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutKey() { s.Equal("application key is incorrect", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutCorrectKey() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestAdminHandlerWithoutCorrectKey() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -106,8 +106,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutCorrectKey() { s.Equal("application key is incorrect", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutUserId() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestAdminHandlerWithoutUserId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -129,8 +129,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithoutUserId() { s.Equal("must use valid UUID for identification", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithIncorrectUserId() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestAdminHandlerWithIncorrectUserId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -164,8 +164,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerWithIncorrectUserId() { s.Equal("must use valid UUID for identification", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerByAnotherUser() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestAdminHandlerByAnotherUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -186,8 +186,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeAdminHandlerByAnotherUser() { s.Require().Equal(401, w.Code) } -func (s *ApiHandlerTestSuite) TestUsersMeAdminHandler() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestAdminHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_me_emails_create_handler.go b/internal/api/self/emails_create_handler.go similarity index 94% rename from internal/api/users_me_emails_create_handler.go rename to internal/api/self/emails_create_handler.go index 30bffc8..ff41710 100644 --- a/internal/api/users_me_emails_create_handler.go +++ b/internal/api/self/emails_create_handler.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -12,7 +12,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersMeEmailsCreateHandler(c *gin.Context) { +func emailsCreateHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/users_me_emails_create_handler_test.go b/internal/api/self/emails_create_handler_test.go similarity index 80% rename from internal/api/users_me_emails_create_handler_test.go rename to internal/api/self/emails_create_handler_test.go index 6c2552c..a3f173f 100644 --- a/internal/api/users_me_emails_create_handler_test.go +++ b/internal/api/self/emails_create_handler_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -11,14 +11,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandlerWithoutHeader() { +func (s *SelfTestSuite) TestEmailsCreateHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "POST", "/api/users/me/emails", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandlerByUnauthenticatedUser() { +func (s *SelfTestSuite) TestEmailsCreateHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -29,12 +29,12 @@ func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandlerByUnauthenticatedUse s.Contains(r.Body, "User must be authenticated") } -func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandlerWithoutActionGrant() { +func (s *SelfTestSuite) TestEmailsCreateHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "POST", "/api/users/me/emails", header, cookie, nil) @@ -44,8 +44,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandlerWithoutActionGrant() s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandlerWithoutData() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestEmailsCreateHandlerWithoutData() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -64,8 +64,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandlerWithoutData() { s.Equal("Email was not created", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersMeEmailsCreateHandler() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestEmailsCreateHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_me_emails_list_handler.go b/internal/api/self/emails_list_handler.go similarity index 93% rename from internal/api/users_me_emails_list_handler.go rename to internal/api/self/emails_list_handler.go index 6d7730a..c68945a 100644 --- a/internal/api/users_me_emails_list_handler.go +++ b/internal/api/self/emails_list_handler.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -12,7 +12,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersMeEmailsListHandler(c *gin.Context) { +func emailsListHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/users_me_emails_list_handler_test.go b/internal/api/self/emails_list_handler_test.go similarity index 81% rename from internal/api/users_me_emails_list_handler_test.go rename to internal/api/self/emails_list_handler_test.go index 793ca17..e8aa05c 100644 --- a/internal/api/users_me_emails_list_handler_test.go +++ b/internal/api/self/emails_list_handler_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -7,14 +7,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMeEmailsListHandlerWithoutHeader() { +func (s *SelfTestSuite) TestEmailsListHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "GET", "/api/users/me/emails", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersMeEmailsListHandlerByUnauthenticatedUser() { +func (s *SelfTestSuite) TestEmailsListHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -26,12 +26,12 @@ func (s *ApiHandlerTestSuite) TestUsersMeEmailsListHandlerByUnauthenticatedUser( s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersMeEmailsListHandlerWithoutActionGrant() { +func (s *SelfTestSuite) TestEmailsListHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "GET", "/api/users/me/emails", header, cookie, nil) @@ -41,8 +41,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeEmailsListHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeEmailsListHandler() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestEmailsListHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/self/endpoints.go b/internal/api/self/endpoints.go new file mode 100644 index 0000000..e0eef03 --- /dev/null +++ b/internal/api/self/endpoints.go @@ -0,0 +1,58 @@ +package self + +import ( + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/api/helpers" +) + +// ExposeRoutes defines and exposes HTTP routes for a given gin.RouterGroup +// +// in the REST API scope, for the users resource +func ExposeRoutes(router *gin.RouterGroup) { + group := router.Group("/users/me") + group.Use(helpers.RequiresConformance()) + { + // Requires X-Requested-By and Origin (same-origin policy) + group.POST("/requests", requestsHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + group.GET("/workspace", + helpers.RequiresApplicationSession(), + workspaceHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + group.PATCH("/password", passwordHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + group.PATCH("/admin", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + adminHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + group.GET("/emails", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + emailsListHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + group.POST("/emails", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + emailsCreateHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + group.GET("/settings", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + settingsListHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + group.PATCH("/settings", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + settingsPatchHandler) + } +} diff --git a/internal/api/users_me_password_handler.go b/internal/api/self/password_handler.go similarity index 96% rename from internal/api/users_me_password_handler.go rename to internal/api/self/password_handler.go index a9d6cbf..785353d 100644 --- a/internal/api/users_me_password_handler.go +++ b/internal/api/self/password_handler.go @@ -1,4 +1,4 @@ -package api +package self import ( "net/http" @@ -10,7 +10,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersMePasswordHandler(c *gin.Context) { +func passwordHandler(c *gin.Context) { var bearer = c.PostForm("_") var newPassword = c.PostForm("new_password") var passwordConfirmation = c.PostForm("password_confirmation") diff --git a/internal/api/users_me_password_handler_test.go b/internal/api/self/password_handler_test.go similarity index 90% rename from internal/api/users_me_password_handler_test.go rename to internal/api/self/password_handler_test.go index 7077917..b73808f 100644 --- a/internal/api/users_me_password_handler_test.go +++ b/internal/api/self/password_handler_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "net/http" @@ -10,14 +10,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerWithoutHeader() { +func (s *SelfTestSuite) TestPasswordHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "PATCH", "/api/users/me/password", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerWithoutToken() { +func (s *SelfTestSuite) TestPasswordHandlerWithoutToken() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -29,7 +29,7 @@ func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerWithoutToken() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerWithoutData() { +func (s *SelfTestSuite) TestPasswordHandlerWithoutData() { user := s.Factory.NewUser().Model actionToken := s.Factory.NewAction(user).Model.Token s.Require().Equal(len(actionToken), 64) @@ -59,7 +59,7 @@ func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerWithoutData() { s.Equal("invalid password update attempt", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerIncorrectData() { +func (s *SelfTestSuite) TestPasswordHandlerIncorrectData() { user := s.Factory.NewUser().Model actionToken := s.Factory.NewAction(user).Model.Token s.Require().Equal(len(actionToken), 64) @@ -92,7 +92,7 @@ func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerIncorrectData() { s.Equal("invalid password update attempt", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerWithoutPermission() { +func (s *SelfTestSuite) TestPasswordHandlerWithoutPermission() { user := s.Factory.NewUser().Model actionToken := s.Factory.NewActionWithoutPermissions(user).Model.Token s.Require().Equal(len(actionToken), 64) @@ -114,7 +114,7 @@ func (s *ApiHandlerTestSuite) TestUsersMePasswordHandlerWithoutPermission() { s.Equal("invalid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMePasswordHandler() { +func (s *SelfTestSuite) TestPasswordHandler() { user := s.Factory.NewUser().Model actionToken := s.Factory.NewAction(user).Model.Token s.Require().Equal(len(actionToken), 64) diff --git a/internal/api/users_me_requests_handler.go b/internal/api/self/requests_handler.go similarity index 98% rename from internal/api/users_me_requests_handler.go rename to internal/api/self/requests_handler.go index 2aece99..a272cd5 100644 --- a/internal/api/users_me_requests_handler.go +++ b/internal/api/self/requests_handler.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -20,7 +20,7 @@ const ( emailVerificationType = "email_verification" ) -func usersMeRequestsHandler(c *gin.Context) { +func requestsHandler(c *gin.Context) { requestType := c.PostForm("request_type") availableTypes := []string{passwordType, secretsType, emailVerificationType} if !slices.Contains(availableTypes, requestType) { diff --git a/internal/api/users_me_requests_handler_test.go b/internal/api/self/requests_handler_test.go similarity index 89% rename from internal/api/users_me_requests_handler_test.go rename to internal/api/self/requests_handler_test.go index 86be17c..96bfc1d 100644 --- a/internal/api/users_me_requests_handler_test.go +++ b/internal/api/self/requests_handler_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "net/http" @@ -10,14 +10,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithoutHeader() { +func (s *SelfTestSuite) TestRequestsHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "POST", "/api/users/me/requests", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithoutRequestType() { +func (s *SelfTestSuite) TestRequestsHandlerWithoutRequestType() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -29,7 +29,7 @@ func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithoutRequestType() { s.Equal("request type not available", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithInvalidRequest() { +func (s *SelfTestSuite) TestRequestsHandlerWithInvalidRequest() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -44,7 +44,7 @@ func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithInvalidRequest() { s.Equal("request type not available", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithUnknownHolder() { +func (s *SelfTestSuite) TestRequestsHandlerWithUnknownHolder() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -82,7 +82,7 @@ func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithUnknownHolder() { s.Equal("must use valid holder field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithKnownHolder() { +func (s *SelfTestSuite) TestRequestsHandlerWithKnownHolder() { userTest := s.Factory.NewUser() header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, @@ -111,7 +111,7 @@ func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithKnownHolder() { s.Require().Equal(204, w.Code) } -func (s *ApiHandlerTestSuite) TestUsersMeRequestsHandlerWithoutEmailForValidation() { +func (s *SelfTestSuite) TestRequestsHandlerWithoutEmailForValidation() { userTest := s.Factory.NewUser() header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, diff --git a/internal/api/self/self_test.go b/internal/api/self/self_test.go new file mode 100644 index 0000000..8bfa01f --- /dev/null +++ b/internal/api/self/self_test.go @@ -0,0 +1,32 @@ +package self + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + + "github.com/earaujoassis/space/test/unit" +) + +type SelfTestSuite struct { + unit.ApiBaseTestSuite + Router *gin.Engine +} + +func (s *SelfTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *SelfTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() + group := s.Router.Group("/api") + ExposeRoutes(group) +} + +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(SelfTestSuite)) +} diff --git a/internal/api/users_me_settings_list_handler.go b/internal/api/self/settings_list_handler.go similarity index 93% rename from internal/api/users_me_settings_list_handler.go rename to internal/api/self/settings_list_handler.go index 1d00539..d41d8c4 100644 --- a/internal/api/users_me_settings_list_handler.go +++ b/internal/api/self/settings_list_handler.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -12,7 +12,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersMeSettingsListHandler(c *gin.Context) { +func settingsListHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/users_me_settings_list_handler_test.go b/internal/api/self/settings_list_handler_test.go similarity index 79% rename from internal/api/users_me_settings_list_handler_test.go rename to internal/api/self/settings_list_handler_test.go index be21ca5..1ac1b31 100644 --- a/internal/api/users_me_settings_list_handler_test.go +++ b/internal/api/self/settings_list_handler_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -7,14 +7,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMeSettingsListHandlerWithoutHeader() { +func (s *SelfTestSuite) TestSettingsListHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "GET", "/api/users/me/settings", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsListHandlerByUnauthenticatedUser() { +func (s *SelfTestSuite) TestSettingsListHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -26,12 +26,12 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsListHandlerByUnauthenticatedUse s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsListHandlerWithoutActionGrant() { +func (s *SelfTestSuite) TestSettingsListHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "GET", "/api/users/me/settings", header, cookie, nil) @@ -41,8 +41,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsListHandlerWithoutActionGrant() s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsListHandler() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestSettingsListHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_me_settings_patch_handler.go b/internal/api/self/settings_patch_handler.go similarity index 97% rename from internal/api/users_me_settings_patch_handler.go rename to internal/api/self/settings_patch_handler.go index 8180bf9..7fe7b40 100644 --- a/internal/api/users_me_settings_patch_handler.go +++ b/internal/api/self/settings_patch_handler.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -17,7 +17,7 @@ const ( especialKeyEmailAddress string = "notifications.system-email-notifications.email-address" ) -func usersMeSettingsPatchHandler(c *gin.Context) { +func settingsPatchHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/users_me_settings_patch_handler_test.go b/internal/api/self/settings_patch_handler_test.go similarity index 89% rename from internal/api/users_me_settings_patch_handler_test.go rename to internal/api/self/settings_patch_handler_test.go index 24ca5ce..8efd3d4 100644 --- a/internal/api/users_me_settings_patch_handler_test.go +++ b/internal/api/self/settings_patch_handler_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "fmt" @@ -9,14 +9,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithoutHeader() { +func (s *SelfTestSuite) TestSettingsPatchHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "PATCH", "/api/users/me/settings", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerByUnauthenticatedUser() { +func (s *SelfTestSuite) TestSettingsPatchHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -28,12 +28,12 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerByUnauthenticatedUs s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithoutActionGrant() { +func (s *SelfTestSuite) TestSettingsPatchHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "PATCH", "/api/users/me/settings", header, cookie, nil) @@ -43,8 +43,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithoutActionGrant( s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithoutKey() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestSettingsPatchHandlerWithoutKey() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -62,8 +62,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithoutKey() { s.Equal("invalid setting key", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithInvalidKey() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestSettingsPatchHandlerWithInvalidKey() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -93,8 +93,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithInvalidKey() { s.Equal("invalid setting", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithInvalidValue() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestSettingsPatchHandlerWithInvalidValue() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -125,8 +125,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithInvalidValue() s.Equal("invalid setting", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithInvalidEmail() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestSettingsPatchHandlerWithInvalidEmail() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -179,8 +179,8 @@ func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandlerWithInvalidEmail() s.Equal("invalid setting", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersMeSettingsPatchHandler() { - cookie := s.createSessionCookie(true) +func (s *SelfTestSuite) TestSettingsPatchHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_me_workspace.go b/internal/api/self/workspace.go similarity index 94% rename from internal/api/users_me_workspace.go rename to internal/api/self/workspace.go index dcbe9a0..40aaa72 100644 --- a/internal/api/users_me_workspace.go +++ b/internal/api/self/workspace.go @@ -1,4 +1,4 @@ -package api +package self import ( "net/http" @@ -11,7 +11,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersMeWorkspaceHandler(c *gin.Context) { +func workspaceHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) fg := ioc.GetFeatureGate(c) user := c.MustGet("User").(models.User) diff --git a/internal/api/users_me_workspace_test.go b/internal/api/self/workspace_test.go similarity index 88% rename from internal/api/users_me_workspace_test.go rename to internal/api/self/workspace_test.go index 55106c7..05b0ad3 100644 --- a/internal/api/users_me_workspace_test.go +++ b/internal/api/self/workspace_test.go @@ -1,4 +1,4 @@ -package api +package self import ( "net/http" @@ -6,7 +6,7 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersMeWorkspaceHandler() { +func (s *SelfTestSuite) TestWorkspaceHandler() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -18,7 +18,7 @@ func (s *ApiHandlerTestSuite) TestUsersMeWorkspaceHandler() { r = utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.Contains(r.Body, "User must be authenticated") - cookie := s.createSessionCookie(false) + cookie := s.CreateSessionCookie(false) s.NotNil(cookie) w = s.PerformRequest(s.Router, "GET", "/api/users/me/workspace", header, cookie, nil) r = utils.ParseResponse(w.Result(), nil) diff --git a/internal/api/services_create_handler.go b/internal/api/services/create_handler.go similarity index 95% rename from internal/api/services_create_handler.go rename to internal/api/services/create_handler.go index 68a2103..1b80ade 100644 --- a/internal/api/services_create_handler.go +++ b/internal/api/services/create_handler.go @@ -1,4 +1,4 @@ -package api +package services import ( "fmt" @@ -12,7 +12,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func servicesCreateHandler(c *gin.Context) { +func createHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/services_create_handler_test.go b/internal/api/services/create_handler_test.go similarity index 85% rename from internal/api/services_create_handler_test.go rename to internal/api/services/create_handler_test.go index 2d5c61e..f7e3695 100644 --- a/internal/api/services_create_handler_test.go +++ b/internal/api/services/create_handler_test.go @@ -1,4 +1,4 @@ -package api +package services import ( "fmt" @@ -11,14 +11,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestServicesCreateHandlerWithoutHeader() { +func (s *ServicesTestSuite) TestCreateHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "POST", "/api/services", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestServicesCreateHandlerByUnauthenticatedUser() { +func (s *ServicesTestSuite) TestCreateHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -29,12 +29,12 @@ func (s *ApiHandlerTestSuite) TestServicesCreateHandlerByUnauthenticatedUser() { s.Contains(r.Body, "User must be authenticated") } -func (s *ApiHandlerTestSuite) TestServicesCreateHandlerWithoutActionGrant() { +func (s *ServicesTestSuite) TestCreateHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "POST", "/api/services", header, cookie, nil) @@ -44,8 +44,8 @@ func (s *ApiHandlerTestSuite) TestServicesCreateHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestServicesCreateHandlerWithoutData() { - cookie := s.createSessionCookie(true) +func (s *ServicesTestSuite) TestCreateHandlerWithoutData() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -64,8 +64,8 @@ func (s *ApiHandlerTestSuite) TestServicesCreateHandlerWithoutData() { s.Equal("Service was not created", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestServicesCreateHandlerByAdminUser() { - cookie := s.createSessionCookie(true) +func (s *ServicesTestSuite) TestCreateHandlerByAdminUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -93,8 +93,8 @@ func (s *ApiHandlerTestSuite) TestServicesCreateHandlerByAdminUser() { s.Require().Equal(204, w.Code) } -func (s *ApiHandlerTestSuite) TestServicesCreateHandlerByCommonUser() { - cookie := s.createSessionCookie(false) +func (s *ServicesTestSuite) TestCreateHandlerByCommonUser() { + cookie := s.CreateSessionCookie(false) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/services/endpoints.go b/internal/api/services/endpoints.go new file mode 100644 index 0000000..0c895e8 --- /dev/null +++ b/internal/api/services/endpoints.go @@ -0,0 +1,26 @@ +package services + +import ( + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/api/helpers" +) + +// ExposeRoutes defines and exposes HTTP routes for a given gin.RouterGroup +// +// in the REST API scope, for the services resource +func ExposeRoutes(router *gin.RouterGroup) { + servicesRoutes := router.Group("/services") + servicesRoutes.Use(helpers.RequiresConformance()) + servicesRoutes.Use(helpers.RequiresApplicationSession()) + servicesRoutes.Use(helpers.ActionTokenBearerAuthorization()) + { + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + servicesRoutes.GET("", listHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + servicesRoutes.POST("", createHandler) + } +} diff --git a/internal/api/services_list_handler.go b/internal/api/services/list_handler.go similarity index 93% rename from internal/api/services_list_handler.go rename to internal/api/services/list_handler.go index 334789f..6cc79ad 100644 --- a/internal/api/services_list_handler.go +++ b/internal/api/services/list_handler.go @@ -1,4 +1,4 @@ -package api +package services import ( "fmt" @@ -12,7 +12,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func servicesListHandler(c *gin.Context) { +func listHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) diff --git a/internal/api/services_list_handler_test.go b/internal/api/services/list_handler_test.go similarity index 82% rename from internal/api/services_list_handler_test.go rename to internal/api/services/list_handler_test.go index 02484b1..fefea07 100644 --- a/internal/api/services_list_handler_test.go +++ b/internal/api/services/list_handler_test.go @@ -1,4 +1,4 @@ -package api +package services import ( "fmt" @@ -7,14 +7,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestServicesListHandlerWithoutHeader() { +func (s *ServicesTestSuite) TestListHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "GET", "/api/services", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestServicesListHandlerByUnauthenticatedUser() { +func (s *ServicesTestSuite) TestListHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -26,12 +26,12 @@ func (s *ApiHandlerTestSuite) TestServicesListHandlerByUnauthenticatedUser() { s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestServicesListHandlerWithoutActionGrant() { +func (s *ServicesTestSuite) TestListHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) w := s.PerformRequest(s.Router, "GET", "/api/services", header, cookie, nil) @@ -41,8 +41,8 @@ func (s *ApiHandlerTestSuite) TestServicesListHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestServicesListHandlerByAdminUser() { - cookie := s.createSessionCookie(true) +func (s *ServicesTestSuite) TestListHandlerByAdminUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -66,8 +66,8 @@ func (s *ApiHandlerTestSuite) TestServicesListHandlerByAdminUser() { s.NotEmpty(service["id"]) } -func (s *ApiHandlerTestSuite) TestServicesListHandlerByCommonUser() { - cookie := s.createSessionCookie(false) +func (s *ServicesTestSuite) TestListHandlerByCommonUser() { + cookie := s.CreateSessionCookie(false) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/services/services_test.go b/internal/api/services/services_test.go new file mode 100644 index 0000000..4a7938c --- /dev/null +++ b/internal/api/services/services_test.go @@ -0,0 +1,32 @@ +package services + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + + "github.com/earaujoassis/space/test/unit" +) + +type ServicesTestSuite struct { + unit.ApiBaseTestSuite + Router *gin.Engine +} + +func (s *ServicesTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *ServicesTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() + group := s.Router.Group("/api") + ExposeRoutes(group) +} + +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(ServicesTestSuite)) +} diff --git a/internal/api/sessions_create_handler.go b/internal/api/sessions/create_handler.go similarity index 97% rename from internal/api/sessions_create_handler.go rename to internal/api/sessions/create_handler.go index f7ae000..6455d22 100644 --- a/internal/api/sessions_create_handler.go +++ b/internal/api/sessions/create_handler.go @@ -1,4 +1,4 @@ -package api +package sessions import ( "net/http" @@ -14,7 +14,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func sessionsCreateHandler(c *gin.Context) { +func createHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) rls := ioc.GetRateLimitService(c) diff --git a/internal/api/sessions_create_handler_test.go b/internal/api/sessions/create_handler_test.go similarity index 92% rename from internal/api/sessions_create_handler_test.go rename to internal/api/sessions/create_handler_test.go index d9b5e8f..efc8dfe 100644 --- a/internal/api/sessions_create_handler_test.go +++ b/internal/api/sessions/create_handler_test.go @@ -1,4 +1,4 @@ -package api +package sessions import ( "net/http" @@ -8,14 +8,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestSessionsCreateHandlerWithoutHeader() { +func (s *SessionsTestSuite) TestCreateHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "POST", "/api/sessions", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestSessionsCreateHandlerWithoutData() { +func (s *SessionsTestSuite) TestCreateHandlerWithoutData() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -27,7 +27,7 @@ func (s *ApiHandlerTestSuite) TestSessionsCreateHandlerWithoutData() { s.Equal("must use valid holder field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestSessionsCreateHandler() { +func (s *SessionsTestSuite) TestCreateHandler() { userTest := s.Factory.NewUser() header := &http.Header{ diff --git a/internal/api/sessions/endpoints.go b/internal/api/sessions/endpoints.go new file mode 100644 index 0000000..985f631 --- /dev/null +++ b/internal/api/sessions/endpoints.go @@ -0,0 +1,22 @@ +package sessions + +import ( + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/api/helpers" +) + +// ExposeRoutes defines and exposes HTTP routes for a given gin.RouterGroup +// +// in the REST API scope, for the sessions resource +func ExposeRoutes(router *gin.RouterGroup) { + sessionsRoutes := router.Group("/sessions") + sessionsRoutes.Use(helpers.RequiresConformance()) + { + // Requires X-Requested-By and Origin (same-origin policy) + sessionsRoutes.POST("", createHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + sessionsRoutes.POST("/requests", requestsHandler) + } +} diff --git a/internal/api/sessions_requests_handler.go b/internal/api/sessions/requests_handler.go similarity index 97% rename from internal/api/sessions_requests_handler.go rename to internal/api/sessions/requests_handler.go index 47e4777..6b21a8b 100644 --- a/internal/api/sessions_requests_handler.go +++ b/internal/api/sessions/requests_handler.go @@ -1,4 +1,4 @@ -package api +package sessions import ( "fmt" @@ -19,7 +19,7 @@ const ( passwordlessSigninType = "passwordless_signin" ) -func sessionsRequestsHandler(c *gin.Context) { +func requestsHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) rls := ioc.GetRateLimitService(c) diff --git a/internal/api/sessions_requests_handler_test.go b/internal/api/sessions/requests_handler_test.go similarity index 84% rename from internal/api/sessions_requests_handler_test.go rename to internal/api/sessions/requests_handler_test.go index 007d3d9..190a8fc 100644 --- a/internal/api/sessions_requests_handler_test.go +++ b/internal/api/sessions/requests_handler_test.go @@ -1,4 +1,4 @@ -package api +package sessions import ( "net/http" @@ -10,14 +10,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithoutHeader() { +func (s *SessionsTestSuite) TestRequestsHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "POST", "/api/sessions/requests", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithoutRequestType() { +func (s *SessionsTestSuite) TestRequestsHandlerWithoutRequestType() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -29,7 +29,7 @@ func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithoutRequestType() { s.Equal("request type not available", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithoutHolder() { +func (s *SessionsTestSuite) TestRequestsHandlerWithoutHolder() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -44,7 +44,7 @@ func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithoutHolder() { s.Equal("must use valid holder field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithInvalidRequest() { +func (s *SessionsTestSuite) TestRequestsHandlerWithInvalidRequest() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -60,7 +60,7 @@ func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithInvalidRequest() { s.Equal("request type not available", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithUnknownHolder() { +func (s *SessionsTestSuite) TestRequestsHandlerWithUnknownHolder() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -73,7 +73,7 @@ func (s *ApiHandlerTestSuite) TestSessionsRequestsHandlerWithUnknownHolder() { s.Require().Equal(204, w.Code) } -func (s *ApiHandlerTestSuite) TestSessionsRequestsHandler() { +func (s *SessionsTestSuite) TestRequestsHandler() { userTest := s.Factory.NewUser() header := &http.Header{ diff --git a/internal/api/sessions/sessions_test.go b/internal/api/sessions/sessions_test.go new file mode 100644 index 0000000..4ad9d51 --- /dev/null +++ b/internal/api/sessions/sessions_test.go @@ -0,0 +1,32 @@ +package sessions + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + + "github.com/earaujoassis/space/test/unit" +) + +type SessionsTestSuite struct { + unit.ApiBaseTestSuite + Router *gin.Engine +} + +func (s *SessionsTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *SessionsTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() + group := s.Router.Group("/api") + ExposeRoutes(group) +} + +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(SessionsTestSuite)) +} diff --git a/internal/api/users_clients_list_handler.go b/internal/api/users/clients_list_handler.go similarity index 95% rename from internal/api/users_clients_list_handler.go rename to internal/api/users/clients_list_handler.go index ab80b34..ea93df7 100644 --- a/internal/api/users_clients_list_handler.go +++ b/internal/api/users/clients_list_handler.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -13,7 +13,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersClientsListHandler(c *gin.Context) { +func clientsListHandler(c *gin.Context) { var uuid = c.Param("user_id") if !security.ValidUUID(uuid) { diff --git a/internal/api/users_clients_list_handler_test.go b/internal/api/users/clients_list_handler_test.go similarity index 83% rename from internal/api/users_clients_list_handler_test.go rename to internal/api/users/clients_list_handler_test.go index e171f85..f7552d8 100644 --- a/internal/api/users_clients_list_handler_test.go +++ b/internal/api/users/clients_list_handler_test.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerWithoutHeader() { +func (s *UsersTestSuite) TestClientsListHandlerWithoutHeader() { uuid := s.Factory.NewUser().Model.UUID path := fmt.Sprintf("/api/users/%s/clients", uuid) w := s.PerformRequest(s.Router, "GET", path, nil, nil, nil) @@ -16,7 +16,7 @@ func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerWithoutHeader() { s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerByUnauthenticatedUser() { +func (s *UsersTestSuite) TestClientsListHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -31,12 +31,12 @@ func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerByUnauthenticatedUser() s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerWithoutActionGrant() { +func (s *UsersTestSuite) TestClientsListHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) uuid := s.Factory.GetAvailableUser().UUID @@ -48,8 +48,8 @@ func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerByAnotherUser() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestClientsListHandlerByAnotherUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -69,8 +69,8 @@ func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerByAnotherUser() { s.Equal("access_denied", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerInvalidId() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestClientsListHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -90,8 +90,8 @@ func (s *ApiHandlerTestSuite) TestUsersClientsListHandlerInvalidId() { s.Require().Equal(400, w.Code) } -func (s *ApiHandlerTestSuite) TestUsersClientsListHandler() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestClientsListHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_clients_revoke_handler.go b/internal/api/users/clients_revoke_handler.go similarity index 95% rename from internal/api/users_clients_revoke_handler.go rename to internal/api/users/clients_revoke_handler.go index 7dd99ca..d436e23 100644 --- a/internal/api/users_clients_revoke_handler.go +++ b/internal/api/users/clients_revoke_handler.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -13,7 +13,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersClientsRevokeHandler(c *gin.Context) { +func clientsRevokeHandler(c *gin.Context) { var userUUID = c.Param("user_id") var clientUUID = c.Param("client_id") diff --git a/internal/api/users_clients_revoke_handler_test.go b/internal/api/users/clients_revoke_handler_test.go similarity index 86% rename from internal/api/users_clients_revoke_handler_test.go rename to internal/api/users/clients_revoke_handler_test.go index 8c430c8..d591599 100644 --- a/internal/api/users_clients_revoke_handler_test.go +++ b/internal/api/users/clients_revoke_handler_test.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerWithoutHeader() { +func (s *UsersTestSuite) TestClientsRevokeHandlerWithoutHeader() { userUuid := s.Factory.NewUser().Model.UUID clientUuid := s.Factory.NewClient().Model.UUID path := fmt.Sprintf("/api/users/%s/clients/%s/revoke", userUuid, clientUuid) @@ -17,7 +17,7 @@ func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerWithoutHeader() { s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerByUnauthenticatedUser() { +func (s *UsersTestSuite) TestClientsRevokeHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -33,12 +33,12 @@ func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerByUnauthenticatedUser s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerWithoutActionGrant() { +func (s *UsersTestSuite) TestClientsRevokeHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) userUuid := s.Factory.GetAvailableUser().UUID @@ -51,8 +51,8 @@ func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerWithoutActionGrant() s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerByAnotherUser() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestClientsRevokeHandlerByAnotherUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -73,8 +73,8 @@ func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerByAnotherUser() { s.Equal("access_denied", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerInvalidId() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestClientsRevokeHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -105,8 +105,8 @@ func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandlerInvalidId() { s.Require().Equal(400, w.Code) } -func (s *ApiHandlerTestSuite) TestUsersClientsRevokeHandler() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestClientsRevokeHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_create_handler.go b/internal/api/users/create_handler.go similarity index 97% rename from internal/api/users_create_handler.go rename to internal/api/users/create_handler.go index 22bec18..2a20fc1 100644 --- a/internal/api/users_create_handler.go +++ b/internal/api/users/create_handler.go @@ -1,4 +1,4 @@ -package api +package users import ( "bytes" @@ -15,7 +15,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersCreateHandler(c *gin.Context) { +func createHandler(c *gin.Context) { var buf bytes.Buffer var imageData string diff --git a/internal/api/users_create_handler_test.go b/internal/api/users/create_handler_test.go similarity index 86% rename from internal/api/users_create_handler_test.go rename to internal/api/users/create_handler_test.go index f5f3fa7..062451b 100644 --- a/internal/api/users_create_handler_test.go +++ b/internal/api/users/create_handler_test.go @@ -1,4 +1,4 @@ -package api +package users import ( "net/http" @@ -10,14 +10,14 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersCreateHandlerWithoutHeader() { +func (s *UsersTestSuite) TestCreateHandlerWithoutHeader() { w := s.PerformRequest(s.Router, "POST", "/api/users", nil, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(400, w.Code) s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersCreateHandlerWhenFeatureIsDisabled() { +func (s *UsersTestSuite) TestCreateHandlerWhenFeatureIsDisabled() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -30,7 +30,7 @@ func (s *ApiHandlerTestSuite) TestUsersCreateHandlerWhenFeatureIsDisabled() { s.Equal("feature is not available at this time", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersCreateHandlerWithoutData() { +func (s *UsersTestSuite) TestCreateHandlerWithoutData() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -43,7 +43,7 @@ func (s *ApiHandlerTestSuite) TestUsersCreateHandlerWithoutData() { s.Equal("missing essential fields", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersCreateHandler() { +func (s *UsersTestSuite) TestCreateHandler() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } diff --git a/internal/api/users/endpoints.go b/internal/api/users/endpoints.go new file mode 100644 index 0000000..6fb85b0 --- /dev/null +++ b/internal/api/users/endpoints.go @@ -0,0 +1,54 @@ +package users + +import ( + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/api/helpers" +) + +// ExposeRoutes defines and exposes HTTP routes for a given gin.RouterGroup +// +// in the REST API scope, for the users resource +func ExposeRoutes(router *gin.RouterGroup) { + usersRoutes := router.Group("/users") + usersRoutes.Use(helpers.RequiresConformance()) + { + // Requires X-Requested-By and Origin (same-origin policy) + usersRoutes.POST("", createHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + usersRoutes.GET("/:user_id/profile", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + profileHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + usersRoutes.GET("/:user_id/clients", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + clientsListHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + usersRoutes.DELETE("/:user_id/clients/:client_id/revoke", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + clientsRevokeHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + usersRoutes.GET("/:user_id/sessions", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + sessionsListHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + usersRoutes.DELETE("/:user_id/sessions/:session_id/revoke", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + sessionsRevokeHandler) + } +} diff --git a/internal/api/users_profile_handler.go b/internal/api/users/profile_handler.go similarity index 96% rename from internal/api/users_profile_handler.go rename to internal/api/users/profile_handler.go index d1a5af1..4b3e749 100644 --- a/internal/api/users_profile_handler.go +++ b/internal/api/users/profile_handler.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -13,7 +13,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersProfileHandler(c *gin.Context) { +func profileHandler(c *gin.Context) { var uuid = c.Param("user_id") if !security.ValidUUID(uuid) { diff --git a/internal/api/users_profile_handler_test.go b/internal/api/users/profile_handler_test.go similarity index 83% rename from internal/api/users_profile_handler_test.go rename to internal/api/users/profile_handler_test.go index 8ad9f3e..749721d 100644 --- a/internal/api/users_profile_handler_test.go +++ b/internal/api/users/profile_handler_test.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersProfileHandlerWithoutHeader() { +func (s *UsersTestSuite) TestProfileHandlerWithoutHeader() { uuid := s.Factory.NewUser().Model.UUID path := fmt.Sprintf("/api/users/%s/profile", uuid) w := s.PerformRequest(s.Router, "GET", path, nil, nil, nil) @@ -16,7 +16,7 @@ func (s *ApiHandlerTestSuite) TestUsersProfileHandlerWithoutHeader() { s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersProfileHandlerByUnauthenticatedUser() { +func (s *UsersTestSuite) TestProfileHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -31,12 +31,12 @@ func (s *ApiHandlerTestSuite) TestUsersProfileHandlerByUnauthenticatedUser() { s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersProfileHandlerWithoutActionGrant() { +func (s *UsersTestSuite) TestProfileHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) uuid := s.Factory.GetAvailableUser().UUID @@ -48,8 +48,8 @@ func (s *ApiHandlerTestSuite) TestUsersProfileHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersProfileHandlerByAnotherUser() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestProfileHandlerByAnotherUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -69,8 +69,8 @@ func (s *ApiHandlerTestSuite) TestUsersProfileHandlerByAnotherUser() { s.Equal("access_denied", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersProfileHandlerInvalidId() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestProfileHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -90,8 +90,8 @@ func (s *ApiHandlerTestSuite) TestUsersProfileHandlerInvalidId() { s.Require().Equal(400, w.Code) } -func (s *ApiHandlerTestSuite) TestUsersProfileHandler() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestProfileHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_sessions_list_handler.go b/internal/api/users/sessions_list_handler.go similarity index 95% rename from internal/api/users_sessions_list_handler.go rename to internal/api/users/sessions_list_handler.go index 3ab4a2b..9942a5f 100644 --- a/internal/api/users_sessions_list_handler.go +++ b/internal/api/users/sessions_list_handler.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -13,7 +13,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersSessionsListHandler(c *gin.Context) { +func sessionsListHandler(c *gin.Context) { var userUUID = c.Param("user_id") if !security.ValidUUID(userUUID) { diff --git a/internal/api/users_sessions_list_handler_test.go b/internal/api/users/sessions_list_handler_test.go similarity index 83% rename from internal/api/users_sessions_list_handler_test.go rename to internal/api/users/sessions_list_handler_test.go index 41bd720..3094858 100644 --- a/internal/api/users_sessions_list_handler_test.go +++ b/internal/api/users/sessions_list_handler_test.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerWithoutHeader() { +func (s *UsersTestSuite) TestSessionsListHandlerWithoutHeader() { uuid := s.Factory.NewUser().Model.UUID path := fmt.Sprintf("/api/users/%s/sessions", uuid) w := s.PerformRequest(s.Router, "GET", path, nil, nil, nil) @@ -16,7 +16,7 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerWithoutHeader() { s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerByUnauthenticatedUser() { +func (s *UsersTestSuite) TestSessionsListHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -31,12 +31,12 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerByUnauthenticatedUser( s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerWithoutActionGrant() { +func (s *UsersTestSuite) TestSessionsListHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) uuid := s.Factory.GetAvailableUser().UUID @@ -48,8 +48,8 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerWithoutActionGrant() { s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerByAnotherUser() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestSessionsListHandlerByAnotherUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -69,8 +69,8 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerByAnotherUser() { s.Equal("access_denied", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerInvalidId() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestSessionsListHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -90,8 +90,8 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsListHandlerInvalidId() { s.Require().Equal(400, w.Code) } -func (s *ApiHandlerTestSuite) TestUsersSessionsListHandler() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestSessionsListHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users_sessions_revoke_handler.go b/internal/api/users/sessions_revoke_handler.go similarity index 95% rename from internal/api/users_sessions_revoke_handler.go rename to internal/api/users/sessions_revoke_handler.go index d8a075e..1b843e6 100644 --- a/internal/api/users_sessions_revoke_handler.go +++ b/internal/api/users/sessions_revoke_handler.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -13,7 +13,7 @@ import ( "github.com/earaujoassis/space/internal/utils" ) -func usersSessionsRevokeHandler(c *gin.Context) { +func sessionsRevokeHandler(c *gin.Context) { var userUUID = c.Param("user_id") var sessionUUID = c.Param("session_id") diff --git a/internal/api/users_sessions_revoke_handler_test.go b/internal/api/users/sessions_revoke_handler_test.go similarity index 85% rename from internal/api/users_sessions_revoke_handler_test.go rename to internal/api/users/sessions_revoke_handler_test.go index 3488986..8b676f8 100644 --- a/internal/api/users_sessions_revoke_handler_test.go +++ b/internal/api/users/sessions_revoke_handler_test.go @@ -1,4 +1,4 @@ -package api +package users import ( "fmt" @@ -7,7 +7,7 @@ import ( "github.com/earaujoassis/space/test/utils" ) -func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerWithoutHeader() { +func (s *UsersTestSuite) TestSessionsRevokeHandlerWithoutHeader() { user := s.Factory.NewUser().Model session := s.Factory.NewApplicationSession(user).Model path := fmt.Sprintf("/api/users/%s/sessions/%s/revoke", user.UUID, session.UUID) @@ -17,7 +17,7 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerWithoutHeader() { s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") } -func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerByUnauthenticatedUser() { +func (s *UsersTestSuite) TestSessionsRevokeHandlerByUnauthenticatedUser() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } @@ -33,12 +33,12 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerByUnauthenticatedUse s.Equal("User must be authenticated", r.JSON["_message"]) } -func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerWithoutActionGrant() { +func (s *UsersTestSuite) TestSessionsRevokeHandlerWithoutActionGrant() { header := &http.Header{ "X-Requested-By": []string{"SpaceApi"}, } - cookie := s.createSessionCookie(true) + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.NewUser().Model @@ -51,8 +51,8 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerWithoutActionGrant() s.Equal("must use valid token field", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerByAnotherUser() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestSessionsRevokeHandlerByAnotherUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -73,8 +73,8 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerByAnotherUser() { s.Equal("access_denied", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerOfAnotherUser() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestSessionsRevokeHandlerOfAnotherUser() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -95,8 +95,8 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerOfAnotherUser() { s.Equal("access_denied", r.JSON["error"]) } -func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerInvalidId() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestSessionsRevokeHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token @@ -125,8 +125,8 @@ func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandlerInvalidId() { s.Require().Equal(400, w.Code) } -func (s *ApiHandlerTestSuite) TestUsersSessionsRevokeHandler() { - cookie := s.createSessionCookie(true) +func (s *UsersTestSuite) TestSessionsRevokeHandler() { + cookie := s.CreateSessionCookie(true) s.NotNil(cookie) user := s.Factory.GetAvailableUser() actionToken := s.Factory.NewAction(user).Model.Token diff --git a/internal/api/users/users_test.go b/internal/api/users/users_test.go new file mode 100644 index 0000000..192a781 --- /dev/null +++ b/internal/api/users/users_test.go @@ -0,0 +1,32 @@ +package users + +import ( + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/suite" + + "github.com/earaujoassis/space/test/unit" +) + +type UsersTestSuite struct { + unit.ApiBaseTestSuite + Router *gin.Engine +} + +func (s *UsersTestSuite) SetupSuite() { + s.ApiBaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *UsersTestSuite) SetupTest() { + s.ApiBaseTestSuite.SetupTest() + s.Router = s.SetupRouter() + group := s.Router.Group("/api") + ExposeRoutes(group) +} + +func TestClientsSuite(t *testing.T) { + suite.Run(t, new(UsersTestSuite)) +} diff --git a/test/unit/api_base_test_suite.go b/test/unit/api_base_test_suite.go new file mode 100644 index 0000000..b0e1ac2 --- /dev/null +++ b/test/unit/api_base_test_suite.go @@ -0,0 +1,92 @@ +package unit + +import ( + "net/http" + + "github.com/brianvoe/gofakeit/v7" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/ioc" + "github.com/earaujoassis/space/internal/models" + "github.com/earaujoassis/space/internal/security" + "github.com/earaujoassis/space/internal/shared" + "github.com/earaujoassis/space/test/factory" + "github.com/earaujoassis/space/test/utils" +) + +type ApiBaseTestSuite struct { + BaseTestSuite + Router *gin.Engine +} + +func (s *ApiBaseTestSuite) SetupSuite() { + s.BaseTestSuite.SetupSuite() + gin.SetMode(gin.TestMode) + s.Router = s.SetupRouter() +} + +func (s *ApiBaseTestSuite) SetupTest() { + s.BaseTestSuite.SetupTest() + s.Router = s.SetupRouter() +} + +func (s *ApiBaseTestSuite) SetupRouter() *gin.Engine { + router := gin.New() + security.SetTrustedProxies(router) + store := cookie.NewStore([]byte(s.Config.SessionSecret)) + store.Options(sessions.Options{Secure: false, HttpOnly: true}) + router.Use(sessions.Sessions("space.session", store)) + router.Use(ioc.InjectAppContext(s.BaseTestSuite.AppCtx)) + router.GET("/set-session", func(c *gin.Context) { + var user models.User + + admin := c.Query("admin") == "true" + session := sessions.Default(c) + repositories := ioc.GetRepositories(c) + if admin { + user = s.Factory.NewUserWithOption(factory.UserOptions{Admin: true}).Model + } else { + user = s.Factory.NewUserWithOption(factory.UserOptions{Admin: false}).Model + } + client := repositories.Clients().FindOrCreate(models.DefaultClient) + applicationSession := models.Session{ + User: user, + Client: client, + IP: gofakeit.IPv4Address(), + UserAgent: gofakeit.UserAgent(), + Scopes: models.PublicScope, + TokenType: models.ApplicationToken, + } + err := repositories.Sessions().Create(&applicationSession) + s.Require().NoError(err) + session.Set(shared.CookieSessionKey, applicationSession.Token) + session.Save() + c.String(200, "Session set") + }) + router.RedirectTrailingSlash = false + return router +} + +func (s *ApiBaseTestSuite) CreateSessionCookie(admin bool) *http.Cookie { + var path string + + if admin { + path = "/set-session?admin=true" + } else { + path = "/set-session?admin=false" + } + w := s.PerformRequest(s.Router, "GET", path, nil, nil, nil) + r := utils.ParseResponse(w.Result(), nil) + s.Equal(200, w.Code) + s.Contains(r.Body, "Session set") + + for _, cookie := range w.Result().Cookies() { + if cookie.Name == "space.session" { + return cookie + } + } + + return nil +} From cd4592a11f99127e4e34b472051ad7fbbaebb089 Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Mon, 7 Jul 2025 01:19:36 -0300 Subject: [PATCH 6/9] feature: add endpoints for groups under the api --- internal/api/helpers/security.go | 20 +++ internal/api/users/endpoints.go | 16 ++ internal/api/users/groups_list_handler.go | 54 +++++++ .../api/users/groups_list_handler_test.go | 143 +++++++++++++++++ internal/api/users/groups_patch_handler.go | 62 ++++++++ .../api/users/groups_patch_handler_test.go | 149 ++++++++++++++++++ internal/models/groups.go | 4 +- internal/repository/group.go | 12 ++ internal/repository/group_test.go | 61 +++++++ internal/repository/user.go | 20 ++- test/factory/group.go | 22 +++ 11 files changed, 557 insertions(+), 6 deletions(-) create mode 100644 internal/api/users/groups_list_handler.go create mode 100644 internal/api/users/groups_list_handler_test.go create mode 100644 internal/api/users/groups_patch_handler.go create mode 100644 internal/api/users/groups_patch_handler_test.go create mode 100644 internal/repository/group_test.go create mode 100644 test/factory/group.go diff --git a/internal/api/helpers/security.go b/internal/api/helpers/security.go index ce73728..0885e7b 100644 --- a/internal/api/helpers/security.go +++ b/internal/api/helpers/security.go @@ -82,7 +82,27 @@ func ActionTokenBearerAuthorization() gin.HandlerFunc { c.Abort() return } + c.Set("Action", action) c.Next() } } + +func RequireMatchBetweenActionTokenAndAuthenticatedUser() gin.HandlerFunc { + return func(c *gin.Context) { + action := c.MustGet("Action").(models.Action) + authenticatedUser := c.MustGet("User").(models.User) + if authenticatedUser.ID != action.UserID { + c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) + c.JSON(http.StatusUnauthorized, utils.H{ + "_status": "error", + "_message": "Groups not available", + "error": shared.AccessDenied, + }) + c.Abort() + return + } + + c.Next() + } +} diff --git a/internal/api/users/endpoints.go b/internal/api/users/endpoints.go index 6fb85b0..8668c93 100644 --- a/internal/api/users/endpoints.go +++ b/internal/api/users/endpoints.go @@ -50,5 +50,21 @@ func ExposeRoutes(router *gin.RouterGroup) { helpers.RequiresApplicationSession(), helpers.ActionTokenBearerAuthorization(), sessionsRevokeHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + usersRoutes.GET("/:user_id/clients/:client_id/groups", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + helpers.RequireMatchBetweenActionTokenAndAuthenticatedUser(), + groupsListHandler) + + // Requires X-Requested-By and Origin (same-origin policy) + // Authorization type: action token / Bearer (for web use) + usersRoutes.PATCH("/:user_id/clients/:client_id/groups", + helpers.RequiresApplicationSession(), + helpers.ActionTokenBearerAuthorization(), + helpers.RequireMatchBetweenActionTokenAndAuthenticatedUser(), + groupsPatchHandler) } } diff --git a/internal/api/users/groups_list_handler.go b/internal/api/users/groups_list_handler.go new file mode 100644 index 0000000..edec55b --- /dev/null +++ b/internal/api/users/groups_list_handler.go @@ -0,0 +1,54 @@ +package users + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/ioc" + "github.com/earaujoassis/space/internal/security" + "github.com/earaujoassis/space/internal/utils" +) + +func groupsListHandler(c *gin.Context) { + var userUUID = c.Param("user_id") + var clientUUID = c.Param("client_id") + + if !security.ValidUUID(userUUID) || !security.ValidUUID(clientUUID) { + c.JSON(http.StatusBadRequest, utils.H{ + "_status": "error", + "_message": "Client application irrevocable", + "error": "must use valid UUID for identification", + }) + return + } + + repositories := ioc.GetRepositories(c) + + user := repositories.Users().FindByUUID(userUUID) + if user.IsNewRecord() { + c.JSON(http.StatusBadRequest, utils.H{ + "_status": "error", + "_message": "Groups not available", + "error": "must use valid UUID for identification", + }) + return + } + + client := repositories.Clients().FindByUUID(clientUUID) + if client.IsNewRecord() { + c.JSON(http.StatusBadRequest, utils.H{ + "_status": "error", + "_message": "Groups not available", + "error": "must use valid UUID for identification", + }) + return + } + + group := repositories.Groups().FindOrCreate(user, client) + c.JSON(http.StatusOK, utils.H{ + "_status": "success", + "_message": "Groups available", + "groups": group.Tags, + }) +} diff --git a/internal/api/users/groups_list_handler_test.go b/internal/api/users/groups_list_handler_test.go new file mode 100644 index 0000000..619fe95 --- /dev/null +++ b/internal/api/users/groups_list_handler_test.go @@ -0,0 +1,143 @@ +package users + +import ( + "fmt" + "net/http" + + "github.com/earaujoassis/space/test/utils" +) + +func (s *UsersTestSuite) TestGroupsListHandlerWithoutHeader() { + userUuid := s.Factory.NewUser().Model.UUID + clientUuid := s.Factory.NewClient().Model.UUID + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", userUuid, clientUuid) + w := s.PerformRequest(s.Router, "GET", path, nil, nil, nil) + r := utils.ParseResponse(w.Result(), nil) + s.Require().Equal(400, w.Code) + s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") +} + +func (s *UsersTestSuite) TestGroupsListHandlerByUnauthenticatedUser() { + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + } + + userUuid := s.Factory.NewUser().Model.UUID + clientUuid := s.Factory.NewClient().Model.UUID + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", userUuid, clientUuid) + + w := s.PerformRequest(s.Router, "GET", path, header, nil, nil) + r := utils.ParseResponse(w.Result(), nil) + s.Require().Equal(401, w.Code) + s.True(r.HasKeyInJSON("error")) + s.Equal("User must be authenticated", r.JSON["_message"]) +} + +func (s *UsersTestSuite) TestGroupsListHandlerWithoutActionGrant() { + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + } + + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + + userUuid := s.Factory.GetAvailableUser().UUID + clientUuid := s.Factory.NewClient().Model.UUID + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", userUuid, clientUuid) + w := s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + r := utils.ParseResponse(w.Result(), nil) + s.Require().Equal(400, w.Code) + s.True(r.HasKeyInJSON("error")) + s.Equal("must use valid token field", r.JSON["error"]) +} + +func (s *UsersTestSuite) TestGroupsListHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + user := s.Factory.GetAvailableUser() + actionToken := s.Factory.NewAction(user).Model.Token + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + userUuid := s.Factory.NewUser().Model.UUID + clientUuid := s.Factory.NewClient().Model.UUID + + path := fmt.Sprintf("/api/users/1/clients/%s/groups", clientUuid) + w := s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + s.Require().Equal(400, w.Code) + + path = fmt.Sprintf("/api/users/4862e6b00d95436d92b1b99eae84be8e/clients/%s/groups", clientUuid) + w = s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + s.Require().Equal(400, w.Code) + + path = fmt.Sprintf("/api/users/%s/clients/1/groups", userUuid) + w = s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + s.Require().Equal(400, w.Code) + + path = fmt.Sprintf("/api/users/%s/clients/4862e6b00d95436d92b1b99eae84be8e/groups", userUuid) + w = s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + s.Require().Equal(400, w.Code) +} + +func (s *UsersTestSuite) TestGroupsListHandlerWithoutMatchBetweenActionAndAuthenticatedUser() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + anotherUser := s.Factory.NewUser().Model + actionToken := s.Factory.NewAction(anotherUser).Model.Token + user := s.Factory.NewUser().Model + client := s.Factory.NewClient().Model + _ = s.Factory.NewGroup(user, client) + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", user.UUID, client.UUID) + w := s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + s.Require().Equal(401, w.Code) +} + +func (s *UsersTestSuite) TestGroupsListHandlerByAnotherAdminUser() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + user := s.Factory.GetAvailableUser() + actionToken := s.Factory.NewAction(user).Model.Token + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + userUuid := s.Factory.NewUser().Model.UUID + clientUuid := s.Factory.NewClient().Model.UUID + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", userUuid, clientUuid) + w := s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + s.Equal(200, w.Code) +} + +func (s *UsersTestSuite) TestGroupsListHandler() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + user := s.Factory.GetAvailableUser() + actionToken := s.Factory.NewAction(user).Model.Token + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + client := s.Factory.NewClient().Model + _ = s.Factory.NewGroup(user, client) + + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", user.UUID, client.UUID) + w := s.PerformRequest(s.Router, "GET", path, header, cookie, nil) + s.Require().Equal(200, w.Code) +} diff --git a/internal/api/users/groups_patch_handler.go b/internal/api/users/groups_patch_handler.go new file mode 100644 index 0000000..e6ab43d --- /dev/null +++ b/internal/api/users/groups_patch_handler.go @@ -0,0 +1,62 @@ +package users + +import ( + "net/http" + + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/ioc" + "github.com/earaujoassis/space/internal/security" + "github.com/earaujoassis/space/internal/utils" +) + +func groupsPatchHandler(c *gin.Context) { + var userUUID = c.Param("user_id") + var clientUUID = c.Param("client_id") + + if !security.ValidUUID(userUUID) || !security.ValidUUID(clientUUID) { + c.JSON(http.StatusBadRequest, utils.H{ + "_status": "error", + "_message": "Client application irrevocable", + "error": "must use valid UUID for identification", + }) + return + } + + repositories := ioc.GetRepositories(c) + + user := repositories.Users().FindByUUID(userUUID) + if user.IsNewRecord() { + c.JSON(http.StatusBadRequest, utils.H{ + "_status": "error", + "_message": "Groups not available", + "error": "must use valid UUID for identification", + }) + return + } + + client := repositories.Clients().FindByUUID(clientUUID) + if client.IsNewRecord() { + c.JSON(http.StatusBadRequest, utils.H{ + "_status": "error", + "_message": "Groups not available", + "error": "must use valid UUID for identification", + }) + return + } + + group := repositories.Groups().FindOrCreate(user, client) + tags := c.PostFormArray("tags") + group.Tags = utils.TrimStrings(tags) + err := repositories.Groups().Save(&group) + if err != nil { + c.JSON(http.StatusBadRequest, utils.H{ + "_status": "error", + "_message": "Groups not patched", + "error": "validation failed", + }) + return + } + + c.JSON(http.StatusNoContent, nil) +} diff --git a/internal/api/users/groups_patch_handler_test.go b/internal/api/users/groups_patch_handler_test.go new file mode 100644 index 0000000..df078a9 --- /dev/null +++ b/internal/api/users/groups_patch_handler_test.go @@ -0,0 +1,149 @@ +package users + +import ( + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/earaujoassis/space/test/utils" +) + +func (s *UsersTestSuite) TestGroupsPatchHandlerWithoutHeader() { + userUuid := s.Factory.NewUser().Model.UUID + clientUuid := s.Factory.NewClient().Model.UUID + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", userUuid, clientUuid) + w := s.PerformRequest(s.Router, "PATCH", path, nil, nil, nil) + r := utils.ParseResponse(w.Result(), nil) + s.Require().Equal(400, w.Code) + s.Contains(r.Body, "missing X-Requested-By header attribute or Origin header does not comply with the same-origin policy") +} + +func (s *UsersTestSuite) TestGroupsPatchHandlerByUnauthenticatedUser() { + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + } + + userUuid := s.Factory.NewUser().Model.UUID + clientUuid := s.Factory.NewClient().Model.UUID + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", userUuid, clientUuid) + + w := s.PerformRequest(s.Router, "PATCH", path, header, nil, nil) + r := utils.ParseResponse(w.Result(), nil) + s.Require().Equal(401, w.Code) + s.True(r.HasKeyInJSON("error")) + s.Equal("User must be authenticated", r.JSON["_message"]) +} + +func (s *UsersTestSuite) TestGroupsPatchHandlerWithoutActionGrant() { + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + } + + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + + userUuid := s.Factory.GetAvailableUser().UUID + clientUuid := s.Factory.NewClient().Model.UUID + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", userUuid, clientUuid) + w := s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) + r := utils.ParseResponse(w.Result(), nil) + s.Require().Equal(400, w.Code) + s.True(r.HasKeyInJSON("error")) + s.Equal("must use valid token field", r.JSON["error"]) +} + +func (s *UsersTestSuite) TestGroupsPatchHandlerInvalidId() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + user := s.Factory.GetAvailableUser() + actionToken := s.Factory.NewAction(user).Model.Token + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + userUuid := s.Factory.NewUser().Model.UUID + clientUuid := s.Factory.NewClient().Model.UUID + + path := fmt.Sprintf("/api/users/1/clients/%s/groups", clientUuid) + w := s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) + s.Require().Equal(400, w.Code) + + path = fmt.Sprintf("/api/users/4862e6b00d95436d92b1b99eae84be8e/clients/%s/groups", clientUuid) + w = s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) + s.Require().Equal(400, w.Code) + + path = fmt.Sprintf("/api/users/%s/clients/1/groups", userUuid) + w = s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) + s.Require().Equal(400, w.Code) + + path = fmt.Sprintf("/api/users/%s/clients/4862e6b00d95436d92b1b99eae84be8e/groups", userUuid) + w = s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) + s.Require().Equal(400, w.Code) +} + +func (s *UsersTestSuite) TestGroupsPatchHandlerWithoutMatchBetweenActionAndAuthenticatedUser() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + anotherUser := s.Factory.NewUser().Model + actionToken := s.Factory.NewAction(anotherUser).Model.Token + user := s.Factory.NewUser().Model + client := s.Factory.NewClient().Model + _ = s.Factory.NewGroup(user, client) + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", user.UUID, client.UUID) + w := s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) + s.Require().Equal(401, w.Code) +} + +func (s *UsersTestSuite) TestGroupsPatchHandlerWithoutData() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + authenticatedUser := s.Factory.GetAvailableUser() + actionToken := s.Factory.NewAction(authenticatedUser).Model.Token + user := s.Factory.NewUser().Model + client := s.Factory.NewClient().Model + _ = s.Factory.NewGroup(user, client) + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", user.UUID, client.UUID) + w := s.PerformRequest(s.Router, "PATCH", path, header, cookie, nil) + s.Require().Equal(204, w.Code) +} + +func (s *UsersTestSuite) TestGroupsPatchHandler() { + cookie := s.CreateSessionCookie(true) + s.NotNil(cookie) + authenticatedUser := s.Factory.GetAvailableUser() + actionToken := s.Factory.NewAction(authenticatedUser).Model.Token + user := s.Factory.NewUser().Model + client := s.Factory.NewClient().Model + s.Require().Equal(len(actionToken), 64) + + header := &http.Header{ + "X-Requested-By": []string{"SpaceApi"}, + "Authorization": []string{fmt.Sprintf("Bearer %s", actionToken)}, + } + + formData := url.Values{} + formData.Add("tags", "testing1") + formData.Add("tags", "testing2") + encoded := formData.Encode() + path := fmt.Sprintf("/api/users/%s/clients/%s/groups", user.UUID, client.UUID) + w := s.PerformRequest(s.Router, "PATCH", path, header, cookie, strings.NewReader(encoded)) + s.Require().Equal(204, w.Code) +} diff --git a/internal/models/groups.go b/internal/models/groups.go index 076b717..389bfa9 100644 --- a/internal/models/groups.go +++ b/internal/models/groups.go @@ -7,9 +7,9 @@ import ( type Group struct { Model - User User `gorm:"not null;foreignKey:UserID" validate:"required" json:"-"` + User User `gorm:"not null;foreignKey:UserID" validate:"nostructlevel" json:"-"` UserID uint `gorm:"not null" json:"-"` - Client Client `gorm:"not null;foreignKey:ClientID" validate:"required" json:"-"` + Client Client `gorm:"not null;foreignKey:ClientID" validate:"nostructlevel" json:"-"` ClientID uint `gorm:"not null" json:"-"` Tags pq.StringArray `gorm:"type:text[];not null" validate:"required" json:"groups"` } diff --git a/internal/repository/group.go b/internal/repository/group.go index d4b4cc3..6092f75 100644 --- a/internal/repository/group.go +++ b/internal/repository/group.go @@ -14,3 +14,15 @@ func NewGroupRepository(db *database.DatabaseService) *GroupRepository { BaseRepository: NewBaseRepository[models.Group](db), } } + +func (r *GroupRepository) FindOrCreate(user models.User, client models.Client) models.Group { + var group models.Group + r.db.GetDB(). + Preload("Client"). + Preload("User"). + Preload("User.Client"). + Preload("User.Language"). + Where("user_id = ? AND client_id = ?", user.ID, client.ID). + First(&group) + return group +} diff --git a/internal/repository/group_test.go b/internal/repository/group_test.go new file mode 100644 index 0000000..aa9180b --- /dev/null +++ b/internal/repository/group_test.go @@ -0,0 +1,61 @@ +package repository + +import ( + "github.com/brianvoe/gofakeit/v7" + + "github.com/earaujoassis/space/internal/models" +) + +func (s *RepositoryTestSuite) TestGroupRepository__FindOrCreate() { + repository := NewGroupRepository(s.db) + clients := NewClientRepository(s.db) + languages := NewLanguageRepository(s.db) + users := NewUserRepository(s.db) + + client := models.Client{ + Name: gofakeit.Company(), + Description: gofakeit.ProductDescription(), + CanonicalURI: []string{"http://localhost"}, + RedirectURI: []string{"http://localhost/callback"}, + Scopes: models.PublicScope, + Type: models.ConfidentialClient, + } + err := clients.Create(&client) + s.Require().NoError(err) + s.Require().NotZero(client.ID) + language := models.Language{ + Name: "Português (Brasil)", + IsoCode: "pt-BR", + } + err = languages.Create(&language) + s.Require().NoError(err) + s.Require().NotZero(language.ID) + user := models.User{ + Client: client, + Language: language, + FirstName: gofakeit.FirstName(), + LastName: gofakeit.LastName(), + Username: gofakeit.Username(), + Email: gofakeit.Email(), + Passphrase: gofakeit.Password(true, true, true, true, false, 10), + CodeSecret: gofakeit.Password(true, true, true, true, false, 64), + RecoverSecret: gofakeit.Password(true, true, true, true, false, 64), + } + err = users.Create(&user) + s.Require().NoError(err) + s.Require().NotZero(user.ID) + group := models.Group{ + User: user, + Client: client, + Tags: []string{"testing"}, + } + err = repository.Create(&group) + s.Require().NoError(err) + s.Require().NotZero(group.ID) + + retrieved := repository.FindOrCreate(user, client) + s.Require().NotZero(retrieved.ID) + s.Equal(client.Name, retrieved.Client.Name) + s.Equal(user.Username, retrieved.User.Username) + s.Equal(group.ID, retrieved.ID) +} diff --git a/internal/repository/user.go b/internal/repository/user.go index 5939568..db1aad2 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -28,28 +28,40 @@ func NewUserRepository(db *database.DatabaseService) *UserRepository { // FindByAccountHolder gets an user by its account holder (username or email) func (r *UserRepository) FindByAccountHolder(holder string) models.User { var user models.User - r.db.GetDB().Preload("Client").Preload("Language").Where("username = ? OR email = ?", holder, holder).First(&user) + r.db.GetDB(). + Preload("Client"). + Preload("Language"). + Where("username = ? OR email = ?", holder, holder).First(&user) return user } // FindByPublicID gets an user by its public ID (used by client applications) func (r *UserRepository) FindByPublicID(publicID string) models.User { var user models.User - r.db.GetDB().Preload("Client").Preload("Language").Where("public_id = ?", publicID).First(&user) + r.db.GetDB(). + Preload("Client"). + Preload("Language"). + Where("public_id = ?", publicID).First(&user) return user } // FindByUUID gets an user by its UUID (internal use only) func (r *UserRepository) FindByUUID(uuid string) models.User { var user models.User - r.db.GetDB().Preload("Client").Preload("Language").Where("uuid = ?", uuid).First(&user) + r.db.GetDB(). + Preload("Client"). + Preload("Language"). + Where("uuid = ?", uuid).First(&user) return user } // FindByID gets an user by its ID (internal use only) func (r *UserRepository) FindByID(id uint) models.User { var user models.User - r.db.GetDB().Preload("Client").Preload("Language").Where("id = ?", id).First(&user) + r.db.GetDB(). + Preload("Client"). + Preload("Language"). + Where("id = ?", id).First(&user) return user } diff --git a/test/factory/group.go b/test/factory/group.go new file mode 100644 index 0000000..8fd2b53 --- /dev/null +++ b/test/factory/group.go @@ -0,0 +1,22 @@ +package factory + +import ( + "github.com/earaujoassis/space/internal/models" +) + +type Group struct { + Model models.Group +} + +func (f *TestRepositoryFactory) NewGroup(user models.User, client models.Client) *Group { + session := models.Group{ + User: user, + Client: client, + Tags: []string{"testing"}, + } + f.manager.Groups().Create(&session) + localGroup := Group{ + Model: session, + } + return &localGroup +} From 6941e8df1a17f76aa786c0d44baba28424538ade Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Mon, 7 Jul 2025 01:21:09 -0300 Subject: [PATCH 7/9] hotfix: fix notifications announce --- internal/notifications/notifier.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/notifications/notifier.go b/internal/notifications/notifier.go index 8f5a4dc..68c33da 100644 --- a/internal/notifications/notifier.go +++ b/internal/notifications/notifier.go @@ -2,7 +2,6 @@ package notifications import ( "encoding/json" - "fmt" "strings" "github.com/earaujoassis/space/internal/config" @@ -44,12 +43,14 @@ func (n *Notifier) Announce(user models.User, name string, data utils.H) { Data: data, }) if err != nil { - logs.Propagatef(logs.LevelError, "could not enqueue task for email delivery: %s", name) + logs.Propagatef(logs.LevelError, "could not enqueue task for email delivery: %s: %s", name, err.Error()) + return + } + _, err = enqueuer.Enqueue(workers.TypeEmailDelivery, payload) + if err != nil { + logs.Propagatef(logs.LevelError, "could not enqueue task for email delivery: %s: %s", name, err.Error()) return } - info, err := enqueuer.Enqueue(workers.TypeEmailDelivery, payload) - fmt.Printf("%v\n", err) - fmt.Printf("%v\n", info) default: logs.Propagatef(logs.LevelInfo, "Action `%s` with data `%v`\n", name, data) } From 856730d2b8ba43d3f5ec1b7afe09dfc73e9fcfe4 Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Mon, 7 Jul 2025 01:42:44 -0300 Subject: [PATCH 8/9] chore: improve middleware for api endpoints --- internal/api/clients/create_handler.go | 3 +-- internal/api/clients/endpoints.go | 1 + internal/api/clients/list_handler.go | 3 +-- internal/api/clients/profile_handler.go | 3 +-- internal/api/helpers/security.go | 2 +- internal/api/self/admin_handler.go | 2 +- internal/api/self/emails_create_handler.go | 13 ------------- internal/api/self/emails_list_handler.go | 12 ------------ internal/api/self/endpoints.go | 4 ++++ internal/api/self/settings_list_handler.go | 12 ------------ internal/api/self/settings_patch_handler.go | 12 ------------ internal/api/services/create_handler.go | 3 +-- internal/api/services/endpoints.go | 1 + internal/api/services/list_handler.go | 14 -------------- internal/api/users/clients_list_handler.go | 2 +- internal/api/users/clients_revoke_handler.go | 2 +- internal/api/users/endpoints.go | 4 ++-- internal/api/users/profile_handler.go | 2 +- internal/api/users/sessions_list_handler.go | 2 +- internal/api/users/sessions_revoke_handler.go | 2 +- internal/models/actions.go | 4 ++-- internal/models/actions_test.go | 2 ++ 22 files changed, 23 insertions(+), 82 deletions(-) diff --git a/internal/api/clients/create_handler.go b/internal/api/clients/create_handler.go index c5d1779..f8f0665 100644 --- a/internal/api/clients/create_handler.go +++ b/internal/api/clients/create_handler.go @@ -14,9 +14,8 @@ import ( func createHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID || !user.Admin { + if !user.Admin { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/clients/endpoints.go b/internal/api/clients/endpoints.go index 797b2d9..fdb9322 100644 --- a/internal/api/clients/endpoints.go +++ b/internal/api/clients/endpoints.go @@ -20,6 +20,7 @@ func ExposeRoutes(router *gin.RouterGroup) { clientsRoutes.Use(helpers.RequiresConformance()) clientsRoutes.Use(helpers.RequiresApplicationSession()) clientsRoutes.Use(helpers.ActionTokenBearerAuthorization()) + clientsRoutes.Use(helpers.RequireActionTokenFromAuthenticatedUser()) { // Requires X-Requested-By and Origin (same-origin policy) // Authorization type: action token / Bearer (for web use) diff --git a/internal/api/clients/list_handler.go b/internal/api/clients/list_handler.go index 17f9217..14ce5ce 100644 --- a/internal/api/clients/list_handler.go +++ b/internal/api/clients/list_handler.go @@ -14,9 +14,8 @@ import ( func listHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID || !user.Admin { + if !user.Admin { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/clients/profile_handler.go b/internal/api/clients/profile_handler.go index a2cfffa..9c21b2c 100644 --- a/internal/api/clients/profile_handler.go +++ b/internal/api/clients/profile_handler.go @@ -17,9 +17,8 @@ import ( func profileHandler(c *gin.Context) { clientUUID := c.Param("client_id") repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID || !user.Admin { + if !user.Admin { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/helpers/security.go b/internal/api/helpers/security.go index 0885e7b..0a71d09 100644 --- a/internal/api/helpers/security.go +++ b/internal/api/helpers/security.go @@ -88,7 +88,7 @@ func ActionTokenBearerAuthorization() gin.HandlerFunc { } } -func RequireMatchBetweenActionTokenAndAuthenticatedUser() gin.HandlerFunc { +func RequireActionTokenFromAuthenticatedUser() gin.HandlerFunc { return func(c *gin.Context) { action := c.MustGet("Action").(models.Action) authenticatedUser := c.MustGet("User").(models.User) diff --git a/internal/api/self/admin_handler.go b/internal/api/self/admin_handler.go index 25f1235..d8efd7d 100644 --- a/internal/api/self/admin_handler.go +++ b/internal/api/self/admin_handler.go @@ -49,7 +49,7 @@ func adminHandler(c *gin.Context) { action := c.MustGet("Action").(models.Action) repositories := ioc.GetRepositories(c) user := repositories.Users().FindByUUID(uuid) - if user.IsNewRecord() || user.ID != action.UserID { + if user.ID != action.UserID { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/self/emails_create_handler.go b/internal/api/self/emails_create_handler.go index ff41710..50f1508 100644 --- a/internal/api/self/emails_create_handler.go +++ b/internal/api/self/emails_create_handler.go @@ -1,31 +1,18 @@ package self import ( - "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/earaujoassis/space/internal/ioc" "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func emailsCreateHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Email was not created", - "error": shared.AccessDenied, - }) - return - } - email := models.Email{ User: user, Address: c.PostForm("address"), diff --git a/internal/api/self/emails_list_handler.go b/internal/api/self/emails_list_handler.go index c68945a..a8b300a 100644 --- a/internal/api/self/emails_list_handler.go +++ b/internal/api/self/emails_list_handler.go @@ -1,30 +1,18 @@ package self import ( - "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/earaujoassis/space/internal/ioc" "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func emailsListHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Email was not created", - "error": shared.AccessDenied, - }) - return - } c.JSON(http.StatusOK, utils.H{ "_status": "success", diff --git a/internal/api/self/endpoints.go b/internal/api/self/endpoints.go index e0eef03..1f7a0d5 100644 --- a/internal/api/self/endpoints.go +++ b/internal/api/self/endpoints.go @@ -35,24 +35,28 @@ func ExposeRoutes(router *gin.RouterGroup) { group.GET("/emails", helpers.RequiresApplicationSession(), helpers.ActionTokenBearerAuthorization(), + helpers.RequireActionTokenFromAuthenticatedUser(), emailsListHandler) // Requires X-Requested-By and Origin (same-origin policy) group.POST("/emails", helpers.RequiresApplicationSession(), helpers.ActionTokenBearerAuthorization(), + helpers.RequireActionTokenFromAuthenticatedUser(), emailsCreateHandler) // Requires X-Requested-By and Origin (same-origin policy) group.GET("/settings", helpers.RequiresApplicationSession(), helpers.ActionTokenBearerAuthorization(), + helpers.RequireActionTokenFromAuthenticatedUser(), settingsListHandler) // Requires X-Requested-By and Origin (same-origin policy) group.PATCH("/settings", helpers.RequiresApplicationSession(), helpers.ActionTokenBearerAuthorization(), + helpers.RequireActionTokenFromAuthenticatedUser(), settingsPatchHandler) } } diff --git a/internal/api/self/settings_list_handler.go b/internal/api/self/settings_list_handler.go index d41d8c4..fc04c68 100644 --- a/internal/api/self/settings_list_handler.go +++ b/internal/api/self/settings_list_handler.go @@ -1,30 +1,18 @@ package self import ( - "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/earaujoassis/space/internal/ioc" "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func settingsListHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Settings are not available", - "error": shared.AccessDenied, - }) - return - } c.JSON(http.StatusOK, utils.H{ "_status": "success", diff --git a/internal/api/self/settings_patch_handler.go b/internal/api/self/settings_patch_handler.go index 7fe7b40..38d6138 100644 --- a/internal/api/self/settings_patch_handler.go +++ b/internal/api/self/settings_patch_handler.go @@ -1,7 +1,6 @@ package self import ( - "fmt" "net/http" "strings" @@ -9,7 +8,6 @@ import ( "github.com/earaujoassis/space/internal/ioc" "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) @@ -19,17 +17,7 @@ const ( func settingsPatchHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Settings are not available", - "error": shared.AccessDenied, - }) - return - } key := c.PostForm("key") parts := strings.Split(key, ".") diff --git a/internal/api/services/create_handler.go b/internal/api/services/create_handler.go index 1b80ade..47c93cd 100644 --- a/internal/api/services/create_handler.go +++ b/internal/api/services/create_handler.go @@ -14,9 +14,8 @@ import ( func createHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) user := c.MustGet("User").(models.User) - if user.ID != action.UserID || !user.Admin { + if !user.Admin { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/services/endpoints.go b/internal/api/services/endpoints.go index 0c895e8..534fbee 100644 --- a/internal/api/services/endpoints.go +++ b/internal/api/services/endpoints.go @@ -14,6 +14,7 @@ func ExposeRoutes(router *gin.RouterGroup) { servicesRoutes.Use(helpers.RequiresConformance()) servicesRoutes.Use(helpers.RequiresApplicationSession()) servicesRoutes.Use(helpers.ActionTokenBearerAuthorization()) + servicesRoutes.Use(helpers.RequireActionTokenFromAuthenticatedUser()) { // Requires X-Requested-By and Origin (same-origin policy) // Authorization type: action token / Bearer (for web use) diff --git a/internal/api/services/list_handler.go b/internal/api/services/list_handler.go index 6cc79ad..552260a 100644 --- a/internal/api/services/list_handler.go +++ b/internal/api/services/list_handler.go @@ -1,30 +1,16 @@ package services import ( - "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/earaujoassis/space/internal/ioc" - "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func listHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - action := c.MustGet("Action").(models.Action) - user := c.MustGet("User").(models.User) - if user.ID != action.UserID { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Services are not available", - "error": shared.AccessDenied, - }) - return - } c.JSON(http.StatusOK, utils.H{ "_status": "success", diff --git a/internal/api/users/clients_list_handler.go b/internal/api/users/clients_list_handler.go index ea93df7..7e50e38 100644 --- a/internal/api/users/clients_list_handler.go +++ b/internal/api/users/clients_list_handler.go @@ -28,7 +28,7 @@ func clientsListHandler(c *gin.Context) { action := c.MustGet("Action").(models.Action) repositories := ioc.GetRepositories(c) user := repositories.Users().FindByUUID(uuid) - if user.IsNewRecord() || user.ID != action.UserID { + if user.ID != action.UserID { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/users/clients_revoke_handler.go b/internal/api/users/clients_revoke_handler.go index d436e23..183e927 100644 --- a/internal/api/users/clients_revoke_handler.go +++ b/internal/api/users/clients_revoke_handler.go @@ -29,7 +29,7 @@ func clientsRevokeHandler(c *gin.Context) { action := c.MustGet("Action").(models.Action) repositories := ioc.GetRepositories(c) user := repositories.Users().FindByUUID(userUUID) - if user.IsNewRecord() || user.ID != action.UserID { + if user.ID != action.UserID { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/users/endpoints.go b/internal/api/users/endpoints.go index 8668c93..086a2d3 100644 --- a/internal/api/users/endpoints.go +++ b/internal/api/users/endpoints.go @@ -56,7 +56,7 @@ func ExposeRoutes(router *gin.RouterGroup) { usersRoutes.GET("/:user_id/clients/:client_id/groups", helpers.RequiresApplicationSession(), helpers.ActionTokenBearerAuthorization(), - helpers.RequireMatchBetweenActionTokenAndAuthenticatedUser(), + helpers.RequireActionTokenFromAuthenticatedUser(), groupsListHandler) // Requires X-Requested-By and Origin (same-origin policy) @@ -64,7 +64,7 @@ func ExposeRoutes(router *gin.RouterGroup) { usersRoutes.PATCH("/:user_id/clients/:client_id/groups", helpers.RequiresApplicationSession(), helpers.ActionTokenBearerAuthorization(), - helpers.RequireMatchBetweenActionTokenAndAuthenticatedUser(), + helpers.RequireActionTokenFromAuthenticatedUser(), groupsPatchHandler) } } diff --git a/internal/api/users/profile_handler.go b/internal/api/users/profile_handler.go index 4b3e749..42e8746 100644 --- a/internal/api/users/profile_handler.go +++ b/internal/api/users/profile_handler.go @@ -28,7 +28,7 @@ func profileHandler(c *gin.Context) { action := c.MustGet("Action").(models.Action) repositories := ioc.GetRepositories(c) user := repositories.Users().FindByUUID(uuid) - if user.IsNewRecord() || user.ID != action.UserID { + if user.ID != action.UserID { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/users/sessions_list_handler.go b/internal/api/users/sessions_list_handler.go index 9942a5f..3860c28 100644 --- a/internal/api/users/sessions_list_handler.go +++ b/internal/api/users/sessions_list_handler.go @@ -29,7 +29,7 @@ func sessionsListHandler(c *gin.Context) { action := c.MustGet("Action").(models.Action) currentSession := c.MustGet("CurrentSession").(models.Session) user := repositories.Users().FindByUUID(userUUID) - if user.IsNewRecord() || user.ID != action.UserID { + if user.ID != action.UserID { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/api/users/sessions_revoke_handler.go b/internal/api/users/sessions_revoke_handler.go index 1b843e6..86f20e7 100644 --- a/internal/api/users/sessions_revoke_handler.go +++ b/internal/api/users/sessions_revoke_handler.go @@ -30,7 +30,7 @@ func sessionsRevokeHandler(c *gin.Context) { action := c.MustGet("Action").(models.Action) user := repositories.Users().FindByUUID(userUUID) session := repositories.Sessions().FindByUUID(sessionUUID) - if user.IsNewRecord() || user.ID != action.UserID || user.ID != session.User.ID { + if user.ID != action.UserID || user.ID != session.User.ID { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ "_status": "error", diff --git a/internal/models/actions.go b/internal/models/actions.go index afcb5d2..399a630 100644 --- a/internal/models/actions.go +++ b/internal/models/actions.go @@ -9,9 +9,9 @@ import ( type Action struct { UUID string `validate:"omitempty,uuid4" json:"uuid"` User User `validate:"required" json:"-"` - UserID uint `json:"user_id"` + UserID uint `validate:"min=1" json:"user_id"` Client Client `validate:"required" json:"-"` - ClientID uint `json:"client_id"` + ClientID uint `validate:"min=1" json:"client_id"` Moment int64 `json:"moment"` ExpiresIn int64 `json:"expires_in"` IP string `validate:"required" json:"ip"` diff --git a/internal/models/actions_test.go b/internal/models/actions_test.go index 705e103..fe54ee4 100644 --- a/internal/models/actions_test.go +++ b/internal/models/actions_test.go @@ -19,6 +19,7 @@ func TestValidActionModel(t *testing.T) { assert.NotNil(t, err) client := Client{ + Model: Model{ID: 1}, Name: "internal", Secret: GenerateRandomString(64), CanonicalURI: []string{"localhost"}, @@ -29,6 +30,7 @@ func TestValidActionModel(t *testing.T) { err = client.BeforeSave(nil) assert.Nil(t, err, fmt.Sprintf("%s", err)) user := User{ + Model: Model{ID: 1}, FirstName: gofakeit.FirstName(), LastName: gofakeit.LastName(), Username: gofakeit.Username(), From 1ac14b33b209a301917842ba570c1b1c3fa11aff Mon Sep 17 00:00:00 2001 From: "Carlos A." <588595+earaujoassis@general.noreply.quatrolabs.com> Date: Mon, 7 Jul 2025 02:18:18 -0300 Subject: [PATCH 9/9] chore: improve permissions checking for endpoints --- internal/api/clients/create_handler.go | 11 -------- internal/api/clients/create_handler_test.go | 5 +--- internal/api/clients/credentials_handler.go | 14 +--------- .../api/clients/credentials_handler_test.go | 5 +--- internal/api/clients/endpoints.go | 2 ++ internal/api/clients/list_handler.go | 13 --------- internal/api/clients/list_handler_test.go | 4 +-- internal/api/clients/profile_handler.go | 15 +--------- internal/api/clients/profile_handler_test.go | 4 +-- internal/api/helpers/permission.go | 28 +++++++++++++++++++ internal/api/helpers/security.go | 10 +++---- internal/api/self/admin_handler_test.go | 1 - .../api/self/emails_create_handler_test.go | 2 +- internal/api/self/emails_list_handler_test.go | 1 - .../api/self/settings_list_handler_test.go | 1 - .../api/self/settings_patch_handler_test.go | 1 - internal/api/self/workspace_test.go | 2 +- internal/api/services/create_handler.go | 15 +--------- internal/api/services/create_handler_test.go | 10 +++---- internal/api/services/endpoints.go | 4 ++- internal/api/services/list_handler_test.go | 1 - .../api/users/clients_list_handler_test.go | 1 - .../api/users/clients_revoke_handler_test.go | 1 - .../api/users/groups_list_handler_test.go | 1 - .../api/users/groups_patch_handler_test.go | 1 - internal/api/users/profile_handler_test.go | 1 - .../api/users/sessions_list_handler_test.go | 1 - .../api/users/sessions_revoke_handler_test.go | 1 - 28 files changed, 51 insertions(+), 105 deletions(-) create mode 100644 internal/api/helpers/permission.go diff --git a/internal/api/clients/create_handler.go b/internal/api/clients/create_handler.go index f8f0665..d81d33b 100644 --- a/internal/api/clients/create_handler.go +++ b/internal/api/clients/create_handler.go @@ -8,22 +8,11 @@ import ( "github.com/earaujoassis/space/internal/ioc" "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func createHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - user := c.MustGet("User").(models.User) - if !user.Admin { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Client was not created", - "error": shared.AccessDenied, - }) - return - } client := models.Client{ Name: c.PostForm("name"), diff --git a/internal/api/clients/create_handler_test.go b/internal/api/clients/create_handler_test.go index fd96263..14e1d93 100644 --- a/internal/api/clients/create_handler_test.go +++ b/internal/api/clients/create_handler_test.go @@ -24,9 +24,7 @@ func (s *ClientsTestSuite) TestCreateHandlerByUnauthenticatedUser() { } w := s.PerformRequest(s.Router, "POST", "/api/clients", header, nil, nil) - r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) - s.Contains(r.Body, "User must be authenticated") } func (s *ClientsTestSuite) TestCreateHandlerWithoutActionGrant() { @@ -96,7 +94,6 @@ func (s *ClientsTestSuite) TestCreateHandlerByCommonUser() { w := s.PerformRequest(s.Router, "POST", "/api/clients", header, cookie, nil) r := utils.ParseResponse(w.Result(), nil) - s.Require().Equal(401, w.Code) + s.Require().Equal(403, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("access_denied", r.JSON["error"]) } diff --git a/internal/api/clients/credentials_handler.go b/internal/api/clients/credentials_handler.go index 45bba97..8e10742 100644 --- a/internal/api/clients/credentials_handler.go +++ b/internal/api/clients/credentials_handler.go @@ -10,24 +10,11 @@ import ( "github.com/earaujoassis/space/internal/ioc" "github.com/earaujoassis/space/internal/models" "github.com/earaujoassis/space/internal/security" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func credentialsHandler(c *gin.Context) { clientUUID := c.Param("client_id") - repositories := ioc.GetRepositories(c) - user := c.MustGet("User").(models.User) - if !user.Admin { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Client credentials are not available", - "error": shared.AccessDenied, - }) - return - } - if !security.ValidUUID(clientUUID) { c.JSON(http.StatusBadRequest, utils.H{ "_status": "error", @@ -37,6 +24,7 @@ func credentialsHandler(c *gin.Context) { return } + repositories := ioc.GetRepositories(c) client := repositories.Clients().FindByUUID(clientUUID) // For security reasons, the client's secret is regenerated clientSecret := models.GenerateRandomString(64) diff --git a/internal/api/clients/credentials_handler_test.go b/internal/api/clients/credentials_handler_test.go index 3a2fb08..910b1fc 100644 --- a/internal/api/clients/credentials_handler_test.go +++ b/internal/api/clients/credentials_handler_test.go @@ -30,7 +30,6 @@ func (s *ClientsTestSuite) TestCredentialsHandlerByAdminUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) header = &http.Header{ "X-Requested-By": []string{"SpaceApi"}, @@ -41,7 +40,6 @@ func (s *ClientsTestSuite) TestCredentialsHandlerByAdminUser() { r = utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) w = s.PerformRequest(s.Router, "GET", path, nil, cookie, nil) r = utils.ParseResponse(w.Result(), nil) @@ -80,7 +78,6 @@ func (s *ClientsTestSuite) TestCredentialsHandlerByCommonUser() { w := s.PerformRequest(s.Router, "GET", path, header, cookie, nil) r := utils.ParseResponse(w.Result(), nil) - s.Require().Equal(401, w.Code) + s.Require().Equal(403, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("access_denied", r.JSON["error"]) } diff --git a/internal/api/clients/endpoints.go b/internal/api/clients/endpoints.go index fdb9322..e29b3bf 100644 --- a/internal/api/clients/endpoints.go +++ b/internal/api/clients/endpoints.go @@ -14,6 +14,7 @@ func ExposeRoutes(router *gin.RouterGroup) { // TODO Improve security for this endpoint avoiding any overhead router.GET("/clients/:client_id/credentials", helpers.RequiresApplicationSession(), + helpers.RequirePermission("role:admin"), credentialsHandler) clientsRoutes := router.Group("/clients") @@ -21,6 +22,7 @@ func ExposeRoutes(router *gin.RouterGroup) { clientsRoutes.Use(helpers.RequiresApplicationSession()) clientsRoutes.Use(helpers.ActionTokenBearerAuthorization()) clientsRoutes.Use(helpers.RequireActionTokenFromAuthenticatedUser()) + clientsRoutes.Use(helpers.RequirePermission("role:admin")) { // Requires X-Requested-By and Origin (same-origin policy) // Authorization type: action token / Bearer (for web use) diff --git a/internal/api/clients/list_handler.go b/internal/api/clients/list_handler.go index 14ce5ce..8cac6d7 100644 --- a/internal/api/clients/list_handler.go +++ b/internal/api/clients/list_handler.go @@ -1,29 +1,16 @@ package clients import ( - "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/earaujoassis/space/internal/ioc" - "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func listHandler(c *gin.Context) { repositories := ioc.GetRepositories(c) - user := c.MustGet("User").(models.User) - if !user.Admin { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Clients are not available", - "error": shared.AccessDenied, - }) - return - } c.JSON(http.StatusOK, utils.H{ "_status": "success", diff --git a/internal/api/clients/list_handler_test.go b/internal/api/clients/list_handler_test.go index 4a207c9..b1d7b4a 100644 --- a/internal/api/clients/list_handler_test.go +++ b/internal/api/clients/list_handler_test.go @@ -23,7 +23,6 @@ func (s *ClientsTestSuite) TestListHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *ClientsTestSuite) TestListHandlerWithoutActionGrant() { @@ -79,7 +78,6 @@ func (s *ClientsTestSuite) TestListHandlerByCommonUser() { w := s.PerformRequest(s.Router, "GET", "/api/clients", header, cookie, nil) r := utils.ParseResponse(w.Result(), nil) - s.Require().Equal(401, w.Code) + s.Require().Equal(403, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("access_denied", r.JSON["error"]) } diff --git a/internal/api/clients/profile_handler.go b/internal/api/clients/profile_handler.go index 9c21b2c..7fa739c 100644 --- a/internal/api/clients/profile_handler.go +++ b/internal/api/clients/profile_handler.go @@ -8,26 +8,12 @@ import ( "github.com/gin-gonic/gin" "github.com/earaujoassis/space/internal/ioc" - "github.com/earaujoassis/space/internal/models" "github.com/earaujoassis/space/internal/security" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func profileHandler(c *gin.Context) { clientUUID := c.Param("client_id") - repositories := ioc.GetRepositories(c) - user := c.MustGet("User").(models.User) - if !user.Admin { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Client was not updated", - "error": shared.AccessDenied, - }) - return - } - if !security.ValidUUID(clientUUID) { c.JSON(http.StatusBadRequest, utils.H{ "_status": "error", @@ -37,6 +23,7 @@ func profileHandler(c *gin.Context) { return } + repositories := ioc.GetRepositories(c) client := repositories.Clients().FindByUUID(clientUUID) canonicalURI := c.PostForm("canonical_uri") redirectURI := c.PostForm("redirect_uri") diff --git a/internal/api/clients/profile_handler_test.go b/internal/api/clients/profile_handler_test.go index b986b90..d0bac31 100644 --- a/internal/api/clients/profile_handler_test.go +++ b/internal/api/clients/profile_handler_test.go @@ -30,7 +30,6 @@ func (s *ClientsTestSuite) TestProfileHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *ClientsTestSuite) TestProfileHandlerWithoutActionGrant() { @@ -100,7 +99,6 @@ func (s *ClientsTestSuite) TestProfileHandlerByAdminUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) formData := url.Values{} formData.Set("canonical_uri", "http://localhost:4000") @@ -143,5 +141,5 @@ func (s *ClientsTestSuite) TestProfileHandlerByCommonUser() { formData.Set("scopes", "openid profile") encoded := formData.Encode() w := s.PerformRequest(s.Router, "PATCH", path, header, cookie, strings.NewReader(encoded)) - s.Require().Equal(401, w.Code) + s.Require().Equal(403, w.Code) } diff --git a/internal/api/helpers/permission.go b/internal/api/helpers/permission.go new file mode 100644 index 0000000..204795a --- /dev/null +++ b/internal/api/helpers/permission.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "net/http" + "slices" + + "github.com/gin-gonic/gin" + + "github.com/earaujoassis/space/internal/models" + "github.com/earaujoassis/space/internal/utils" +) + +func RequirePermission(permissions ...string) gin.HandlerFunc { + return func(c *gin.Context) { + if slices.Contains(permissions, "role:admin") { + user := c.MustGet("User").(models.User) + if !user.Admin { + c.JSON(http.StatusForbidden, utils.H{ + "error": "authorization error", + }) + c.Abort() + return + } + } + + c.Next() + } +} diff --git a/internal/api/helpers/security.go b/internal/api/helpers/security.go index 0a71d09..cb0454a 100644 --- a/internal/api/helpers/security.go +++ b/internal/api/helpers/security.go @@ -51,10 +51,10 @@ func RequiresApplicationSession() gin.HandlerFunc { c.Next() return } + + c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "User must be authenticated", - "error": "unauthorized request", + "error": shared.AccessDenied, }) c.Abort() } @@ -95,9 +95,7 @@ func RequireActionTokenFromAuthenticatedUser() gin.HandlerFunc { if authenticatedUser.ID != action.UserID { c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Groups not available", - "error": shared.AccessDenied, + "error": shared.AccessDenied, }) c.Abort() return diff --git a/internal/api/self/admin_handler_test.go b/internal/api/self/admin_handler_test.go index 0ec522e..cd34148 100644 --- a/internal/api/self/admin_handler_test.go +++ b/internal/api/self/admin_handler_test.go @@ -25,7 +25,6 @@ func (s *SelfTestSuite) TestAdminHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *SelfTestSuite) TestAdminHandlerWithoutActionGrant() { diff --git a/internal/api/self/emails_create_handler_test.go b/internal/api/self/emails_create_handler_test.go index a3f173f..022f819 100644 --- a/internal/api/self/emails_create_handler_test.go +++ b/internal/api/self/emails_create_handler_test.go @@ -26,7 +26,7 @@ func (s *SelfTestSuite) TestEmailsCreateHandlerByUnauthenticatedUser() { w := s.PerformRequest(s.Router, "POST", "/api/users/me/emails", header, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) - s.Contains(r.Body, "User must be authenticated") + s.Contains(r.Body, "access_denied") } func (s *SelfTestSuite) TestEmailsCreateHandlerWithoutActionGrant() { diff --git a/internal/api/self/emails_list_handler_test.go b/internal/api/self/emails_list_handler_test.go index e8aa05c..2c33b4f 100644 --- a/internal/api/self/emails_list_handler_test.go +++ b/internal/api/self/emails_list_handler_test.go @@ -23,7 +23,6 @@ func (s *SelfTestSuite) TestEmailsListHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *SelfTestSuite) TestEmailsListHandlerWithoutActionGrant() { diff --git a/internal/api/self/settings_list_handler_test.go b/internal/api/self/settings_list_handler_test.go index 1ac1b31..d2720b9 100644 --- a/internal/api/self/settings_list_handler_test.go +++ b/internal/api/self/settings_list_handler_test.go @@ -23,7 +23,6 @@ func (s *SelfTestSuite) TestSettingsListHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *SelfTestSuite) TestSettingsListHandlerWithoutActionGrant() { diff --git a/internal/api/self/settings_patch_handler_test.go b/internal/api/self/settings_patch_handler_test.go index 8efd3d4..3bd1336 100644 --- a/internal/api/self/settings_patch_handler_test.go +++ b/internal/api/self/settings_patch_handler_test.go @@ -25,7 +25,6 @@ func (s *SelfTestSuite) TestSettingsPatchHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *SelfTestSuite) TestSettingsPatchHandlerWithoutActionGrant() { diff --git a/internal/api/self/workspace_test.go b/internal/api/self/workspace_test.go index 05b0ad3..8802a42 100644 --- a/internal/api/self/workspace_test.go +++ b/internal/api/self/workspace_test.go @@ -17,7 +17,7 @@ func (s *SelfTestSuite) TestWorkspaceHandler() { w = s.PerformRequest(s.Router, "GET", "/api/users/me/workspace", header, nil, nil) r = utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) - s.Contains(r.Body, "User must be authenticated") + s.Contains(r.Body, "access_denied") cookie := s.CreateSessionCookie(false) s.NotNil(cookie) w = s.PerformRequest(s.Router, "GET", "/api/users/me/workspace", header, cookie, nil) diff --git a/internal/api/services/create_handler.go b/internal/api/services/create_handler.go index 47c93cd..1822108 100644 --- a/internal/api/services/create_handler.go +++ b/internal/api/services/create_handler.go @@ -1,30 +1,16 @@ package services import ( - "fmt" "net/http" "github.com/gin-gonic/gin" "github.com/earaujoassis/space/internal/ioc" "github.com/earaujoassis/space/internal/models" - "github.com/earaujoassis/space/internal/shared" "github.com/earaujoassis/space/internal/utils" ) func createHandler(c *gin.Context) { - repositories := ioc.GetRepositories(c) - user := c.MustGet("User").(models.User) - if !user.Admin { - c.Header("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"%s\"", c.Request.RequestURI)) - c.JSON(http.StatusUnauthorized, utils.H{ - "_status": "error", - "_message": "Service was not created", - "error": shared.AccessDenied, - }) - return - } - service := models.Service{ Name: c.PostForm("name"), Description: c.PostForm("description"), @@ -32,6 +18,7 @@ func createHandler(c *gin.Context) { LogoURI: c.PostForm("logo_uri"), Type: models.PublicService, } + repositories := ioc.GetRepositories(c) err := repositories.Services().Create(&service) if err != nil { diff --git a/internal/api/services/create_handler_test.go b/internal/api/services/create_handler_test.go index f7e3695..d19c92f 100644 --- a/internal/api/services/create_handler_test.go +++ b/internal/api/services/create_handler_test.go @@ -26,7 +26,7 @@ func (s *ServicesTestSuite) TestCreateHandlerByUnauthenticatedUser() { w := s.PerformRequest(s.Router, "POST", "/api/services", header, nil, nil) r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) - s.Contains(r.Body, "User must be authenticated") + s.True(r.HasKeyInJSON("error")) } func (s *ServicesTestSuite) TestCreateHandlerWithoutActionGrant() { @@ -107,9 +107,9 @@ func (s *ServicesTestSuite) TestCreateHandlerByCommonUser() { w := s.PerformRequest(s.Router, "POST", "/api/services", header, cookie, nil) r := utils.ParseResponse(w.Result(), nil) - s.Require().Equal(401, w.Code) + s.Require().Equal(403, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("access_denied", r.JSON["error"]) + s.Equal("authorization error", r.JSON["error"]) formData := url.Values{} formData.Set("name", gofakeit.Company()) @@ -117,7 +117,7 @@ func (s *ServicesTestSuite) TestCreateHandlerByCommonUser() { formData.Set("canonical_uri", "http://localhost") encoded := formData.Encode() w = s.PerformRequest(s.Router, "POST", "/api/services", header, cookie, strings.NewReader(encoded)) - s.Require().Equal(401, w.Code) + s.Require().Equal(403, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("access_denied", r.JSON["error"]) + s.Equal("authorization error", r.JSON["error"]) } diff --git a/internal/api/services/endpoints.go b/internal/api/services/endpoints.go index 534fbee..2b30028 100644 --- a/internal/api/services/endpoints.go +++ b/internal/api/services/endpoints.go @@ -22,6 +22,8 @@ func ExposeRoutes(router *gin.RouterGroup) { // Requires X-Requested-By and Origin (same-origin policy) // Authorization type: action token / Bearer (for web use) - servicesRoutes.POST("", createHandler) + servicesRoutes.POST("", + helpers.RequirePermission("role:admin"), + createHandler) } } diff --git a/internal/api/services/list_handler_test.go b/internal/api/services/list_handler_test.go index fefea07..6bc11b9 100644 --- a/internal/api/services/list_handler_test.go +++ b/internal/api/services/list_handler_test.go @@ -23,7 +23,6 @@ func (s *ServicesTestSuite) TestListHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *ServicesTestSuite) TestListHandlerWithoutActionGrant() { diff --git a/internal/api/users/clients_list_handler_test.go b/internal/api/users/clients_list_handler_test.go index f7552d8..361c320 100644 --- a/internal/api/users/clients_list_handler_test.go +++ b/internal/api/users/clients_list_handler_test.go @@ -28,7 +28,6 @@ func (s *UsersTestSuite) TestClientsListHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *UsersTestSuite) TestClientsListHandlerWithoutActionGrant() { diff --git a/internal/api/users/clients_revoke_handler_test.go b/internal/api/users/clients_revoke_handler_test.go index d591599..baae432 100644 --- a/internal/api/users/clients_revoke_handler_test.go +++ b/internal/api/users/clients_revoke_handler_test.go @@ -30,7 +30,6 @@ func (s *UsersTestSuite) TestClientsRevokeHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *UsersTestSuite) TestClientsRevokeHandlerWithoutActionGrant() { diff --git a/internal/api/users/groups_list_handler_test.go b/internal/api/users/groups_list_handler_test.go index 619fe95..9bb16e2 100644 --- a/internal/api/users/groups_list_handler_test.go +++ b/internal/api/users/groups_list_handler_test.go @@ -30,7 +30,6 @@ func (s *UsersTestSuite) TestGroupsListHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *UsersTestSuite) TestGroupsListHandlerWithoutActionGrant() { diff --git a/internal/api/users/groups_patch_handler_test.go b/internal/api/users/groups_patch_handler_test.go index df078a9..c264466 100644 --- a/internal/api/users/groups_patch_handler_test.go +++ b/internal/api/users/groups_patch_handler_test.go @@ -32,7 +32,6 @@ func (s *UsersTestSuite) TestGroupsPatchHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *UsersTestSuite) TestGroupsPatchHandlerWithoutActionGrant() { diff --git a/internal/api/users/profile_handler_test.go b/internal/api/users/profile_handler_test.go index 749721d..7698c69 100644 --- a/internal/api/users/profile_handler_test.go +++ b/internal/api/users/profile_handler_test.go @@ -28,7 +28,6 @@ func (s *UsersTestSuite) TestProfileHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *UsersTestSuite) TestProfileHandlerWithoutActionGrant() { diff --git a/internal/api/users/sessions_list_handler_test.go b/internal/api/users/sessions_list_handler_test.go index 3094858..517ed40 100644 --- a/internal/api/users/sessions_list_handler_test.go +++ b/internal/api/users/sessions_list_handler_test.go @@ -28,7 +28,6 @@ func (s *UsersTestSuite) TestSessionsListHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *UsersTestSuite) TestSessionsListHandlerWithoutActionGrant() { diff --git a/internal/api/users/sessions_revoke_handler_test.go b/internal/api/users/sessions_revoke_handler_test.go index 8b676f8..652b305 100644 --- a/internal/api/users/sessions_revoke_handler_test.go +++ b/internal/api/users/sessions_revoke_handler_test.go @@ -30,7 +30,6 @@ func (s *UsersTestSuite) TestSessionsRevokeHandlerByUnauthenticatedUser() { r := utils.ParseResponse(w.Result(), nil) s.Require().Equal(401, w.Code) s.True(r.HasKeyInJSON("error")) - s.Equal("User must be authenticated", r.JSON["_message"]) } func (s *UsersTestSuite) TestSessionsRevokeHandlerWithoutActionGrant() {