diff --git a/backend/emm-sale/src/docs/asciidoc/index.adoc b/backend/emm-sale/src/docs/asciidoc/index.adoc index 23bdef692..e850797cb 100644 --- a/backend/emm-sale/src/docs/asciidoc/index.adoc +++ b/backend/emm-sale/src/docs/asciidoc/index.adoc @@ -495,3 +495,30 @@ include::{snippets}/find-tags/http-response.adoc[] .HTTP response 설명 include::{snippets}/find-tags/response-fields.adoc[] + +== Report + +=== `POST` : 특정 사용자의 게시물 신고 + +.HTTP request +include::{snippets}/add-report/http-request.adoc[] + +.HTTP request 설명 +include::{snippets}/add-report/request-fields.adoc[] + +.HTTP response +include::{snippets}/add-report/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/add-report/response-fields.adoc[] + +=== `GET` : 신고 목록 전체 조회 + +.HTTP request +include::{snippets}/find-reports/http-request.adoc[] + +.HTTP response +include::{snippets}/find-reports/http-response.adoc[] + +.HTTP response 설명 +include::{snippets}/find-reports/response-fields.adoc[] diff --git a/backend/emm-sale/src/main/java/com/emmsale/comment/domain/Comment.java b/backend/emm-sale/src/main/java/com/emmsale/comment/domain/Comment.java index 2265b56a5..a93e61bdd 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/comment/domain/Comment.java +++ b/backend/emm-sale/src/main/java/com/emmsale/comment/domain/Comment.java @@ -115,6 +115,10 @@ public Long getParentIdOrSelfId() { return parent.id; } + public boolean isNotOwner(final Long memberId) { + return member.isNotMe(memberId); + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/backend/emm-sale/src/main/java/com/emmsale/event/domain/RecruitmentPost.java b/backend/emm-sale/src/main/java/com/emmsale/event/domain/RecruitmentPost.java index c50b9624b..401ed8eaf 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/event/domain/RecruitmentPost.java +++ b/backend/emm-sale/src/main/java/com/emmsale/event/domain/RecruitmentPost.java @@ -49,6 +49,10 @@ public boolean isSameMember(final Member member) { return this.member.isMe(member); } + public boolean isNotOwner(final Long memberId) { + return member.isNotMe(memberId); + } + public void updateContent(final String content) { this.content = content; } diff --git a/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java b/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java index 70d34ac70..72fffccd9 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java +++ b/backend/emm-sale/src/main/java/com/emmsale/member/domain/Member.java @@ -79,13 +79,18 @@ private void validateDescriptionLength(final String description) { } } + public boolean isMe(final Member member) { + return isMe(member.getId()); + } + + public boolean isMe(final Long id) { + return this.id.equals((id)); + } + public boolean isNotMe(final Member member) { return isNotMe(member.getId()); } - public boolean isMe(final Member member) { - return this.id.equals(member.id); - } public boolean isNotMe(final Long id) { return !this.id.equals(id); diff --git a/backend/emm-sale/src/main/java/com/emmsale/notification/domain/RequestNotification.java b/backend/emm-sale/src/main/java/com/emmsale/notification/domain/RequestNotification.java index 66299f743..0938ad07e 100644 --- a/backend/emm-sale/src/main/java/com/emmsale/notification/domain/RequestNotification.java +++ b/backend/emm-sale/src/main/java/com/emmsale/notification/domain/RequestNotification.java @@ -54,6 +54,10 @@ public boolean isNotOwner(final Long memberId) { return !receiverId.equals(memberId); } + public boolean isNotSender(final Long memberId) { + return !senderId.equals(memberId); + } + public void modifyStatus(final RequestNotificationStatus status) { this.status = status; } diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/api/ReportApi.java b/backend/emm-sale/src/main/java/com/emmsale/report/api/ReportApi.java new file mode 100644 index 000000000..3779984ca --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/api/ReportApi.java @@ -0,0 +1,36 @@ +package com.emmsale.report.api; + +import com.emmsale.member.domain.Member; +import com.emmsale.report.application.ReportCommandService; +import com.emmsale.report.application.ReportQueryService; +import com.emmsale.report.application.dto.ReportCreateRequest; +import com.emmsale.report.application.dto.ReportCreateResponse; +import com.emmsale.report.application.dto.ReportFindResponse; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +public class ReportApi { + + private final ReportCommandService reportCommandService; + private final ReportQueryService reportQueryService; + + @PostMapping("/reports") + @ResponseStatus(HttpStatus.CREATED) + public ReportCreateResponse create(@RequestBody final ReportCreateRequest reportRequest, + final Member member) { + return reportCommandService.create(reportRequest, member); + } + + @GetMapping("/reports") + public List findReports() { + return reportQueryService.findReports(); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/application/ReportCommandService.java b/backend/emm-sale/src/main/java/com/emmsale/report/application/ReportCommandService.java new file mode 100644 index 000000000..e5906546d --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/application/ReportCommandService.java @@ -0,0 +1,124 @@ +package com.emmsale.report.application; + +import com.emmsale.block.domain.Block; +import com.emmsale.block.domain.BlockRepository; +import com.emmsale.comment.domain.Comment; +import com.emmsale.comment.domain.CommentRepository; +import com.emmsale.event.domain.RecruitmentPost; +import com.emmsale.event.domain.repository.RecruitmentPostRepository; +import com.emmsale.member.domain.Member; +import com.emmsale.member.domain.MemberRepository; +import com.emmsale.notification.domain.RequestNotification; +import com.emmsale.notification.domain.RequestNotificationRepository; +import com.emmsale.report.application.dto.ReportCreateRequest; +import com.emmsale.report.application.dto.ReportCreateResponse; +import com.emmsale.report.domain.Report; +import com.emmsale.report.domain.ReportType; +import com.emmsale.report.domain.repository.ReportRepository; +import com.emmsale.report.exception.ReportException; +import com.emmsale.report.exception.ReportExceptionType; +import javax.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@Transactional +@RequiredArgsConstructor +public class ReportCommandService { + + private final ReportRepository reportRepository; + private final MemberRepository memberRepository; + private final CommentRepository commentRepository; + private final RecruitmentPostRepository recruitmentPostRepository; + private final RequestNotificationRepository requestNotificationRepository; + private final BlockRepository blockRepository; + + public ReportCreateResponse create(final ReportCreateRequest reportRequest, final Member member) { + validateReportRequest(reportRequest, member); + final Report report = reportRequest.toReport(); + blockReportedMember(reportRequest); + return ReportCreateResponse.from(reportRepository.save(report)); + } + + + private void validateReportRequest(final ReportCreateRequest reportRequest, final Member member) { + validateReporterMismatch(reportRequest, member); + validateReportMySelf(reportRequest, member); + validateNotFoundReportedMember(reportRequest); + validateAlreadyExistReport(reportRequest); + validateContent(reportRequest); + } + + private void validateReporterMismatch(final ReportCreateRequest reportRequest, + final Member member) { + if (member.isNotMe(reportRequest.getReporterId())) { + throw new ReportException(ReportExceptionType.REPORTER_MISMATCH); + } + } + + private void validateReportMySelf(final ReportCreateRequest reportRequest, final Member member) { + if (member.isMe(reportRequest.getReportedId())) { + throw new ReportException(ReportExceptionType.FORBIDDEN_REPORT_MYSELF); + } + } + + private void validateNotFoundReportedMember(final ReportCreateRequest reportRequest) { + if (!memberRepository.existsById(reportRequest.getReportedId())) { + throw new ReportException(ReportExceptionType.NOT_FOUND_MEMBER); + } + } + + private void validateAlreadyExistReport(final ReportCreateRequest reportRequest) { + if (reportRepository.existsReportByReporterIdAndReportedId( + reportRequest.getReporterId(), reportRequest.getReportedId())) { + throw new ReportException(ReportExceptionType.ALREADY_EXIST_REPORT); + } + } + + private void validateContent(final ReportCreateRequest reportRequest) { + if (reportRequest.getType() == ReportType.COMMENT) { + validateComment(reportRequest); + } + if (reportRequest.getType() == ReportType.RECRUITMENT_POST) { + validateRecruitmentPost(reportRequest); + } + if (reportRequest.getType() == ReportType.REQUEST_NOTIFICATION) { + validateRequestNotification(reportRequest); + } + } + + private void validateComment(final ReportCreateRequest reportRequest) { + Comment comment = commentRepository.findById(reportRequest.getContentId()) + .orElseThrow(() -> new ReportException(ReportExceptionType.NOT_FOUND_CONTENT)); + if (comment.isNotOwner(reportRequest.getReportedId())) { + throw new ReportException(ReportExceptionType.REPORTED_MISMATCH_WRITER); + } + } + + private void validateRecruitmentPost(final ReportCreateRequest reportRequest) { + RecruitmentPost recruitmentPost = recruitmentPostRepository.findById( + reportRequest.getContentId()) + .orElseThrow(() -> new ReportException(ReportExceptionType.NOT_FOUND_CONTENT)); + if (recruitmentPost.isNotOwner(reportRequest.getReportedId())) { + throw new ReportException(ReportExceptionType.REPORTED_MISMATCH_WRITER); + } + } + + private void validateRequestNotification(final ReportCreateRequest reportCreateRequest) { + RequestNotification requestNotification = requestNotificationRepository.findById( + reportCreateRequest.getContentId()) + .orElseThrow(() -> new ReportException(ReportExceptionType.NOT_FOUND_CONTENT)); + if (requestNotification.isNotSender(reportCreateRequest.getReportedId())) { + throw new ReportException(ReportExceptionType.REPORTED_MISMATCH_WRITER); + } + } + + private void blockReportedMember(final ReportCreateRequest reportCreateRequest) { + final Long reporterId = reportCreateRequest.getReporterId(); + final Long reportedId = reportCreateRequest.getReportedId(); + if (!blockRepository.existsByRequestMemberIdAndBlockMemberId(reporterId, reportedId)) { + final Block block = new Block(reporterId, reportedId); + blockRepository.save(block); + } + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/application/ReportQueryService.java b/backend/emm-sale/src/main/java/com/emmsale/report/application/ReportQueryService.java new file mode 100644 index 000000000..6ca29076a --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/application/ReportQueryService.java @@ -0,0 +1,20 @@ +package com.emmsale.report.application; + +import com.emmsale.report.application.dto.ReportFindResponse; +import com.emmsale.report.domain.repository.ReportRepository; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ReportQueryService { + + private final ReportRepository reportRepository; + + public List findReports() { + return ReportFindResponse.from(reportRepository.findAll()); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportCreateRequest.java b/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportCreateRequest.java new file mode 100644 index 000000000..7d44735fe --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportCreateRequest.java @@ -0,0 +1,20 @@ +package com.emmsale.report.application.dto; + +import com.emmsale.report.domain.Report; +import com.emmsale.report.domain.ReportType; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class ReportCreateRequest { + + private final Long reporterId; + private final Long reportedId; + private final ReportType type; + private final Long contentId; + + public Report toReport() { + return new Report(reporterId, reportedId, type, contentId); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportCreateResponse.java b/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportCreateResponse.java new file mode 100644 index 000000000..f2c47ee6c --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportCreateResponse.java @@ -0,0 +1,27 @@ +package com.emmsale.report.application.dto; + +import com.emmsale.report.domain.Report; +import com.emmsale.report.domain.ReportType; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class ReportCreateResponse { + + private final Long id; + private final Long reporterId; + private final Long reportedId; + private final ReportType type; + private final Long contentId; + @JsonFormat(pattern = "yyyy:MM:dd:HH:mm:ss") + private final LocalDateTime createdAt; + + public static ReportCreateResponse from(final Report report) { + return new ReportCreateResponse(report.getId(), report.getReporterId(), + report.getReportedId(), report.getType(), report.getContentId(), + report.getCreatedAt()); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportFindResponse.java b/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportFindResponse.java new file mode 100644 index 000000000..bda5d8b0f --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/application/dto/ReportFindResponse.java @@ -0,0 +1,35 @@ +package com.emmsale.report.application.dto; + +import com.emmsale.report.domain.Report; +import com.emmsale.report.domain.ReportType; +import com.fasterxml.jackson.annotation.JsonFormat; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Getter +public class ReportFindResponse { + + private final Long id; + private final Long reporterId; + private final Long reportedId; + private final ReportType type; + private final Long contentId; + @JsonFormat(pattern = "yyyy:MM:dd:HH:mm:ss") + private final LocalDateTime createdAt; + + public static List from(final List reports) { + return reports.stream() + .map(ReportFindResponse::from) + .collect(Collectors.toList()); + } + + private static ReportFindResponse from(final Report report) { + return new ReportFindResponse(report.getId(), report.getReporterId(), + report.getReportedId(), report.getType(), report.getContentId(), + report.getCreatedAt()); + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/domain/Report.java b/backend/emm-sale/src/main/java/com/emmsale/report/domain/Report.java new file mode 100644 index 000000000..43b92854e --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/domain/Report.java @@ -0,0 +1,40 @@ +package com.emmsale.report.domain; + +import com.emmsale.base.BaseEntity; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.EnumType; +import javax.persistence.Enumerated; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class Report extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private Long reporterId; + @Column(nullable = false) + private Long reportedId; + @Column(nullable = false) + @Enumerated(EnumType.STRING) + private ReportType type; + @Column(nullable = false) + private Long contentId; + + public Report(final Long reporterId, final Long reportedId, final ReportType type, + final Long contentId) { + this.reporterId = reporterId; + this.reportedId = reportedId; + this.type = type; + this.contentId = contentId; + } +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/domain/ReportType.java b/backend/emm-sale/src/main/java/com/emmsale/report/domain/ReportType.java new file mode 100644 index 000000000..cc3ff509e --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/domain/ReportType.java @@ -0,0 +1,7 @@ +package com.emmsale.report.domain; + +public enum ReportType { + COMMENT, + RECRUITMENT_POST, + REQUEST_NOTIFICATION; +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/domain/repository/ReportRepository.java b/backend/emm-sale/src/main/java/com/emmsale/report/domain/repository/ReportRepository.java new file mode 100644 index 000000000..9dd13c30f --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/domain/repository/ReportRepository.java @@ -0,0 +1,10 @@ +package com.emmsale.report.domain.repository; + +import com.emmsale.report.domain.Report; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ReportRepository extends JpaRepository { + + boolean existsReportByReporterIdAndReportedId(final Long reporterId, final Long reportedId); + +} diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/exception/ReportException.java b/backend/emm-sale/src/main/java/com/emmsale/report/exception/ReportException.java new file mode 100644 index 000000000..2fa12aa33 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/exception/ReportException.java @@ -0,0 +1,20 @@ +package com.emmsale.report.exception; + +import com.emmsale.base.BaseException; +import com.emmsale.base.BaseExceptionType; + +public class ReportException extends BaseException { + + private final ReportExceptionType exceptionType; + + public ReportException(final ReportExceptionType exceptionType) { + super(exceptionType.errorMessage()); + this.exceptionType = exceptionType; + } + + @Override + public BaseExceptionType exceptionType() { + return exceptionType; + } +} + diff --git a/backend/emm-sale/src/main/java/com/emmsale/report/exception/ReportExceptionType.java b/backend/emm-sale/src/main/java/com/emmsale/report/exception/ReportExceptionType.java new file mode 100644 index 000000000..dc54d8142 --- /dev/null +++ b/backend/emm-sale/src/main/java/com/emmsale/report/exception/ReportExceptionType.java @@ -0,0 +1,50 @@ +package com.emmsale.report.exception; + +import com.emmsale.base.BaseExceptionType; +import org.springframework.http.HttpStatus; + +public enum ReportExceptionType implements BaseExceptionType { + + NOT_FOUND_MEMBER( + HttpStatus.NOT_FOUND, + "존재하지 않는 사용자입니다." + ), + NOT_FOUND_CONTENT( + HttpStatus.NOT_FOUND, + "존재하지 않는 게시물입니다." + ), + REPORTED_MISMATCH_WRITER( + HttpStatus.BAD_REQUEST, + "신고한 게시물이 신고 대상자의 게시물이 아닙니다." + ), + FORBIDDEN_REPORT_MYSELF( + HttpStatus.BAD_REQUEST, + "자기 자신은 신고할 수 없습니다." + ), + REPORTER_MISMATCH( + HttpStatus.FORBIDDEN, + "신고 권한이 없습니다." + ), + ALREADY_EXIST_REPORT( + HttpStatus.BAD_REQUEST, + "이미 신고한 사용자입니다." + ); + + private final HttpStatus httpStatus; + private final String errorMessage; + + ReportExceptionType(final HttpStatus httpStatus, final String errorMessage) { + this.httpStatus = httpStatus; + this.errorMessage = errorMessage; + } + + @Override + public HttpStatus httpStatus() { + return httpStatus; + } + + @Override + public String errorMessage() { + return errorMessage; + } +} diff --git a/backend/emm-sale/src/main/resources/data.sql b/backend/emm-sale/src/main/resources/data.sql index 0705a2a30..e166ba759 100644 --- a/backend/emm-sale/src/main/resources/data.sql +++ b/backend/emm-sale/src/main/resources/data.sql @@ -11,6 +11,7 @@ truncate table request_notification; truncate table fcm_token; truncate table update_notification; truncate table block; +truncate table report; insert into activity(id, type, name) values (1, 'CLUB', 'YAPP'); diff --git a/backend/emm-sale/src/main/resources/http/report.http b/backend/emm-sale/src/main/resources/http/report.http new file mode 100644 index 000000000..a5306ee7c --- /dev/null +++ b/backend/emm-sale/src/main/resources/http/report.http @@ -0,0 +1,19 @@ +### 게시물 신고 + +POST http://localhost:8080/reports +Content-Type: application/json +Authorization: bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNjkwMTk2OTY5LCJleHAiOjE2OTM3OTY5Njl9.yahaEBvKBA7xelNuykx8TROhemnzJAsu1Sv5rrSfCM0 + +{ + "reporterId": 1, + "reportedId": 2, + "type": "COMMENT", + "contentId": 1 +} + +### 게시물 신고 목록 조회 + +GET http://localhost:8080/reports +Content-Type: application/json + + diff --git a/backend/emm-sale/src/main/resources/schema.sql b/backend/emm-sale/src/main/resources/schema.sql index 968bb3039..9fce9efe6 100644 --- a/backend/emm-sale/src/main/resources/schema.sql +++ b/backend/emm-sale/src/main/resources/schema.sql @@ -11,6 +11,7 @@ drop table if exists kerdy.request_notification; drop table if exists kerdy.fcm_token; drop table if exists kerdy.block; drop table if exists kerdy.update_notification; +drop table if exists kerdy.report; create table activity ( @@ -158,3 +159,15 @@ alter table update_notification add column is_read bit not null; + +-- 2023-08-14 13:10 +create table report +( + id bigint auto_increment primary key, + reporter_id bigint not null, + reported_id bigint not null, + type varchar(20) not null, + content_id bigint not null, + created_at datetime(6), + updated_at datetime(6) +); \ No newline at end of file diff --git a/backend/emm-sale/src/test/java/com/emmsale/report/api/ReportApiTest.java b/backend/emm-sale/src/test/java/com/emmsale/report/api/ReportApiTest.java new file mode 100644 index 000000000..51641f82e --- /dev/null +++ b/backend/emm-sale/src/test/java/com/emmsale/report/api/ReportApiTest.java @@ -0,0 +1,119 @@ +package com.emmsale.report.api; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +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.result.MockMvcResultMatchers.status; + +import com.emmsale.helper.MockMvcTestHelper; +import com.emmsale.report.application.ReportCommandService; +import com.emmsale.report.application.ReportQueryService; +import com.emmsale.report.application.dto.ReportCreateRequest; +import com.emmsale.report.application.dto.ReportCreateResponse; +import com.emmsale.report.application.dto.ReportFindResponse; +import com.emmsale.report.domain.ReportType; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +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.restdocs.payload.JsonFieldType; +import org.springframework.restdocs.payload.RequestFieldsSnippet; +import org.springframework.restdocs.payload.ResponseFieldsSnippet; + +@WebMvcTest(ReportApi.class) +class ReportApiTest extends MockMvcTestHelper { + + @MockBean + private ReportCommandService reportCommandService; + @MockBean + private ReportQueryService reportQueryService; + + @Test + @DisplayName("특정 게시글을 신고할 수 있다.") + void addReport() throws Exception { + // given + final String accessToken = "Bearer access_token"; + final RequestFieldsSnippet requestFields = requestFields( + fieldWithPath("reporterId").type(JsonFieldType.NUMBER).description("신고자의 Id"), + fieldWithPath("reportedId").type(JsonFieldType.NUMBER).description("신고 대상자의 Id(당하는 사람)"), + fieldWithPath("type").type(JsonFieldType.STRING).description( + "신고 게시글의 유형(COMMENT, PARTICIPANT, REQUEST_NOTIFICATION)"), + fieldWithPath("contentId").type(JsonFieldType.NUMBER).description("신고 게시물의 Id") + ); + + final ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER).description("신고 id"), + fieldWithPath("reporterId").type(JsonFieldType.NUMBER).description("신고자의 Id"), + fieldWithPath("reportedId").type(JsonFieldType.NUMBER).description("신고 대상자의 Id)"), + fieldWithPath("type").type(JsonFieldType.STRING) + .description("신고 게시글의 유형(COMMENT, PARTICIPANT, REQUEST_NOTIFICATION)"), + fieldWithPath("contentId").type(JsonFieldType.NUMBER).description("신고 게시물의 Id"), + fieldWithPath("createdAt").type(JsonFieldType.STRING) + .description("신고 일자(yyyy:MM:dd:HH:mm:ss)") + ); + final ReportCreateRequest reportRequest = new ReportCreateRequest(1L, 2L, ReportType.COMMENT, + 1L); + + final ReportCreateResponse reportCreateResponse = new ReportCreateResponse(1L, + reportRequest.getReporterId(), + reportRequest.getReportedId(), reportRequest.getType(), reportRequest.getContentId(), + LocalDateTime.parse("2023-08-09T13:25:00")); + + when(reportCommandService.create(any(), any())).thenReturn(reportCreateResponse); + + // when & then + mockMvc.perform(post("/reports") + .header("Authorization", accessToken) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .content(objectMapper.writeValueAsString(reportRequest))) + .andExpect(status().isCreated()) + .andDo(document("add-report", requestFields, responseFields)); + + + } + + @Test + @DisplayName("신고 목록을 조회할 수 있다.") + void findReports() throws Exception { + // given + final ResponseFieldsSnippet responseFields = responseFields( + fieldWithPath("[].id").type(JsonFieldType.NUMBER).description("신고 id"), + fieldWithPath("[].reporterId").type(JsonFieldType.NUMBER).description("신고자의 Id"), + fieldWithPath("[].reportedId").type(JsonFieldType.NUMBER).description("신고 대상자의 Id)"), + fieldWithPath("[].type").type(JsonFieldType.STRING) + .description("신고 게시글의 유형(COMMENT, PARTICIPANT, REQUEST_NOTIFICATION)"), + fieldWithPath("[].contentId").type(JsonFieldType.NUMBER).description("신고 게시물의 Id)"), + fieldWithPath("[].createdAt").type(JsonFieldType.STRING) + .description("신고 일자(yyyy:MM:dd:HH:mm:ss)") + ); + + final List reportFindResponse = List.of( + new ReportFindResponse(1L, 1L, 2L, ReportType.COMMENT, 3L, + LocalDateTime.parse("2023-08-09T13:25:00")), + new ReportFindResponse(2L, 2L, 1L, ReportType.RECRUITMENT_POST, 1L, + LocalDateTime.parse("2023-08-11T13:25:00")), + new ReportFindResponse(3L, 1L, 3L, ReportType.REQUEST_NOTIFICATION, 5L, + LocalDateTime.parse("2023-08-11T13:50:00")), + new ReportFindResponse(4L, 4L, 1L, ReportType.COMMENT, 2L, + LocalDateTime.parse("2023-08-12T13:25:00")) + + ); + + when(reportQueryService.findReports()).thenReturn(reportFindResponse); + + // when & then + mockMvc.perform(get("/reports")) + .andExpect(status().isOk()) + .andDo(document("find-reports", responseFields)); + + + } +} \ No newline at end of file diff --git a/backend/emm-sale/src/test/java/com/emmsale/report/application/ReportCommandServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/report/application/ReportCommandServiceTest.java new file mode 100644 index 000000000..ec940c568 --- /dev/null +++ b/backend/emm-sale/src/test/java/com/emmsale/report/application/ReportCommandServiceTest.java @@ -0,0 +1,278 @@ +package com.emmsale.report.application; + +import static com.emmsale.event.EventFixture.eventFixture; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.emmsale.block.domain.BlockRepository; +import com.emmsale.comment.domain.Comment; +import com.emmsale.comment.domain.CommentRepository; +import com.emmsale.event.domain.Event; +import com.emmsale.event.domain.RecruitmentPost; +import com.emmsale.event.domain.repository.EventRepository; +import com.emmsale.event.domain.repository.RecruitmentPostRepository; +import com.emmsale.helper.ServiceIntegrationTestHelper; +import com.emmsale.member.domain.Member; +import com.emmsale.member.domain.MemberRepository; +import com.emmsale.notification.domain.RequestNotification; +import com.emmsale.notification.domain.RequestNotificationRepository; +import com.emmsale.report.application.dto.ReportCreateRequest; +import com.emmsale.report.application.dto.ReportCreateResponse; +import com.emmsale.report.domain.ReportType; +import com.emmsale.report.exception.ReportException; +import com.emmsale.report.exception.ReportExceptionType; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class ReportCommandServiceTest extends ServiceIntegrationTestHelper { + + private static Long 신고자_ID; + private static Long 신고_대상자_ID; + + @Autowired + private ReportCommandService reportCommandService; + @Autowired + private MemberRepository memberRepository; + @Autowired + private EventRepository eventRepository; + @Autowired + private BlockRepository blockRepository; + @Autowired + private CommentRepository commentRepository; + @Autowired + private RecruitmentPostRepository recruitmentPostRepository; + @Autowired + private RequestNotificationRepository requestNotificationRepository; + + @BeforeEach + void init() { + final Event event = eventRepository.save(eventFixture()); + final Member 신고자 = memberRepository.findById(1L).get(); + final Member 신고_대상자 = memberRepository.findById(2L).get(); + 신고자_ID = 신고자.getId(); + 신고_대상자_ID = 신고_대상자.getId(); + commentRepository.save(Comment.createRoot(event, 신고_대상자, "상대방에게 불쾌감을 줄 수 있는 내용")); + commentRepository.save(Comment.createRoot(event, 신고자, "그냥 댓글")); + recruitmentPostRepository.save(new RecruitmentPost(신고_대상자, event, "사회적 논란을 불러일으킬 수 있는 내용")); + requestNotificationRepository.save( + new RequestNotification(신고_대상자_ID, 신고자_ID, event.getId(), "모욕감을 줄 수 있는 내용")); + } + + @Nested + @DisplayName("특정 게시글을 신고할 수 있다.") + class Create { + + @Test + @DisplayName("신고 대상자가 존재하지 않을 경우 예외를 반환한다.") + void create_fail_not_found_member() { + // given + final Long wrongReportedId = 9999L; + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, wrongReportedId, + ReportType.COMMENT, + abusingContentId); + + // when + final ThrowingCallable actual = () -> reportCommandService.create(request, reporter); + + // then + assertThatThrownBy(actual) + .isInstanceOf(ReportException.class) + .hasMessage(ReportExceptionType.NOT_FOUND_MEMBER.errorMessage()); + } + + @Test + @DisplayName("자신을 신고할 경우 예외를 반환한다.") + void create_fail_report_self() { + // given + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고자_ID, + ReportType.COMMENT, + abusingContentId); + + // when + final ThrowingCallable actual = () -> reportCommandService.create(request, reporter); + + // then + assertThatThrownBy(actual) + .isInstanceOf(ReportException.class) + .hasMessage(ReportExceptionType.FORBIDDEN_REPORT_MYSELF.errorMessage()); + } + + @Test + @DisplayName("신고자가 자신이 아닐 경우 예외를 반환한다.") + void create_fail_forbidden_report() { + // given + final Long otherMemberId = 2L; + final Long reportedId = 1L; + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(otherMemberId, reportedId, + ReportType.COMMENT, + abusingContentId); + + // when + final ThrowingCallable actual = () -> reportCommandService.create(request, reporter); + + // then + assertThatThrownBy(actual) + .isInstanceOf(ReportException.class) + .hasMessage(ReportExceptionType.REPORTER_MISMATCH.errorMessage()); + } + + @Test + @DisplayName("이미 신고한 사용자를 한 번 더 신고할 경우 예외를 반환한다.") + void create_fail_already_exist_report() { + // given + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고_대상자_ID, + ReportType.COMMENT, + abusingContentId); + reportCommandService.create(request, reporter); + + // when + final ThrowingCallable actual = () -> reportCommandService.create(request, reporter); + + // then + assertThatThrownBy(actual) + .isInstanceOf(ReportException.class) + .hasMessage(ReportExceptionType.ALREADY_EXIST_REPORT.errorMessage()); + } + + @Test + @DisplayName("존재하지 않는 게시물을 신고할 경우 예외를 반환한다.") + void create_fail_not_found_content() { + // given + final Long abusingContentId = 9999L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고_대상자_ID, + ReportType.COMMENT, + abusingContentId); + + // when + final ThrowingCallable actual = () -> reportCommandService.create(request, reporter); + + // then + assertThatThrownBy(actual) + .isInstanceOf(ReportException.class) + .hasMessage(ReportExceptionType.NOT_FOUND_CONTENT.errorMessage()); + } + + @Test + @DisplayName("신고한 게시물이 신고 대상자의 게시물이 아닌 경우 예외를 반환한다.") + void create_fail_reported_mismatch_writer() { + // given + final Long abusingContentId = 2L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고_대상자_ID, + ReportType.COMMENT, + abusingContentId); + + // when + final ThrowingCallable actual = () -> reportCommandService.create(request, reporter); + + // then + assertThatThrownBy(actual) + .isInstanceOf(ReportException.class) + .hasMessage(ReportExceptionType.REPORTED_MISMATCH_WRITER.errorMessage()); + } + + @Nested + @DisplayName("신고자와 신고 대상자, 신고 유형 등을 입력하면 특정 게시물을 정상적으로 신고할 수 있고, 게시물 작성자가 차단된다.") + class CreateSuccess { + + @Test + @DisplayName("댓글을 신고할 수 있다.") + void create_success_comment() { + // given + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고_대상자_ID, + ReportType.COMMENT, + abusingContentId); + final ReportCreateResponse expected = new ReportCreateResponse(1L, 신고자_ID, 신고_대상자_ID, + ReportType.COMMENT, abusingContentId, null); + + // when + final ReportCreateResponse actual = reportCommandService.create(request, reporter); + final boolean isBlocked = blockRepository.existsByRequestMemberIdAndBlockMemberId(신고자_ID, + 신고_대상자_ID); + + // then + Assertions.assertAll( + () -> assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expected), + () -> assertThat(isBlocked).isTrue() + ); + + } + + @Test + @DisplayName("함께해요 게시글을 신고할 수 있다.") + void create_success_participant() { + // given + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고_대상자_ID, + ReportType.RECRUITMENT_POST, + abusingContentId); + final ReportCreateResponse expected = new ReportCreateResponse(1L, 신고자_ID, 신고_대상자_ID, + ReportType.RECRUITMENT_POST, abusingContentId, null); + + // when + final ReportCreateResponse actual = reportCommandService.create(request, reporter); + final boolean isBlocked = blockRepository.existsByRequestMemberIdAndBlockMemberId(신고자_ID, + 신고_대상자_ID); + + // then + Assertions.assertAll( + () -> assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expected), + () -> assertThat(isBlocked).isTrue() + ); + } + + @Test + @DisplayName("같이가요 요청을 신고할 수 있다.") + void create_success_request_notification() { + // given + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고_대상자_ID, + ReportType.REQUEST_NOTIFICATION, + abusingContentId); + final ReportCreateResponse expected = new ReportCreateResponse(1L, 신고자_ID, 신고_대상자_ID, + ReportType.REQUEST_NOTIFICATION, abusingContentId, null); + + // when + final ReportCreateResponse actual = reportCommandService.create(request, reporter); + final boolean isBlocked = blockRepository.existsByRequestMemberIdAndBlockMemberId(신고자_ID, + 신고_대상자_ID); + + // then + Assertions.assertAll( + () -> assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("id", "createdAt") + .isEqualTo(expected), + () -> assertThat(isBlocked).isTrue() + ); + } + + } + + } + +} \ No newline at end of file diff --git a/backend/emm-sale/src/test/java/com/emmsale/report/application/ReportQueryServiceTest.java b/backend/emm-sale/src/test/java/com/emmsale/report/application/ReportQueryServiceTest.java new file mode 100644 index 000000000..769913c89 --- /dev/null +++ b/backend/emm-sale/src/test/java/com/emmsale/report/application/ReportQueryServiceTest.java @@ -0,0 +1,74 @@ +package com.emmsale.report.application; + + +import static com.emmsale.event.EventFixture.eventFixture; + +import com.emmsale.comment.domain.Comment; +import com.emmsale.comment.domain.CommentRepository; +import com.emmsale.event.domain.Event; +import com.emmsale.event.domain.repository.EventRepository; +import com.emmsale.helper.ServiceIntegrationTestHelper; +import com.emmsale.member.domain.Member; +import com.emmsale.member.domain.MemberRepository; +import com.emmsale.report.application.dto.ReportCreateRequest; +import com.emmsale.report.application.dto.ReportCreateResponse; +import com.emmsale.report.application.dto.ReportFindResponse; +import com.emmsale.report.domain.ReportType; +import java.util.List; +import org.assertj.core.api.Assertions; +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; + +class ReportQueryServiceTest extends ServiceIntegrationTestHelper { + + private static Long 신고자_ID; + private static Long 신고_대상자_ID; + @Autowired + private ReportQueryService reportQueryService; + @Autowired + private ReportCommandService reportCommandService; + @Autowired + private MemberRepository memberRepository; + @Autowired + private EventRepository eventRepository; + @Autowired + private CommentRepository commentRepository; + + @BeforeEach + void init() { + final Event event = eventRepository.save(eventFixture()); + final Member 신고자 = memberRepository.findById(1L).get(); + final Member 신고_대상자 = memberRepository.findById(2L).get(); + 신고자_ID = 신고자.getId(); + 신고_대상자_ID = 신고_대상자.getId(); + commentRepository.save(Comment.createRoot(event, 신고_대상자, "상대방에게 불쾌감을 줄 수 있는 내용")); + commentRepository.save(Comment.createRoot(event, 신고자, "그냥 댓글")); + } + + @Test + @DisplayName("모든 신고 목록을 조회할 수 있다.") + void findReports() { + // given + final Long abusingContentId = 1L; + final Member reporter = memberRepository.findById(신고자_ID).get(); + final ReportCreateRequest request = new ReportCreateRequest(신고자_ID, 신고_대상자_ID, + ReportType.COMMENT, + abusingContentId); + final ReportCreateResponse report = reportCommandService.create(request, reporter); + final List expected = List.of( + new ReportFindResponse(report.getId(), report.getReporterId(), report.getReportedId(), + report.getType(), report.getContentId(), report.getCreatedAt())); + + // when + List actual = reportQueryService.findReports(); + + // then + Assertions.assertThat(actual) + .usingRecursiveComparison() + .ignoringFields("createdAt") + .isEqualTo(expected); + + } +} \ No newline at end of file diff --git a/backend/emm-sale/src/test/resources/data-test.sql b/backend/emm-sale/src/test/resources/data-test.sql index 69ba49799..ea2c57165 100644 --- a/backend/emm-sale/src/test/resources/data-test.sql +++ b/backend/emm-sale/src/test/resources/data-test.sql @@ -11,6 +11,7 @@ truncate table request_notification; truncate table update_notification; truncate table block; truncate table fcm_token; +truncate table report; insert into activity(id, type, name) values (1, 'CLUB', 'YAPP');