Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

상세 조회에서 다른 사람의 비공개 템플릿 확인 시 예외 처리 #852

Merged
merged 8 commits into from
Oct 23, 2024
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 = "해당 템플릿은 비공개 템플릿입니다."),
zangsu marked this conversation as resolved.
Show resolved Hide resolved
})
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