Implements Wrtie Article HTTP API
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user