diff --git a/Makefile b/Makefile index 6cd96a4..35820a5 100644 --- a/Makefile +++ b/Makefile @@ -85,3 +85,5 @@ generatemocks: cd $(CURDIR)/repository/incidentStatus && minimock -i IncidentStatusRepository -s _mock.go -o $(CURDIR)/mocks/incidentStatus cd $(CURDIR)/service/incidentStatus && minimock -i IncidentStatusService -s _mock.go -o $(CURDIR)/mocks cd $(CURDIR)/repository/incidentUser && minimock -i IncidentUserRepository -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/repository/requestStatus && minimock -i RequestStatusRepository -s _mock.go -o $(CURDIR)/mocks + cd $(CURDIR)/service/requestStatus && minimock -i RequestStatusService -s _mock.go -o $(CURDIR)/mocks diff --git a/appcontext/app.go b/appcontext/app.go index 1e86999..4364320 100644 --- a/appcontext/app.go +++ b/appcontext/app.go @@ -21,6 +21,7 @@ import ( incidentUserRepo "houston/repository/incidentUser" rcaRepository "houston/repository/rca/impl" "houston/repository/rcaInput" + requestStatusRepo "houston/repository/requestStatus" "houston/repository/severity" teamSeverityRepo "houston/repository/teamSeverity" teamUserRepo "houston/repository/teamUser" @@ -36,6 +37,7 @@ import ( "houston/service/productsTeams" rcaService "houston/service/rca/impl" "houston/service/reminder" + "houston/service/requestStatus" severityService "houston/service/severity" severityServiceImpl "houston/service/severity/impl" "houston/service/slack" @@ -82,6 +84,7 @@ type houstonServices struct { reminderService reminder.ReminderService incidentStatusService incidentStatus.IncidentStatusService incidentUserService incidentUser.IncidentUserService + requestStatusService requestStatus.RequestStatusService } var appContext *applicationContext @@ -100,6 +103,8 @@ func InitializeServices() { slaService := initSlackService() severityService := initSeverityService() incidentStatusService := initIncidentStatusService() + teamUserService := initTeamUserService() + requestStatusService := initRequestStatusService() services = &houstonServices{ logRepo: logRepo, teamRepo: teamRepo, @@ -120,14 +125,15 @@ func InitializeServices() { productTeamsService: initProductTeamsService(), severityService: severityService, teamSeverityService: initTeamSeverityService(), - teamUserService: initTeamUserService(), + teamUserService: teamUserService, teamUserSeverityService: initTeamUserSeverityService(), incidentStatusService: incidentStatusService, - incidentUserService: initIncidentUserService(), + requestStatusService: requestStatusService, } services.userService = initUserService() services.teamService = initTeamService() services.reminderService = initReminderService() + services.incidentUserService = initIncidentUserService(services.teamUserService, services.userService, services.requestStatusService) } func initDB() *gorm.DB { @@ -381,8 +387,24 @@ func GetReminderService() reminder.ReminderService { return services.reminderService } -func initIncidentUserService() incidentUser.IncidentUserService { - return incidentUser.NewIncidentUserService(incidentUserRepo.NewIncidentUserRepository(GetDB())) +func initRequestStatusService() requestStatus.RequestStatusService { + return requestStatus.NewRequestStatusService(requestStatusRepo.NewRequestStatusRepository(GetDB())) +} + +func GetRequestStatusService() requestStatus.RequestStatusService { + return initRequestStatusService() +} + +func initIncidentUserService( + teamUserService teamUser.ITeamUserService, userService userService.UserService, requestStatusService requestStatus.RequestStatusService) incidentUser.IncidentUserService { + return incidentUser.NewIncidentUserService( + incidentUserRepo.NewIncidentUserRepository(GetDB()), + teamUserService, + userService, + GetIncidentService(), + GetSlackService(), + requestStatusService, + ) } func GetIncidentUserService() incidentUser.IncidentUserService { diff --git a/cmd/app/handler/incident_user_handler.go b/cmd/app/handler/incident_user_handler.go index 0b34e0b..b546033 100644 --- a/cmd/app/handler/incident_user_handler.go +++ b/cmd/app/handler/incident_user_handler.go @@ -8,6 +8,7 @@ import ( incidentUserRequest "houston/service/request/incidentUser" common "houston/service/response/common" "net/http" + "strconv" ) type IncidentUserHandler struct { @@ -25,18 +26,37 @@ func NewIncidentUserHandler( } } -func (handler *IncidentUserHandler) HandleAddIncidentUser(c *gin.Context) { - var addIncidentUserRequest incidentUserRequest.AddIncidentUserRequest - if err := c.ShouldBindJSON(&addIncidentUserRequest); err != nil { +func (handler *IncidentUserHandler) HandleGetIncidentUsers(c *gin.Context) { + incidentId, err := strconv.ParseUint(c.Query(INCIDENT_ID_QUERY), 10, 32) + if err != nil { c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) return } - err := handler.service.AddIncidentUser(addIncidentUserRequest) + response, err := handler.service.GetIncidentUsersByIncidentId(uint(incidentId)) if err != nil { c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil)) - metrics.PublishHoustonFlowFailureMetrics(util.ADD_INCIDENT_USER_MAPPING, err.Error()) + metrics.PublishHoustonFlowFailureMetrics(util.GET_INCIDENT_USERS, err.Error()) return } - c.JSON(http.StatusOK, common.SuccessResponse("User added to incident successfully", http.StatusOK)) + + c.JSON(http.StatusOK, common.SuccessResponse(response, http.StatusOK)) } + +func (handler *IncidentUserHandler) HandleSyncIncidentUsers(c *gin.Context) { + var syncIncidentUserRequest incidentUserRequest.SyncIncidentUserRequest + if err := c.ShouldBindJSON(&syncIncidentUserRequest); err != nil { + c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) + return + } + + requestStatus, err := handler.service.SyncIncidentUsers(syncIncidentUserRequest) + if err != nil { + c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil)) + return + } + + c.JSON(http.StatusOK, common.SuccessResponse("Acknowledged request to sync incident-user data with request status ID: "+requestStatus.RequestID.String(), http.StatusOK)) +} + +const INCIDENT_ID_QUERY = "incident_id" diff --git a/cmd/app/handler/request_status_handler.go b/cmd/app/handler/request_status_handler.go new file mode 100644 index 0000000..5b7e04e --- /dev/null +++ b/cmd/app/handler/request_status_handler.go @@ -0,0 +1,42 @@ +package handler + +import ( + "github.com/gin-gonic/gin" + "github.com/google/uuid" + "houston/service/requestStatus" + common "houston/service/response/common" + "net/http" +) + +type RequestStatusHandler struct { + gin *gin.Engine + service requestStatus.RequestStatusService +} + +func NewRequestStatusHandler( + gin *gin.Engine, + requestStatusService requestStatus.RequestStatusService, +) *RequestStatusHandler { + return &RequestStatusHandler{ + gin: gin, + service: requestStatusService, + } +} + +func (handler *RequestStatusHandler) HandleGetRequestStatusByRequestId(c *gin.Context) { + requestId, err := uuid.Parse(c.Param(REQUEST_ID_PARAM)) + if err != nil { + c.JSON(http.StatusBadRequest, common.ErrorResponse(err, http.StatusBadRequest, nil)) + return + } + + response, err := handler.service.GetRequestStatusByRequestId(requestId) + if err != nil { + c.JSON(http.StatusInternalServerError, common.ErrorResponse(err, http.StatusInternalServerError, nil)) + return + } + + c.JSON(http.StatusOK, common.SuccessResponse(response, http.StatusOK)) +} + +const REQUEST_ID_PARAM = "request_id" diff --git a/cmd/app/server.go b/cmd/app/server.go index 887294d..6e0005b 100644 --- a/cmd/app/server.go +++ b/cmd/app/server.go @@ -71,6 +71,7 @@ func (s *Server) Handler(houstonGroup *gin.RouterGroup) { s.tagHandler(houstonGroup) s.filtersHandler(houstonGroup) s.incidentUserHandler(houstonGroup) + s.requestStatusHandler(houstonGroup) //this should always be at the end since it opens websocket to connect to slackbot s.houstonHandler() @@ -253,7 +254,14 @@ func (s *Server) incidentChannelHandler(houstonGroup *gin.RouterGroup) { func (s *Server) incidentUserHandler(houstonGroup *gin.RouterGroup) { incidentUserHandler := handler.NewIncidentUserHandler(s.gin, appcontext.GetIncidentUserService()) - houstonGroup.POST("/incident-user", s.authService.IfAdmin(incidentUserHandler.HandleAddIncidentUser)) + houstonGroup.GET("/incident-user", s.authService.IfValidHoustonUser(incidentUserHandler.HandleGetIncidentUsers)) + houstonGroup.POST("/incident-user/synchronization", s.authService.IfAdmin(incidentUserHandler.HandleSyncIncidentUsers)) +} + +func (s *Server) requestStatusHandler(houstonGroup *gin.RouterGroup) { + requestStatusHandler := handler.NewRequestStatusHandler(s.gin, appcontext.GetRequestStatusService()) + + houstonGroup.GET("/request-status/:request_id", requestStatusHandler.HandleGetRequestStatusByRequestId) } func (s *Server) logHandler(houstonGroup *gin.RouterGroup) { diff --git a/common/util/constant.go b/common/util/constant.go index 9e7fde6..3650359 100644 --- a/common/util/constant.go +++ b/common/util/constant.go @@ -141,6 +141,7 @@ const ( const ( ADD_INCIDENT_USER_MAPPING = "ADD_INCIDENT_USER_MAPPING" + GET_INCIDENT_USERS = "GET_INCIDENT_USERS" ) const ( diff --git a/common/util/requestStatus/request_status.go b/common/util/requestStatus/request_status.go new file mode 100644 index 0000000..dcec0b9 --- /dev/null +++ b/common/util/requestStatus/request_status.go @@ -0,0 +1,39 @@ +package requestStatus + +type RequestType string + +const SyncIncidentUsersRequestType RequestType = "SYNC_INCIDENT_USERS" + +type RequestStatus interface { + Value() string + IsTerminal() bool +} + +type Pending struct{} + +func (Pending) Value() string { return "PENDING" } + +func (Pending) IsTerminal() bool { return false } + +type Success struct{} + +func (Success) Value() string { return "SUCCESS" } +func (Success) IsTerminal() bool { return true } + +type Failed struct{} + +func (Failed) Value() string { return "FAILED" } +func (Failed) IsTerminal() bool { return true } + +func GetAllStatusValuesByTerminalState(isTerminal bool) []string { + var statusValues []string + allStatuses := []RequestStatus{Pending{}, Success{}, Failed{}} + + for _, status := range allStatuses { + if status.IsTerminal() == isTerminal { + statusValues = append(statusValues, status.Value()) + } + } + + return statusValues +} diff --git a/config/application.properties b/config/application.properties index 14d12df..54b1a32 100644 --- a/config/application.properties +++ b/config/application.properties @@ -97,4 +97,6 @@ jira.username=JIRA_USERNAME jira.api.token=JIRA_API_TOKEN jira.link.max.length=JIRA_LINK_MAX_LENGTH -archival.batch.interval.in.seconds=70 \ No newline at end of file +archival.batch.interval.in.seconds=70 +slack.api.tier3.batch.size=90 +slack.api.get.users.in.conversation.interval.in.seconds=70 \ No newline at end of file diff --git a/db/migration/000030_add_request_status_table.up.sql b/db/migration/000030_add_request_status_table.up.sql new file mode 100644 index 0000000..4ae3e3c --- /dev/null +++ b/db/migration/000030_add_request_status_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS request_status ( + id SERIAL PRIMARY KEY, + request_id UUID DEFAULT gen_random_uuid(), + request_type VARCHAR(255) NOT NULL, + status VARCHAR(255) NOT NULL, + message JSONB +); diff --git a/model/incident/incident.go b/model/incident/incident.go index 429dde7..3260bdf 100644 --- a/model/incident/incident.go +++ b/model/incident/incident.go @@ -289,9 +289,18 @@ func (r *Repository) FindIncidentById(Id uint) (*IncidentEntity, error) { return &incidentEntity, nil } -func (r *Repository) GetAllIncidents(teamsIds, severityIds, statusIds []uint, isPrivate *bool) (*[]IncidentEntity, int, error) { +func (r *Repository) GetAllIncidents(teamsIds, severityIds, statusIds []uint, isPrivate *bool, isArchived *bool) (*[]IncidentEntity, int, error) { var query = r.gormClient.Model([]IncidentEntity{}) var incidentEntity []IncidentEntity + + if isArchived != nil { + query = query.Joins( + "JOIN incident_channel on incident.slack_channel = incident_channel.slack_channel "+ + "and incident_channel.is_archived = ?", + isArchived, + ) + } + if len(teamsIds) != 0 { query = query.Where("team_id IN ?", teamsIds) } @@ -656,6 +665,15 @@ func (r *Repository) CanUserWithEmailAccessIncidentWithId(userEmail string, inci return count > 0, nil } +func (r *Repository) GetIncidentsByIds(incidentIds []uint) ([]IncidentEntity, error) { + var incidents []IncidentEntity + query := r.gormClient.Where("id IN ?", incidentIds).Find(&incidents) + if query.Error != nil { + return nil, query.Error + } + return incidents, nil +} + var statusesForEscalation = []uint{1, 2} var severitiesForSLABreach = []uint{2, 3, 4} diff --git a/model/incident/incident_repository_interface.go b/model/incident/incident_repository_interface.go index 2e17668..b140917 100644 --- a/model/incident/incident_repository_interface.go +++ b/model/incident/incident_repository_interface.go @@ -13,7 +13,7 @@ type IIncidentRepository interface { CreateIncidentTagsInBatchesForAnIncident(incidentId uint, tagIds []uint) (*[]IncidentTagEntity, error) FindIncidentByChannelId(channelId string) (*IncidentEntity, error) FindIncidentById(Id uint) (*IncidentEntity, error) - GetAllIncidents(teamsIds, severityIds, statusIds []uint, isPrivate *bool) (*[]IncidentEntity, int, error) + GetAllIncidents(teamsIds, severityIds, statusIds []uint, isPrivate *bool, isArchived *bool) (*[]IncidentEntity, int, error) FetchAllIncidentsPaginated( productIds []uint, reportingTeamIds []uint, @@ -44,4 +44,5 @@ type IIncidentRepository interface { GetIncidentTagsByTagIds(incidentId uint, tagIds []uint) (*IncidentTagEntity, error) FetchIncidentsWithSeverityTatBetweenGivenRange(slaStart, slaEnd string) (*[]IncidentEntity, error) CanUserWithEmailAccessIncidentWithId(userEmail string, incidentId uint) (bool, error) + GetIncidentsByIds(incidentIds []uint) ([]IncidentEntity, error) } diff --git a/model/requestStatus/entity.go b/model/requestStatus/entity.go new file mode 100644 index 0000000..c41653c --- /dev/null +++ b/model/requestStatus/entity.go @@ -0,0 +1,28 @@ +package requestStatus + +import ( + "github.com/google/uuid" + "gorm.io/datatypes" +) + +type RequestStatusEntity struct { + ID uint `gorm:"primaryKey"` + RequestID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();not null"` + RequestType string `gorm:"type:varchar(255);not null"` + Status string `gorm:"type:varchar(255);not null"` + Message datatypes.JSON `gorm:"type:jsonb"` +} + +func (RequestStatusEntity) TableName() string { + return "request_status" +} + +func (entity RequestStatusEntity) ToDTO() RequestStatusDTO { + return RequestStatusDTO{ + ID: entity.ID, + RequestID: entity.RequestID, + RequestType: entity.RequestType, + Status: entity.Status, + Message: entity.Message, + } +} diff --git a/model/requestStatus/model.go b/model/requestStatus/model.go new file mode 100644 index 0000000..904a725 --- /dev/null +++ b/model/requestStatus/model.go @@ -0,0 +1,14 @@ +package requestStatus + +import ( + "github.com/google/uuid" + "gorm.io/datatypes" +) + +type RequestStatusDTO struct { + ID uint `json:"id"` + RequestID uuid.UUID `json:"request_id"` + RequestType string `json:"request_type"` + Status string `json:"status"` + Message datatypes.JSON `json:"message"` +} diff --git a/model/user/user.go b/model/user/user.go index b9eef70..87d115e 100644 --- a/model/user/user.go +++ b/model/user/user.go @@ -135,3 +135,12 @@ func (r *Repository) GetHoustonUserById(id uint) (*UserEntity, error) { } return &user, nil } + +func (r *Repository) FindHoustonUsersBySlackUserIds(slackUserIds []string) ([]UserEntity, error) { + var users []UserEntity + result := r.gormClient.Where("slack_user_id IN ?", slackUserIds).Find(&users) + if result.Error != nil { + return nil, result.Error + } + return users, nil +} diff --git a/model/user/user_repository_interface.go b/model/user/user_repository_interface.go index 29d94d8..7c292e0 100644 --- a/model/user/user_repository_interface.go +++ b/model/user/user_repository_interface.go @@ -12,4 +12,5 @@ type IUserRepository interface { GetAllActiveHoustonUserBots() ([]UserEntity, error) GetHoustonUserByEmailId(emailId string) (*UserEntity, error) GetHoustonUserById(id uint) (*UserEntity, error) + FindHoustonUsersBySlackUserIds(slackUserIds []string) ([]UserEntity, error) } diff --git a/repository/incidentUser/incident_user_repository_impl.go b/repository/incidentUser/incident_user_repository_impl.go index e1a1d5d..3a878d2 100644 --- a/repository/incidentUser/incident_user_repository_impl.go +++ b/repository/incidentUser/incident_user_repository_impl.go @@ -39,3 +39,59 @@ func (repo *incidentUserRepositoryImpl) RemoveIncidentUser(incidentId uint, user return nil } + +func (repo *incidentUserRepositoryImpl) GetIncidentUsersByIncidentId(incidentId uint) ([]incidentUser.IncidentUserEntity, error) { + var incidentUsers []incidentUser.IncidentUserEntity + result := repo.gormClient.Where("incident_id = ?", incidentId).Preload("Incident").Preload("User").Find(&incidentUsers) + + if result.Error != nil { + return nil, result.Error + } + + return incidentUsers, nil +} + +func (repo *incidentUserRepositoryImpl) GetIncidentUsersByIncidentIds(incidentIds []uint) ([]incidentUser.IncidentUserEntity, error) { + var incidentUsers []incidentUser.IncidentUserEntity + result := repo.gormClient.Where("incident_id IN ?", incidentIds).Preload("Incident").Preload("User").Find(&incidentUsers) + + if result.Error != nil { + return nil, result.Error + } + + return incidentUsers, nil +} + +func (repo *incidentUserRepositoryImpl) ReplaceIncidentUsers(incidentUserIdsToRemove []uint, incidentIdToUserIdMappings [][]uint) error { + return repo.gormClient.Transaction(func(tx *gorm.DB) error { + if len(incidentUserIdsToRemove) != 0 { + removeResult := tx.Where("id IN ?", incidentUserIdsToRemove).Delete(&incidentUser.IncidentUserEntity{}) + if removeResult.Error != nil { + logger.Error(fmt.Sprintf("failed to remove incident-user mappings with incidentIds %v: %v", incidentUserIdsToRemove, removeResult.Error)) + return removeResult.Error + } + } + + if len(incidentIdToUserIdMappings) != 0 { + incidentUsers := createIncidentUserEntitiesFromIncidentUserPairs(incidentIdToUserIdMappings) + createResult := tx.Create(&incidentUsers) + if createResult.Error != nil { + logger.Error(fmt.Sprintf("failed to add incident-user mappings with incidentIdToUserIdMappings %v: %v", incidentIdToUserIdMappings, createResult.Error)) + return createResult.Error + } + } + + return nil + }) +} + +func createIncidentUserEntitiesFromIncidentUserPairs(incidentUserPairs [][]uint) []incidentUser.IncidentUserEntity { + var incidentUsers []incidentUser.IncidentUserEntity + for _, pair := range incidentUserPairs { + incidentUsers = append(incidentUsers, incidentUser.IncidentUserEntity{ + IncidentID: pair[0], + UserID: pair[1], + }) + } + return incidentUsers +} diff --git a/repository/incidentUser/incident_user_repository_interface.go b/repository/incidentUser/incident_user_repository_interface.go index 93b12a1..2e7cb50 100644 --- a/repository/incidentUser/incident_user_repository_interface.go +++ b/repository/incidentUser/incident_user_repository_interface.go @@ -2,11 +2,15 @@ package incidentUser import ( "gorm.io/gorm" + "houston/model/incidentUser" ) type IncidentUserRepository interface { AddIncidentUser(incidentId uint, userId uint) error RemoveIncidentUser(incidentId uint, userId uint) error + GetIncidentUsersByIncidentId(incidentId uint) ([]incidentUser.IncidentUserEntity, error) + GetIncidentUsersByIncidentIds(incidentIds []uint) ([]incidentUser.IncidentUserEntity, error) + ReplaceIncidentUsers(incidentUserIdsToRemove []uint, incidentIdToUserIdMappings [][]uint) error } func NewIncidentUserRepository(gormClient *gorm.DB) IncidentUserRepository { diff --git a/repository/requestStatus/request_status_repository_impl.go b/repository/requestStatus/request_status_repository_impl.go new file mode 100644 index 0000000..ebfcc91 --- /dev/null +++ b/repository/requestStatus/request_status_repository_impl.go @@ -0,0 +1,50 @@ +package requestStatus + +import ( + "github.com/google/uuid" + "gorm.io/datatypes" + "gorm.io/gorm" + requestStatusUtil "houston/common/util/requestStatus" + "houston/model/requestStatus" +) + +type requestStatusRepositoryImpl struct { + gormClient *gorm.DB +} + +func (repo *requestStatusRepositoryImpl) AddRequestStatus( + requestType requestStatusUtil.RequestType, status requestStatusUtil.RequestStatus, message datatypes.JSON, +) (requestStatus.RequestStatusEntity, error) { + entity := requestStatus.RequestStatusEntity{ + RequestType: string(requestType), + Status: status.Value(), + Message: message, + } + result := repo.gormClient.Create(&entity) + return entity, result.Error +} + +func (repo *requestStatusRepositoryImpl) GetRequestStatusByRequestId(requestId uuid.UUID) (*requestStatus.RequestStatusEntity, error) { + var entity requestStatus.RequestStatusEntity + result := repo.gormClient.Where("request_id = ?", requestId).First(&entity) + return &entity, result.Error +} + +func (repo *requestStatusRepositoryImpl) UpdateRequestStatus( + requestId uuid.UUID, status requestStatusUtil.RequestStatus, message datatypes.JSON, +) (requestStatus.RequestStatusEntity, error) { + entity := requestStatus.RequestStatusEntity{ + Status: status.Value(), + Message: message, + } + result := repo.gormClient.Model(&requestStatus.RequestStatusEntity{}).Where("request_id = ?", requestId).Updates(&entity) + return entity, result.Error +} + +func (repo *requestStatusRepositoryImpl) GetAllRequestStatusesByTerminalState(terminalState bool) ([]requestStatus.RequestStatusEntity, error) { + var entities []requestStatus.RequestStatusEntity + + result := repo.gormClient.Where("status IN ?", requestStatusUtil.GetAllStatusValuesByTerminalState(terminalState)).Find(&entities) + + return entities, result.Error +} diff --git a/repository/requestStatus/request_status_repository_interface.go b/repository/requestStatus/request_status_repository_interface.go new file mode 100644 index 0000000..29501c6 --- /dev/null +++ b/repository/requestStatus/request_status_repository_interface.go @@ -0,0 +1,24 @@ +package requestStatus + +import ( + "github.com/google/uuid" + "gorm.io/datatypes" + "gorm.io/gorm" + requestStatusUtil "houston/common/util/requestStatus" + "houston/model/requestStatus" +) + +type RequestStatusRepository interface { + AddRequestStatus( + requestType requestStatusUtil.RequestType, status requestStatusUtil.RequestStatus, message datatypes.JSON, + ) (requestStatus.RequestStatusEntity, error) + GetRequestStatusByRequestId(requestId uuid.UUID) (*requestStatus.RequestStatusEntity, error) + UpdateRequestStatus(requestId uuid.UUID, status requestStatusUtil.RequestStatus, message datatypes.JSON) (requestStatus.RequestStatusEntity, error) + GetAllRequestStatusesByTerminalState(terminalState bool) ([]requestStatus.RequestStatusEntity, error) +} + +func NewRequestStatusRepository(gormClient *gorm.DB) RequestStatusRepository { + return &requestStatusRepositoryImpl{ + gormClient: gormClient, + } +} diff --git a/service/incident/impl/fetch_incidents.go b/service/incident/impl/fetch_incidents.go index 174959f..973a3db 100644 --- a/service/incident/impl/fetch_incidents.go +++ b/service/incident/impl/fetch_incidents.go @@ -16,3 +16,11 @@ func (i *IncidentServiceV2) FetchIncidentsApproachingSlaBreach() ([]incident.Inc } return dto.ToDtoArray[incident.IncidentEntity, incident.IncidentDTO](*incidents), nil } + +func (i *IncidentServiceV2) GetIncidentsByIds(incidentIds []uint) ([]incident.IncidentDTO, error) { + incidents, err := i.incidentRepository.GetIncidentsByIds(incidentIds) + if err != nil { + return nil, err + } + return dto.ToDtoArray[incident.IncidentEntity, incident.IncidentDTO](incidents), nil +} diff --git a/service/incident/impl/incident_service_test.go b/service/incident/impl/incident_service_test.go index f404600..84aa9d0 100644 --- a/service/incident/impl/incident_service_test.go +++ b/service/incident/impl/incident_service_test.go @@ -391,7 +391,7 @@ func (suite *IncidentServiceSuite) Test_UpdateIncident_ChannelRenameError() { func (suite *IncidentServiceSuite) Test_GetAllIncidents_FailureCase() { suite.incidentRepository.GetAllIncidentsMock.Return(nil, 0, errors.New("error while fetching incidents")) - incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil, nil) + incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil, nil, nil) suite.Nil(incidents, "Incidents should be nil") suite.Equal(0, count, "Count should be 0") suite.Error(err, "Error should not be nil") @@ -400,7 +400,7 @@ func (suite *IncidentServiceSuite) Test_GetAllIncidents_FailureCase() { func (suite *IncidentServiceSuite) Test_GetAllIncidents_SuccessCase() { mockIncidents := &[]incident.IncidentEntity{*GetMockIncident()} suite.incidentRepository.GetAllIncidentsMock.Return(mockIncidents, 1, nil) - incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil, nil) + incidents, count, err := suite.incidentService.GetAllIncidents(nil, nil, nil, nil, nil) suite.NotNil(incidents, "Incidents should not be nil") suite.Equal(1, count, "Count should be 1") suite.Nil(err, "Error should be nil") diff --git a/service/incident/impl/incident_service_v2.go b/service/incident/impl/incident_service_v2.go index 05d74de..31a793f 100644 --- a/service/incident/impl/incident_service_v2.go +++ b/service/incident/impl/incident_service_v2.go @@ -445,7 +445,7 @@ func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentDTO, int, return nil, 0, err } - incidents, resultLength, err := i.incidentRepository.GetAllIncidents(nil, nil, nonTerminalStatusIds, nil) + incidents, resultLength, err := i.incidentRepository.GetAllIncidents(nil, nil, nonTerminalStatusIds, nil, nil) if err != nil { logger.Error(fmt.Sprintf("%s failed to get all incidents", logTag), zap.Error(err)) return nil, 0, err @@ -454,8 +454,8 @@ func (i *IncidentServiceV2) GetAllOpenIncidents() ([]incident.IncidentDTO, int, return dto.ToDtoArray[incident.IncidentEntity, incident.IncidentDTO](*incidents), resultLength, err } -func (i *IncidentServiceV2) GetAllIncidents(teamIds, severityIds, statusIds []uint, isPrivate *bool) ([]incident.IncidentDTO, int, error) { - incidents, resultLength, err := i.incidentRepository.GetAllIncidents(teamIds, severityIds, statusIds, isPrivate) +func (i *IncidentServiceV2) GetAllIncidents(teamIds, severityIds, statusIds []uint, isPrivate *bool, isArchived *bool) ([]incident.IncidentDTO, int, error) { + incidents, resultLength, err := i.incidentRepository.GetAllIncidents(teamIds, severityIds, statusIds, isPrivate, isArchived) if err != nil { return nil, 0, err } diff --git a/service/incident/incident_service_v2_interface.go b/service/incident/incident_service_v2_interface.go index f43ed56..ee414d2 100644 --- a/service/incident/incident_service_v2_interface.go +++ b/service/incident/incident_service_v2_interface.go @@ -20,7 +20,7 @@ type IIncidentService interface { LinkJiraToIncident(incidentId uint, linkedBy string, jiraLinks ...string) error UnLinkJiraFromIncident(incidentId uint, unLinkedBy, jiraLink string) error GetAllOpenIncidents() ([]incident.IncidentDTO, int, error) - GetAllIncidents(teamIds, severityIds, statusIds []uint, isPrivate *bool) ([]incident.IncidentDTO, int, error) + GetAllIncidents(teamIds, severityIds, statusIds []uint, isPrivate *bool, isArchived *bool) ([]incident.IncidentDTO, int, error) GetIncidentRoleByIncidentIdAndRole(incidentId uint, role string) (*incident.IncidentRoleEntity, error) GetIncidentRolesByIncidentIdsAndRole(incidentIds []uint, role string) ([]incident.IncidentRoleDTO, error) UpdateIncidentJiraLinksEntity(incidentEntity *incident.IncidentEntity, updatedBy string, jiraLinks []string) error @@ -39,4 +39,5 @@ type IIncidentService interface { FetchIncidentsApproachingSlaBreach() ([]incident.IncidentDTO, error) AddBotUponChannelUnarchival(channelID string) error CanUserWithEmailAccessIncidentWithId(userEmail string, incidentId uint) bool + GetIncidentsByIds(incidentIds []uint) ([]incident.IncidentDTO, error) } diff --git a/service/incidentUser/incident_user_service_impl.go b/service/incidentUser/incident_user_service_impl.go index daffc97..6416959 100644 --- a/service/incidentUser/incident_user_service_impl.go +++ b/service/incidentUser/incident_user_service_impl.go @@ -1,14 +1,35 @@ package incidentUser import ( + "encoding/json" "fmt" + "github.com/google/uuid" + "github.com/spf13/viper" + "golang.org/x/exp/slices" + requestStatusUtil "houston/common/util/requestStatus" "houston/logger" + incidentModel "houston/model/incident" + incidentUserModel "houston/model/incidentUser" + requestStatusModel "houston/model/requestStatus" "houston/repository/incidentUser" + "houston/service/incident" incidentUserRequest "houston/service/request/incidentUser" + "houston/service/requestStatus" + service "houston/service/response" + slackService "houston/service/slack" + "houston/service/teamUser" + userService "houston/service/user" + "sync" + "time" ) type incidentUserServiceImpl struct { incidentUserRepository incidentUser.IncidentUserRepository + teamUserService teamUser.ITeamUserService + incidentService incident.IIncidentService + slackService slackService.ISlackService + userService userService.UserService + requestStatusService requestStatus.RequestStatusService } const logTag = "[incident-user-service]" @@ -36,3 +57,314 @@ func (i *incidentUserServiceImpl) RemoveIncidentUser(incidentId uint, userId uin } return err } + +func (i *incidentUserServiceImpl) GetIncidentUsersByIncidentId(incidentID uint) (*service.ChannelMembersResponse, error) { + logger.Info(fmt.Sprintf("%s received request to get incident users by incidentId %d", logTag, incidentID)) + + response := &service.ChannelMembersResponse{} + + incidentUsers, err := i.incidentUserRepository.GetIncidentUsersByIncidentId(incidentID) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get incident users by incidentId %d: %v", logTag, incidentID, err)) + return nil, err + } + + if len(incidentUsers) == 0 { + logger.Info(fmt.Sprintf("%s no incident users found for incidentId %d", logTag, incidentID)) + return response, nil + } + + teamUsersExistenceMap, err := i.teamUserService.GetUsersExistenceInTeamMap(incidentUsers[0].Incident.TeamId) + if err != nil { + return nil, err + } + + populateChannelMembersResponse(response, incidentUsers, teamUsersExistenceMap) + + return response, nil +} + +func populateChannelMembersResponse( + response *service.ChannelMembersResponse, + incidentUsers []incidentUserModel.IncidentUserEntity, + teamUsersExistenceMap map[uint]bool, +) { + for _, incidentUser := range incidentUsers { + userInfo := service.UserResponse{ + Id: incidentUser.User.SlackUserId, + Name: incidentUser.User.RealName, + Email: incidentUser.User.Email, + Image: incidentUser.User.Image, + } + if teamUsersExistenceMap[incidentUser.UserID] { + response.Participants = append(response.Participants, userInfo) + } else { + response.Others = append(response.Others, userInfo) + } + } +} + +func (i *incidentUserServiceImpl) SyncIncidentUsers(request incidentUserRequest.SyncIncidentUserRequest) (*requestStatusModel.RequestStatusDTO, error) { + logger.Info(fmt.Sprintf("%s received request to sync incident users", logTag)) + + nonTerminalRequestStatuses, err := i.requestStatusService.GetAllRequestStatusesByTerminalState(false) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get non-terminal request statuses: %v", logTag, err)) + return nil, err + } else if len(nonTerminalRequestStatuses) != 0 { + logger.Info(fmt.Sprintf("%s found %d non-terminal request statuses", logTag, len(nonTerminalRequestStatuses))) + return nil, fmt.Errorf("there are %d non-terminal request statuses. please wait for a while", len(nonTerminalRequestStatuses)) + } + + requestStatus, err := i.requestStatusService.AddRequestStatus( + requestStatusUtil.SyncIncidentUsersRequestType, + requestStatusUtil.Pending{}, + nil, + ) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to add request status: %v", logTag, err)) + return nil, err + } + + go i.manageIncidentUserSync(request, requestStatus.RequestID) + + return requestStatus, nil +} + +func (i *incidentUserServiceImpl) manageIncidentUserSync(request incidentUserRequest.SyncIncidentUserRequest, requestId uuid.UUID) { + err := i.performIncidentUserSync(request) + if err != nil { + errJson, marshallErr := json.Marshal(err.Error()) + if marshallErr != nil { + logger.Error(fmt.Sprintf("%s failed to scan error: %v", logTag, marshallErr)) + return + } + _, updateErr := i.requestStatusService.UpdateRequestStatus(requestId, requestStatusUtil.Failed{}, errJson) + if updateErr != nil { + logger.Error(fmt.Sprintf("%s failed to update request status: %v", logTag, err)) + } + } else { + _, updateErr := i.requestStatusService.UpdateRequestStatus(requestId, requestStatusUtil.Success{}, nil) + if updateErr != nil { + logger.Error(fmt.Sprintf("%s failed to update request status: %v", logTag, updateErr)) + } + } +} + +func (i *incidentUserServiceImpl) performIncidentUserSync(request incidentUserRequest.SyncIncidentUserRequest) error { + incidents, err := i.getIncidentsToSync(request) + if err != nil { + return err + } + + logger.Info(fmt.Sprintf("%s found %d incidents to sync users", logTag, len(incidents))) + incidentToSlackUserIdMap, failedIncidents := i.getIncidentToSlackUserIdMap(incidents) + logger.Info(fmt.Sprintf("%s incidentToSlackUserIdMap: %v", logTag, incidentToSlackUserIdMap)) + logger.Info(fmt.Sprintf("%s failedIncidents: %v", logTag, failedIncidents)) + + if incidentToSlackUserIdMap != nil && len(incidentToSlackUserIdMap) != 0 { + uniqueSlackUserIds := getAllUniqueSlackUserIds(incidentToSlackUserIdMap) + slackUserToHoustonUserIdMap, err := i.userService.GetSlackUserIdToHoustonUserIdMap(uniqueSlackUserIds) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get slack user id to houston user id map: %v", logTag, err)) + return err + } + logger.Info(fmt.Sprintf("%s slackUserToHoustonUserIdMap: %v", logTag, slackUserToHoustonUserIdMap)) + + existingIncidentUsers, err := i.incidentUserRepository.GetIncidentUsersByIncidentIds(getIncidentIds(incidentToSlackUserIdMap)) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get incident users by incident ids: %v", logTag, err)) + return err + } + + incidentIdToIncidentUsers := getIncidentIdToIncidentUsersMap(existingIncidentUsers) + logger.Info(fmt.Sprintf("%s incidentIdToIncidentUsers: %v", logTag, incidentIdToIncidentUsers)) + + incidentUserIdsToRemove, incidentUserMappingsToAdd := getIncidentUserDataToReplace(incidentToSlackUserIdMap, incidentIdToIncidentUsers, slackUserToHoustonUserIdMap) + + err = i.incidentUserRepository.ReplaceIncidentUsers(incidentUserIdsToRemove, incidentUserMappingsToAdd) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to replace incident users: %v", logTag, err)) + return err + } + } + + if len(failedIncidents) != 0 { + return fmt.Errorf("failed to sync users for incidents %v", failedIncidents) + } + + return nil +} + +func (i *incidentUserServiceImpl) getIncidentsToSync(request incidentUserRequest.SyncIncidentUserRequest) ([]incidentModel.IncidentDTO, error) { + if len(request.IncidentIds) == 0 { + isArchived := false + incidents, _, err := i.incidentService.GetAllIncidents(nil, nil, nil, nil, &isArchived) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get all incidents: %v", logTag, err)) + return nil, err + } + return incidents, nil + } + + incidents, err := i.incidentService.GetIncidentsByIds(request.IncidentIds) + if err != nil { + logger.Error(fmt.Sprintf("%s failed to get incidents by ids: %v", logTag, err)) + } + + return incidents, err +} + +func (i *incidentUserServiceImpl) getIncidentToSlackUserIdMap(incidents []incidentModel.IncidentDTO) (map[uint][]string, []uint) { + incidentToSlackUserIdMap := make(map[uint][]string) + + batchSize := viper.GetInt("slack.api.tier3.batch.size") + numberOfBatches := (len(incidents) + batchSize - 1) / batchSize + logger.Info(fmt.Sprintf("%s batch size is: %d", logTag, numberOfBatches)) + + failureIncidentIds := i.processNextBatch(incidents, 0, numberOfBatches, []uint{}, &incidentToSlackUserIdMap, batchSize) + + return incidentToSlackUserIdMap, failureIncidentIds +} + +func (i *incidentUserServiceImpl) processNextBatch( + incidents []incidentModel.IncidentDTO, batchIndex, numBatches int, + failureIncidentIds []uint, incidentToSlackUserIdMap *map[uint][]string, batchSize int, +) []uint { + logger.Info(fmt.Sprintf("%s processing batch %d of %d", logTag, batchIndex+1, numBatches)) + start := batchIndex * batchSize + end := start + batchSize + if end > len(incidents) { + end = len(incidents) + } + + failureIds := i.processCurrentBatch(incidents, start, end, incidentToSlackUserIdMap) + if len(failureIds) != 0 { + logger.Error(fmt.Sprintf("%s failed to process incidents %d in batch %d: %v", logTag, failureIds, batchIndex+1, failureIds)) + failureIncidentIds = append(failureIncidentIds, failureIds...) + } + + logger.Info(fmt.Sprintf("%s processed batch %d of %d", logTag, batchIndex+1, numBatches)) + + if batchIndex+1 < numBatches { + var waitGroup sync.WaitGroup + waitGroup.Add(1) + nextBatchIndex := batchIndex + 1 + time.AfterFunc( + time.Duration(viper.GetInt("slack.api.get.users.in.conversation.interval.in.seconds"))*time.Second, func(nextBatchInd int) func() { + return func() { + failureIncidentIds = i.processNextBatch( + incidents, nextBatchInd, numBatches, failureIncidentIds, incidentToSlackUserIdMap, batchSize, + ) + waitGroup.Done() + } + }(nextBatchIndex)) + waitGroup.Wait() + } + + return failureIncidentIds +} + +func (i *incidentUserServiceImpl) processCurrentBatch( + incidents []incidentModel.IncidentDTO, start, end int, incidentToSlackUserIdMap *map[uint][]string, +) []uint { + var failureIncidentIds []uint + + for _, incident := range incidents[start:end] { + users, err := i.slackService.GetAllUsersInConversation(incident.SlackChannel) + if err != nil { + logger.Error(fmt.Sprintf("%s error in fetching users in conversation %v", logTag, err)) + failureIncidentIds = append(failureIncidentIds, incident.ID) + } else { + (*incidentToSlackUserIdMap)[incident.ID] = users + } + } + + return failureIncidentIds +} + +func getAllUniqueSlackUserIds(incidentToSlackUserIdMap map[uint][]string) []string { + slackUserIdsSet := make(map[string]bool) + for _, slackUserIds := range incidentToSlackUserIdMap { + for _, slackUserId := range slackUserIds { + slackUserIdsSet[slackUserId] = true + } + } + + var uniqueSlackUserIds []string + for slackUserId := range slackUserIdsSet { + uniqueSlackUserIds = append(uniqueSlackUserIds, slackUserId) + } + + return uniqueSlackUserIds +} + +func getIncidentIds(incidentToSlackUserIdMap map[uint][]string) []uint { + incidentIds := make([]uint, 0, len(incidentToSlackUserIdMap)) + for key := range incidentToSlackUserIdMap { + incidentIds = append(incidentIds, key) + } + return incidentIds +} + +func getIncidentIdToIncidentUsersMap(incidentUsers []incidentUserModel.IncidentUserEntity) map[uint][]incidentUserModel.IncidentUserEntity { + incidentIdToIncidentUsers := make(map[uint][]incidentUserModel.IncidentUserEntity) + for _, incidentUser := range incidentUsers { + incidentIdToIncidentUsers[incidentUser.IncidentID] = append(incidentIdToIncidentUsers[incidentUser.IncidentID], incidentUser) + } + logger.Info(fmt.Sprintf("%s incidentIdToIncidentUsers: %v", logTag, incidentIdToIncidentUsers)) + return incidentIdToIncidentUsers +} + +func getIncidentUserDataToReplace( + incidentToSlackUserIdMap map[uint][]string, + incidentIdToIncidentUsersMap map[uint][]incidentUserModel.IncidentUserEntity, + slackUserIdToHoustonUserMap map[string]uint, +) ([]uint, [][]uint) { + var incidentUserIdsToRemove []uint + var incidentIdToUserIdMappingToAdd [][]uint + incidentIdToSlackUserIdExistenceMap := getIncidentIdToSlackUserIdExistenceMap(incidentIdToIncidentUsersMap) + + for incidentId, slackUserIds := range incidentToSlackUserIdMap { + for _, slackUserId := range slackUserIds { + if _, exists := incidentIdToSlackUserIdExistenceMap[incidentAndSlackUserIdPair{ + incidentId: incidentId, + slackUserId: slackUserId, + }]; !exists { + incidentIdToUserIdMappingToAdd = append(incidentIdToUserIdMappingToAdd, []uint{incidentId, slackUserIdToHoustonUserMap[slackUserId]}) + } + } + } + + for _, incidentUsers := range incidentIdToIncidentUsersMap { + for _, incidentUser := range incidentUsers { + slackUsers, exists := incidentToSlackUserIdMap[incidentUser.IncidentID] + if !exists || !slices.Contains(slackUsers, incidentUser.User.SlackUserId) { + incidentUserIdsToRemove = append(incidentUserIdsToRemove, incidentUser.ID) + } + } + } + + logger.Info(fmt.Sprintf("%s incidentUserIdsToRemove: %v", logTag, incidentUserIdsToRemove)) + logger.Info(fmt.Sprintf("%s incidentIdToUserIdMappingToAdd: %v", logTag, incidentIdToUserIdMappingToAdd)) + + return incidentUserIdsToRemove, incidentIdToUserIdMappingToAdd +} + +func getIncidentIdToSlackUserIdExistenceMap(incidentIdToIncidentUsersMap map[uint][]incidentUserModel.IncidentUserEntity) map[incidentAndSlackUserIdPair]bool { + incidentIdToSlackUserIdExistenceMap := make(map[incidentAndSlackUserIdPair]bool) + for incidentId, incidentUsers := range incidentIdToIncidentUsersMap { + for _, incidentUser := range incidentUsers { + incidentIdToSlackUserIdExistenceMap[incidentAndSlackUserIdPair{ + incidentId: incidentId, + slackUserId: incidentUser.User.SlackUserId, + }] = true + } + } + return incidentIdToSlackUserIdExistenceMap +} + +type incidentAndSlackUserIdPair struct { + incidentId uint + slackUserId string +} diff --git a/service/incidentUser/incident_user_service_interface.go b/service/incidentUser/incident_user_service_interface.go index 73bdf0d..578f697 100644 --- a/service/incidentUser/incident_user_service_interface.go +++ b/service/incidentUser/incident_user_service_interface.go @@ -1,17 +1,38 @@ package incidentUser import ( + requestStatusModel "houston/model/requestStatus" "houston/repository/incidentUser" + "houston/service/incident" incidentUserRequest "houston/service/request/incidentUser" + "houston/service/requestStatus" + service "houston/service/response" + "houston/service/slack" + "houston/service/teamUser" + userService "houston/service/user" ) type IncidentUserService interface { AddIncidentUser(request incidentUserRequest.AddIncidentUserRequest) error RemoveIncidentUser(incidentId uint, userId uint) error + GetIncidentUsersByIncidentId(incidentID uint) (*service.ChannelMembersResponse, error) + SyncIncidentUsers(request incidentUserRequest.SyncIncidentUserRequest) (*requestStatusModel.RequestStatusDTO, error) } -func NewIncidentUserService(incidentUserRepository incidentUser.IncidentUserRepository) IncidentUserService { +func NewIncidentUserService( + incidentUserRepository incidentUser.IncidentUserRepository, + teamUserService teamUser.ITeamUserService, + userService userService.UserService, + incidentService incident.IIncidentService, + slackService slack.ISlackService, + requestStatusService requestStatus.RequestStatusService, +) IncidentUserService { return &incidentUserServiceImpl{ incidentUserRepository: incidentUserRepository, + teamUserService: teamUserService, + userService: userService, + incidentService: incidentService, + slackService: slackService, + requestStatusService: requestStatusService, } } diff --git a/service/incidentUser/incident_user_service_test.go b/service/incidentUser/incident_user_service_test.go index 0274c20..da3f67e 100644 --- a/service/incidentUser/incident_user_service_test.go +++ b/service/incidentUser/incident_user_service_test.go @@ -2,9 +2,13 @@ package incidentUser import ( "errors" + "github.com/spf13/viper" "github.com/stretchr/testify/suite" "houston/logger" "houston/mocks" + incidentModel "houston/model/incident" + incidentUserModel "houston/model/incidentUser" + requestStatusModel "houston/model/requestStatus" "houston/service/request/incidentUser" "testing" ) @@ -12,6 +16,11 @@ import ( type IncidentUserServiceSuite struct { suite.Suite incidentUserRepository *mocks.IncidentUserRepositoryMock + teamUserService *mocks.ITeamUserServiceMock + requestStatusService *mocks.RequestStatusServiceMock + incidentService *mocks.IIncidentServiceMock + slackService *mocks.ISlackServiceMock + userService *mocks.UserServiceMock incidentUserService *incidentUserServiceImpl } @@ -45,16 +54,202 @@ func (suite *IncidentUserServiceSuite) Test_RemoveIncidentUser_SuccessCase() { suite.Nil(err, "error should be nil") } -func (suite *IncidentUserServiceSuite) SetupSuite() { +func (suite *IncidentUserServiceSuite) Test_GetIncidentUsersByIncidentId_FailureCase() { + suite.incidentUserRepository.GetIncidentUsersByIncidentIdMock.Return(nil, errors.New("DB error")) + + _, err := suite.incidentUserService.GetIncidentUsersByIncidentId(1) + suite.NotNil(err, "error should not be nil") + suite.EqualError(err, "DB error", "error should be DB error") +} + +func (suite *IncidentUserServiceSuite) Test_GetIncidentUsersByIncidentId_EmptyCase() { + suite.incidentUserRepository.GetIncidentUsersByIncidentIdMock.Return([]incidentUserModel.IncidentUserEntity{}, nil) + + response, err := suite.incidentUserService.GetIncidentUsersByIncidentId(1) + suite.Nil(err, "error should be nil") + suite.Empty(response, "response should be empty") +} + +func (suite *IncidentUserServiceSuite) Test_GetIncidentUsersByIncidentId_UserExistenceError() { + suite.incidentUserRepository.GetIncidentUsersByIncidentIdMock.Return([]incidentUserModel.IncidentUserEntity{{IncidentID: 1, UserID: 1}}, nil) + suite.teamUserService.GetUsersExistenceInTeamMapMock.Return(nil, errors.New("DB error")) + + _, err := suite.incidentUserService.GetIncidentUsersByIncidentId(1) + suite.NotNil(err, "error should not be nil") + suite.EqualError(err, "DB error", "error should be DB error") +} + +func (suite *IncidentUserServiceSuite) Test_GetIncidentUsersByIncidentId_SuccessCase() { + suite.incidentUserRepository.GetIncidentUsersByIncidentIdMock.Return([]incidentUserModel.IncidentUserEntity{{ + IncidentID: 1, UserID: 1}, + {IncidentID: 1, UserID: 2}, + }, nil) + suite.teamUserService.GetUsersExistenceInTeamMapMock.Return(map[uint]bool{1: true, 2: false}, nil) + + response, err := suite.incidentUserService.GetIncidentUsersByIncidentId(1) + suite.Nil(err, "error should be nil") + suite.NotEmpty(response, "response should not be empty") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_RequestStatusDuplicateFailure() { + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return(nil, errors.New("DB error")) + + requestStatus, err := suite.incidentUserService.SyncIncidentUsers(incidentUser.SyncIncidentUserRequest{}) + suite.NotNil(err, "error should not be nil") + suite.EqualError(err, "DB error", "error should be DB error") + suite.Nil(requestStatus, "requestStatus should be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_RequestStatusDuplicateExists() { + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{ + getMockRequestStatus(), getMockRequestStatus(), getMockRequestStatus(), + }, nil) + + requestStatus, err := suite.incidentUserService.SyncIncidentUsers(incidentUser.SyncIncidentUserRequest{}) + suite.NotNil(err, "error should not be nil") + suite.Nil(requestStatus, "requestStatus should be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_AddRequestStatusFailure() { + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{}, nil) + suite.requestStatusService.AddRequestStatusMock.Return(nil, errors.New("DB error")) + + requestStatus, err := suite.incidentUserService.SyncIncidentUsers(incidentUser.SyncIncidentUserRequest{}) + suite.NotNil(err, "error should not be nil") + suite.EqualError(err, "DB error", "error should be DB error") + suite.Nil(requestStatus, "requestStatus should be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_AddRequestStatusSuccess() { + mockRequestStatus := getMockRequestStatus() + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{}, nil) + suite.requestStatusService.AddRequestStatusMock.Return(&mockRequestStatus, nil) + suite.incidentService.GetAllIncidentsMock.Return(nil, 0, errors.New("DB error")) + suite.requestStatusService.UpdateRequestStatusMock.Return(nil, errors.New("DB error")) + + requestStatus, err := suite.incidentUserService.SyncIncidentUsers(incidentUser.SyncIncidentUserRequest{}) + suite.Nil(err, "error should be nil") + suite.NotNil(requestStatus, "requestStatus should not be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_HappyFlow() { + suite.incidentService.GetAllIncidentsMock.Return(getMockIncidents(), 0, nil) + suite.slackService.GetAllUsersInConversationMock.Return([]string{"1", "2", "3"}, nil) + suite.incidentUserRepository.GetIncidentUsersByIncidentIdsMock.Return(getMockIncidentUsers(), nil) + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{}, nil) + suite.incidentUserRepository.ReplaceIncidentUsersMock.Return(nil) + suite.userService.GetSlackUserIdToHoustonUserIdMapMock.Return(map[string]uint{"1": 1, "2": 2, "3": 3}, nil) + + err := suite.incidentUserService.performIncidentUserSync(incidentUser.SyncIncidentUserRequest{}) + suite.Nil(err, "error should be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_GetIncidentsByIdsFailure() { + suite.incidentService.GetIncidentsByIdsMock.Return(nil, errors.New("DB error")) + + err := suite.incidentUserService.performIncidentUserSync(incidentUser.SyncIncidentUserRequest{[]uint{1, 2, 3}}) + suite.NotNil(err, "error should not be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_IncidentIdsHappyFlow() { + suite.incidentService.GetIncidentsByIdsMock.Return(getMockIncidents(), nil) + suite.slackService.GetAllUsersInConversationMock.Return([]string{"1", "2", "3"}, nil) + suite.incidentUserRepository.GetIncidentUsersByIncidentIdsMock.Return(getMockIncidentUsers(), nil) + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{}, nil) + suite.incidentUserRepository.ReplaceIncidentUsersMock.Return(nil) + suite.userService.GetSlackUserIdToHoustonUserIdMapMock.Return(map[string]uint{"1": 1, "2": 2, "3": 3}, nil) + + err := suite.incidentUserService.performIncidentUserSync(incidentUser.SyncIncidentUserRequest{[]uint{1, 2, 3}}) + suite.Nil(err, "error should be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_ReplaceUsersError() { + suite.incidentService.GetAllIncidentsMock.Return(getMockIncidents(), 0, nil) + suite.slackService.GetAllUsersInConversationMock.Return([]string{"1", "2", "3"}, nil) + suite.incidentUserRepository.GetIncidentUsersByIncidentIdsMock.Return(getMockIncidentUsers(), nil) + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{}, nil) + suite.userService.GetSlackUserIdToHoustonUserIdMapMock.Return(map[string]uint{"1": 1, "2": 2, "3": 3}, nil) + suite.incidentUserRepository.ReplaceIncidentUsersMock.Return(errors.New("DB error")) + + err := suite.incidentUserService.performIncidentUserSync(incidentUser.SyncIncidentUserRequest{}) + suite.NotNil(err, "error should not be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_GetIncidentUsersByIncidentIdsError() { + suite.incidentService.GetAllIncidentsMock.Return(getMockIncidents(), 0, nil) + suite.slackService.GetAllUsersInConversationMock.Return([]string{"1", "2", "3"}, nil) + suite.incidentUserRepository.GetIncidentUsersByIncidentIdsMock.Return(nil, errors.New("DB error")) + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{}, nil) + suite.userService.GetSlackUserIdToHoustonUserIdMapMock.Return(map[string]uint{"1": 1, "2": 2, "3": 3}, nil) + + err := suite.incidentUserService.performIncidentUserSync(incidentUser.SyncIncidentUserRequest{}) + suite.NotNil(err, "error should not be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_GetSlackUserIdToHoustonUserIdMapError() { + suite.incidentService.GetAllIncidentsMock.Return(getMockIncidents(), 0, nil) + suite.slackService.GetAllUsersInConversationMock.Return([]string{"1", "2", "3"}, nil) + suite.incidentUserRepository.GetIncidentUsersByIncidentIdsMock.Return(getMockIncidentUsers(), nil) + suite.requestStatusService.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusDTO{}, nil) + suite.userService.GetSlackUserIdToHoustonUserIdMapMock.Return(nil, errors.New("DB error")) + + err := suite.incidentUserService.performIncidentUserSync(incidentUser.SyncIncidentUserRequest{}) + suite.NotNil(err, "error should not be nil") +} + +func (suite *IncidentUserServiceSuite) Test_SyncIncidentUsers_GetAllUsersInConversationError() { + suite.incidentService.GetAllIncidentsMock.Return(getMockIncidents(), 0, nil) + suite.slackService.GetAllUsersInConversationMock.Return(nil, errors.New("Slack error")) + + err := suite.incidentUserService.performIncidentUserSync(incidentUser.SyncIncidentUserRequest{}) + suite.NotNil(err, "error should not be nil") +} + +func (suite *IncidentUserServiceSuite) SetupTest() { logger.InitLogger() + viper.Set("slack.api.tier3.batch.size", 2) + viper.Set("slack.api.get.users.in.conversation.interval.in.seconds", 1) suite.incidentUserRepository = mocks.NewIncidentUserRepositoryMock(suite.T()) - suite.incidentUserService = &incidentUserServiceImpl{incidentUserRepository: suite.incidentUserRepository} + suite.teamUserService = mocks.NewITeamUserServiceMock(suite.T()) + suite.requestStatusService = mocks.NewRequestStatusServiceMock(suite.T()) + suite.incidentService = mocks.NewIIncidentServiceMock(suite.T()) + suite.slackService = mocks.NewISlackServiceMock(suite.T()) + suite.userService = mocks.NewUserServiceMock(suite.T()) + suite.incidentUserService = &incidentUserServiceImpl{ + incidentUserRepository: suite.incidentUserRepository, + teamUserService: suite.teamUserService, + requestStatusService: suite.requestStatusService, + incidentService: suite.incidentService, + slackService: suite.slackService, + userService: suite.userService, + } } func getMockIncidentUserRequest() incidentUser.AddIncidentUserRequest { return incidentUser.AddIncidentUserRequest{IncidentID: 1, UserID: 1} } +func getMockRequestStatus() requestStatusModel.RequestStatusDTO { + return requestStatusModel.RequestStatusDTO{} +} + +func getMockIncidents() []incidentModel.IncidentDTO { + return []incidentModel.IncidentDTO{ + {ID: 1, IncidentName: "Incident 1"}, + {ID: 2, IncidentName: "Incident 2"}, + {ID: 3, IncidentName: "Incident 3"}, + } +} + +func getMockIncidentUsers() []incidentUserModel.IncidentUserEntity { + return []incidentUserModel.IncidentUserEntity{ + {IncidentID: 1, UserID: 1}, + {IncidentID: 2, UserID: 2}, + {IncidentID: 3, UserID: 3}, + {IncidentID: 1, UserID: 4}, + } +} + func TestIncidentUserService(t *testing.T) { suite.Run(t, new(IncidentUserServiceSuite)) } diff --git a/service/reminder/team_incidents_reminder.go b/service/reminder/team_incidents_reminder.go index 0dfc783..77e719b 100644 --- a/service/reminder/team_incidents_reminder.go +++ b/service/reminder/team_incidents_reminder.go @@ -22,7 +22,7 @@ var isPrivate = false func (service *reminderServiceImpl) PostTeamIncidents() error { logger.Info(fmt.Sprintf("%s received request to post team metrics", postTeamMetricsLogTag)) - openIncidents, _, err := service.incidentService.GetAllIncidents(nil, teamMetricsSeverities, teamMetricsStatuses, &isPrivate) + openIncidents, _, err := service.incidentService.GetAllIncidents(nil, teamMetricsSeverities, teamMetricsStatuses, &isPrivate, nil) if err != nil { logger.Error(fmt.Sprintf("error while fetching open incidents. %+v", err)) return err diff --git a/service/request/incidentUser/sync_incident_user.go b/service/request/incidentUser/sync_incident_user.go new file mode 100644 index 0000000..2138846 --- /dev/null +++ b/service/request/incidentUser/sync_incident_user.go @@ -0,0 +1,5 @@ +package incidentUser + +type SyncIncidentUserRequest struct { + IncidentIds []uint `json:"incident_ids,omitempty"` +} diff --git a/service/requestStatus/request_status_service_impl.go b/service/requestStatus/request_status_service_impl.go new file mode 100644 index 0000000..ea1887d --- /dev/null +++ b/service/requestStatus/request_status_service_impl.go @@ -0,0 +1,57 @@ +package requestStatus + +import ( + "github.com/google/uuid" + "gorm.io/datatypes" + "houston/common/util/dto" + requestStatusUtil "houston/common/util/requestStatus" + requestStatusModel "houston/model/requestStatus" + "houston/repository/requestStatus" +) + +type requestStatusServiceImpl struct { + requestStatusRepo requestStatus.RequestStatusRepository +} + +func (service *requestStatusServiceImpl) AddRequestStatus( + requestType requestStatusUtil.RequestType, status requestStatusUtil.RequestStatus, message datatypes.JSON, +) (*requestStatusModel.RequestStatusDTO, error) { + entity, err := service.requestStatusRepo.AddRequestStatus(requestType, status, message) + if err != nil { + return nil, err + } + + requestStatusDTO := entity.ToDTO() + return &requestStatusDTO, nil +} + +func (service *requestStatusServiceImpl) GetRequestStatusByRequestId(requestId uuid.UUID) (*requestStatusModel.RequestStatusDTO, error) { + entity, err := service.requestStatusRepo.GetRequestStatusByRequestId(requestId) + if err != nil { + return nil, err + } + + requestStatusDTO := entity.ToDTO() + return &requestStatusDTO, nil +} + +func (service *requestStatusServiceImpl) UpdateRequestStatus( + requestId uuid.UUID, status requestStatusUtil.RequestStatus, message datatypes.JSON, +) (*requestStatusModel.RequestStatusDTO, error) { + entity, err := service.requestStatusRepo.UpdateRequestStatus(requestId, status, message) + if err != nil { + return nil, err + } + + requestStatusDTO := entity.ToDTO() + return &requestStatusDTO, nil +} + +func (service *requestStatusServiceImpl) GetAllRequestStatusesByTerminalState(terminalState bool) ([]requestStatusModel.RequestStatusDTO, error) { + entities, err := service.requestStatusRepo.GetAllRequestStatusesByTerminalState(terminalState) + if err != nil { + return nil, err + } + + return dto.ToDtoArray[requestStatusModel.RequestStatusEntity, requestStatusModel.RequestStatusDTO](entities), nil +} diff --git a/service/requestStatus/request_status_service_interface.go b/service/requestStatus/request_status_service_interface.go new file mode 100644 index 0000000..ac9dcef --- /dev/null +++ b/service/requestStatus/request_status_service_interface.go @@ -0,0 +1,26 @@ +package requestStatus + +import ( + "github.com/google/uuid" + "gorm.io/datatypes" + requestStatusUtil "houston/common/util/requestStatus" + "houston/model/requestStatus" + requestStatusRepo "houston/repository/requestStatus" +) + +type RequestStatusService interface { + AddRequestStatus( + requestType requestStatusUtil.RequestType, status requestStatusUtil.RequestStatus, message datatypes.JSON, + ) (*requestStatus.RequestStatusDTO, error) + GetRequestStatusByRequestId(requestId uuid.UUID) (*requestStatus.RequestStatusDTO, error) + UpdateRequestStatus( + requestId uuid.UUID, status requestStatusUtil.RequestStatus, message datatypes.JSON, + ) (*requestStatus.RequestStatusDTO, error) + GetAllRequestStatusesByTerminalState(terminalState bool) ([]requestStatus.RequestStatusDTO, error) +} + +func NewRequestStatusService(requestStatusRepo requestStatusRepo.RequestStatusRepository) RequestStatusService { + return &requestStatusServiceImpl{ + requestStatusRepo: requestStatusRepo, + } +} diff --git a/service/requestStatus/request_status_service_test.go b/service/requestStatus/request_status_service_test.go new file mode 100644 index 0000000..9c06fa6 --- /dev/null +++ b/service/requestStatus/request_status_service_test.go @@ -0,0 +1,90 @@ +package requestStatus + +import ( + "errors" + "github.com/google/uuid" + "github.com/stretchr/testify/suite" + "houston/common/util/requestStatus" + "houston/logger" + "houston/mocks" + requestStatusModel "houston/model/requestStatus" + "testing" +) + +type RequestStatusServiceSuite struct { + suite.Suite + requestStatusRepository *mocks.RequestStatusRepositoryMock + requestStatusService *requestStatusServiceImpl +} + +func (suite *RequestStatusServiceSuite) Test_AddRequestStatus_FailureCase() { + requestStatusEntity := requestStatusModel.RequestStatusEntity{} + suite.requestStatusRepository.AddRequestStatusMock.Return(requestStatusEntity, errors.New("test error")) + + _, err := suite.requestStatusService.AddRequestStatus("REQUEST_TYPE", requestStatus.Pending{}, nil) + suite.NotNil(err, "error should not be nil") +} + +func (suite *RequestStatusServiceSuite) Test_AddRequestStatus_SuccessCase() { + requestStatusEntity := requestStatusModel.RequestStatusEntity{} + suite.requestStatusRepository.AddRequestStatusMock.Return(requestStatusEntity, nil) + + _, err := suite.requestStatusService.AddRequestStatus("REQUEST_TYPE", requestStatus.Pending{}, nil) + suite.Nil(err, "error should be nil") +} + +func (suite *RequestStatusServiceSuite) Test_GetRequestStatusByRequestId_FailureCase() { + suite.requestStatusRepository.GetRequestStatusByRequestIdMock.Return(nil, errors.New("test error")) + + _, err := suite.requestStatusService.GetRequestStatusByRequestId(uuid.UUID{}) + suite.NotNil(err, "error should not be nil") +} + +func (suite *RequestStatusServiceSuite) Test_GetRequestStatusByRequestId_SuccessCase() { + requestStatusEntity := requestStatusModel.RequestStatusEntity{} + suite.requestStatusRepository.GetRequestStatusByRequestIdMock.Return(&requestStatusEntity, nil) + + _, err := suite.requestStatusService.GetRequestStatusByRequestId(uuid.UUID{}) + suite.Nil(err, "error should be nil") +} + +func (suite *RequestStatusServiceSuite) Test_UpdateRequestStatus_FailureCase() { + requestStatusEntity := requestStatusModel.RequestStatusEntity{} + suite.requestStatusRepository.UpdateRequestStatusMock.Return(requestStatusEntity, errors.New("test error")) + + _, err := suite.requestStatusService.UpdateRequestStatus(uuid.UUID{}, requestStatus.Pending{}, nil) + suite.NotNil(err, "error should not be nil") +} + +func (suite *RequestStatusServiceSuite) Test_UpdateRequestStatus_SuccessCase() { + requestStatusEntity := requestStatusModel.RequestStatusEntity{} + suite.requestStatusRepository.UpdateRequestStatusMock.Return(requestStatusEntity, nil) + + _, err := suite.requestStatusService.UpdateRequestStatus(uuid.UUID{}, requestStatus.Pending{}, nil) + suite.Nil(err, "error should be nil") +} + +func (suite *RequestStatusServiceSuite) Test_GetAllRequestStatusesByTerminalState_FailureCase() { + suite.requestStatusRepository.GetAllRequestStatusesByTerminalStateMock.Return(nil, errors.New("test error")) + + _, err := suite.requestStatusService.GetAllRequestStatusesByTerminalState(true) + suite.NotNil(err, "error should not be nil") +} + +func (suite *RequestStatusServiceSuite) Test_GetAllRequestStatusesByTerminalState_SuccessCase() { + requestStatusEntity := requestStatusModel.RequestStatusEntity{} + suite.requestStatusRepository.GetAllRequestStatusesByTerminalStateMock.Return([]requestStatusModel.RequestStatusEntity{requestStatusEntity}, nil) + + _, err := suite.requestStatusService.GetAllRequestStatusesByTerminalState(true) + suite.Nil(err, "error should be nil") +} + +func (suite *RequestStatusServiceSuite) SetupSuite() { + logger.InitLogger() + suite.requestStatusRepository = mocks.NewRequestStatusRepositoryMock(suite.T()) + suite.requestStatusService = &requestStatusServiceImpl{requestStatusRepo: suite.requestStatusRepository} +} + +func TestRequestStatusService(t *testing.T) { + suite.Run(t, new(RequestStatusServiceSuite)) +} diff --git a/service/slack/slack_service.go b/service/slack/slack_service.go index 92db66d..0701a8d 100644 --- a/service/slack/slack_service.go +++ b/service/slack/slack_service.go @@ -459,13 +459,41 @@ func (s *SlackService) getConversationHistoryWithParams(params slack.GetConversa // GetAllUsersInConversation - gets list of members in a particular Slack channel func (s *SlackService) GetAllUsersInConversation(channelId string) ([]string, error) { - users, _, err := s.SocketModeClientWrapper.GetUsersInConversation(&slack.GetUsersInConversationParameters{ - ChannelID: channelId, - }) - if err != nil { - return nil, err + var allUsers []string + cursor := "" + + for { + users, nextCursor, err := s.fetchUsersFromConversation(channelId, cursor) + if err != nil { + logger.Error(fmt.Sprintf("error in getting users from conversation %v", err)) + return nil, err + } + + allUsers = append(allUsers, users...) + + if util.IsBlank(nextCursor) { + break + } + + cursor = nextCursor } - return users, nil + + return allUsers, nil +} + +func (s *SlackService) fetchUsersFromConversation(channelId, cursor string) ([]string, string, error) { + users, nextCursor, err := s.SocketModeClientWrapper.GetUsersInConversation( + &slack.GetUsersInConversationParameters{ + ChannelID: channelId, + Cursor: cursor, + Limit: usersInConversationLimit, + }, + ) + if err != nil { + return nil, "", err + } + + return users, nextCursor, nil } // GetNonBotUsersInConversation - gets list of non bot members in a particular Slack channel @@ -663,3 +691,4 @@ const ( const userOptionLimit = 200 const channelTopicLengthLimit = 250 const maxAllowedTopicIndex = 247 +const usersInConversationLimit = 100 diff --git a/service/teamUser/impl/team_user_service.go b/service/teamUser/impl/team_user_service.go index 106aacf..09ec1ec 100644 --- a/service/teamUser/impl/team_user_service.go +++ b/service/teamUser/impl/team_user_service.go @@ -43,6 +43,16 @@ func (service *TeamUserService) GetTeamUsersByTeamId(teamId uint) ([]teamUserMod return dtoConverter.TeamUserEntitiesToDTOs(teamUsers), nil } +func (service *TeamUserService) GetUsersExistenceInTeamMap(teamId uint) (map[uint]bool, error) { + teamUsers, err := service.teamUserRepository.GetTeamUsersByTeamId(teamId) + if err != nil { + logger.Error("error in fetching team users", zap.Error(err)) + return nil, err + } + + return convertToExistenceMap(teamUsers), nil +} + func (service *TeamUserService) GetTeamsByUserId(userId uint) ([]teamUserModel.TeamUserDTO, error) { teamUsers, err := service.teamUserRepository.GetTeamsByUserId(userId) if err != nil { @@ -122,3 +132,12 @@ func (service *TeamUserService) GetAllTeamUsers() ([]teamUserModel.TeamUserDTO, return dtoConverter.TeamUserEntitiesToDTOs(teamUsers), nil } + +func convertToExistenceMap(teamUsers []teamUserModel.TeamUserEntity) map[uint]bool { + teamUserMap := make(map[uint]bool) + for _, teamUser := range teamUsers { + teamUserMap[teamUser.UserID] = true + } + + return teamUserMap +} diff --git a/service/teamUser/impl/team_user_service_test.go b/service/teamUser/impl/team_user_service_test.go index ba57b83..9347a9b 100644 --- a/service/teamUser/impl/team_user_service_test.go +++ b/service/teamUser/impl/team_user_service_test.go @@ -144,6 +144,22 @@ func (suite *TeamUserServiceSuite) Test_GetTeamUsersWithMinimumSeverityIdLessTha suite.NotNil(teamUsers, "teamUsers should not be nil") } +func (suite *TeamUserServiceSuite) Test_GetUsersExistenceInTeamMap_RepoFailureCase() { + suite.teamUserRepository.GetTeamUsersByTeamIdMock.Return(nil, errors.New("error")) + + teamUsers, err := suite.teamUserService.GetUsersExistenceInTeamMap(1) + suite.Nil(teamUsers, "teamUsers should be nil") + suite.EqualError(err, "error", "error should be error") +} + +func (suite *TeamUserServiceSuite) Test_GetUsersExistenceInTeamMap_SuccessCase() { + suite.teamUserRepository.GetTeamUsersByTeamIdMock.Return(GetMockTeamUserEntities(), nil) + + teamUsers, err := suite.teamUserService.GetUsersExistenceInTeamMap(1) + suite.Nil(err, "error should be nil") + suite.NotNil(teamUsers, "teamUsers should not be nil") +} + func (suite *TeamUserServiceSuite) SetupSuite() { logger.InitLogger() suite.teamUserRepository = mocks.NewTeamUserRepositoryMock(suite.T()) diff --git a/service/teamUser/team_user_service_interface.go b/service/teamUser/team_user_service_interface.go index 5735875..bedff08 100644 --- a/service/teamUser/team_user_service_interface.go +++ b/service/teamUser/team_user_service_interface.go @@ -13,4 +13,5 @@ type ITeamUserService interface { GetTeamUsersWithMinimumSeverityIdLessThanOrEqualToGivenSeverity(teamId, severityId uint) ([]teamUserModel.TeamUserDTO, error) GetTeamUsersForGivenSeverity(teamId, severityId uint) ([]teamUserModel.TeamUserDTO, error) GetAllTeamUsers() ([]teamUserModel.TeamUserDTO, error) + GetUsersExistenceInTeamMap(teamId uint) (map[uint]bool, error) } diff --git a/service/user/user_service.go b/service/user/user_service.go index 4c68f2c..5a9b100 100644 --- a/service/user/user_service.go +++ b/service/user/user_service.go @@ -72,3 +72,18 @@ func (service *userServiceImpl) GetHoustonUserBySlackUserId(slackUserId string) return userEntity.ToDTO(), nil } + +func (service *userServiceImpl) GetSlackUserIdToHoustonUserIdMap(slackUserIds []string) (map[string]uint, error) { + users, err := service.userRepository.FindHoustonUsersBySlackUserIds(slackUserIds) + if err != nil { + logger.Error("error in fetching users by slack user ids", zap.Error(err)) + return nil, err + } + + slackUserIdToHoustonUserIdMap := make(map[string]uint) + for _, user := range users { + slackUserIdToHoustonUserIdMap[user.SlackUserId] = user.ID + } + + return slackUserIdToHoustonUserIdMap, nil +} diff --git a/service/user/user_service_interface.go b/service/user/user_service_interface.go index 6cdb71a..a6fe729 100644 --- a/service/user/user_service_interface.go +++ b/service/user/user_service_interface.go @@ -11,6 +11,7 @@ type UserService interface { GetHoustonUserById(id uint) (*user.UserDTO, error) GetHoustonUserBySlackUserId(slackUserId string) (*user.UserDTO, error) UpsertUsers() + GetSlackUserIdToHoustonUserIdMap(slackUserIds []string) (map[string]uint, error) } func NewUserService(userRepository user.IUserRepository, slackService slack.ISlackService) UserService { diff --git a/service/user/user_service_test.go b/service/user/user_service_test.go index 41b5174..9d80c66 100644 --- a/service/user/user_service_test.go +++ b/service/user/user_service_test.go @@ -92,6 +92,48 @@ func (suite *UserServiceSuite) Test_GetHoustonUserBySlackID_SuccessCase() { suite.NotNil(user, "user should not be nil") } +func (suite *UserServiceSuite) Test_GetHoustonUserBySlackUserId_RepoFailureCase() { + suite.userRepository.FindHoustonUserBySlackUserIdMock.Return(nil, errors.New("error")) + + user, err := suite.userService.GetHoustonUserBySlackUserId("slackUserId") + suite.NotNil(err, "error should not be nil") + suite.Nil(user, "user should be nil") +} + +func (suite *UserServiceSuite) Test_GetHoustonUserBySlackUserId_NilUserCase() { + suite.userRepository.FindHoustonUserBySlackUserIdMock.Return(nil, nil) + + user, err := suite.userService.GetHoustonUserBySlackUserId("slackUserId") + suite.Nil(err, "error should be nil") + suite.Nil(user, "user should be nil") +} + +func (suite *UserServiceSuite) Test_GetHoustonUserBySlackUserId_SuccessCase() { + mockUserEntity := GetMockUserEntity() + suite.userRepository.FindHoustonUserBySlackUserIdMock.Return(&mockUserEntity, nil) + + user, err := suite.userService.GetHoustonUserBySlackUserId("slackUserId") + suite.Nil(err, "error should be nil") + suite.NotNil(user, "user should not be nil") +} + +func (suite *UserServiceSuite) Test_GetSlackUserIdToHoustonUserIdMap_RepoFailureCase() { + suite.userRepository.FindHoustonUsersBySlackUserIdsMock.Return(nil, errors.New("error")) + + user, err := suite.userService.GetSlackUserIdToHoustonUserIdMap([]string{"slackUserId"}) + suite.NotNil(err, "error should not be nil") + suite.Nil(user, "user should be nil") +} + +func (suite *UserServiceSuite) Test_GetSlackUserIdToHoustonUserIdMap_SuccessCase() { + mockUserEntities := *GetMockHoustonUsers() + suite.userRepository.FindHoustonUsersBySlackUserIdsMock.Return(mockUserEntities, nil) + + user, err := suite.userService.GetSlackUserIdToHoustonUserIdMap([]string{"slackUserId"}) + suite.Nil(err, "error should be nil") + suite.NotNil(user, "user should not be nil") +} + func (suite *UserServiceSuite) SetupSuite() { logger.InitLogger() suite.userRepository = mocks.NewIUserRepositoryMock(suite.T())