Skip to content

Commit

Permalink
Merge pull request #723 from traPtitech/improve/duplicate_project_name
Browse files Browse the repository at this point in the history
プロジェクト名とコンテスト名の重複を禁止して、名前の上限を伸ばした
  • Loading branch information
ras0q authored Sep 9, 2024
2 parents f946fa5 + 0f5ef59 commit f9b3e3d
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 8 deletions.
11 changes: 11 additions & 0 deletions integration_tests/handler/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func TestCreateProject(t *testing.T) {
tooLongName = strings.Repeat("亜", 33)
tooLongDescriptionKanji = strings.Repeat("亜", 257)
duration = schema.ConvertDuration(random.Duration())
conflictedProject = random.CreateProjectArgs()
)

t.Parallel()
Expand Down Expand Up @@ -179,13 +180,23 @@ func TestCreateProject(t *testing.T) {
},
httpError(t, "Bad Request: argument error"),
},
"400 project already exists": {
http.StatusBadRequest,
schema.CreateProjectRequest{
Name: conflictedProject.Name,
Link: &link,
Description: description,
},
httpError(t, "Bad Request: argument error"),
},
}

e := echo.New()
api := setupRoutes(t, e)
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
_ = doRequest(t, e, http.MethodPost, e.URL(api.Project.CreateProject), &conflictedProject)
res := doRequest(t, e, http.MethodPost, e.URL(api.Project.CreateProject), &tt.reqBody)
switch want := tt.want.(type) {
case schema.Project:
Expand Down
1 change: 1 addition & 0 deletions internal/infrastructure/migration/current.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
func Migrations() []*gormigrate.Migration {
return []*gormigrate.Migration{
v1(),
v2(), // プロジェクト名とコンテスト名の重複禁止と文字数制限増加(32->128)
}
}

Expand Down
169 changes: 169 additions & 0 deletions internal/infrastructure/migration/v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Package migration migrate current struct
package migration

import (
"fmt"
"time"

"github.com/go-gormigrate/gormigrate/v2"
"github.com/gofrs/uuid"
"github.com/traPtitech/traPortfolio/internal/infrastructure/repository/model"
"gorm.io/gorm"
)

// v1 unique_index:idx_room_uniqueの削除
func v2() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "2",
Migrate: func(db *gorm.DB) error {
if err := db.AutoMigrate(&v2Project{}, &v2Contest{}, &v2ContestTeam{}); err != nil {
return err
}

// プロジェクト名の重複禁止
{
projects := make([]*model.Project, 0)
if err := db.Find(&projects).Error; err != nil {
return err
}

projectMap := make(map[string][]uuid.UUID, len(projects))
for _, p := range projects {
projectMap[p.Name] = append(projectMap[p.Name], p.ID)
}

updates := make(map[uuid.UUID]string, len(projects))
for {
noDuplicate := true
for name, ids := range projectMap {
if len(ids) <= 1 {
continue
}
noDuplicate = false
for i, pid := range ids {
if i == 0 {
projectMap[name] = []uuid.UUID{pid}
continue
}
nameNew := fmt.Sprintf("%s (%d)", name, i)
updates[pid] = nameNew
projectMap[nameNew] = append(projectMap[nameNew], pid)
}
}
if noDuplicate {
break
}
}

for id, nameNew := range updates {
err := db.
Model(&model.Project{}).
Where(&model.Project{ID: id}).
Update("name", nameNew).
Error
if err != nil {
return err
}
}
}

// コンテスト名の重複禁止
{
contests := make([]*model.Contest, 0)
if err := db.Find(&contests).Error; err != nil {
return err
}

contestMap := make(map[string][]uuid.UUID, len(contests))
for _, c := range contests {
contestMap[c.Name] = append(contestMap[c.Name], c.ID)
}

updates := make(map[uuid.UUID]string, len(contests))
noDuplicate := false
for !noDuplicate {
noDuplicate = true
for name, ids := range contestMap {
if len(ids) <= 1 {
continue
}
noDuplicate = false
for i, cid := range ids {
if i == 0 {
contestMap[name] = []uuid.UUID{cid}
continue
}
nameNew := fmt.Sprintf("%s (%d)", name, i)
updates[cid] = nameNew
contestMap[nameNew] = append(contestMap[nameNew], cid)
}
}
}

for id, nameNew := range updates {
err := db.
Model(&model.Contest{}).
Where(&model.Contest{ID: id}).
Update("name", nameNew).
Error
if err != nil {
return err
}
}
}

return db.
Table("portfolio").
Error
},
}
}

type v2Project struct {
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
Name string `gorm:"type:varchar(128)"` // 制限増加 (32->128)
Description string `gorm:"type:text"`
Link string `gorm:"type:text"`
SinceYear int `gorm:"type:smallint(4);not null"`
SinceSemester int `gorm:"type:tinyint(1);not null"`
UntilYear int `gorm:"type:smallint(4);not null"`
UntilSemester int `gorm:"type:tinyint(1);not null"`
CreatedAt time.Time `gorm:"precision:6"`
UpdatedAt time.Time `gorm:"precision:6"`
}

func (*v2Project) TableName() string {
return "projects"
}

type v2Contest struct {
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
Name string `gorm:"type:varchar(128)"` // 制限増加 (32->128)
Description string `gorm:"type:text"`
Link string `gorm:"type:text"`
Since time.Time `gorm:"precision:6"`
Until time.Time `gorm:"precision:6"`
CreatedAt time.Time `gorm:"precision:6"`
UpdatedAt time.Time `gorm:"precision:6"`
}

func (*v2Contest) TableName() string {
return "contests"
}

type v2ContestTeam struct {
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
ContestID uuid.UUID `gorm:"type:char(36);not null"`
Name string `gorm:"type:varchar(128)"` // 制限増加 (32->128)
Description string `gorm:"type:text"`
Result string `gorm:"type:text"`
Link string `gorm:"type:text"`
CreatedAt time.Time `gorm:"precision:6"`
UpdatedAt time.Time `gorm:"precision:6"`

Contest model.Contest `gorm:"foreignKey:ContestID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
}

func (*v2ContestTeam) TableName() string {
return "contest_teams"
}
15 changes: 14 additions & 1 deletion internal/infrastructure/repository/contest_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package repository

import (
"context"
"errors"

"github.com/gofrs/uuid"
"github.com/traPtitech/traPortfolio/internal/domain"
Expand Down Expand Up @@ -80,7 +81,19 @@ func (r *ContestRepository) CreateContest(ctx context.Context, args *repository.
Until: args.Until.ValueOrZero(),
}

err := r.h.WithContext(ctx).Create(contest).Error
// 既に同名のコンテストが存在するか
err := r.h.
WithContext(ctx).
Where(&model.Contest{Name: contest.Name}).
First(&model.Contest{}).
Error
if err == nil {
return nil, repository.ErrAlreadyExists
} else if !errors.Is(err, repository.ErrNotFound) {
return nil, err
}

err = r.h.WithContext(ctx).Create(contest).Error
if err != nil {
return nil, err
}
Expand Down
32 changes: 31 additions & 1 deletion internal/infrastructure/repository/contest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,37 @@ func Test_GetContest(t *testing.T) {
})
}

func Test_CreateContest(t *testing.T) {}
func Test_CreateContest(t *testing.T) {
t.Parallel()

db := SetupTestGormDB(t)
portalAPI := mock_external.NewMockPortalAPI(gomock.NewController(t))
repo := NewContestRepository(db, portalAPI)

t.Run("create a contest", func(t *testing.T) {
ctx := context.Background()
args := random.CreateContestArgs()
contest, err := repo.CreateContest(ctx, args)
assert.NoError(t, err)

gotContest, err := repo.GetContest(ctx, contest.ID)
assert.NoError(t, err)
assert.Equal(t, contest, gotContest)
})

t.Run("create contests which name duplicated", func(t *testing.T) {
ctx := context.Background()
arg1 := random.CreateContestArgs()
arg2 := random.CreateContestArgs()
arg2.Name = arg1.Name

_, err := repo.CreateContest(ctx, arg1)
assert.NoError(t, err)

_, err = repo.CreateContest(ctx, arg2)
assert.Error(t, err)
})
}

func Test_UpdateContest(t *testing.T) {
t.Parallel()
Expand Down
4 changes: 2 additions & 2 deletions internal/infrastructure/repository/model/contests.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type Contest struct {
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
Name string `gorm:"type:varchar(32)"`
Name string `gorm:"type:varchar(128)"`
Description string `gorm:"type:text"`
Link string `gorm:"type:text"`
Since time.Time `gorm:"precision:6"`
Expand All @@ -24,7 +24,7 @@ func (*Contest) TableName() string {
type ContestTeam struct {
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
ContestID uuid.UUID `gorm:"type:char(36);not null"`
Name string `gorm:"type:varchar(32)"`
Name string `gorm:"type:varchar(128)"`
Description string `gorm:"type:text"`
Result string `gorm:"type:text"`
Link string `gorm:"type:text"`
Expand Down
2 changes: 1 addition & 1 deletion internal/infrastructure/repository/model/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type Project struct {
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
Name string `gorm:"type:varchar(32)"`
Name string `gorm:"type:varchar(128)"`
Description string `gorm:"type:text"`
Link string `gorm:"type:text"`
SinceYear int `gorm:"type:smallint(4);not null"`
Expand Down
15 changes: 14 additions & 1 deletion internal/infrastructure/repository/project_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package repository

import (
"context"
"errors"
"fmt"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -106,7 +107,19 @@ func (r *ProjectRepository) CreateProject(ctx context.Context, args *repository.
}
p.Link = args.Link.ValueOr(p.Link)

err := r.h.WithContext(ctx).Create(&p).Error
// 既に同名のプロジェクトが存在するか
err := r.h.
WithContext(ctx).
Where(&model.Project{Name: p.Name}).
First(&model.Project{}).
Error
if err == nil {
return nil, repository.ErrAlreadyExists
} else if !errors.Is(err, repository.ErrNotFound) {
return nil, err
}

err = r.h.WithContext(ctx).Create(&p).Error
if err != nil {
return nil, err
}
Expand Down
49 changes: 47 additions & 2 deletions internal/infrastructure/repository/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,53 @@ func TestProjectRepository_GetProject(t *testing.T) {
}
}

// func TestProjectRepository_CreateProject(t *testing.T) {
// }
func TestProjectRepository_CreateProject(t *testing.T) {
t.Parallel()

db := SetupTestGormDB(t)
repo := NewProjectRepository(db, mock_external_e2e.NewMockPortalAPI())

t.Run("create project success", func(t *testing.T) {
ctx := context.Background()
arg := random.CreateProjectArgs()

project, err := repo.CreateProject(ctx, arg)
assert.NoError(t, err)

got, err := repo.GetProject(ctx, project.ID)
assert.NoError(t, err)

opts := []cmp.Option{
cmpopts.EquateEmpty(),
cmp.AllowUnexported(optional.Of[domain.YearWithSemester]{}),
}
if diff := cmp.Diff(project, got, opts...); diff != "" {
t.Error(diff)
}
})

t.Run("create project with invalid args", func(t *testing.T) {
ctx := context.Background()
arg := random.CreateProjectArgs()
arg.Name = ""

_, err := repo.CreateProject(ctx, arg)
assert.Error(t, err)
})

t.Run("create projects which name duplicated", func(t *testing.T) {
ctx := context.Background()
arg1 := random.CreateProjectArgs()
arg2 := random.CreateProjectArgs()
arg2.Name = arg1.Name

_, err := repo.CreateProject(ctx, arg1)
assert.NoError(t, err)

_, err = repo.CreateProject(ctx, arg2)
assert.Error(t, err)
})
}

func TestProjectRepository_UpdateProject(t *testing.T) {
t.Parallel()
Expand Down
Loading

0 comments on commit f9b3e3d

Please sign in to comment.