From 709dcbc93dc81864b931f2ffb694d754072728b4 Mon Sep 17 00:00:00 2001 From: LeeJaeHoon Date: Thu, 22 Aug 2024 10:24:03 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=9E=AC=EC=84=A4=EC=A0=95=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/dnd/snappy/common/logger/MDCFilter.java | 13 +++++-------- .../snappy/common/logger/RequestResponseLogger.java | 2 +- src/main/resources/application.yml | 2 +- .../resources/logger/console/console-appender.xml | 2 +- src/main/resources/logger/file/error-appender.xml | 2 +- src/main/resources/logger/file/info-appender.xml | 2 +- src/main/resources/logger/file/warn-appender.xml | 2 +- src/main/resources/logger/logback-spring-dev.xml | 2 +- 8 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/dnd/snappy/common/logger/MDCFilter.java b/src/main/java/com/dnd/snappy/common/logger/MDCFilter.java index 0bd6183..6322735 100644 --- a/src/main/java/com/dnd/snappy/common/logger/MDCFilter.java +++ b/src/main/java/com/dnd/snappy/common/logger/MDCFilter.java @@ -14,20 +14,17 @@ @Slf4j @Component public class MDCFilter implements Filter { - private static final String MDC_CORRELATION_ID_KEY = "correlationId"; + private final String REQUEST_ID = "request_id"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - try { - MDC.put(MDC_CORRELATION_ID_KEY, generateCorrelationId()); - chain.doFilter(request, response); - } finally { - MDC.clear(); - } + MDC.put(REQUEST_ID, generateRequestId()); + chain.doFilter(request, response); + MDC.clear(); } - private String generateCorrelationId() { + private String generateRequestId() { return UUID.randomUUID() .toString() .substring(0, 8); diff --git a/src/main/java/com/dnd/snappy/common/logger/RequestResponseLogger.java b/src/main/java/com/dnd/snappy/common/logger/RequestResponseLogger.java index 48c69c8..564eb33 100644 --- a/src/main/java/com/dnd/snappy/common/logger/RequestResponseLogger.java +++ b/src/main/java/com/dnd/snappy/common/logger/RequestResponseLogger.java @@ -44,6 +44,6 @@ private String getHeaderAndValue(ContentCachingRequestWrapper requestWrapper) { @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); - return path.contains("/api/docs"); + return path.contains("/docs") || path.contains("/favicon.ico"); } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index a0b9a8d..4460cae 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,7 @@ server: port: 8080 spring: profiles: - active: local + active: dev include: secret jwt: secretKey: '${custom.jwt.secretKey}' diff --git a/src/main/resources/logger/console/console-appender.xml b/src/main/resources/logger/console/console-appender.xml index e907023..7c446dc 100644 --- a/src/main/resources/logger/console/console-appender.xml +++ b/src/main/resources/logger/console/console-appender.xml @@ -3,7 +3,7 @@ - [%highlight(%level)][%d{MM-dd HH:mm:ss}][%magenta(%8X{correlationId})][%highlight(%replace(%logger{20}){'\\w\\.', ''})] %msg %n + [%highlight(%level)][%d{MM-dd HH:mm:ss}][%magenta(%X{request_id:-startup})][%highlight(%replace(%logger{20}){'\\w\\.', ''})] %msg %n \ No newline at end of file diff --git a/src/main/resources/logger/file/error-appender.xml b/src/main/resources/logger/file/error-appender.xml index 51a40e8..47820db 100644 --- a/src/main/resources/logger/file/error-appender.xml +++ b/src/main/resources/logger/file/error-appender.xml @@ -12,7 +12,7 @@ - [%boldRed(%level)][%d{MM-dd HH:mm:ss}][%magenta(%8X{correlationId})][%boldRed(%replace(%logger{20}){'\\w\\.', ''}:%line)] %msg %n + [%boldRed(%level)][%d{MM-dd HH:mm:ss}][%magenta(%X{request_id:-startup})][%boldRed(%replace(%logger{20}){'\\w\\.', ''}:%line)] %msg %n diff --git a/src/main/resources/logger/file/info-appender.xml b/src/main/resources/logger/file/info-appender.xml index f88a4d3..a52e751 100644 --- a/src/main/resources/logger/file/info-appender.xml +++ b/src/main/resources/logger/file/info-appender.xml @@ -12,7 +12,7 @@ - [%cyan(%level)][%d{MM-dd HH:mm:ss}][%magenta(%8X{correlationId})][%blue(%replace(%logger{20}){'\\w\\.', ''})] %msg %n + [%cyan(%level)][%d{MM-dd HH:mm:ss}][%magenta(%X{request_id:-startup})][%blue(%replace(%logger{20}){'\\w\\.', ''})] %msg %n diff --git a/src/main/resources/logger/file/warn-appender.xml b/src/main/resources/logger/file/warn-appender.xml index ae28c33..fa524e7 100644 --- a/src/main/resources/logger/file/warn-appender.xml +++ b/src/main/resources/logger/file/warn-appender.xml @@ -12,7 +12,7 @@ - [%red(%level)][%d{MM-dd HH:mm:ss}][%magenta(%8X{correlationId})][%red(%replace(%logger{20}){'\\w\\.', ''}:%line)] %msg %n + [%red(%level)][%d{MM-dd HH:mm:ss}][%magenta(%X{request_id:-startup})][%red(%replace(%logger{20}){'\\w\\.', ''}:%line)] %msg %n diff --git a/src/main/resources/logger/logback-spring-dev.xml b/src/main/resources/logger/logback-spring-dev.xml index 0f51bbb..391b8f4 100644 --- a/src/main/resources/logger/logback-spring-dev.xml +++ b/src/main/resources/logger/logback-spring-dev.xml @@ -4,7 +4,7 @@ - + From 7fd3ecda4c9acd01aafcc43b3cec2c2790de5614 Mon Sep 17 00:00:00 2001 From: LeeJaeHoon Date: Fri, 23 Aug 2024 12:08:02 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20snap=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response/MissionDetailResponseDto.java | 10 +++ .../ParticipantDetailResponseDto.java | 7 ++ .../dto/response/SnapDetailResponseDto.java | 13 ++++ .../snap/entity/MeetingMissionSnap.java | 8 +++ .../domain/snap/entity/RandomMissionSnap.java | 8 +++ .../dnd/snappy/domain/snap/entity/Snap.java | 8 +++ .../domain/snap/exception/SnapErrorCode.java | 1 + .../snap/repository/SnapRepository.java | 8 +++ .../service/SnapDetailResponseDtoFactory.java | 71 +++++++++++++++++++ .../domain/snap/service/SnapService.java | 18 +++++ 10 files changed, 152 insertions(+) create mode 100644 src/main/java/com/dnd/snappy/domain/snap/dto/response/MissionDetailResponseDto.java create mode 100644 src/main/java/com/dnd/snappy/domain/snap/dto/response/ParticipantDetailResponseDto.java create mode 100644 src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java create mode 100644 src/main/java/com/dnd/snappy/domain/snap/service/SnapDetailResponseDtoFactory.java diff --git a/src/main/java/com/dnd/snappy/domain/snap/dto/response/MissionDetailResponseDto.java b/src/main/java/com/dnd/snappy/domain/snap/dto/response/MissionDetailResponseDto.java new file mode 100644 index 0000000..838f282 --- /dev/null +++ b/src/main/java/com/dnd/snappy/domain/snap/dto/response/MissionDetailResponseDto.java @@ -0,0 +1,10 @@ +package com.dnd.snappy.domain.snap.dto.response; + +public record MissionDetailResponseDto( + Long missionId, + String content +) { + public MissionDetailResponseDto(Integer missionId, String content) { + this(Long.valueOf(missionId), content); + } +} diff --git a/src/main/java/com/dnd/snappy/domain/snap/dto/response/ParticipantDetailResponseDto.java b/src/main/java/com/dnd/snappy/domain/snap/dto/response/ParticipantDetailResponseDto.java new file mode 100644 index 0000000..b8b185b --- /dev/null +++ b/src/main/java/com/dnd/snappy/domain/snap/dto/response/ParticipantDetailResponseDto.java @@ -0,0 +1,7 @@ +package com.dnd.snappy.domain.snap.dto.response; + +public record ParticipantDetailResponseDto( + Long participantId, + String nickname +) { +} diff --git a/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java b/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java new file mode 100644 index 0000000..b872115 --- /dev/null +++ b/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java @@ -0,0 +1,13 @@ +package com.dnd.snappy.domain.snap.dto.response; + +import java.time.LocalDateTime; + +public record SnapDetailResponseDto( + Long snapId, + String snapUrl, + LocalDateTime shootDate, + String type, + ParticipantDetailResponseDto photographer, + MissionDetailResponseDto mission +) { +} diff --git a/src/main/java/com/dnd/snappy/domain/snap/entity/MeetingMissionSnap.java b/src/main/java/com/dnd/snappy/domain/snap/entity/MeetingMissionSnap.java index fad8737..4096918 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/entity/MeetingMissionSnap.java +++ b/src/main/java/com/dnd/snappy/domain/snap/entity/MeetingMissionSnap.java @@ -33,4 +33,12 @@ public static MeetingMissionSnap create(String snapUrl, LocalDateTime shootDate, participant.addShootCount(); return new MeetingMissionSnap(snapUrl, shootDate, meeting, participant, mission); } + + public Long getMissionId() { + return mission.getId(); + } + + public String getMissionContent() { + return mission.getContent(); + } } diff --git a/src/main/java/com/dnd/snappy/domain/snap/entity/RandomMissionSnap.java b/src/main/java/com/dnd/snappy/domain/snap/entity/RandomMissionSnap.java index 3fc40c0..19f7d8d 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/entity/RandomMissionSnap.java +++ b/src/main/java/com/dnd/snappy/domain/snap/entity/RandomMissionSnap.java @@ -33,4 +33,12 @@ public static RandomMissionSnap create(String snapUrl, LocalDateTime shootDate, participant.addShootCount(); return new RandomMissionSnap(snapUrl, shootDate, meeting, participant, randomMission); } + + public Integer getMissionId() { + return randomMission.getId(); + } + + public String getMissionContent() { + return randomMission.getContent(); + } } diff --git a/src/main/java/com/dnd/snappy/domain/snap/entity/Snap.java b/src/main/java/com/dnd/snappy/domain/snap/entity/Snap.java index 426e64e..f5377b3 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/entity/Snap.java +++ b/src/main/java/com/dnd/snappy/domain/snap/entity/Snap.java @@ -48,4 +48,12 @@ protected Snap(String snapUrl, LocalDateTime shootDate, Meeting meeting, Partici this.meeting = meeting; this.participant = participant; } + + public Long getPhotographerId() { + return participant.getId(); + } + + public String getPhotographerNickname() { + return participant.getNickname(); + } } diff --git a/src/main/java/com/dnd/snappy/domain/snap/exception/SnapErrorCode.java b/src/main/java/com/dnd/snappy/domain/snap/exception/SnapErrorCode.java index b5f43c3..f8dd7b6 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/exception/SnapErrorCode.java +++ b/src/main/java/com/dnd/snappy/domain/snap/exception/SnapErrorCode.java @@ -9,6 +9,7 @@ @Getter @AllArgsConstructor public enum SnapErrorCode implements ErrorCodeInterface { + NOT_FOUND_SNAP(HttpStatus.NOT_FOUND, "NOT_FOUND_SNAP","snap을 찾을 수 없습니다.") ; private final HttpStatus status; diff --git a/src/main/java/com/dnd/snappy/domain/snap/repository/SnapRepository.java b/src/main/java/com/dnd/snappy/domain/snap/repository/SnapRepository.java index f0e07f3..a768b84 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/repository/SnapRepository.java +++ b/src/main/java/com/dnd/snappy/domain/snap/repository/SnapRepository.java @@ -43,4 +43,12 @@ List findParticipantSnapsInMeetingByCursorId( Pageable pageable ); + @Query(""" + select s + from Snap s + join fetch s.participant + where s.id = :snapId + """) + Optional findSnapByIdWithParticipant(@Param("snapId") Long snapId); + } diff --git a/src/main/java/com/dnd/snappy/domain/snap/service/SnapDetailResponseDtoFactory.java b/src/main/java/com/dnd/snappy/domain/snap/service/SnapDetailResponseDtoFactory.java new file mode 100644 index 0000000..e232b05 --- /dev/null +++ b/src/main/java/com/dnd/snappy/domain/snap/service/SnapDetailResponseDtoFactory.java @@ -0,0 +1,71 @@ +package com.dnd.snappy.domain.snap.service; + + +import com.dnd.snappy.domain.snap.dto.response.MissionDetailResponseDto; +import com.dnd.snappy.domain.snap.dto.response.ParticipantDetailResponseDto; +import com.dnd.snappy.domain.snap.dto.response.SnapDetailResponseDto; +import com.dnd.snappy.domain.snap.entity.MeetingMissionSnap; +import com.dnd.snappy.domain.snap.entity.RandomMissionSnap; +import com.dnd.snappy.domain.snap.entity.Snap; + +public class SnapDetailResponseDtoFactory { + + public static SnapDetailResponseDto createSnapDetailResponseDto(Snap snap) { + if (snap instanceof RandomMissionSnap) { + return createRandomMissionSnapDetail((RandomMissionSnap) snap); + } else if (snap instanceof MeetingMissionSnap) { + return createMeetingMissionSnapDetail((MeetingMissionSnap) snap); + } else { + return createSimpleSnapDetail(snap); + } + } + + private static SnapDetailResponseDto createRandomMissionSnapDetail(RandomMissionSnap snap) { + return new SnapDetailResponseDto( + snap.getId(), + snap.getSnapUrl(), + snap.getShootDate(), + "RANDOM_MISSION", + new ParticipantDetailResponseDto( + snap.getPhotographerId(), + snap.getPhotographerNickname() + ), + new MissionDetailResponseDto( + snap.getMissionId(), + snap.getMissionContent() + ) + ); + } + + private static SnapDetailResponseDto createMeetingMissionSnapDetail(MeetingMissionSnap snap) { + return new SnapDetailResponseDto( + snap.getId(), + snap.getSnapUrl(), + snap.getShootDate(), + "MEETING_MISSION", + new ParticipantDetailResponseDto( + snap.getPhotographerId(), + snap.getPhotographerNickname() + ), + new MissionDetailResponseDto( + snap.getMissionId(), + snap.getMissionContent() + ) + ); + } + + private static SnapDetailResponseDto createSimpleSnapDetail(Snap snap) { + return new SnapDetailResponseDto( + snap.getId(), + snap.getSnapUrl(), + snap.getShootDate(), + "SIMPLE", + new ParticipantDetailResponseDto( + snap.getPhotographerId(), + snap.getPhotographerNickname() + ), + null + ); + } +} + diff --git a/src/main/java/com/dnd/snappy/domain/snap/service/SnapService.java b/src/main/java/com/dnd/snappy/domain/snap/service/SnapService.java index 99989aa..34a825e 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/service/SnapService.java +++ b/src/main/java/com/dnd/snappy/domain/snap/service/SnapService.java @@ -1,8 +1,19 @@ package com.dnd.snappy.domain.snap.service; +import static com.dnd.snappy.domain.snap.exception.SnapErrorCode.*; + +import com.dnd.snappy.common.error.exception.NotFoundException; import com.dnd.snappy.domain.common.dto.request.CursorBasedRequestDto; import com.dnd.snappy.domain.common.dto.response.CursorBasedResponseDto; +import com.dnd.snappy.domain.snap.dto.response.MissionDetailResponseDto; +import com.dnd.snappy.domain.snap.dto.response.ParticipantDetailResponseDto; +import com.dnd.snappy.domain.snap.dto.response.SnapDetailResponseDto; import com.dnd.snappy.domain.snap.dto.response.SnapResponseDto; +import com.dnd.snappy.domain.snap.entity.MeetingMissionSnap; +import com.dnd.snappy.domain.snap.entity.RandomMissionSnap; +import com.dnd.snappy.domain.snap.entity.SimpleSnap; +import com.dnd.snappy.domain.snap.entity.Snap; +import com.dnd.snappy.domain.snap.exception.SnapErrorCode; import com.dnd.snappy.domain.snap.repository.SnapRepository; import java.util.List; import java.util.Optional; @@ -64,6 +75,13 @@ public CursorBasedResponseDto> findParticipantSnapsInMeeti ); } + public SnapDetailResponseDto findSnapById(Long snapId) { + Snap snap = snapRepository.findSnapByIdWithParticipant(snapId) + .orElseThrow(() -> new NotFoundException(NOT_FOUND_SNAP)); + + return SnapDetailResponseDtoFactory.createSnapDetailResponseDto(snap); + } + private Long getCursorId(Optional cursorId, Long meetingId) { return cursorId.orElseGet(() -> snapRepository.findFirstByMeetingIdOrderByIdDesc(meetingId) From 83871a224653b7e24ddbc4131193924ca93e2a3f Mon Sep 17 00:00:00 2001 From: LeeJaeHoon Date: Fri, 23 Aug 2024 12:11:43 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20snap=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20api=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dnd/snappy/controller/v1/snap/SnapController.java | 9 +++++++++ .../domain/snap/dto/response/SnapDetailResponseDto.java | 2 ++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/com/dnd/snappy/controller/v1/snap/SnapController.java b/src/main/java/com/dnd/snappy/controller/v1/snap/SnapController.java index 570f38f..b2a9d32 100644 --- a/src/main/java/com/dnd/snappy/controller/v1/snap/SnapController.java +++ b/src/main/java/com/dnd/snappy/controller/v1/snap/SnapController.java @@ -9,6 +9,7 @@ import com.dnd.snappy.controller.v1.snap.request.CursorBasedRequest; import com.dnd.snappy.domain.common.dto.response.CursorBasedResponseDto; import com.dnd.snappy.domain.snap.dto.response.CreateSnapResponseDto; +import com.dnd.snappy.domain.snap.dto.response.SnapDetailResponseDto; import com.dnd.snappy.domain.snap.dto.response.SnapResponseDto; import com.dnd.snappy.domain.snap.service.MeetingMissionSnapService; import com.dnd.snappy.domain.snap.service.RandomMissionSnapService; @@ -106,4 +107,12 @@ public ResponseEntity>> var data = snapService.findParticipantSnapsInMeeting(cursorBasedRequest.toCursorBasedRequestDto(), meetingId, authInfo.participantId()); return ResponseDto.ok(data); } + + @GetMapping("/{snapId}") + public ResponseEntity> findSnapById( + @PathVariable("snapId") Long snapId + ) { + var data = snapService.findSnapById(snapId); + return ResponseDto.ok(data); + } } diff --git a/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java b/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java index b872115..0fe6a5b 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java +++ b/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java @@ -1,5 +1,6 @@ package com.dnd.snappy.domain.snap.dto.response; +import com.fasterxml.jackson.annotation.JsonInclude; import java.time.LocalDateTime; public record SnapDetailResponseDto( @@ -8,6 +9,7 @@ public record SnapDetailResponseDto( LocalDateTime shootDate, String type, ParticipantDetailResponseDto photographer, + @JsonInclude(JsonInclude.Include.NON_NULL) MissionDetailResponseDto mission ) { } From 0100502d67715ebdc676dba6afab963e553ac2be Mon Sep 17 00:00:00 2001 From: LeeJaeHoon Date: Fri, 23 Aug 2024 12:32:29 +0900 Subject: [PATCH 4/7] =?UTF-8?q?test:=20snap=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20api=20test=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/response/SnapDetailResponseDto.java | 2 + src/main/resources/application.yml | 2 +- .../resources/logger/logback-spring-dev.xml | 2 +- .../v1/snap/SnapControllerTest.java | 128 ++++++++++++++++++ 4 files changed, 132 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java b/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java index 0fe6a5b..28c0fbd 100644 --- a/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java +++ b/src/main/java/com/dnd/snappy/domain/snap/dto/response/SnapDetailResponseDto.java @@ -1,11 +1,13 @@ package com.dnd.snappy.domain.snap.dto.response; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonInclude; import java.time.LocalDateTime; public record SnapDetailResponseDto( Long snapId, String snapUrl, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm", timezone = "Asia/Seoul") LocalDateTime shootDate, String type, ParticipantDetailResponseDto photographer, diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4460cae..a0b9a8d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,7 @@ server: port: 8080 spring: profiles: - active: dev + active: local include: secret jwt: secretKey: '${custom.jwt.secretKey}' diff --git a/src/main/resources/logger/logback-spring-dev.xml b/src/main/resources/logger/logback-spring-dev.xml index 391b8f4..0f51bbb 100644 --- a/src/main/resources/logger/logback-spring-dev.xml +++ b/src/main/resources/logger/logback-spring-dev.xml @@ -4,7 +4,7 @@ - + diff --git a/src/test/java/com/dnd/snappy/controller/v1/snap/SnapControllerTest.java b/src/test/java/com/dnd/snappy/controller/v1/snap/SnapControllerTest.java index 04e764c..5d88145 100644 --- a/src/test/java/com/dnd/snappy/controller/v1/snap/SnapControllerTest.java +++ b/src/test/java/com/dnd/snappy/controller/v1/snap/SnapControllerTest.java @@ -740,6 +740,134 @@ void findParticipantMeetingMissionSnapsInMeeting() throws Exception { } + @DisplayName("snap id를 이용하여 snap을 조회한다.") + @Test + void findSnapById() throws Exception { + Meeting meeting = appendMeeting(LocalDateTime.now(), LocalDateTime.now().plusDays(1)); + Participant participant = appendParticipant(meeting, "nick", 2); + Snap snap = appendSimpleSnap(meeting, participant); + + mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/v1/meetings/{meetingId}/snaps/{snapId}", meeting.getId(), snap.getId()) + .cookie(new Cookie("ACCESS_TOKEN_" + meeting.getId(), tokenProvider.issueToken(participant.getId(), TokenType.ACCESS_TOKEN))) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + pathParameters( + parameterWithName("meetingId").description("모임 ID"), + parameterWithName("snapId").description("snap ID") + ), + requestCookies( + cookieWithName("ACCESS_TOKEN_" + meeting.getId()).description("인증을 위한 access token") + ) + , + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), + fieldWithPath("data").type(JsonFieldType.OBJECT).description("데이터"), + fieldWithPath("data.snapId").type(JsonFieldType.NUMBER).description("snap ID"), + fieldWithPath("data.snapUrl").type(JsonFieldType.STRING).description("snap url"), + fieldWithPath("data.shootDate").type(JsonFieldType.STRING).description("snap 촬영 날짜"), + fieldWithPath("data.type").attributes(key("format").value("SIMPLE(미션 x 기본 사진) | RANDOM_MISSION(랜덤 미션 사진) | MEETING_MISSION(모임 미션 사진)")).description("snap 타입"), + fieldWithPath("data.photographer").type(JsonFieldType.OBJECT).description("snap 촬영자"), + fieldWithPath("data.photographer.participantId").type(JsonFieldType.NUMBER).description("촬영자 id"), + fieldWithPath("data.photographer.nickname").type(JsonFieldType.STRING).description("촬영자 nickname"), + fieldWithPath("data.mission").type(JsonFieldType.OBJECT).description("수행한 미션").optional(), + fieldWithPath("data.mission.missionId").type(JsonFieldType.NUMBER).description("미션 id").optional(), + fieldWithPath("data.mission.content").type(JsonFieldType.NUMBER).description("미션 내용").optional() + ) + ) + ); + + } + + @DisplayName("snap id를 이용하여 random mission snap을 조회한다.") + @Test + void findRandomMissionSnapById() throws Exception { + Meeting meeting = appendMeeting(LocalDateTime.now(), LocalDateTime.now().plusDays(1)); + Participant participant = appendParticipant(meeting, "nick", 2); + RandomMission randomMission = appendRandomMission(); + Snap snap = appendRandomMissionSnap(meeting, participant, randomMission); + + mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/v1/meetings/{meetingId}/snaps/{snapId}", meeting.getId(), snap.getId()) + .cookie(new Cookie("ACCESS_TOKEN_" + meeting.getId(), tokenProvider.issueToken(participant.getId(), TokenType.ACCESS_TOKEN))) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + pathParameters( + parameterWithName("meetingId").description("모임 ID"), + parameterWithName("snapId").description("snap ID") + ), + requestCookies( + cookieWithName("ACCESS_TOKEN_" + meeting.getId()).description("인증을 위한 access token") + ) + , + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), + fieldWithPath("data").type(JsonFieldType.OBJECT).description("데이터"), + fieldWithPath("data.snapId").type(JsonFieldType.NUMBER).description("snap ID"), + fieldWithPath("data.snapUrl").type(JsonFieldType.STRING).description("snap url"), + fieldWithPath("data.shootDate").type(JsonFieldType.STRING).description("snap 촬영 날짜"), + fieldWithPath("data.type").attributes(key("format").value("SIMPLE(미션 x 기본 사진) | RANDOM_MISSION(랜덤 미션 사진) | MEETING_MISSION(모임 미션 사진)")).description("snap 타입"), + fieldWithPath("data.photographer").type(JsonFieldType.OBJECT).description("snap 촬영자"), + fieldWithPath("data.photographer.participantId").type(JsonFieldType.NUMBER).description("촬영자 id"), + fieldWithPath("data.photographer.nickname").type(JsonFieldType.STRING).description("촬영자 nickname"), + fieldWithPath("data.mission").type(JsonFieldType.OBJECT).description("수행한 미션").optional(), + fieldWithPath("data.mission.missionId").type(JsonFieldType.NUMBER).description("미션 id").optional(), + fieldWithPath("data.mission.content").type(JsonFieldType.STRING).description("미션 내용").optional() + ) + ) + ); + + } + + @DisplayName("snap id를 이용하여 meeting mission snap을 조회한다.") + @Test + void findMeetingMissionSnapById() throws Exception { + Meeting meeting = appendMeeting(LocalDateTime.now(), LocalDateTime.now().plusDays(1)); + Participant participant = appendParticipant(meeting, "nick", 2); + Mission mission = appendMission(meeting); + Snap snap = appendMeetingMissionSnap(meeting, participant, mission); + + mockMvc.perform( + RestDocumentationRequestBuilders.get("/api/v1/meetings/{meetingId}/snaps/{snapId}", meeting.getId(), snap.getId()) + .cookie(new Cookie("ACCESS_TOKEN_" + meeting.getId(), tokenProvider.issueToken(participant.getId(), TokenType.ACCESS_TOKEN))) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + pathParameters( + parameterWithName("meetingId").description("모임 ID"), + parameterWithName("snapId").description("snap ID") + ), + requestCookies( + cookieWithName("ACCESS_TOKEN_" + meeting.getId()).description("인증을 위한 access token") + ) + , + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), + fieldWithPath("data").type(JsonFieldType.OBJECT).description("데이터"), + fieldWithPath("data.snapId").type(JsonFieldType.NUMBER).description("snap ID"), + fieldWithPath("data.snapUrl").type(JsonFieldType.STRING).description("snap url"), + fieldWithPath("data.shootDate").type(JsonFieldType.STRING).description("snap 촬영 날짜"), + fieldWithPath("data.type").attributes(key("format").value("SIMPLE(미션 x 기본 사진) | RANDOM_MISSION(랜덤 미션 사진) | MEETING_MISSION(모임 미션 사진)")).description("snap 타입"), + fieldWithPath("data.photographer").type(JsonFieldType.OBJECT).description("snap 촬영자"), + fieldWithPath("data.photographer.participantId").type(JsonFieldType.NUMBER).description("촬영자 id"), + fieldWithPath("data.photographer.nickname").type(JsonFieldType.STRING).description("촬영자 nickname"), + fieldWithPath("data.mission").type(JsonFieldType.OBJECT).description("수행한 미션").optional(), + fieldWithPath("data.mission.missionId").type(JsonFieldType.NUMBER).description("미션 id").optional(), + fieldWithPath("data.mission.content").type(JsonFieldType.STRING).description("미션 내용").optional() + ) + ) + ); + + } + private RandomMission appendRandomMission() { RandomMission randomMission = RandomMission.builder().content("test random mission content").build(); return randomMissionRepository.save(randomMission); From 6ec9bf1ddb878c55fdb7cf12e68b6c61ca081b78 Mon Sep 17 00:00:00 2001 From: LeeJaeHoon Date: Fri, 23 Aug 2024 12:39:50 +0900 Subject: [PATCH 5/7] =?UTF-8?q?docs:=20snap=20=EC=83=81=EC=84=B8=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20api=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=ED=99=94=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/snap/find-snap-by-id.adoc | 22 +++++++++++++++++++++ src/docs/asciidoc/snap/snap.adoc | 4 +++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 src/docs/asciidoc/snap/find-snap-by-id.adoc diff --git a/src/docs/asciidoc/snap/find-snap-by-id.adoc b/src/docs/asciidoc/snap/find-snap-by-id.adoc new file mode 100644 index 0000000..6d91d16 --- /dev/null +++ b/src/docs/asciidoc/snap/find-snap-by-id.adoc @@ -0,0 +1,22 @@ +=== snap id로 snap 상세 조회 + +==== 성공 + +===== Request +include::{snippets}/snap-controller-test/find-snap-by-id/http-request.adoc[] +===== Query Parameter +include::{snippets}/snap-controller-test/find-snap-by-id/path-parameters.adoc[] +===== Request Cookie +include::{snippets}/snap-controller-test/find-snap-by-id/request-cookies.adoc[] + +===== Response +SIMPLE 타입의 snap 일떄 +include::{snippets}/snap-controller-test/find-snap-by-id/http-response.adoc[] + +RANDOM_MISSION 타입의 snap 일떄 +include::{snippets}/snap-controller-test/find-random-mission-snap-by-id/http-response.adoc[] + +MEETING_MISSION 타입의 snap 일떄 +include::{snippets}/snap-controller-test/find-meeting-mission-snap-by-id/http-response.adoc[] + +include::{snippets}/snap-controller-test/find-snap-by-id/response-fields.adoc[] diff --git a/src/docs/asciidoc/snap/snap.adoc b/src/docs/asciidoc/snap/snap.adoc index 7d0de93..d8147aa 100644 --- a/src/docs/asciidoc/snap/snap.adoc +++ b/src/docs/asciidoc/snap/snap.adoc @@ -13,4 +13,6 @@ include::find-random-mission-snap-in-meeting.adoc[] include::find-meeting-mission-snap-in-meeting.adoc[] -include::find-participant-snaps-in-meeting.adoc[] \ No newline at end of file +include::find-participant-snaps-in-meeting.adoc[] + +include::find-snap-by-id.adoc[] \ No newline at end of file From 80649ea8adf5243a10236219901a72c652cd2797 Mon Sep 17 00:00:00 2001 From: LeeJaeHoon Date: Fri, 23 Aug 2024 13:22:08 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat:=20=EB=AA=A8=EC=9E=84=20=EC=9E=AC?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20?= =?UTF-8?q?(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/v1/auth/AuthController.java | 27 +++++ .../v1/auth/interceptor/LoginInterceptor.java | 37 ++++++ .../interceptor/config/InterceptorConfig.java | 9 +- .../v1/auth/request/LoginRequest.java | 9 ++ .../resolver/AuthInfoArgumentResolver.java | 2 +- .../domain/auth/service/AuthService.java | 15 +++ .../domain/auth/service/JwtTokenStrategy.java | 6 + .../v1/auth/AuthControllerTest.java | 105 ++++++++++++++++++ 8 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/LoginInterceptor.java create mode 100644 src/main/java/com/dnd/snappy/controller/v1/auth/request/LoginRequest.java diff --git a/src/main/java/com/dnd/snappy/controller/v1/auth/AuthController.java b/src/main/java/com/dnd/snappy/controller/v1/auth/AuthController.java index 8b0119e..2b3b653 100644 --- a/src/main/java/com/dnd/snappy/controller/v1/auth/AuthController.java +++ b/src/main/java/com/dnd/snappy/controller/v1/auth/AuthController.java @@ -1,11 +1,15 @@ package com.dnd.snappy.controller.v1.auth; import com.dnd.snappy.common.dto.ResponseDto; +import com.dnd.snappy.controller.v1.auth.request.LoginRequest; +import com.dnd.snappy.controller.v1.auth.resolver.AuthInfo; +import com.dnd.snappy.controller.v1.auth.resolver.AuthPrincipal; import com.dnd.snappy.controller.v1.auth.resolver.RefreshAuthInfo; import com.dnd.snappy.controller.v1.auth.resolver.RefreshAuthPrincipal; import com.dnd.snappy.domain.auth.service.AuthCookieManager; import com.dnd.snappy.domain.auth.service.AuthService; import com.dnd.snappy.domain.token.service.TokenType; +import jakarta.validation.Valid; import java.time.Duration; import java.time.LocalDateTime; import lombok.RequiredArgsConstructor; @@ -14,6 +18,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; 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; @@ -45,4 +50,26 @@ public ResponseEntity reissueToken( null )); } + + @PostMapping("/meetings/{meetingId}/auth/login") + public ResponseEntity login( + @PathVariable Long meetingId, + @AuthPrincipal AuthInfo authInfo, + @Valid @RequestBody LoginRequest loginRequest + ) { + var response = authService.login(meetingId, authInfo.participantId(), loginRequest.password()); + + Duration duration = Duration.between(LocalDateTime.now(), response.meetingExpiredDate()); + String accessTokenCookie = authCookieManager.createTokenCookie(TokenType.ACCESS_TOKEN, response.accessToken(), meetingId, duration); + String refreshTokenCookie = authCookieManager.createTokenCookie(TokenType.REFRESH_TOKEN, response.refreshToken(), meetingId, duration); + + return ResponseEntity.ok() + .header(HttpHeaders.SET_COOKIE, accessTokenCookie) + .header(HttpHeaders.SET_COOKIE, refreshTokenCookie) + .body(new ResponseDto<>( + HttpStatus.OK.value(), + null, + null + )); + } } diff --git a/src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/LoginInterceptor.java b/src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/LoginInterceptor.java new file mode 100644 index 0000000..c3a12de --- /dev/null +++ b/src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/LoginInterceptor.java @@ -0,0 +1,37 @@ +package com.dnd.snappy.controller.v1.auth.interceptor; + +import com.dnd.snappy.common.error.exception.BusinessException; +import com.dnd.snappy.domain.auth.dto.response.TokenInfo; +import com.dnd.snappy.domain.auth.exception.AuthErrorCode; +import com.dnd.snappy.domain.auth.service.JwtTokenStrategy; +import com.dnd.snappy.domain.auth.service.PathVariableExtractor; +import com.dnd.snappy.domain.participant.repository.ParticipantRepository; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +@RequiredArgsConstructor +public class LoginInterceptor implements HandlerInterceptor { + + private final PathVariableExtractor pathVariableExtractor; + private final JwtTokenStrategy jwtTokenStrategy; + private final ParticipantRepository participantRepository; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + + final Long meetingId = pathVariableExtractor.extractMeetingId(request); + TokenInfo tokenInfo = jwtTokenStrategy.loginProcess(request); + validationParticipantInMeeting(tokenInfo.payload(), meetingId); + return true; + } + + private void validationParticipantInMeeting(Long participantId, Long meetingId) { + if(!participantRepository.existsByIdAndMeetingId(participantId, meetingId)) { + throw new BusinessException(AuthErrorCode.FORBIDDEN); + } + } +} diff --git a/src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/config/InterceptorConfig.java b/src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/config/InterceptorConfig.java index 5d32d8a..a1a300c 100644 --- a/src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/config/InterceptorConfig.java +++ b/src/main/java/com/dnd/snappy/controller/v1/auth/interceptor/config/InterceptorConfig.java @@ -1,6 +1,7 @@ package com.dnd.snappy.controller.v1.auth.interceptor.config; +import com.dnd.snappy.controller.v1.auth.interceptor.LoginInterceptor; import com.dnd.snappy.controller.v1.auth.interceptor.MeetingParticipationInterceptor; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -12,6 +13,7 @@ public class InterceptorConfig implements WebMvcConfigurer { private final MeetingParticipationInterceptor meetingParticipationInterceptor; + private final LoginInterceptor loginInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { @@ -23,6 +25,11 @@ public void addInterceptors(InterceptorRegistry registry) { .excludePathPatterns("/api/**/meetings/{meetingId}/validate-leader-key") .excludePathPatterns("/api/**/meetings/{meetingId}/share") .excludePathPatterns("/api/**/meetings/{meetingId}/tokens/refresh") - .excludePathPatterns("/api/v1/meetings/{meetingId}/participants/check-nickname"); + .excludePathPatterns("/api/**/meetings/{meetingId}/participants/check-nickname") + .excludePathPatterns("/api/**/meetings/{meetingId}/auth/login"); + + registry.addInterceptor(loginInterceptor) + .addPathPatterns("/api/**/meetings/{meetingId}/auth/login"); + } } diff --git a/src/main/java/com/dnd/snappy/controller/v1/auth/request/LoginRequest.java b/src/main/java/com/dnd/snappy/controller/v1/auth/request/LoginRequest.java new file mode 100644 index 0000000..a09afb6 --- /dev/null +++ b/src/main/java/com/dnd/snappy/controller/v1/auth/request/LoginRequest.java @@ -0,0 +1,9 @@ +package com.dnd.snappy.controller.v1.auth.request; + +import jakarta.validation.constraints.NotBlank; + +public record LoginRequest( + @NotBlank(message = "비밀번호는 필수입니다.") + String password +) { +} diff --git a/src/main/java/com/dnd/snappy/controller/v1/auth/resolver/AuthInfoArgumentResolver.java b/src/main/java/com/dnd/snappy/controller/v1/auth/resolver/AuthInfoArgumentResolver.java index ceb67b8..6e5c512 100644 --- a/src/main/java/com/dnd/snappy/controller/v1/auth/resolver/AuthInfoArgumentResolver.java +++ b/src/main/java/com/dnd/snappy/controller/v1/auth/resolver/AuthInfoArgumentResolver.java @@ -28,7 +28,7 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); - TokenInfo tokenInfo = jwtTokenStrategy.process(request, TokenType.ACCESS_TOKEN); + TokenInfo tokenInfo = jwtTokenStrategy.loginProcess(request); return new AuthInfo(tokenInfo.payload()); } } diff --git a/src/main/java/com/dnd/snappy/domain/auth/service/AuthService.java b/src/main/java/com/dnd/snappy/domain/auth/service/AuthService.java index 0ce4c72..121586d 100644 --- a/src/main/java/com/dnd/snappy/domain/auth/service/AuthService.java +++ b/src/main/java/com/dnd/snappy/domain/auth/service/AuthService.java @@ -1,5 +1,7 @@ package com.dnd.snappy.domain.auth.service; +import static com.dnd.snappy.domain.meeting.exception.MeetingErrorCode.MEETING_INVALIDATE_PASSWORD; + import com.dnd.snappy.common.error.exception.BusinessException; import com.dnd.snappy.domain.auth.dto.response.ReissueTokenResponseDto; import com.dnd.snappy.domain.auth.exception.AuthErrorCode; @@ -34,4 +36,17 @@ public ReissueTokenResponseDto reissueTokens(Long meetingId, Long participantId, meeting.getExpiredDate() ); } + + public ReissueTokenResponseDto login(Long meetingId, Long participantId, String password) { + Meeting meeting = meetingService.findByMeetingIdOrThrow(meetingId); + if(!meeting.isCorrectPassword(password)) { + throw new BusinessException(MEETING_INVALIDATE_PASSWORD); + } + Tokens tokens = tokenService.createTokens(participantId); + return new ReissueTokenResponseDto( + tokens.accessToken(), + tokens.refreshToken(), + meeting.getExpiredDate() + ); + } } diff --git a/src/main/java/com/dnd/snappy/domain/auth/service/JwtTokenStrategy.java b/src/main/java/com/dnd/snappy/domain/auth/service/JwtTokenStrategy.java index ce5c35b..37e4bf3 100644 --- a/src/main/java/com/dnd/snappy/domain/auth/service/JwtTokenStrategy.java +++ b/src/main/java/com/dnd/snappy/domain/auth/service/JwtTokenStrategy.java @@ -19,4 +19,10 @@ public TokenInfo process(HttpServletRequest request, TokenType tokenType) { Long payload = tokenProvider.extractPayload(token); return new TokenInfo(token, payload); } + + public TokenInfo loginProcess(HttpServletRequest request) { + String token = jwtTokenExtractor.extractToken(request, TokenType.ACCESS_TOKEN); + Long payload = tokenProvider.extractPayloadIgnoringExpiration(token); + return new TokenInfo(token, payload); + } } diff --git a/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java b/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java index f3668ab..9b13127 100644 --- a/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java +++ b/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java @@ -11,6 +11,7 @@ import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.dnd.snappy.controller.v1.auth.request.LoginRequest; import com.dnd.snappy.domain.auth.service.AuthTokenCookieNameGenerator; import com.dnd.snappy.domain.meeting.entity.Meeting; import com.dnd.snappy.domain.meeting.repository.MeetingRepository; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; import org.springframework.restdocs.operation.RequestCookie; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.restdocs.payload.ResponseFieldsSnippet; @@ -147,6 +149,109 @@ void reissue_token_with_other_meeting_refresh_token_is_fail() throws Exception { ); } + @DisplayName("access token과 password를 이용하여 다시 로그인한다.") + @Test + void login() throws Exception { + Meeting meeting = createMeeting(); + Participant participant = createParticipant(meeting); + TokenProvider fakeTokenProvider = new TokenProvider(new JwtProperties(jwtSecretKey, 1L, 1L)); + Tokens tokens = fakeTokenProvider.issueTokens(participant.getId()); + String accessTokenCookieName = authTokenCookieNameGenerator.generateCookieName(TokenType.ACCESS_TOKEN, meeting.getId()); + String refreshTokenCookieName = authTokenCookieNameGenerator.generateCookieName(TokenType.REFRESH_TOKEN, meeting.getId()); + + mockMvc.perform( + post("/api/v1/meetings/{meetingId}/auth/login", meeting.getId()) + .content(objectMapper.writeValueAsString(new LoginRequest(meeting.getPassword()))) + .cookie(new Cookie(accessTokenCookieName, tokens.accessToken())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk()) + .andDo( + restDocs.document( + requestCookies( + cookieWithName(accessTokenCookieName).description("재발급을 위한 refresh token") + ), + pathParameters( + parameterWithName("meetingId").description("모임 ID") + ), + responseCookies( + cookieWithName(accessTokenCookieName).description("인증에 사용되는 access token"), + cookieWithName(refreshTokenCookieName).description("재발급을 위한 refresh token") + ) + ) + ); + } + + @DisplayName("login시 access token이 없는 경우 예외가 발생한다.") + @Test + void login_no_access_token() throws Exception { + Meeting meeting = createMeeting(); + + mockMvc.perform( + post("/api/v1/meetings/{meetingId}/auth/login", meeting.getId()) + .content(objectMapper.writeValueAsString(new LoginRequest("incorrect password"))) + .cookie(new Cookie("ACCESS_TOKEN_" + meeting.getId(), "")) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isUnauthorized()) + .andDo( + restDocs.document( + getErrorResponseFields() + ) + ); + } + + @DisplayName("login시 password가 틀린 경우 예외가 발생한다.") + @Test + void login_incorrect_password() throws Exception { + Meeting meeting = createMeeting(); + Participant participant = createParticipant(meeting); + TokenProvider fakeTokenProvider = new TokenProvider(new JwtProperties(jwtSecretKey, 1L, 1L)); + Tokens tokens = fakeTokenProvider.issueTokens(participant.getId()); + String accessTokenCookieName = authTokenCookieNameGenerator.generateCookieName(TokenType.ACCESS_TOKEN, meeting.getId()); + String refreshTokenCookieName = authTokenCookieNameGenerator.generateCookieName(TokenType.REFRESH_TOKEN, meeting.getId()); + + mockMvc.perform( + post("/api/v1/meetings/{meetingId}/auth/login", meeting.getId()) + .content(objectMapper.writeValueAsString(new LoginRequest("incorrect password"))) + .cookie(new Cookie(accessTokenCookieName, tokens.accessToken())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isBadRequest()) + .andDo( + restDocs.document( + getErrorResponseFields() + ) + ); + } + + @DisplayName("login시 해당 모임의 참가자가 아니라면 예외가 발생한다.") + @Test + void login_no_participation_meeting() throws Exception { + Meeting meeting = createMeeting(); + Meeting meeting2 = createMeeting(); + Participant participant = createParticipant(meeting); + TokenProvider fakeTokenProvider = new TokenProvider(new JwtProperties(jwtSecretKey, 1L, 1L)); + Tokens tokens = fakeTokenProvider.issueTokens(participant.getId()); + String accessTokenCookieName = authTokenCookieNameGenerator.generateCookieName(TokenType.ACCESS_TOKEN, meeting.getId()); + String refreshTokenCookieName = authTokenCookieNameGenerator.generateCookieName(TokenType.REFRESH_TOKEN, meeting.getId()); + + mockMvc.perform( + post("/api/v1/meetings/{meetingId}/auth/login", meeting2.getId()) + .content(objectMapper.writeValueAsString(new LoginRequest(meeting.getPassword()))) + .cookie(new Cookie(accessTokenCookieName, tokens.accessToken())) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isUnauthorized()) + .andDo( + restDocs.document( + getErrorResponseFields() + ) + ); + } + + + private ResponseFieldsSnippet getErrorResponseFields() { return responseFields( fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), From a6e2eb507e0cfa50961fc2e05f63f064e4c33a6f Mon Sep 17 00:00:00 2001 From: LeeJaeHoon Date: Fri, 23 Aug 2024 13:29:48 +0900 Subject: [PATCH 7/7] =?UTF-8?q?docs:=20=EB=AA=A8=EC=9E=84=20=EC=9E=AC?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=20api=20=EB=AC=B8=EC=84=9C=ED=99=94=20(#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/docs/asciidoc/auth/auth.adoc | 2 ++ src/docs/asciidoc/auth/login.adoc | 26 +++++++++++++++++++ .../v1/auth/AuthControllerTest.java | 4 +++ 3 files changed, 32 insertions(+) create mode 100644 src/docs/asciidoc/auth/login.adoc diff --git a/src/docs/asciidoc/auth/auth.adoc b/src/docs/asciidoc/auth/auth.adoc index 0214c3a..c74efe7 100644 --- a/src/docs/asciidoc/auth/auth.adoc +++ b/src/docs/asciidoc/auth/auth.adoc @@ -1,4 +1,6 @@ [[인증-API]] == 인증 API +include::login.adoc[] + include::refresh-token.adoc[] \ No newline at end of file diff --git a/src/docs/asciidoc/auth/login.adoc b/src/docs/asciidoc/auth/login.adoc new file mode 100644 index 0000000..a0e1df5 --- /dev/null +++ b/src/docs/asciidoc/auth/login.adoc @@ -0,0 +1,26 @@ +=== 모임 재참여 +==== 성공 +Request +include::{snippets}/auth-controller-test/login/http-request.adoc[] +include::{snippets}/auth-controller-test/login/path-parameters.adoc[] +Request Cookie +include::{snippets}/auth-controller-test/login/request-cookies.adoc[] +Request Body +include::{snippets}/auth-controller-test/login/request-body.adoc[] +include::{snippets}/auth-controller-test/login/request-fields.adoc[] + +Response +include::{snippets}/auth-controller-test/login/http-response.adoc[] +Response Cookie +include::{snippets}/auth-controller-test/login/response-cookies.adoc[] + + +==== 실패 +===== `요청 쿠키로 access token이 없는경우` +include::{snippets}/auth-controller-test/login_no_access_token/response-body.adoc[] + +===== `모임의 참여자가 아닌 사용자가 로그인을 시도하는 경우` +include::{snippets}/auth-controller-test/login_no_participation_meeting/response-body.adoc[] + +===== `모임의 비밀번호가 틀린 경우` +include::{snippets}/auth-controller-test/login_incorrect_password/response-body.adoc[] diff --git a/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java b/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java index 9b13127..ccdc21e 100644 --- a/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java +++ b/src/test/java/com/dnd/snappy/controller/v1/auth/AuthControllerTest.java @@ -6,6 +6,7 @@ import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; 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.restdocs.request.RequestDocumentation.parameterWithName; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; @@ -171,6 +172,9 @@ void login() throws Exception { requestCookies( cookieWithName(accessTokenCookieName).description("재발급을 위한 refresh token") ), + requestFields( + fieldWithPath("password").description("모임의 비밀번호") + ), pathParameters( parameterWithName("meetingId").description("모임 ID") ),