Skip to content

Commit

Permalink
feat: forbid create/update teams with same names
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandrMatsko committed Nov 13, 2024
1 parent a16c390 commit d7641a5
Show file tree
Hide file tree
Showing 14 changed files with 420 additions and 28 deletions.
19 changes: 19 additions & 0 deletions api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ const (
type LimitsConfig struct {
// Trigger contains limits for triggers.
Trigger TriggerLimits
// Trigger contains limits for teams.
Team TeamLimits
}

// TriggerLimits contains all limits applied for triggers.
Expand All @@ -85,5 +87,22 @@ func GetTestLimitsConfig() LimitsConfig {
Trigger: TriggerLimits{
MaxNameSize: DefaultTriggerNameMaxSize,
},
Team: TeamLimits{
MaxNameSize: DefaultTeamNameMaxSize,
MaxDescriptionSize: DefaultTeamDescriptionMaxSize,
},
}
}

const (
DefaultTeamNameMaxSize = 100
DefaultTeamDescriptionMaxSize = 1000
)

// TeamLimits contains all limits applied for triggers.
type TeamLimits struct {
// MaxNameSize is the amount of characters allowed in team name.
MaxNameSize int
// MaxNameSize is the amount of characters allowed in team description.
MaxDescriptionSize int
}
9 changes: 9 additions & 0 deletions api/controller/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ func CreateTeam(dataBase moira.Database, team dto.TeamModel, userID string) (dto
return dto.SaveTeamResponse{}, api.ErrorInternalServer(fmt.Errorf("cannot generate unique id for team"))
}
}

err := dataBase.SaveTeam(teamID, team.ToMoiraTeam())
if err != nil {
if errors.Is(err, database.ErrTeamWithNameAlreadyExists) {
return dto.SaveTeamResponse{}, api.ErrorInvalidRequest(fmt.Errorf("cannot save team: %w", err))
}

return dto.SaveTeamResponse{}, api.ErrorInternalServer(fmt.Errorf("cannot save team: %w", err))
}

Expand Down Expand Up @@ -295,6 +300,10 @@ func AddTeamUsers(dataBase moira.Database, teamID string, newUsers []string) (dt
func UpdateTeam(dataBase moira.Database, teamID string, team dto.TeamModel) (dto.SaveTeamResponse, *api.ErrorResponse) {
err := dataBase.SaveTeam(teamID, team.ToMoiraTeam())
if err != nil {
if errors.Is(err, database.ErrTeamWithNameAlreadyExists) {
return dto.SaveTeamResponse{}, api.ErrorInvalidRequest(fmt.Errorf("cannot save team: %w", err))
}

return dto.SaveTeamResponse{}, api.ErrorInternalServer(fmt.Errorf("cannot save team: %w", err))
}
return dto.SaveTeamResponse{ID: teamID}, nil
Expand Down
8 changes: 8 additions & 0 deletions api/controller/team_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ func TestCreateTeam(t *testing.T) {
So(response, ShouldResemble, dto.SaveTeamResponse{})
So(err, ShouldResemble, api.ErrorInternalServer(fmt.Errorf("cannot save team: %w", returnErr)))
})

Convey("team with name already exists error, while saving", func() {
dataBase.EXPECT().GetTeam(gomock.Any()).Return(moira.Team{}, database.ErrNil)
dataBase.EXPECT().SaveTeam(gomock.Any(), team.ToMoiraTeam()).Return(database.ErrTeamWithNameAlreadyExists)
response, err := CreateTeam(dataBase, team, user)
So(response, ShouldResemble, dto.SaveTeamResponse{})
So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("cannot save team: %w", database.ErrTeamWithNameAlreadyExists)))
})
})
}

Expand Down
23 changes: 14 additions & 9 deletions api/dto/team.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package dto

import (
"errors"
"fmt"
"net/http"
"unicode/utf8"

"github.com/moira-alert/moira/api/middleware"

"github.com/moira-alert/moira"
)

const (
teamNameLimit = 100
teamDescriptionLimit = 1000
)
var errEmptyTeamName = errors.New("team name cannot be empty")

// TeamModel is a structure that represents team entity in HTTP transfer.
type TeamModel struct {
Expand All @@ -31,15 +31,20 @@ func NewTeamModel(team moira.Team) TeamModel {

// Bind is a method that implements Binder interface from chi and checks that validity of data in request.
func (t TeamModel) Bind(request *http.Request) error {
limits := middleware.GetLimits(request)

if t.Name == "" {
return fmt.Errorf("team name cannot be empty")
return errEmptyTeamName
}
if utf8.RuneCountInString(t.Name) > teamNameLimit {
return fmt.Errorf("team name cannot be longer than %d characters", teamNameLimit)

if utf8.RuneCountInString(t.Name) > limits.Team.MaxNameSize {
return fmt.Errorf("team name cannot be longer than %d characters", limits.Team.MaxNameSize)
}
if utf8.RuneCountInString(t.Description) > teamDescriptionLimit {
return fmt.Errorf("team description cannot be longer than %d characters", teamNameLimit)

if utf8.RuneCountInString(t.Description) > limits.Team.MaxDescriptionSize {
return fmt.Errorf("team description cannot be longer than %d characters", limits.Team.MaxDescriptionSize)
}

return nil
}

Expand Down
57 changes: 57 additions & 0 deletions api/dto/team_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dto

import (
"fmt"
"net/http"
"strings"
"testing"

"github.com/moira-alert/moira/api"
"github.com/moira-alert/moira/api/middleware"

. "github.com/smartystreets/goconvey/convey"
)

func TestTeamValidation(t *testing.T) {
Convey("Test team validation", t, func() {
teamModel := TeamModel{}

limits := api.GetTestLimitsConfig()

request, _ := http.NewRequest("POST", "/api/teams", nil)
request.Header.Set("Content-Type", "application/json")
request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", limits))

Convey("with empty team.Name", func() {
err := teamModel.Bind(request)

So(err, ShouldResemble, errEmptyTeamName)
})

Convey("with team.Name has characters more than in limit", func() {
teamModel.Name = strings.Repeat("ё", limits.Team.MaxNameSize+1)

err := teamModel.Bind(request)

So(err, ShouldResemble, fmt.Errorf("team name cannot be longer than %d characters", limits.Team.MaxNameSize))
})

Convey("with team.Description has characters more than in limit", func() {
teamModel.Name = strings.Repeat("ё", limits.Team.MaxNameSize)
teamModel.Description = strings.Repeat("ё", limits.Team.MaxDescriptionSize+1)

err := teamModel.Bind(request)

So(err, ShouldResemble, fmt.Errorf("team description cannot be longer than %d characters", limits.Team.MaxDescriptionSize))
})

Convey("with valid team", func() {
teamModel.Name = strings.Repeat("ё", limits.Team.MaxNameSize)
teamModel.Description = strings.Repeat("ё", limits.Team.MaxDescriptionSize)

err := teamModel.Bind(request)

So(err, ShouldBeNil)
})
})
}
18 changes: 18 additions & 0 deletions cmd/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ type apiConfig struct {
type LimitsConfig struct {
// Trigger contains the limits applied to triggers.
Trigger TriggerLimitsConfig `yaml:"trigger"`
// Team contains the limits applied to teams.
Team TeamLimitsConfig `yaml:"team"`
}

// TriggerLimitsConfig represents the limits which will be applied to all triggers.
Expand All @@ -64,12 +66,24 @@ type TriggerLimitsConfig struct {
MaxNameSize int `yaml:"max_name_size"`
}

// TeamLimitsConfig represents the limits which will be applied to all teams.
type TeamLimitsConfig struct {
// MaxNameSize is the max amount of characters allowed in team name.
MaxNameSize int `yaml:"max_name_size"`
// MaxDescriptionSize is the max amount of characters allowed in team description.
MaxDescriptionSize int `yaml:"max_description_size"`
}

// ToLimits converts LimitsConfig to api.LimitsConfig.
func (conf LimitsConfig) ToLimits() api.LimitsConfig {
return api.LimitsConfig{
Trigger: api.TriggerLimits{
MaxNameSize: conf.Trigger.MaxNameSize,
},
Team: api.TeamLimits{
MaxNameSize: conf.Team.MaxNameSize,
MaxDescriptionSize: conf.Team.MaxDescriptionSize,
},
}
}

Expand Down Expand Up @@ -259,6 +273,10 @@ func getDefault() config {
Trigger: TriggerLimitsConfig{
MaxNameSize: api.DefaultTriggerNameMaxSize,
},
Team: TeamLimitsConfig{
MaxNameSize: api.DefaultTeamNameMaxSize,
MaxDescriptionSize: api.DefaultTeamDescriptionMaxSize,
},
},
},
Web: webConfig{
Expand Down
4 changes: 4 additions & 0 deletions cmd/api/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ func Test_webConfig_getDefault(t *testing.T) {
Trigger: TriggerLimitsConfig{
MaxNameSize: api.DefaultTriggerNameMaxSize,
},
Team: TeamLimitsConfig{
MaxNameSize: api.DefaultTeamNameMaxSize,
MaxDescriptionSize: api.DefaultTeamDescriptionMaxSize,
},
},
},
Web: webConfig{
Expand Down
3 changes: 3 additions & 0 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ type ErrLockNotAcquired struct {
func (e *ErrLockNotAcquired) Error() string {
return fmt.Sprintf("lock was not acquired: %v", e.Err)
}

// ErrTeamWithNameAlreadyExists may be returned from SaveTeam method.
var ErrTeamWithNameAlreadyExists = fmt.Errorf("team with such name alredy exists")
2 changes: 2 additions & 0 deletions database/redis/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
testSource DBSource = "test"
)

var _ moira.Database = &DbConnector{}

// DbConnector contains redis client.
type DbConnector struct {
client *redis.UniversalClient
Expand Down
Loading

0 comments on commit d7641a5

Please sign in to comment.