Skip to content

Commit f9b3e3d

Browse files
authored
Merge pull request #723 from traPtitech/improve/duplicate_project_name
プロジェクト名とコンテスト名の重複を禁止して、名前の上限を伸ばした
2 parents f946fa5 + 0f5ef59 commit f9b3e3d

10 files changed

Lines changed: 303 additions & 8 deletions

File tree

integration_tests/handler/project_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ func TestCreateProject(t *testing.T) {
8686
tooLongName = strings.Repeat("亜", 33)
8787
tooLongDescriptionKanji = strings.Repeat("亜", 257)
8888
duration = schema.ConvertDuration(random.Duration())
89+
conflictedProject = random.CreateProjectArgs()
8990
)
9091

9192
t.Parallel()
@@ -179,13 +180,23 @@ func TestCreateProject(t *testing.T) {
179180
},
180181
httpError(t, "Bad Request: argument error"),
181182
},
183+
"400 project already exists": {
184+
http.StatusBadRequest,
185+
schema.CreateProjectRequest{
186+
Name: conflictedProject.Name,
187+
Link: &link,
188+
Description: description,
189+
},
190+
httpError(t, "Bad Request: argument error"),
191+
},
182192
}
183193

184194
e := echo.New()
185195
api := setupRoutes(t, e)
186196
for name, tt := range tests {
187197
t.Run(name, func(t *testing.T) {
188198
t.Parallel()
199+
_ = doRequest(t, e, http.MethodPost, e.URL(api.Project.CreateProject), &conflictedProject)
189200
res := doRequest(t, e, http.MethodPost, e.URL(api.Project.CreateProject), &tt.reqBody)
190201
switch want := tt.want.(type) {
191202
case schema.Project:

internal/infrastructure/migration/current.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
func Migrations() []*gormigrate.Migration {
1010
return []*gormigrate.Migration{
1111
v1(),
12+
v2(), // プロジェクト名とコンテスト名の重複禁止と文字数制限増加(32->128)
1213
}
1314
}
1415

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Package migration migrate current struct
2+
package migration
3+
4+
import (
5+
"fmt"
6+
"time"
7+
8+
"github.com/go-gormigrate/gormigrate/v2"
9+
"github.com/gofrs/uuid"
10+
"github.com/traPtitech/traPortfolio/internal/infrastructure/repository/model"
11+
"gorm.io/gorm"
12+
)
13+
14+
// v1 unique_index:idx_room_uniqueの削除
15+
func v2() *gormigrate.Migration {
16+
return &gormigrate.Migration{
17+
ID: "2",
18+
Migrate: func(db *gorm.DB) error {
19+
if err := db.AutoMigrate(&v2Project{}, &v2Contest{}, &v2ContestTeam{}); err != nil {
20+
return err
21+
}
22+
23+
// プロジェクト名の重複禁止
24+
{
25+
projects := make([]*model.Project, 0)
26+
if err := db.Find(&projects).Error; err != nil {
27+
return err
28+
}
29+
30+
projectMap := make(map[string][]uuid.UUID, len(projects))
31+
for _, p := range projects {
32+
projectMap[p.Name] = append(projectMap[p.Name], p.ID)
33+
}
34+
35+
updates := make(map[uuid.UUID]string, len(projects))
36+
for {
37+
noDuplicate := true
38+
for name, ids := range projectMap {
39+
if len(ids) <= 1 {
40+
continue
41+
}
42+
noDuplicate = false
43+
for i, pid := range ids {
44+
if i == 0 {
45+
projectMap[name] = []uuid.UUID{pid}
46+
continue
47+
}
48+
nameNew := fmt.Sprintf("%s (%d)", name, i)
49+
updates[pid] = nameNew
50+
projectMap[nameNew] = append(projectMap[nameNew], pid)
51+
}
52+
}
53+
if noDuplicate {
54+
break
55+
}
56+
}
57+
58+
for id, nameNew := range updates {
59+
err := db.
60+
Model(&model.Project{}).
61+
Where(&model.Project{ID: id}).
62+
Update("name", nameNew).
63+
Error
64+
if err != nil {
65+
return err
66+
}
67+
}
68+
}
69+
70+
// コンテスト名の重複禁止
71+
{
72+
contests := make([]*model.Contest, 0)
73+
if err := db.Find(&contests).Error; err != nil {
74+
return err
75+
}
76+
77+
contestMap := make(map[string][]uuid.UUID, len(contests))
78+
for _, c := range contests {
79+
contestMap[c.Name] = append(contestMap[c.Name], c.ID)
80+
}
81+
82+
updates := make(map[uuid.UUID]string, len(contests))
83+
noDuplicate := false
84+
for !noDuplicate {
85+
noDuplicate = true
86+
for name, ids := range contestMap {
87+
if len(ids) <= 1 {
88+
continue
89+
}
90+
noDuplicate = false
91+
for i, cid := range ids {
92+
if i == 0 {
93+
contestMap[name] = []uuid.UUID{cid}
94+
continue
95+
}
96+
nameNew := fmt.Sprintf("%s (%d)", name, i)
97+
updates[cid] = nameNew
98+
contestMap[nameNew] = append(contestMap[nameNew], cid)
99+
}
100+
}
101+
}
102+
103+
for id, nameNew := range updates {
104+
err := db.
105+
Model(&model.Contest{}).
106+
Where(&model.Contest{ID: id}).
107+
Update("name", nameNew).
108+
Error
109+
if err != nil {
110+
return err
111+
}
112+
}
113+
}
114+
115+
return db.
116+
Table("portfolio").
117+
Error
118+
},
119+
}
120+
}
121+
122+
type v2Project struct {
123+
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
124+
Name string `gorm:"type:varchar(128)"` // 制限増加 (32->128)
125+
Description string `gorm:"type:text"`
126+
Link string `gorm:"type:text"`
127+
SinceYear int `gorm:"type:smallint(4);not null"`
128+
SinceSemester int `gorm:"type:tinyint(1);not null"`
129+
UntilYear int `gorm:"type:smallint(4);not null"`
130+
UntilSemester int `gorm:"type:tinyint(1);not null"`
131+
CreatedAt time.Time `gorm:"precision:6"`
132+
UpdatedAt time.Time `gorm:"precision:6"`
133+
}
134+
135+
func (*v2Project) TableName() string {
136+
return "projects"
137+
}
138+
139+
type v2Contest struct {
140+
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
141+
Name string `gorm:"type:varchar(128)"` // 制限増加 (32->128)
142+
Description string `gorm:"type:text"`
143+
Link string `gorm:"type:text"`
144+
Since time.Time `gorm:"precision:6"`
145+
Until time.Time `gorm:"precision:6"`
146+
CreatedAt time.Time `gorm:"precision:6"`
147+
UpdatedAt time.Time `gorm:"precision:6"`
148+
}
149+
150+
func (*v2Contest) TableName() string {
151+
return "contests"
152+
}
153+
154+
type v2ContestTeam struct {
155+
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
156+
ContestID uuid.UUID `gorm:"type:char(36);not null"`
157+
Name string `gorm:"type:varchar(128)"` // 制限増加 (32->128)
158+
Description string `gorm:"type:text"`
159+
Result string `gorm:"type:text"`
160+
Link string `gorm:"type:text"`
161+
CreatedAt time.Time `gorm:"precision:6"`
162+
UpdatedAt time.Time `gorm:"precision:6"`
163+
164+
Contest model.Contest `gorm:"foreignKey:ContestID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
165+
}
166+
167+
func (*v2ContestTeam) TableName() string {
168+
return "contest_teams"
169+
}

internal/infrastructure/repository/contest_impl.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package repository
22

33
import (
44
"context"
5+
"errors"
56

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

83-
err := r.h.WithContext(ctx).Create(contest).Error
84+
// 既に同名のコンテストが存在するか
85+
err := r.h.
86+
WithContext(ctx).
87+
Where(&model.Contest{Name: contest.Name}).
88+
First(&model.Contest{}).
89+
Error
90+
if err == nil {
91+
return nil, repository.ErrAlreadyExists
92+
} else if !errors.Is(err, repository.ErrNotFound) {
93+
return nil, err
94+
}
95+
96+
err = r.h.WithContext(ctx).Create(contest).Error
8497
if err != nil {
8598
return nil, err
8699
}

internal/infrastructure/repository/contest_test.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,37 @@ func Test_GetContest(t *testing.T) {
6262
})
6363
}
6464

65-
func Test_CreateContest(t *testing.T) {}
65+
func Test_CreateContest(t *testing.T) {
66+
t.Parallel()
67+
68+
db := SetupTestGormDB(t)
69+
portalAPI := mock_external.NewMockPortalAPI(gomock.NewController(t))
70+
repo := NewContestRepository(db, portalAPI)
71+
72+
t.Run("create a contest", func(t *testing.T) {
73+
ctx := context.Background()
74+
args := random.CreateContestArgs()
75+
contest, err := repo.CreateContest(ctx, args)
76+
assert.NoError(t, err)
77+
78+
gotContest, err := repo.GetContest(ctx, contest.ID)
79+
assert.NoError(t, err)
80+
assert.Equal(t, contest, gotContest)
81+
})
82+
83+
t.Run("create contests which name duplicated", func(t *testing.T) {
84+
ctx := context.Background()
85+
arg1 := random.CreateContestArgs()
86+
arg2 := random.CreateContestArgs()
87+
arg2.Name = arg1.Name
88+
89+
_, err := repo.CreateContest(ctx, arg1)
90+
assert.NoError(t, err)
91+
92+
_, err = repo.CreateContest(ctx, arg2)
93+
assert.Error(t, err)
94+
})
95+
}
6696

6797
func Test_UpdateContest(t *testing.T) {
6898
t.Parallel()

internal/infrastructure/repository/model/contests.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
type Contest struct {
1010
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
11-
Name string `gorm:"type:varchar(32)"`
11+
Name string `gorm:"type:varchar(128)"`
1212
Description string `gorm:"type:text"`
1313
Link string `gorm:"type:text"`
1414
Since time.Time `gorm:"precision:6"`
@@ -24,7 +24,7 @@ func (*Contest) TableName() string {
2424
type ContestTeam struct {
2525
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
2626
ContestID uuid.UUID `gorm:"type:char(36);not null"`
27-
Name string `gorm:"type:varchar(32)"`
27+
Name string `gorm:"type:varchar(128)"`
2828
Description string `gorm:"type:text"`
2929
Result string `gorm:"type:text"`
3030
Link string `gorm:"type:text"`

internal/infrastructure/repository/model/project.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88

99
type Project struct {
1010
ID uuid.UUID `gorm:"type:char(36);not null;primaryKey"`
11-
Name string `gorm:"type:varchar(32)"`
11+
Name string `gorm:"type:varchar(128)"`
1212
Description string `gorm:"type:text"`
1313
Link string `gorm:"type:text"`
1414
SinceYear int `gorm:"type:smallint(4);not null"`

internal/infrastructure/repository/project_impl.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package repository
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

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

109-
err := r.h.WithContext(ctx).Create(&p).Error
110+
// 既に同名のプロジェクトが存在するか
111+
err := r.h.
112+
WithContext(ctx).
113+
Where(&model.Project{Name: p.Name}).
114+
First(&model.Project{}).
115+
Error
116+
if err == nil {
117+
return nil, repository.ErrAlreadyExists
118+
} else if !errors.Is(err, repository.ErrNotFound) {
119+
return nil, err
120+
}
121+
122+
err = r.h.WithContext(ctx).Create(&p).Error
110123
if err != nil {
111124
return nil, err
112125
}

internal/infrastructure/repository/project_test.go

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,53 @@ func TestProjectRepository_GetProject(t *testing.T) {
6060
}
6161
}
6262

63-
// func TestProjectRepository_CreateProject(t *testing.T) {
64-
// }
63+
func TestProjectRepository_CreateProject(t *testing.T) {
64+
t.Parallel()
65+
66+
db := SetupTestGormDB(t)
67+
repo := NewProjectRepository(db, mock_external_e2e.NewMockPortalAPI())
68+
69+
t.Run("create project success", func(t *testing.T) {
70+
ctx := context.Background()
71+
arg := random.CreateProjectArgs()
72+
73+
project, err := repo.CreateProject(ctx, arg)
74+
assert.NoError(t, err)
75+
76+
got, err := repo.GetProject(ctx, project.ID)
77+
assert.NoError(t, err)
78+
79+
opts := []cmp.Option{
80+
cmpopts.EquateEmpty(),
81+
cmp.AllowUnexported(optional.Of[domain.YearWithSemester]{}),
82+
}
83+
if diff := cmp.Diff(project, got, opts...); diff != "" {
84+
t.Error(diff)
85+
}
86+
})
87+
88+
t.Run("create project with invalid args", func(t *testing.T) {
89+
ctx := context.Background()
90+
arg := random.CreateProjectArgs()
91+
arg.Name = ""
92+
93+
_, err := repo.CreateProject(ctx, arg)
94+
assert.Error(t, err)
95+
})
96+
97+
t.Run("create projects which name duplicated", func(t *testing.T) {
98+
ctx := context.Background()
99+
arg1 := random.CreateProjectArgs()
100+
arg2 := random.CreateProjectArgs()
101+
arg2.Name = arg1.Name
102+
103+
_, err := repo.CreateProject(ctx, arg1)
104+
assert.NoError(t, err)
105+
106+
_, err = repo.CreateProject(ctx, arg2)
107+
assert.Error(t, err)
108+
})
109+
}
65110

66111
func TestProjectRepository_UpdateProject(t *testing.T) {
67112
t.Parallel()

0 commit comments

Comments
 (0)