Implements Wrtie Article HTTP API

This commit is contained in:
Rebwon
2021-10-02 17:52:05 +09:00
committed by MaengSol
parent 1023c8cded
commit 21e6162d11
14 changed files with 312 additions and 39 deletions

View File

@@ -3,13 +3,14 @@ package com.yam.app.account.domain;
import com.yam.app.common.EntityStatus;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@ToString(exclude = "password")
@NoArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class Account {
private Long id;

View File

@@ -0,0 +1,23 @@
package com.yam.app.article.application;
import com.yam.app.article.domain.WriteArticleProcessor;
import com.yam.app.article.presentation.WriteArticleCommand;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ArticleFacade {
private final WriteArticleProcessor writeArticleProcessor;
public ArticleFacade(WriteArticleProcessor writeArticleProcessor) {
this.writeArticleProcessor = writeArticleProcessor;
}
@Transactional
public void write(Long memberId, WriteArticleCommand command) {
writeArticleProcessor.write(memberId,
command.getTitle(), command.getContent(),
command.getImage(), command.getTags());
}
}

View File

@@ -6,6 +6,7 @@ import com.yam.app.common.EntityStatus;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
@@ -13,7 +14,7 @@ import lombok.ToString;
@Getter
@ToString
@NoArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(of = "id")
public final class Article {

View File

@@ -1,12 +1,13 @@
package com.yam.app.article.domain;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@ToString
@NoArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class ArticleTag {
private Long id;

View File

@@ -1,12 +1,13 @@
package com.yam.app.article.domain;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@ToString
@NoArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class Tag {
private Long id;

View File

@@ -0,0 +1,35 @@
package com.yam.app.article.presentation;
import com.yam.app.article.application.ArticleFacade;
import com.yam.app.common.Authentication;
import com.yam.app.common.AuthenticationPrincipal;
import javax.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(
produces = MediaType.APPLICATION_JSON_VALUE,
consumes = MediaType.APPLICATION_JSON_VALUE
)
public final class ArticleCommandApi {
private final ArticleFacade articleFacade;
public ArticleCommandApi(ArticleFacade articleFacade) {
this.articleFacade = articleFacade;
}
@PostMapping("/api/articles/write")
public ResponseEntity<Void> writeArticle(
@RequestBody @Valid WriteArticleCommand command,
@AuthenticationPrincipal Authentication authentication) {
articleFacade.write(authentication.getMemberId(), command);
return ResponseEntity.status(HttpStatus.CREATED).build();
}
}

View File

@@ -0,0 +1,19 @@
package com.yam.app.article.presentation;
import java.util.List;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
public final class WriteArticleCommand {
@NotBlank
private String title;
@NotBlank
private String content;
@NotBlank
private String image;
@Size(max = 3)
private List<String> tags;
}

View File

@@ -1,13 +1,14 @@
package com.yam.app.member.domain;
import com.yam.app.common.EntityStatus;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Getter
@ToString
@NoArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public final class Member {
private Long id;

View File

@@ -22,8 +22,8 @@
atg.id AS article_tag_id, atg.article_id AS article_tag_article_id,
t.id AS tag_id, t.name AS tag_name
FROM article a
LEFT OUTER JOIN article_tag atg ON atg.article_id = a.id
LEFT OUTER JOIN tag t ON t.id = atg.tag_id
INNER JOIN article_tag atg ON atg.article_id = a.id
INNER JOIN tag t ON t.id = atg.tag_id
WHERE a.id = #{articleId};
</select>

View File

@@ -0,0 +1,8 @@
package com.yam.app.article.presentation;
public final class ArticleApiUri {
public static final String WRITE_ARTICLE = "/api/articles/write";
private ArticleApiUri() {}
}

View File

@@ -0,0 +1,122 @@
package com.yam.app.article.presentation;
import static com.yam.app.article.presentation.ArticleApiUri.WRITE_ARTICLE;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yam.app.article.application.ArticleFacade;
import java.util.List;
import org.javaunit.autoparams.AutoSource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
@DisplayName("Article Command HTTP API")
@WebMvcTest(ArticleCommandApi.class)
final class ArticleCommandApiTests {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private ArticleFacade articleFacade;
private void assertThatInvalidArgumentError(ResultActions actions) throws Exception {
actions
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.message").value("Invalid argument"));
}
@Nested
@DisplayName("게시글 작성 HTTP API")
class WriteArticleApi {
@ParameterizedTest
@AutoSource
@DisplayName("인증되지 않은 사용자가 게시글을 작성하려고 시도하면 401 에러를 반환한다.")
void not_authentication_user_write_article_fail(
String args, List<String> list) throws Exception {
var command = new WriteArticleCommand();
command.setTitle(args);
command.setContent(args);
command.setImage(args);
command.setTags(list);
//Act
final var actions = mockMvc.perform(post(WRITE_ARTICLE)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(command))
);
//Assert
actions
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.data").doesNotExist())
.andExpect(jsonPath("$.message").value("Unauthorized request"));
}
@ParameterizedTest
@NullAndEmptySource
@DisplayName("인증된 사용자의 요청 Body가 null이거나 empty인 경우 400 에러를 반환한다.")
void authentication_user_write_article_http_body_null_or_empty_fail(
String args) throws Exception {
// Arrange
var session = new MockHttpSession();
var command = new WriteArticleCommand();
command.setTitle(args);
command.setContent(args);
command.setImage(args);
// Act
final var actions = mockMvc.perform(post(WRITE_ARTICLE)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(command))
.session(session)
);
// Assert
assertThatInvalidArgumentError(actions);
}
@ParameterizedTest
@AutoSource
@DisplayName("인증된 사용자의 요청 Body중 Tag의 개수가 3개 이상일 경우 400 에러를 반환한다.")
void authentication_user_write_article_http_body_tag_size_over_fail(
String args) throws Exception {
// Arrange
var session = new MockHttpSession();
var command = new WriteArticleCommand();
command.setTitle(args);
command.setContent(args);
command.setImage(args);
command.setTags(List.of("good", "job", "sam", "kim"));
// Act
final var actions = mockMvc.perform(post(WRITE_ARTICLE)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(command))
.session(session)
);
// Assert
assertThatInvalidArgumentError(actions);
}
}
}

View File

@@ -0,0 +1,35 @@
package com.yam.app.integration;
import static org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer.sharedHttpSession;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
abstract class AbstractIntegrationTests {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
private WebApplicationContext wac;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(sharedHttpSession())
.build();
}
}

View File

@@ -1,4 +1,4 @@
package com.yam.app.account.integration;
package com.yam.app.integration;
import static com.yam.app.account.presentation.AccountApiUri.EMAIL_CONFIRM;
import static com.yam.app.account.presentation.AccountApiUri.FIND_INFO;
@@ -13,48 +13,19 @@ import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.SharedHttpSessionConfigurer.sharedHttpSession;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yam.app.account.presentation.LoginAccountCommand;
import com.yam.app.account.presentation.RegisterAccountCommand;
import com.yam.app.account.presentation.UpdateAccountCommand;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
@SpringBootTest
@AutoConfigureMockMvc
@DisplayName("회원가입 통합 인수 테스트")
@ActiveProfiles("test")
final class AccountIntegrationTests {
@DisplayName("회원 모듈 통합 인수 테스트")
final class AccountIntegrationTests extends AbstractIntegrationTests {
private static final String EMAIL_CONFIRM_SUCCESS_REDIRECT_URI = "http://localhost:3000/login";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private WebApplicationContext wac;
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders
.webAppContextSetup(wac)
.apply(sharedHttpSession())
.build();
}
@Test
@DisplayName("새로운 계정 등록에 적절한 파라미터가 입력되고, 계정이 성공적으로 등록된다.")
void new_account_request_in_register_correctly() throws Exception {

View File

@@ -0,0 +1,55 @@
package com.yam.app.integration;
import static com.yam.app.account.presentation.AccountApiUri.LOGIN;
import static com.yam.app.article.presentation.ArticleApiUri.WRITE_ARTICLE;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.yam.app.account.presentation.LoginAccountCommand;
import com.yam.app.article.presentation.WriteArticleCommand;
import java.util.List;
import org.javaunit.autoparams.AutoSource;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.springframework.http.MediaType;
@DisplayName("게시글 모듈 통합 인수 테스트")
final class ArticleIntegrationTests extends AbstractIntegrationTests {
@ParameterizedTest
@AutoSource
@DisplayName("로그인에 적절한 파라미터를 입력하여, 성공하고 "
+ "인증된 사용자가 게시글 한 건을 작성하는 시나리오 테스트.")
void login_success_and_authentication_member_logout_scenarios(String title, String content,
String image, List<String> tags) throws Exception {
//Arrange
var loginCommand = new LoginAccountCommand();
loginCommand.setEmail("loginCheck@gmail.com");
loginCommand.setPassword("password!");
var writeArticleCommand = new WriteArticleCommand();
writeArticleCommand.setTitle(title);
writeArticleCommand.setContent(content);
writeArticleCommand.setImage(image);
writeArticleCommand.setTags(tags);
// Act & Assert
mockMvc.perform(post(LOGIN)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(loginCommand))
)
.andExpect(status().isOk())
.andDo(
result -> {
final var actions = mockMvc.perform(post(WRITE_ARTICLE)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(writeArticleCommand))
);
actions
.andExpect(status().isCreated());
});
}
}