Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin' into fix/changes_until_to_nil
Browse files Browse the repository at this point in the history
  • Loading branch information
Tennessine699 committed Sep 9, 2024
2 parents 16ab8f6 + f9b3e3d commit 156767b
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 11 deletions.
13 changes: 13 additions & 0 deletions docs/swagger/traPortfolio.v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,19 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/CreateProjectRequest"
delete:
summary: プロジェクトの削除
operationId: deleteProject
responses:
"204":
description: No Content
"403":
description: Forbidden
"404":
description: Not Found
description: プロジェクトを削除します
tags:
- project
"/projects/{projectId}":
parameters:
- $ref: "#/components/parameters/projectIdInPath"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/labstack/echo/v4 v4.12.0
github.com/ory/dockertest v3.3.5+incompatible
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/samber/lo v1.46.0
github.com/samber/lo v1.47.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ=
github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
Expand Down
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/handler/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func setupV1API(g *echo.Group, api API, isProduction bool) {
projectAPI.POST("", api.Project.CreateProject)
projectAPI.GET("/:projectID", api.Project.GetProject)
projectAPI.PATCH("/:projectID", api.Project.EditProject)
projectAPI.DELETE("/:projectID", api.Project.DeleteProject)
projectAPI.GET("/:projectID/members", api.Project.GetProjectMembers)
projectAPI.PUT("/:projectID/members", api.Project.EditProjectMembers)
}
Expand Down
16 changes: 16 additions & 0 deletions internal/handler/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,22 @@ func (h *ProjectHandler) EditProject(c echo.Context) error {
return c.NoContent(http.StatusNoContent)
}

// DeleteProject DELETE /projects/:projectID
func (h *ProjectHandler) DeleteProject(c echo.Context) error {
projectID, err := getID(c, keyProject)
if err != nil {
return err
}

ctx := c.Request().Context()
err = h.project.DeleteProject(ctx, projectID)
if err != nil {
return err
}

return c.NoContent(http.StatusNoContent)
}

// GetProjectMembers GET /projects/:projectID/members
func (h *ProjectHandler) GetProjectMembers(c echo.Context) error {
projectID, err := getID(c, keyProject)
Expand Down
41 changes: 41 additions & 0 deletions internal/handler/project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,47 @@ func TestProjectHandler_CreateProject(t *testing.T) {
}
}

func TestProjectHandler_DeleteProject(t *testing.T) {
t.Parallel()

tests := []struct {
name string
setup func(mr MockRepository) (path string)
statusCode int
}{
{
name: "Success",
setup: func(mr MockRepository) (path string) {
projectID := random.UUID()
mr.project.EXPECT().DeleteProject(anyCtx{}, projectID).Return(nil)
return fmt.Sprintf("/api/v1/projects/%s", projectID)
},
statusCode: http.StatusNoContent,
},
{
name: "Bad Request: Invalid Project ID",
setup: func(mr MockRepository) (path string) {
return fmt.Sprintf("/api/v1/projects/%s", invalidID)
},
statusCode: http.StatusBadRequest,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock
s, api := setupProjectMock(t)

path := tt.setup(s)

statusCode, _ := doRequest(t, api, http.MethodDelete, path, nil, nil)

// Assertion
assert.Equal(t, tt.statusCode, statusCode)
})
}
}

func TestProjectHandler_EditProjectMembers(t *testing.T) {
t.Parallel()

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"
"time"

"github.com/gofrs/uuid"
Expand Down Expand Up @@ -81,7 +82,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 @@ -64,7 +64,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
Loading

0 comments on commit 156767b

Please sign in to comment.