Skip to content

Commit

Permalink
상세 조회에서 다른 사람의 비공개 템플릿 확인 시 예외 처리 (#852)
Browse files Browse the repository at this point in the history
* refactor(template): 기본 값 설정

* feat(template): 다른 사람의 비공개 템플릿 단건 조회 시 예외 발생

* feat(domain): 메서드명 변경

* refactor(domain): 주생성자 활용하도록 수정

* refactor(domain): 도메인에서 예외 발생시키지 않도록 수정

* docs: 실패 ErrorCode 문서 수정

* refactor(domain): 필드 사용으로 변경

* refactor(service): 함수형 인터페이스를 이용해 메서드 추출 및 재사용
  • Loading branch information
kyum-q authored Oct 23, 2024
1 parent a4b72bb commit d89ee41
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ ResponseEntity<FindAllTemplatesResponse> findAllTemplates(
@ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/templates/1/login", errorCases = {
@ErrorCase(description = "해당하는 ID 값인 템플릿이 없는 경우", exampleMessage = "식별자 1에 해당하는 템플릿이 존재하지 않습니다."),
})
@ApiErrorResponse(status = HttpStatus.FORBIDDEN, instance = "/templates/1", errorCases = {
@ErrorCase(description = "다른 사람의 private 템플릿인 경우", exampleMessage = "해당 템플릿은 비공개 템플릿입니다."),
})
ResponseEntity<FindTemplateResponse> findTemplateById(Member member, Long id);

@SecurityRequirement(name = "쿠키 인증 토큰")
Expand Down
20 changes: 9 additions & 11 deletions backend/src/main/java/codezap/template/domain/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;

import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.Formula;

import codezap.category.domain.Category;
import codezap.global.auditing.BaseTimeEntity;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.member.domain.Member;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
Expand Down Expand Up @@ -57,6 +56,7 @@ public class Template extends BaseTimeEntity {
private Long likesCount;

@Column(nullable = false)
@ColumnDefault("'PUBLIC'")
@Enumerated(EnumType.STRING)
private Visibility visibility;

Expand All @@ -65,11 +65,7 @@ public Template(Member member, String title, String description, Category catego
}

public Template(Member member, String title, String description, Category category, Visibility visibility) {
this.member = member;
this.title = title;
this.description = description;
this.category = category;
this.visibility = visibility;
this(null, member, title, description, category, null, 0L, visibility);
}

public void updateTemplate(String title, String description, Category category, Visibility visibility) {
Expand All @@ -79,9 +75,11 @@ public void updateTemplate(String title, String description, Category category,
this.visibility = visibility;
}

public void validateAuthorization(Member member) {
if (!getMember().equals(member)) {
throw new CodeZapException(ErrorCode.FORBIDDEN_ACCESS, "해당 템플릿에 대한 권한이 없습니다.");
}
public boolean matchMember(Member member) {
return this.member.equals(member);
}

public boolean isPrivate() {
return visibility == Visibility.PRIVATE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ public Template update(
Category category
) {
Template template = templateRepository.fetchById(templateId);
template.validateAuthorization(member);
if (!template.matchMember(member)) {
throw new CodeZapException(ErrorCode.FORBIDDEN_ACCESS, "해당 템플릿에 대한 권한이 없습니다.");
}
template.updateTemplate(
updateTemplateRequest.title(),
updateTemplateRequest.description(),
Expand All @@ -86,7 +88,9 @@ public void deleteByMemberAndIds(Member member, List<Long> ids) {

private void deleteById(Member member, Long id) {
Template template = templateRepository.fetchById(id);
template.validateAuthorization(member);
if (!template.matchMember(member)) {
throw new CodeZapException(ErrorCode.FORBIDDEN_ACCESS, "해당 템플릿에 대한 권한이 없습니다.");
}
templateRepository.deleteById(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import codezap.category.domain.Category;
import codezap.category.service.CategoryService;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.likes.service.LikedChecker;
import codezap.likes.service.LikesService;
import codezap.member.domain.Member;
Expand Down Expand Up @@ -58,18 +60,29 @@ public Long create(Member member, CreateTemplateRequest createTemplateRequest) {
}

public FindTemplateResponse findById(Long id) {
Template template = templateService.getById(id);
List<Tag> tags = tagService.findAllByTemplate(template);
List<SourceCode> sourceCodes = sourceCodeService.findAllByTemplate(template);
return FindTemplateResponse.of(template, sourceCodes, tags, false);
return makeTemplateResponse(id, template -> false, template -> false);
}

public FindTemplateResponse findById(Long id, Member loginMember) {
return makeTemplateResponse(
id,
template -> template.matchMember(loginMember),
template -> likesService.isLiked(loginMember, template)
);
}

private FindTemplateResponse makeTemplateResponse(
Long id,
TemplateOwnershipChecker templateOwnershipChecker,
LikedChecker likedChecker
) {
Template template = templateService.getById(id);
if (!templateOwnershipChecker.isOwner(template) && template.isPrivate()) {
throw new CodeZapException(ErrorCode.FORBIDDEN_ACCESS, "해당 템플릿은 비공개 템플릿입니다.");
}
List<Tag> tags = tagService.findAllByTemplate(template);
List<SourceCode> sourceCodes = sourceCodeService.findAllByTemplate(template);
boolean isLiked = likesService.isLiked(loginMember, template);
return FindTemplateResponse.of(template, sourceCodes, tags, isLiked);
return FindTemplateResponse.of(template, sourceCodes, tags, likedChecker.isLiked(template));
}

public FindAllTemplatesResponse findAllBy(
Expand All @@ -82,7 +95,7 @@ public FindAllTemplatesResponse findAllBy(
Page<Template> templates = templateService.findAllBy(
memberId, keyword, categoryId, tagIds, Visibility.PUBLIC, pageable
);
return makeResponse(templates, (template) -> false);
return makeAllTemplatesResponse(templates, (template) -> false);
}

public FindAllTemplatesResponse findAllBy(
Expand All @@ -96,7 +109,7 @@ public FindAllTemplatesResponse findAllBy(
Page<Template> templates = templateService.findAllBy(
memberId, keyword, categoryId, tagIds, getVisibilityLevel(memberId, loginMember), pageable
);
return makeResponse(templates, (template -> likesService.isLiked(loginMember, template)));
return makeAllTemplatesResponse(templates, (template -> likesService.isLiked(loginMember, template)));
}

@Nullable
Expand All @@ -107,7 +120,7 @@ private Visibility getVisibilityLevel(Long memberId, Member loginMember) {
return null;
}

private FindAllTemplatesResponse makeResponse(Page<Template> page, LikedChecker likedChecker) {
private FindAllTemplatesResponse makeAllTemplatesResponse(Page<Template> page, LikedChecker likedChecker) {
List<Template> templates = page.getContent();
List<FindAllTemplateItemResponse> findAllTemplateByResponse =
getFindAllTemplateItemResponses(templates, likedChecker);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package codezap.template.service.facade;

import codezap.template.domain.Template;

@FunctionalInterface
public interface TemplateOwnershipChecker {

boolean isOwner(Template template);
}
71 changes: 71 additions & 0 deletions backend/src/test/java/codezap/template/domain/TemplateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package codezap.template.domain;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import codezap.category.domain.Category;
import codezap.fixture.MemberFixture;
import codezap.fixture.TemplateFixture;
import codezap.member.domain.Member;

class TemplateTest {

@Nested
@DisplayName("멤버 확인")
class MatchMember {

@Test
@DisplayName("성공: 같은 사용자일 경우 true")
void matchMemberSuccess() {
Member member = MemberFixture.getFirstMember();
Template template = TemplateFixture.get(member, Category.createDefaultCategory(member));

boolean actual = template.matchMember(member);

assertThat(actual).isTrue();
}

@Test
@DisplayName("성공: 다른 사용자일 경우 false")
void matchMemberFail() {
Member member = MemberFixture.getFirstMember();
Member otherMember = MemberFixture.getSecondMember();
Template template = TemplateFixture.get(member, Category.createDefaultCategory(member));

boolean actual = template.matchMember(otherMember);

assertThat(actual).isFalse();
}
}

@Nested
@DisplayName("공개 범위 확인")
class IsPrivate {

@Test
@DisplayName("성공: 비공개 템플릿일 경우 true")
void isPrivateTrue() {
Member member = MemberFixture.getFirstMember();
Template template = TemplateFixture.getPrivate(member, Category.createDefaultCategory(member));

boolean actual = template.isPrivate();

assertThat(actual).isTrue();
}

@Test
@DisplayName("성공: 공개 템플릿일 경우 false")
void isPrivateFalse() {
Member member = MemberFixture.getFirstMember();
Member otherMember = MemberFixture.getSecondMember();
Template template = TemplateFixture.get(member, Category.createDefaultCategory(member));

boolean actual = template.isPrivate();

assertThat(actual).isFalse();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
import org.springframework.data.jpa.domain.Specification;

import codezap.category.domain.Category;
import codezap.category.repository.CategoryRepository;
import codezap.fixture.CategoryFixture;
import codezap.fixture.MemberFixture;
import codezap.fixture.SourceCodeFixture;
import codezap.fixture.TemplateFixture;
import codezap.global.ServiceTest;
import codezap.global.exception.CodeZapException;
import codezap.global.exception.ErrorCode;
import codezap.likes.domain.Likes;
import codezap.member.domain.Member;
import codezap.template.domain.SourceCode;
Expand Down Expand Up @@ -120,6 +120,21 @@ void findByTemplateId() {
() -> assertThat(actual.isLiked()).isFalse()
);
}

@Test
@DisplayName("ID로 템플릿 조회 실패: 다른 사람의 private 템플릿 조회 불가")
void findByTemplateIdFailOtherPersonPrivateTemplate() {
// given
var member = memberRepository.save(MemberFixture.getFirstMember());
var category = categoryRepository.save(Category.createDefaultCategory(member));
var template = templateRepository.save(TemplateFixture.getPrivate(member, category));

// when & then
assertThatThrownBy(() -> sut.findById(template.getId()))
.isInstanceOf(CodeZapException.class)
.hasMessage("해당 템플릿은 비공개 템플릿입니다.")
.extracting("errorCode").isEqualTo(ErrorCode.FORBIDDEN_ACCESS);
}
}

@Nested
Expand Down Expand Up @@ -162,6 +177,40 @@ void getByIdWithMemberNoLikes() {
() -> assertThat(actual.isLiked()).isFalse()
);
}

@Test
@DisplayName("ID로 템플릿 조회 실패: 다른 사람의 private 템플릿 조회 불가")
void findByTemplateIdFailOtherPersonPrivateTemplate() {
// given
var member = memberRepository.save(MemberFixture.getFirstMember());
var otherMember = memberRepository.save(MemberFixture.getSecondMember());
var category = categoryRepository.save(Category.createDefaultCategory(member));
var template = templateRepository.save(TemplateFixture.getPrivate(member, category));

// when & then
assertThatThrownBy(() -> sut.findById(template.getId(), otherMember))
.isInstanceOf(CodeZapException.class)
.hasMessage("해당 템플릿은 비공개 템플릿입니다.")
.extracting("errorCode").isEqualTo(ErrorCode.FORBIDDEN_ACCESS);
}

@Test
@DisplayName("ID로 템플릿 조회 성공: 내 private 템플릿 조회 가능")
void findByTemplateIdSuccessPrivateTemplate() {
// given
var member = memberRepository.save(MemberFixture.getFirstMember());
var category = categoryRepository.save(Category.createDefaultCategory(member));
var template = templateRepository.save(TemplateFixture.getPrivate(member, category));

// when
var actual = sut.findById(template.getId(), member);

// then
assertAll(
() -> assertThat(actual.id()).isEqualTo(1L),
() -> assertThat(actual.isLiked()).isFalse()
);
}
}

@Nested
Expand Down

0 comments on commit d89ee41

Please sign in to comment.