Skip to content

Commit

Permalink
test: 카테고리 편집 테스트 코드 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
HoeSeong123 committed Dec 23, 2024
1 parent 11ab595 commit 9eea751
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 243 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
Expand All @@ -15,7 +14,6 @@

import codezap.auth.configuration.AuthenticationPrinciple;
import codezap.category.dto.request.CreateCategoryRequest;
import codezap.category.dto.request.DeleteAllCategoriesRequest;
import codezap.category.dto.request.UpdateAllCategoriesRequest;
import codezap.category.dto.response.CreateCategoryResponse;
import codezap.category.dto.response.FindAllCategoriesResponse;
Expand Down Expand Up @@ -53,13 +51,4 @@ public ResponseEntity<Void> updateCategory(
categoryService.updateCategories(member, request);
return ResponseEntity.ok().build();
}

@DeleteMapping
public ResponseEntity<Void> deleteCategory(
@AuthenticationPrinciple Member member,
@Validated(ValidationSequence.class) @RequestBody DeleteAllCategoriesRequest request
) {
categoryService.deleteCategories(member, request);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import org.springframework.http.ResponseEntity;

import codezap.category.dto.request.CreateCategoryRequest;
import codezap.category.dto.request.DeleteAllCategoriesRequest;
import codezap.category.dto.request.UpdateAllCategoriesRequest;
import codezap.category.dto.response.CreateCategoryResponse;
import codezap.category.dto.response.FindAllCategoriesResponse;
Expand Down Expand Up @@ -69,24 +68,4 @@ ResponseEntity<CreateCategoryResponse> createCategory(
@ErrorCase(description = "카테고리를 수정할 권한이 없는 경우", exampleMessage = "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다.")
})
ResponseEntity<Void> updateCategory(Member member, UpdateAllCategoriesRequest request);

@SecurityRequirement(name = "쿠키 인증 토큰")
@Operation(summary = "카테고리 삭제", description = "해당하는 식별자의 카테고리를 삭제합니다.")
@ApiResponse(responseCode = "204", description = "카테고리 삭제 성공")
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories", errorCases = {
@ErrorCase(description = "삭제하려는 카테고리에 템플릿이 존재하는 경우",
exampleMessage = "템플릿이 존재하는 카테고리는 삭제할 수 없습니다."),
})
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/categories", errorCases = {
@ErrorCase(description = "기본 카테고리를 수정한 경우", exampleMessage = "기본 카테고리는 수정 및 삭제할 수 없습니다.")
})
@ApiErrorResponse(status = HttpStatus.NOT_FOUND, instance = "/categories", errorCases = {
@ErrorCase(description = "존재하지 않는 카테고리인 경우",
exampleMessage = "식별자 1에 해당하는 카테고리가 존재하지 않습니다."),
})
@ApiErrorResponse(status = HttpStatus.FORBIDDEN, instance = "/categories", errorCases = {
@ErrorCase(description = "카테고리를 삭제할 권한이 없는 경우",
exampleMessage = "해당 카테고리를 수정 또는 삭제할 권한이 없는 유저입니다.")
})
ResponseEntity<Void> deleteCategory(Member member, DeleteAllCategoriesRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
public class CategoryRepository {

private final CategoryJpaRepository categoryJpaRepository;
private final CategoryQueryDslRepository categoryQueryDslRepository;

public Category save(Category category) {
return categoryJpaRepository.save(category);
Expand Down Expand Up @@ -46,11 +45,7 @@ public long countByMember(Member member) {
return categoryJpaRepository.countByMember(member);
}

public void shiftOrdinal(Member member, Long ordinal) {
categoryQueryDslRepository.shiftOrdinal(member, ordinal);
}

public boolean existsDuplicateOrdinalsByMember(Member member) {
return categoryQueryDslRepository.existsDuplicateOrdinalsByMember(member);
public List<Category> saveAll(Iterable<Category> entities) {
return categoryJpaRepository.saveAll(entities);
}
}
100 changes: 66 additions & 34 deletions backend/src/main/java/codezap/category/service/CategoryService.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package codezap.category.service;

import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import codezap.category.domain.Category;
import codezap.category.dto.request.CreateCategoryRequest;
import codezap.category.dto.request.DeleteAllCategoriesRequest;
import codezap.category.dto.request.DeleteCategoryRequest;
import codezap.category.dto.request.UpdateAllCategoriesRequest;
import codezap.category.dto.request.UpdateCategoryRequest;
import codezap.category.dto.response.CreateCategoryResponse;
Expand All @@ -30,14 +30,17 @@ public class CategoryService {
private final TemplateRepository templateRepository;

@Transactional
public CreateCategoryResponse create(Member member, CreateCategoryRequest createCategoryRequest) {
String categoryName = createCategoryRequest.name();
validateDuplicatedCategory(categoryName, member);
long count = categoryRepository.countByMember(member);
Category category = categoryRepository.save(new Category(categoryName, member, count + 1));
public CreateCategoryResponse create(Member member, CreateCategoryRequest request) {
validateDuplicatedCategory(request.name(), member);
validateOrdinal(member, request);
Category category = categoryRepository.save(createCategory(member, request));
return CreateCategoryResponse.from(category);
}

private Category createCategory(Member member, CreateCategoryRequest request) {
return new Category(request.name(), member, request.ordinal());
}

public FindAllCategoriesResponse findAllByMemberId(Long memberId) {
return FindAllCategoriesResponse.from(categoryRepository.findAllByMemberIdOrderById(memberId));
}
Expand All @@ -52,10 +55,24 @@ public Category fetchById(Long id) {

@Transactional
public void updateCategories(Member member, UpdateAllCategoriesRequest request) {
for (UpdateCategoryRequest categoryRequest : request.categories()) {
update(member, categoryRequest);
validateDuplicateNameRequest(request);
validateOrdinals(request);

createCategories(member, request);
for (UpdateCategoryRequest updateCategory : request.updateCategories()) {
update(member, updateCategory);
}
for (Long deleteCategoryId : request.deleteCategoryIds()) {
delete(member, deleteCategoryId);
}
validateOrdinal(member);
}

private void createCategories(Member member, UpdateAllCategoriesRequest request) {
categoryRepository.saveAll(
request.createCategories().stream()
.map(createRequest -> createCategory(member, createRequest))
.toList()
);
}

private void update(Member member, UpdateCategoryRequest request) {
Expand All @@ -66,31 +83,13 @@ private void update(Member member, UpdateCategoryRequest request) {
category.update(request.name(), request.ordinal());
}

@Transactional
public void deleteCategories(Member member, DeleteAllCategoriesRequest request) {
List<DeleteCategoryRequest> sortedCategoryRequests = request.categories().stream()
.sorted(Comparator.comparing(DeleteCategoryRequest::ordinal).reversed())
.toList();
for (DeleteCategoryRequest categoryRequest : sortedCategoryRequests) {
delete(member, categoryRequest);
}
}

private void delete(Member member, DeleteCategoryRequest request) {
Long id = request.id();
Category category = categoryRepository.fetchById(id);
private void delete(Member member, Long categoryId) {
Category category = categoryRepository.fetchById(categoryId);
category.validateAuthorization(member);

validateHasTemplate(id);
validateHasTemplate(categoryId);
validateDefaultCategory(category);
categoryRepository.deleteById(id);
categoryRepository.shiftOrdinal(member, request.ordinal());
}

private void validateOrdinal(Member member) {
if (categoryRepository.existsDuplicateOrdinalsByMember(member)) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "템플릿 순서가 중복됩니다.");
}
categoryRepository.deleteById(categoryId);
}

private void validateDuplicatedCategory(String categoryName, Member member) {
Expand All @@ -100,7 +99,7 @@ private void validateDuplicatedCategory(String categoryName, Member member) {
}

private void validateDefaultCategory(Category category) {
if(category.isDefault()) {
if (category.isDefault()) {
throw new CodeZapException(ErrorCode.DEFAULT_CATEGORY, "기본 카테고리는 수정 및 삭제할 수 없습니다.");
}
}
Expand All @@ -110,4 +109,37 @@ private void validateHasTemplate(Long id) {
throw new CodeZapException(ErrorCode.CATEGORY_HAS_TEMPLATES, "템플릿이 존재하는 카테고리는 삭제할 수 없습니다.");
}
}

private void validateDuplicateNameRequest(UpdateAllCategoriesRequest request) {
List<String> allNames = Stream.concat(
request.createCategories().stream().map(CreateCategoryRequest::name),
request.updateCategories().stream().map(UpdateCategoryRequest::name)
).toList();

if (allNames.size() != new HashSet<>(allNames).size()) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "요청에 중복된 카테고리 이름이 존재합니다.");
}
}

private void validateOrdinal(Member member, CreateCategoryRequest request) {
long ordinal = request.ordinal();
long count = categoryRepository.countByMember(member);
if (ordinal != count) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "카테고리 순서가 잘못되었습니다.");
}
}

private void validateOrdinals(UpdateAllCategoriesRequest request) {
List<Long> allOrdinals = Stream.concat(
request.createCategories().stream().map(CreateCategoryRequest::ordinal),
request.updateCategories().stream().map(UpdateCategoryRequest::ordinal)
).sorted().toList();

boolean isSequential = IntStream.range(0, allOrdinals.size())
.allMatch(i -> allOrdinals.get(i) == i + 1);

if (!isSequential) {
throw new CodeZapException(ErrorCode.INVALID_REQUEST, "카테고리 순서가 연속적이지 않습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package codezap.category.controller;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
Expand All @@ -24,8 +20,6 @@

import codezap.category.domain.Category;
import codezap.category.dto.request.CreateCategoryRequest;
import codezap.category.dto.request.DeleteAllCategoriesRequest;
import codezap.category.dto.request.DeleteCategoryRequest;
import codezap.category.dto.request.UpdateAllCategoriesRequest;
import codezap.category.dto.request.UpdateCategoryRequest;
import codezap.category.dto.response.CreateCategoryResponse;
Expand All @@ -50,7 +44,7 @@ class CreateCategoryTest {
void createCategorySuccess() throws Exception {
// given
long categoryId = 1L;
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest("category");
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest("category", 1L);

when(categoryService.create(
MemberFixture.getFirstMember(), createCategoryRequest))
Expand All @@ -68,7 +62,7 @@ void createCategorySuccess() throws Exception {
@DisplayName("카테고리 생성 실패: 로그인을 하지 않은 회원")
void createCategoryFailWithNotLogin() throws Exception {
// given
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest("category");
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest("category", 1L);

doThrow(new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증에 대한 쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."))
.when(credentialManager).getCredential(any());
Expand All @@ -87,7 +81,7 @@ void createCategoryFailWithNotLogin() throws Exception {
@DisplayName("카테고리 생성 실패: 카테고리 이름 길이 초과")
void createCategoryFailWithLongName() throws Exception {
// given
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest("a".repeat(MAX_LENGTH + 1));
CreateCategoryRequest createCategoryRequest = new CreateCategoryRequest("a".repeat(MAX_LENGTH + 1), 1L);

// when & then
mvc.perform(post("/categories")
Expand Down Expand Up @@ -122,15 +116,18 @@ void findAllCategoriesSuccess() throws Exception {
}

@Nested
@DisplayName("카테고리 수정 테스트")
@DisplayName("카테고리 편집 테스트")
class UpdateCategoryTest {

@Test
@DisplayName("카테고리 수정 성공")
@DisplayName("카테고리 편집 성공")
void updateCategorySuccess() throws Exception {
// given
var updateCategoryRequest = new UpdateCategoryRequest(1L, "updateCategory", 1L);
var request = new UpdateAllCategoriesRequest(List.of(updateCategoryRequest));
var request = new UpdateAllCategoriesRequest(
List.of(),
List.of(updateCategoryRequest),
List.of());

// when & then
mvc.perform(put("/categories")
Expand All @@ -141,11 +138,14 @@ void updateCategorySuccess() throws Exception {
}

@Test
@DisplayName("카테고리 수정 실패: 로그인 되지 않은 경우")
@DisplayName("카테고리 편집 실패: 로그인 되지 않은 경우")
void updateCategoryFailWithUnauthorized() throws Exception {
// given
var updateCategoryRequest = new UpdateCategoryRequest(1L, "a".repeat(MAX_LENGTH), 1L);
var request = new UpdateAllCategoriesRequest(List.of(updateCategoryRequest));
var request = new UpdateAllCategoriesRequest(
List.of(),
List.of(updateCategoryRequest),
List.of());

doThrow(new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증에 대한 쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."))
.when(credentialManager).getCredential(any());
Expand All @@ -159,11 +159,14 @@ void updateCategoryFailWithUnauthorized() throws Exception {
}

@Test
@DisplayName("카테고리 수정 실패: 카테고리 이름 길이 초과")
@DisplayName("카테고리 편집 실패: 카테고리 이름 길이 초과")
void updateCategoryFailWithLongName() throws Exception {
// given
var updateCategoryRequest = new UpdateCategoryRequest(1L, "a".repeat(MAX_LENGTH + 1), 1L);
var request = new UpdateAllCategoriesRequest(List.of(updateCategoryRequest));
var request = new UpdateAllCategoriesRequest(
List.of(),
List.of(updateCategoryRequest),
List.of());

// when & then
mvc.perform(put("/categories")
Expand All @@ -174,47 +177,4 @@ void updateCategoryFailWithLongName() throws Exception {
.andExpect(jsonPath("$.errorCode").value(1101));
}
}

@Nested
@DisplayName("카테고리 삭제 테스트")
class DeleteCategoryTest {

@Test
@DisplayName("카테고리 삭제 성공")
void deleteCategorySuccess() throws Exception {
// given
var deleteCategoryRequest = new DeleteCategoryRequest(1L, 1L);
var request = new DeleteAllCategoriesRequest(List.of(deleteCategoryRequest));

// when
mvc.perform(delete("/categories")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isNoContent());

// then
verify(categoryService, times(1))
.deleteCategories(MemberFixture.getFirstMember(), request);
}

@Test
@DisplayName("카테고리 삭제 실패: 로그인 되지 않은 경우")
void deleteCategoryFailWithUnauthorized() throws Exception {
// given
var deleteCategoryRequest = new DeleteCategoryRequest(1L, 1L);
var request = new DeleteAllCategoriesRequest(List.of(deleteCategoryRequest));
doThrow(new CodeZapException(ErrorCode.UNAUTHORIZED_USER, "인증에 대한 쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."))
.when(credentialManager).getCredential(any());

// when & then
mvc.perform(delete("/categories")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.detail").value("인증에 대한 쿠키가 없어서 회원 정보를 찾을 수 없습니다. 다시 로그인해주세요."))
.andExpect(jsonPath("$.errorCode").value(1301));
}
}
}
Loading

0 comments on commit 9eea751

Please sign in to comment.