Skip to content

Commit

Permalink
[#54] feat: Article 삭제 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
kwj1270 committed Dec 2, 2021
1 parent 62d7925 commit e039757
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class QArticle extends EntityPathBase<Article> {

public final com.study.realworld.global.common.QBaseTimeEntity _super = new com.study.realworld.global.common.QBaseTimeEntity(this);

public final BooleanPath activated = createBoolean("activated");

public final com.study.realworld.domain.article.domain.vo.QArticleBody articleBody;

public final com.study.realworld.domain.article.domain.vo.QArticleDescription articleDescription;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.study.realworld.domain.article.dto.ArticleSave;
import com.study.realworld.domain.article.dto.ArticleUpdate;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
Expand All @@ -32,4 +33,11 @@ public ResponseEntity<ArticleUpdate.Response> update(@AuthenticationPrincipal fi
@Valid @RequestBody final ArticleUpdate.Request request) {
return ResponseEntity.ok().body(articleCommandService.update(userId, articleSlug, request));
}

@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/articles/{articleSlug}")
public void delete(@AuthenticationPrincipal final Long userId,
@Valid @PathVariable final ArticleSlug articleSlug) {
articleCommandService.delete(userId, articleSlug);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,29 @@ public ArticleUpdate.Response update(final Long userId,
final ArticleSlug articleSlug,
final ArticleUpdate.Request request) {
final User user = userQueryService.findById(userId);
final Article article = articleRepository
.findByArticleSlug(articleSlug)
.orElseThrow(IllegalArgumentException::new);

final Article article = findArticleBySlug(articleSlug);
validateSameAuthor(article, user);

article.changeArticleTitleAndSlug(request.optionalArticleTitle().orElse(article.articleTitle()), slugStrategy)
.changeArticleBody(request.optionalArticleBody().orElse(article.articleBody()))
.changeArticleDescription(request.optionalArticleDescription().orElse(article.articleDescription()));
return ArticleUpdate.Response.from(article);
}

public void delete(final Long userId, final ArticleSlug articleSlug) {
final User user = userQueryService.findById(userId);
final Article article = findArticleBySlug(articleSlug);
validateSameAuthor(article, user);
article.delete();
}

private Article findArticleBySlug(final ArticleSlug articleSlug) {
return articleRepository
.findByArticleSlug(articleSlug)
.orElseThrow(IllegalArgumentException::new);
}

private void validateSameAuthor(final Article article, final User user) {
if (!article.isSameAuthor(user)) {
if (!article.isAuthor(user)) {
throw new AuthorMissMatchException();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
import lombok.AccessLevel;
import lombok.Builder;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Where;

import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.Objects;

@Where(clause = "activated = true")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Article extends BaseTimeEntity {
Expand All @@ -39,6 +42,9 @@ public class Article extends BaseTimeEntity {
@JoinColumn(name = "author", nullable = false, updatable = false)
private User author;

@Column(name = "activated")
private boolean activated = true;

@Builder
public Article(final ArticleSlug articleSlug, final ArticleTitle articleTitle,
final ArticleBody articleBody, final ArticleDescription articleDescription,
Expand All @@ -50,7 +56,7 @@ public Article(final ArticleSlug articleSlug, final ArticleTitle articleTitle,
this.author = author;
}

public boolean isSameAuthor(final User user) {
public boolean isAuthor(final User user) {
return author.equals(user);
}

Expand Down Expand Up @@ -109,6 +115,11 @@ public Article changeArticleDescription(final ArticleDescription articleDescript
return this;
}

public void delete() {
activated = false;
recordDeletedTime(LocalDateTime.now());
}

@Override
public boolean equals(final Object o) {
if (this == o) return true;
Expand All @@ -121,4 +132,5 @@ public boolean equals(final Object o) {
public int hashCode() {
return Objects.hash(articleId());
}

}
23 changes: 22 additions & 1 deletion src/main/java/com/study/realworld/http/article.http
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ Authorization: {{Authorization}}
### ARTICLE_FIND_NO_AUTHORIATION
GET localhost:8080/api/articles/how-to-train-your-dragon

### LOGIN
POST localhost:8080/api/users/login
Content-Type: application/json

{
"user": {
"email": "woojiji@jake.jake",
"password": "woojijiwoojiji"
}
}

> {%
client.global.set("Authorization", response.body.user.token);
client.log("생성된 Authorization : " + client.global.get("Authorization"));
%}

### ARTICLE_UPDATE
PUT http://localhost:8080/api/articles/how-to-train-your-dragon
Authorization: {{Authorization}}
Expand All @@ -100,4 +116,9 @@ Content-Type: application/json
"article": {
"title": "Did you train your dragon?"
}
}
}

### ARTICLE_DELTE
DELETE http://localhost:8080/api/articles/did-you-train-your-dragon
Authorization: {{Authorization}}
Content-Type: application/json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.study.realworld.domain.article.dto.ArticleUpdate;
import com.study.realworld.domain.article.error.ArticleErrorResponse;
import com.study.realworld.domain.auth.dto.Login;
import com.study.realworld.domain.follow.error.FollowErrorResponse;
import com.study.realworld.domain.user.dto.UserJoin;
import com.study.realworld.global.common.AccessToken;
import io.restassured.RestAssured;
Expand Down Expand Up @@ -147,9 +146,18 @@ public void setUp() {
final Login.Response otherLoginResponse = 로그인_되어있음(user2.userEmail().userEmail());
final ArticleSave.Response saveResponse = 정상적인_게시글_등록되어_있음(authorLoginResponse.accessToken());
final ExtractableResponse<Response> response = 정상적인_게시글_변경_요청(otherLoginResponse.accessToken(), saveResponse.articleSlug());
final ArticleErrorResponse ArticleErrorResponse = response.as(ArticleErrorResponse.class);
final ArticleErrorResponse articleErrorResponse = response.as(ArticleErrorResponse.class);

assertThat(ArticleErrorResponse.body().get(0)).isEqualTo("login user is not author");
assertThat(articleErrorResponse.body().get(0)).isEqualTo("login user is not author");
}

@Test
void 게시글을_삭제한다() {
final Login.Response authorLoginResponse = 로그인_되어있음(user1.userEmail().userEmail());
final ArticleSave.Response saveResponse = 정상적인_게시글_등록되어_있음(authorLoginResponse.accessToken());
final ExtractableResponse<Response> response = 정상적인_게시글_삭제_요청(authorLoginResponse.accessToken(), saveResponse.articleSlug());

assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value());
}

protected ArticleSave.Response 정상적인_게시글_등록되어_있음(final AccessToken accessToken) {
Expand Down Expand Up @@ -198,6 +206,16 @@ public void setUp() {
.extract();
}

protected ExtractableResponse<Response> 정상적인_게시글_삭제_요청(final AccessToken accessToken, final ArticleSlug articleSlug) {
return RestAssured.given()
.header(AUTHORIZATION, BEARER + accessToken.accessToken())
.contentType(MediaType.APPLICATION_JSON_VALUE)
.when()
.delete(String.format("/api/articles/%s", articleSlug.articleSlug()))
.then()
.extract();
}

private ArticleUpdate.Request 정상적인_게시글_변경_정보() {
return ArticleUpdate.Request.builder()
.articleTitle(OTHER_ARTICLE_TITLE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,39 @@ class ArticleCommandServiceTest {
);
}

@Test
void 저자는_게시글을_변경_수_있다() {
final User author = createUser(USER_EMAIL, USER_NAME, USER_PASSWORD, USER_BIO, USER_IMAGE);
final Article article = createArticle(ARTICLE_SLUG, ARTICLE_TITLE, ARTICLE_BODY, ARTICLE_DESCRIPTION, author);
final ArticleUpdate.Request request = createArticleUpdateRequest(OTHER_ARTICLE_TITLE, OTHER_ARTICLE_BODY, OTHER_ARTICLE_DESCRIPTION);

ReflectionTestUtils.setField(author, "userId", 1L);
ReflectionTestUtils.setField(article, "articleId", 1L);
ReflectionTestUtils.setField(article, "createdAt", LocalDateTime.now());
ReflectionTestUtils.setField(article, "updatedAt", LocalDateTime.now());

willReturn(author).given(userQueryService).findById(any());
willReturn(Optional.of(article)).given(articleRepository).findByArticleSlug(any());
willReturn(OTHER_ARTICLE_SLUG.articleSlug()).given(slugStrategy).mapToSlug(any());

final ArticleUpdate.Response response = articleCommandService.update(1L, article.articleSlug(), request);
assertAll(
() -> assertThat(response.articleSlug()).isEqualTo(OTHER_ARTICLE_SLUG),
() -> assertThat(response.articleTitle()).isEqualTo(OTHER_ARTICLE_TITLE),
() -> assertThat(response.articleBody()).isEqualTo(OTHER_ARTICLE_BODY),
() -> assertThat(response.articleDescription()).isEqualTo(OTHER_ARTICLE_DESCRIPTION),
() -> assertThat(response.createdAt()).isNotNull(),
() -> assertThat(response.updatedAt()).isNotNull(),
() -> assertThat(response.favorited()).isFalse(),
() -> assertThat(response.favoritesCount()).isZero(),
() -> assertThat(response.tags()).isEqualTo(List.of("reactjs", "angularjs", "dragons")),
() -> assertThat(response.author().userName()).isEqualTo(author.userName()),
() -> assertThat(response.author().userBio()).isEqualTo(author.userBio()),
() -> assertThat(response.author().userImage()).isEqualTo(author.userImage()),
() -> assertThat(response.author().following()).isEqualTo(false)
);
}

@Test
void 저자_정보가_일치하지_않는다면_게시글을_변경_수_없다() {
final User loginUser = createUser(OTHER_USER_EMAIL, OTHER_USER_NAME, OTHER_USER_PASSWORD, OTHER_USER_BIO, OTHER_USER_IMAGE);
Expand All @@ -92,4 +125,32 @@ class ArticleCommandServiceTest {
.isExactlyInstanceOf(AuthorMissMatchException.class)
.hasMessage("수정자와 저자가 다릅니다");
}

@Test
void 저자는_게시글을_삭제할_수_있다() {
final User author = createUser(USER_EMAIL, USER_NAME, USER_PASSWORD, USER_BIO, USER_IMAGE);
final Article article = createArticle(ARTICLE_SLUG, ARTICLE_TITLE, ARTICLE_BODY, ARTICLE_DESCRIPTION, author);

willReturn(author).given(userQueryService).findById(any());
willReturn(Optional.of(article)).given(articleRepository).findByArticleSlug(any());

articleCommandService.delete(1L, article.articleSlug());
}

@Test
void 저자가_아니면_게시글을_삭제할_수_없다() {
final User loginUser = createUser(OTHER_USER_EMAIL, OTHER_USER_NAME, OTHER_USER_PASSWORD, OTHER_USER_BIO, OTHER_USER_IMAGE);
final User author = createUser(USER_EMAIL, USER_NAME, USER_PASSWORD, USER_BIO, USER_IMAGE);
final Article article = createArticle(ARTICLE_SLUG, ARTICLE_TITLE, ARTICLE_BODY, ARTICLE_DESCRIPTION, author);

ReflectionTestUtils.setField(loginUser, "userId", 1L);
ReflectionTestUtils.setField(author, "userId", 2L);

willReturn(loginUser).given(userQueryService).findById(any());
willReturn(Optional.of(article)).given(articleRepository).findByArticleSlug(any());

assertThatThrownBy(() -> articleCommandService.delete(1L, article.articleSlug()))
.isExactlyInstanceOf(AuthorMissMatchException.class)
.hasMessage("수정자와 저자가 다릅니다");
}
}

0 comments on commit e039757

Please sign in to comment.