Skip to content

Commit 75e2269

Browse files
committed
resolve conflicts
2 parents 3063f30 + cf1bed3 commit 75e2269

File tree

11 files changed

+294
-5
lines changed

11 files changed

+294
-5
lines changed

controller/post.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func (ctrl *PostController) Create(c echo.Context) error {
9797
return c.JSON(http.StatusCreated, post)
9898
}
9999

100-
// Update は Post /post/{postID}のハンドラです
100+
// Update は PUT /post/{postID}のハンドラです
101101
func (ctrl *PostController) Update(c echo.Context) error {
102102
logger := log.New()
103103

@@ -132,7 +132,42 @@ func (ctrl *PostController) Update(c echo.Context) error {
132132
return echo.NewHTTPError(http.StatusForbidden, errNF.Error())
133133
}
134134

135-
logger.Errorf("error POST /post: %s", err.Error())
135+
logger.Errorf("error PUT /post/{postID}: %s", err.Error())
136+
return echo.NewHTTPError(http.StatusInternalServerError)
137+
}
138+
139+
return c.NoContent(http.StatusOK)
140+
}
141+
142+
// Delete は DELETE /post/{postID}のハンドラです
143+
func (ctrl *PostController) Delete(c echo.Context) error {
144+
logger := log.New()
145+
146+
post := &entity.Post{}
147+
var ok bool
148+
if post.UserID, ok = c.Get("userID").(string); !ok {
149+
logger.Errorf("Failed type assertion of userID: %#v", c.Get("userID"))
150+
return echo.NewHTTPError(http.StatusInternalServerError)
151+
}
152+
153+
var err error
154+
if post.ID, err = strconv.Atoi(c.Param("postID")); err != nil {
155+
return echo.NewHTTPError(http.StatusBadRequest)
156+
}
157+
158+
ctx := c.Request().Context()
159+
if err := ctrl.uc.Delete(ctx, post); err != nil {
160+
if errors.Is(err, entity.ErrIsNotAuthor) {
161+
logger.Errorf("forbidden update occurs: %s", err.Error())
162+
return echo.NewHTTPError(http.StatusForbidden, err.Error())
163+
}
164+
errNF := &entity.ErrNotFound{}
165+
if errors.As(err, errNF) {
166+
logger.Errorf("forbidden update occurs: %s", err.Error())
167+
return echo.NewHTTPError(http.StatusForbidden, errNF.Error())
168+
}
169+
170+
logger.Errorf("error DELETE /post/{postID}: %s", err.Error())
136171
return echo.NewHTTPError(http.StatusInternalServerError)
137172
}
138173

controller/post_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,90 @@ func TestPostController_Update(t *testing.T) {
447447
})
448448
}
449449
}
450+
451+
func TestPostController_Delete(t *testing.T) {
452+
tests := []struct {
453+
name string
454+
userID string
455+
postID string
456+
prepareMockPost func(ctx context.Context, post *mock.MockPost)
457+
wantErr bool
458+
wantCode int
459+
}{
460+
{
461+
name: "正しく投稿を削除できる",
462+
userID: "user-id",
463+
postID: "1",
464+
prepareMockPost: func(ctx context.Context, post *mock.MockPost) {
465+
post.EXPECT().Delete(ctx, &entity.Post{
466+
ID: 1,
467+
UserID: "user-id",
468+
}).Return(nil)
469+
},
470+
wantErr: false,
471+
wantCode: 200,
472+
},
473+
{
474+
name: "存在しないポストならばErrNotFound",
475+
userID: "user-id",
476+
postID: "100",
477+
prepareMockPost: func(ctx context.Context, post *mock.MockPost) {
478+
post.EXPECT().Delete(ctx, &entity.Post{
479+
ID: 100,
480+
UserID: "user-id",
481+
}).Return(entity.NewErrorNotFound("post"))
482+
},
483+
wantErr: true,
484+
wantCode: http.StatusForbidden,
485+
},
486+
{
487+
name: "ユーザーに削除権限がないならばErrIsNotAuthor",
488+
userID: "other-user-id",
489+
postID: "1",
490+
prepareMockPost: func(ctx context.Context, post *mock.MockPost) {
491+
post.EXPECT().Delete(ctx, &entity.Post{
492+
ID: 1,
493+
UserID: "other-user-id",
494+
}).Return(entity.ErrIsNotAuthor)
495+
},
496+
wantErr: true,
497+
wantCode: http.StatusForbidden,
498+
},
499+
}
500+
for _, tt := range tests {
501+
t.Run(tt.name, func(t *testing.T) {
502+
e := echo.New()
503+
req := httptest.NewRequest("DELETE", "/", nil)
504+
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
505+
rec := httptest.NewRecorder()
506+
c := e.NewContext(req, rec)
507+
c.SetParamNames("postID")
508+
c.SetParamValues(tt.postID)
509+
c.Set("userID", tt.userID)
510+
511+
ctx := context.Background()
512+
ctrl := gomock.NewController(t)
513+
defer ctrl.Finish()
514+
postRepo := mock.NewMockPost(ctrl)
515+
tt.prepareMockPost(ctx, postRepo)
516+
userRepo := mock.NewMockUser(ctrl)
517+
518+
con := NewPostController(usecase.NewPostUsecase(postRepo, userRepo))
519+
err := con.Delete(c)
520+
521+
if (err != nil) != tt.wantErr {
522+
t.Errorf("error = %v, wantErr = %v", err, tt.wantErr)
523+
}
524+
525+
if he, ok := err.(*echo.HTTPError); ok {
526+
if he.Code != tt.wantCode {
527+
t.Errorf("code = %d, want = %d", he.Code, tt.wantCode)
528+
}
529+
} else {
530+
if rec.Code != tt.wantCode {
531+
t.Errorf("code = %d, want = %d", rec.Code, tt.wantCode)
532+
}
533+
}
534+
})
535+
}
536+
}

domain/entity/errors.go

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ var (
1818
ErrCannotCommit = errors.New("non-post-owner cannot commit")
1919
// ErrIsNotAuthor はユーザがAuthorではないことが原因で生じたエラー
2020
ErrIsNotAuthor = errors.New("user is not the author")
21+
// ErrCannotDelete はforeign keyの関係で生じたエラー
22+
ErrCannotDelete = errors.New("cannot delete by foreign key restriction")
2123
)
2224

2325
// ErrTooLong はフィールドの内容が長すぎるときのエラー

infra/mock/mock_post.go

+12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

infra/post.go

+37
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,43 @@ func (p *PostRepository) Update(ctx context.Context, post *entity.Post) error {
202202
return nil
203203
}
204204

205+
// Delete は引数で渡したIDの投稿を削除します
206+
// 投稿の所有者以外が削除する場合、削除は行われません
207+
func (p *PostRepository) Delete(ctx context.Context, post *entity.Post) error {
208+
select {
209+
// echoのリクエストが途切れた場合は早めにリソースを開放するために処理を中断する
210+
case <-ctx.Done():
211+
return ctx.Err()
212+
default:
213+
// 該当するポストがあるか確認
214+
getPost, err := p.FindByID(ctx, post.ID)
215+
if err != nil {
216+
return entity.NewErrorNotFound("post")
217+
}
218+
219+
// 所有者でなければ削除処理は行わない
220+
if getPost.UserID != post.UserID {
221+
return entity.ErrIsNotAuthor
222+
}
223+
224+
postDTO := &PostInsertDTO{
225+
ID: post.ID,
226+
}
227+
228+
if _, err := p.dbMap.Delete(postDTO); err != nil {
229+
if sqlerr, ok := err.(*mysql.MySQLError); ok {
230+
// 外部キーの照合で落ちる場合はこのエラーを返す
231+
if sqlerr.Number == mysqlerr.ER_ROW_IS_REFERENCED_2 && strings.Contains(sqlerr.Message, "post_id") {
232+
return entity.ErrCannotDelete
233+
}
234+
}
235+
return err
236+
}
237+
}
238+
239+
return nil
240+
}
241+
205242
// PostDTO はDBとやりとりするためのDataTransferObjectです
206243
// ref: migrations/20210319141439-CreatePosts.sql
207244
type PostDTO struct {

infra/post_test.go

+106
Original file line numberDiff line numberDiff line change
@@ -590,3 +590,109 @@ func TestPostRepository_Update(t *testing.T) {
590590
})
591591
}
592592
}
593+
594+
func TestPostRepository_Delete(t *testing.T) {
595+
dbMap, err := NewDB()
596+
if err != nil {
597+
t.Fatalf(err.Error())
598+
}
599+
600+
dbMap.AddTableWithName(UserDTO{}, "users")
601+
truncateTable(t, dbMap, "users")
602+
603+
preparedUsers := []*UserDTO{
604+
{
605+
ID: "user-id",
606+
Name: "test user",
607+
Profile: "test profile",
608+
TwitterID: "twitter",
609+
},
610+
}
611+
612+
for _, user := range preparedUsers {
613+
if err := dbMap.Insert(user); err != nil {
614+
t.Fatal(err)
615+
}
616+
}
617+
618+
dbMap.AddTableWithName(PostDTO{}, "posts").SetKeys(true, "id")
619+
dbMap.AddTableWithName(PostInsertDTO{}, "posts").SetKeys(true, "id")
620+
truncateTable(t, dbMap, "posts")
621+
622+
validPosts := []*PostInsertDTO{
623+
{
624+
ID: 1,
625+
UserID: "user-id",
626+
Title: "test title",
627+
Code: "package main\n\nimport \"fmt\"\n\nfunc main(){fmt.Println(\"This is test.\")}",
628+
Language: "Go",
629+
Content: "Test code",
630+
Source: "github.com",
631+
},
632+
{
633+
ID: 2,
634+
UserID: "user-id",
635+
Title: "test title",
636+
Code: "package main\n\nimport \"fmt\"\n\nfunc main(){fmt.Println(\"This is test.\")}",
637+
Language: "Go",
638+
Content: "Test code",
639+
Source: "github.com",
640+
},
641+
}
642+
// デフォルトの投稿追加
643+
for _, post := range validPosts {
644+
if err := dbMap.Insert(post); err != nil {
645+
t.Fatal(err)
646+
}
647+
}
648+
649+
postRepo := NewPostRepository(dbMap)
650+
651+
tests := []struct {
652+
name string
653+
post *entity.Post
654+
wantErr error
655+
}{
656+
{
657+
name: "正常に削除できる",
658+
post: &entity.Post{
659+
ID: 1,
660+
UserID: "user-id",
661+
},
662+
wantErr: nil,
663+
},
664+
{
665+
name: `存在しないPostならErrNotFound`,
666+
post: &entity.Post{
667+
ID: 100,
668+
UserID: "user-id",
669+
},
670+
wantErr: entity.NewErrorNotFound("post"),
671+
},
672+
{
673+
name: "投稿元のユーザ以外が削除するとエラー",
674+
post: &entity.Post{
675+
ID: 2,
676+
UserID: "user-id100",
677+
},
678+
wantErr: entity.ErrIsNotAuthor,
679+
},
680+
}
681+
for _, tt := range tests {
682+
tt := tt
683+
t.Run(tt.name, func(t *testing.T) {
684+
ctx := context.Background()
685+
err := postRepo.Delete(ctx, tt.post)
686+
687+
if err == nil || tt.wantErr == nil {
688+
if err == tt.wantErr {
689+
return
690+
}
691+
// どちらかがnilの場合は%vを使う
692+
t.Errorf("error = %v, wantErr = %v", err, tt.wantErr)
693+
} else if err.Error() != tt.wantErr.Error() {
694+
t.Errorf("error = %s, wantErr = %s", err.Error(), tt.wantErr.Error())
695+
}
696+
})
697+
}
698+
}

main.go

+1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ func main() {
7171
post.POST("", postController.Create, authMiddleware.Authenticate)
7272
post.GET("/:postID", postController.Get)
7373
post.PUT("/:postID", postController.Update, authMiddleware.Authenticate)
74+
post.DELETE("/:postID", postController.Delete, authMiddleware.Authenticate)
7475

7576
comment := v1.Group("/post/:postID/comment")
7677
comment.GET("", commentController.GetByPostID)

migrations/20210319141439-CreatePosts.sql

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS posts (
1010
source VARCHAR(2048),
1111
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1212
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
13-
FOREIGN KEY (user_id) REFERENCES users (id)
13+
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
1414
);
1515
-- +migrate Down
1616
DROP TABLE IF EXISTS posts;

migrations/20210319143039-CreateComments.sql

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ CREATE TABLE IF NOT EXISTS comments (
1212
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
1313
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
1414
PRIMARY KEY (id, post_id),
15-
FOREIGN KEY (post_id) REFERENCES posts (id),
16-
FOREIGN KEY (user_id) REFERENCES users (id)
15+
FOREIGN KEY (post_id) REFERENCES posts (id) ON DELETE CASCADE,
16+
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
1717
);
1818
-- +migrate Down
1919
DROP TABLE IF EXISTS comments;

repository/post.go

+1
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ type Post interface {
1515
FindByUserID(ctx context.Context, uid string) ([]*entity.Post, error)
1616
Insert(ctx context.Context, post *entity.Post) error
1717
Update(ctx context.Context, post *entity.Post) error
18+
Delete(ctx context.Context, post *entity.Post) error
1819
}

usecase/post.go

+8
Original file line numberDiff line numberDiff line change
@@ -56,3 +56,11 @@ func (p *PostUsecase) Update(ctx context.Context, post *entity.Post) error {
5656
}
5757
return nil
5858
}
59+
60+
// Delete は引数のpostエンティティをもとに投稿を削除します.
61+
func (p *PostUsecase) Delete(ctx context.Context, post *entity.Post) error {
62+
if err := p.postRepo.Delete(ctx, post); err != nil {
63+
return fmt.Errorf("failed Delete Post: %w", err)
64+
}
65+
return nil
66+
}

0 commit comments

Comments
 (0)