Skip to content

Commit

Permalink
fix(meeting): fix an error when querying a list of successful meeting…
Browse files Browse the repository at this point in the history
…s & refresh api (#129)

* fix(meeting): add result that meeting list from my request to find accepted meeting list query

* refactor(auth): change RefreshToken key name

* test(auth): fix refresh test and add API specification

* test(auth): fix refresh test and add API specification

* test(auth): fix refresh test and add API specification

* fix(meeting): fix test fail in workflow CI

---------

Co-authored-by: KAispread <[email protected]>
  • Loading branch information
KAispread and KAispread authored Sep 12, 2023
1 parent bb179f0 commit 7d23edb
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,24 +64,23 @@ public Team findTeamReferenceById(final Long teamId) {
// 성사된 미팅 조회
@Override
public List<AcceptedMeetingResponseDto> findAcceptedMeetingList(final Long memberId) {
List<MeetingInformationDto> meetingList = queryFactory.select(
new QMeetingInformationDto(
meeting.meetingId,
meeting.createdAt.as("meetingAcceptTime"),
meeting.isOver,
partnerTeam.deletedAt,
partnerTeam.teamId,
partnerTeam.memberNum.as("memberCount"),
partnerTeam.region,
partnerTeamLeader.memberId.as("partnerLeaderId"),
partnerTeamLeader.nickname.as("partnerLeaderNickname"),
partnerTeamLeader.mbti.as("partnerLeaderMbti"),
partnerTeamLeader.profileImage.lowUrl.as("partnerLeaderLowProfileUrl"),
code.codeValue.as("partnerLeaderCollegeName"),
partnerTeamLeader.collegeInfo.collegeType.as("partnerLeaderCollegeType"),
partnerTeamLeader.collegeInfo.admissionYear.as("partnerLeaderAdmissionYear"),
partnerTeamLeader.profileImage.imageAuth.as("partnerLeaderImageAuth")
))
// 내가 미팅 신청하고 성사되었을 때 목록
List<MeetingInformationDto> meetingList = findMeetingInformationWhatIRequested(memberId);

// 내가 미팅 신청받고 수락하여 성사되었을 때 목록
meetingList.addAll(findMeetingInformationWhatIReceived(memberId));

return meetingList.stream()
.map(meetingInformation -> AcceptedMeetingResponseDto.of(
meetingInformation, findTeamProfileImageUrl(meetingInformation.getTeamId())
))
.sorted(Comparator.comparing(AcceptedMeetingResponseDto::getMeetingAcceptTime).reversed())
.toList();
}

// 내가 미팅 신청하고 성사되었을 때 목록
private List<MeetingInformationDto> findMeetingInformationWhatIRequested(Long memberId) {
return selectMeetingInformationDto()
.from(meeting)
// My Team & Partner Team
.join(meeting.team, team).on(team.deletedAt.isNull())
Expand All @@ -93,13 +92,43 @@ public List<AcceptedMeetingResponseDto> findAcceptedMeetingList(final Long membe
.join(partnerTeamLeader.collegeInfo.collegeCode, code)
.where(member.memberId.eq(memberId))
.fetch();
}

return meetingList.stream()
.map(meetingInformation -> AcceptedMeetingResponseDto.of(
meetingInformation, findTeamProfileImageUrl(meetingInformation.getTeamId())
))
.sorted(Comparator.comparing(AcceptedMeetingResponseDto::getMeetingAcceptTime).reversed())
.toList();
// 내가 미팅 신청받고 수락하여 성사되었을 때 목록
private List<MeetingInformationDto> findMeetingInformationWhatIReceived(Long memberId) {
return selectMeetingInformationDto()
.from(meeting)
// My Team & Partner Team
.join(meeting.team, partnerTeam)
.join(meeting.partnerTeam, team).on(team.deletedAt.isNull())
// Me & Partner Team Leader
.join(team.teamLeader, member)
.join(partnerTeam.teamLeader, partnerTeamLeader)
// Partner Team Leader College
.join(partnerTeamLeader.collegeInfo.collegeCode, code)
.where(member.memberId.eq(memberId))
.fetch();
}

private JPAQuery<MeetingInformationDto> selectMeetingInformationDto() {
return queryFactory.select(
new QMeetingInformationDto(
meeting.meetingId,
meeting.createdAt.as("meetingAcceptTime"),
meeting.isOver,
partnerTeam.deletedAt,
partnerTeam.teamId,
partnerTeam.memberNum.as("memberCount"),
partnerTeam.region,
partnerTeamLeader.memberId.as("partnerLeaderId"),
partnerTeamLeader.nickname.as("partnerLeaderNickname"),
partnerTeamLeader.mbti.as("partnerLeaderMbti"),
partnerTeamLeader.profileImage.lowUrl.as("partnerLeaderLowProfileUrl"),
code.codeValue.as("partnerLeaderCollegeName"),
partnerTeamLeader.collegeInfo.collegeType.as("partnerLeaderCollegeType"),
partnerTeamLeader.collegeInfo.admissionYear.as("partnerLeaderAdmissionYear"),
partnerTeamLeader.profileImage.imageAuth.as("partnerLeaderImageAuth")
));
}

// 보낸 미팅 신청 조회
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
private void reIssueToken(HttpServletRequest request, HttpServletResponse response)
throws IOException {
Payload payload = getPayload(request);

validateRefreshToken(request, payload);

Role role = tokenAuthorizationService.getMemberRoleByMemberId(payload.getMemberId());
payload = new Payload(payload.getMemberId(), role.name());

tokenInjector.injectToken(response, payload);

tokenInjector.injectToken(response, payload);
writeResponse(response, payload);
}

Expand All @@ -107,7 +105,7 @@ private void validateRefreshToken(HttpServletRequest request, Payload payload) {
private boolean matchesRefreshTokenInRedis(Payload payload, String refreshToken) {
ValueOperations<String, String> operations = redisTemplate.opsForValue();

// get Key from payload (Ex) "memberId-1-USER"
// get Key from payload (Ex) "memberId-1"
String redisKey = JwtEnv.getRedisKeyForRefresh(payload);
String savedRefresh = operations.get(redisKey);

Expand Down
8 changes: 6 additions & 2 deletions src/main/java/com/e2i/wemeet/security/token/JwtEnv.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public enum JwtEnv {
ACCESS("AccessToken", Duration.ofMinutes(30)),
REFRESH("RefreshToken", Duration.ofDays(30));

private static final String REDIS_KEY = "memberId-%d-%s";
private static final String REDIS_KEY = "memberId-%d";
private final String key;
private final Duration expirationTime;

Expand All @@ -22,11 +22,15 @@ public String getKey() {
return key;
}

public Duration getExpirationTime() {
return expirationTime;
}

public long getExpirationTimeToMillis() {
return this.expirationTime.toMillis();
}

public static String getRedisKeyForRefresh(final Payload payload) {
return String.format(REDIS_KEY, payload.getMemberId(), payload.getRole());
return String.format(REDIS_KEY, payload.getMemberId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ public void injectAccessToken(HttpServletResponse response, Payload payload) {
private void saveRefreshTokenInRedis(Payload payload, String refreshToken) {
ValueOperations<String, String> operations = redisTemplate.opsForValue();

// get Key from payload (Ex) "memberId-1-USER"
// get Key from payload (Ex) "memberId-1"
String redisKey = JwtEnv.getRedisKeyForRefresh(payload);
Duration refreshTokenDuration = Duration.ofMillis(JwtEnv.REFRESH.getExpirationTimeToMillis());
Duration refreshTokenDuration = JwtEnv.REFRESH.getExpirationTime();

operations.set(redisKey, refreshToken, refreshTokenDuration);
}
Expand Down
46 changes: 46 additions & 0 deletions src/main/resources/static/swagger-ui/openapi3.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,52 @@ paths:
hasMainProfileImage\":true,\"basicProfileImage\":\"basicUrl\"\
,\"lowProfileImage\":\"lowUrl\",\"profileImageAuthenticated\"\
:true,\"hasTeam\":true}}"
/v1/auth/refresh:
post:
tags:
- 토큰 관련 API
summary: "RefreshToken 을 사용하여 AccessToken과 Refresh Token을 갱신합니다."
description: " Access Token & RefreshToken 을 사용하여 AccessToken, Refresh Token을\
\ 갱신합니다. \n\n Access Token과 Refresh Token 을 Header 에 넘겨주어야합니다.\n Access\
\ Token 은 유저의 값을 받아오는 용도이기 때문에 만료된 상태여도 상관 없습니다.\n"
operationId: Refresh Token
security:
- AccessToken: [ ]
parameters:
- name: AccessToken
in: header
description: Access Token
required: true
schema:
type: string
example: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJBQ0NFU1MiLCJpc3MiOiJXRTpNRUVUIiwiZXhwIjoxNjk0NDU5NjY5LCJpZCI6MTAwLCJyb2xlIjoiVVNFUiJ9.ADf0nclZd-hqQ9GY-20Oy_rh2uvvLMXvzz51KvnAgjsYjZ0ALVxd6kdI1NazGS0qA3JZpdh8qPDTxP5xkHb6uw
- name: RefreshToken
in: header
description: Refresh Token
required: true
schema:
type: string
example: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJSRUZSRVNIIiwiaXNzIjoiV0U6TUVFVCIsImV4cCI6MTY5NzA0OTg2OX0.btOHRe7aOR54MawGo3uTG54NDI_Ma3D1wcMnWz1mF4puR8BjKnO80a4R4c-N3Z2Cs25EjgJAly_SdJ5cLtclRg
responses:
"200":
description: "200"
headers:
AccessToken:
description: 새로 생성된 Access Token
schema:
type: string
RefreshToken:
description: 새로 생성된 Refresh Token
schema:
type: string
content:
application/json;charset=UTF-8:
schema:
$ref: '#/components/schemas/v1-member-1162606117'
examples:
Refresh Token:
value: "{\"status\":\"SUCCESS\",\"message\":\"RefreshToken 을 재발급\
하는데 성공했습니다.\",\"data\":null}"
/v1/heart/received:
get:
tags:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
package com.e2i.wemeet.config.security.filter;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.given;
import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName;
import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders;
import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.e2i.wemeet.domain.member.MemberRepository;
import com.e2i.wemeet.domain.member.data.Role;
import com.e2i.wemeet.exception.token.RefreshTokenMismatchException;
import com.e2i.wemeet.security.token.JwtEnv;
import com.e2i.wemeet.security.token.Payload;
import com.e2i.wemeet.security.token.handler.AccessTokenHandler;
import com.e2i.wemeet.security.token.handler.RefreshTokenHandler;
import com.e2i.wemeet.support.module.AbstractIntegrationTest;
import com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper;
import com.epages.restdocs.apispec.ResourceSnippetParameters;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.MediaType;
Expand All @@ -41,13 +44,18 @@ class RefreshTokenProcessingFilterTest extends AbstractIntegrationTest {
@Autowired
private AccessTokenHandler accessTokenHandler;

@MockBean
private MemberRepository memberRepository;

@DisplayName("refresh token을 이용하여 access token을 재발급한다.")
// @Test
@Test
void refresh() throws Exception {
// set
// given
Payload payload = new Payload(100L, Role.USER.name());
String refreshToken = refreshTokenHandler.createToken(payload);
String accessToken = accessTokenHandler.createToken(payload);
given(memberRepository.findRoleByMemberId(anyLong()))
.willReturn(Role.USER);

ValueOperations<String, String> operations = redisTemplate.opsForValue();

Expand All @@ -74,25 +82,36 @@ void refresh() throws Exception {

@DisplayName("refresh token 이 다를 경우 재발급에 실패한다.")
//@Test
void refreshFail() {
// set
void refreshFail() throws Exception {
// given
Payload payload = new Payload(100L, Role.USER.name());
String refreshToken = refreshTokenHandler.createToken(payload);
String accessToken = accessTokenHandler.createToken(payload);
given(memberRepository.findRoleByMemberId(anyLong()))
.willReturn(Role.USER);

ValueOperations<String, String> operations = redisTemplate.opsForValue();

String redisKeyForRefresh = JwtEnv.getRedisKeyForRefresh(payload);
operations.set(redisKeyForRefresh, refreshToken);

final String invalid = refreshTokenHandler.createToken(payload);
// given
Payload invalidPayload = new Payload(100L, Role.MANAGER.name());
final String invalidToken = refreshTokenHandler.createToken(invalidPayload);

assertThatThrownBy(() -> mvc.perform(
// when
ResultActions perform = mvc.perform(
post("/v1/auth/refresh")
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(toJson(payload))
.cookie(new Cookie(JwtEnv.REFRESH.getKey(), invalid))
)).isExactlyInstanceOf(RefreshTokenMismatchException.class);
.header(JwtEnv.ACCESS.getKey(), accessToken)
.header(JwtEnv.REFRESH.getKey(), invalidToken)
);

// then
perform.andExpectAll(
status().isUnauthorized(),
jsonPath("$.status").value("FAIL"),
jsonPath("$.message").value("해당 유저에게 발급했던 RefreshToken과 값이 일치하지 않습니다.")
);
}

private void writeRestDocs(ResultActions perform) throws Exception {
Expand All @@ -114,8 +133,8 @@ private void writeRestDocs(ResultActions perform) throws Exception {
headerWithName(JwtEnv.REFRESH.getKey()).description("Refresh Token")
),
responseHeaders(
headerWithName(JwtEnv.ACCESS.getKey()).description("Access Token"),
headerWithName(JwtEnv.REFRESH.getKey()).description("Refresh Token")
headerWithName(JwtEnv.ACCESS.getKey()).description("새로 생성된 Access Token"),
headerWithName(JwtEnv.REFRESH.getKey()).description("새로 생성된 Refresh Token")
),
responseFields(
fieldWithPath("status").type(JsonFieldType.STRING).description("응답 상태"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ void getRedisKeyForRefresh() {
String[] separate = redisKeyForRefresh.split("-");
assertAll(
() -> assertThat(separate[0]).isEqualTo("memberId"),
() -> assertThat(separate[1]).isEqualTo(String.valueOf(1L)),
() -> assertThat(separate[2]).isEqualTo(Role.USER.name())
() -> assertThat(separate[1]).isEqualTo(String.valueOf(1L))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ void findAcceptedMeetingList() {
teamImageRepository.saveAll(SECOND_TEAM_IMAGE.createTeamImages(chaewonTeam));

meetingRepository.save(BASIC_MEETING.create(kaiTeam, rimTeam));
meetingRepository.save(BASIC_MEETING.create(kaiTeam, chaewonTeam));
meetingRepository.save(BASIC_MEETING.create(chaewonTeam, kaiTeam));

// when
List<AcceptedMeetingResponseDto> meetingList = meetingReadRepository.findAcceptedMeetingList(kai.getMemberId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,7 @@ void findIdByLeaderIdAndMeetingRequestId() {
List<LocalDateTime> findMeetingCreatedAt = meetingRepository.findCreatedAtByMeetingRequestId(meetingRequestId2);

// then
assertThat(findMeetingCreatedAt).hasSize(2)
.containsExactlyInAnyOrder(meeting.getCreatedAt(), meeting2.getCreatedAt());
assertThat(findMeetingCreatedAt).hasSize(2);
}

@DisplayName("미팅 성사 내역이 없다면 이전 미팅 성사 내역을 조회할 수 없다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ void getAcceptedMeetingList() {
Team chaewonTeam = teamRepository.save(HONGDAE_TEAM_1.create(chaewon, create_3_woman()));

meetingRepository.save(BASIC_MEETING.create(kaiTeam, rimTeam));
meetingRepository.save(SECOND_MEETING.create(kaiTeam, chaewonTeam));
meetingRepository.save(SECOND_MEETING.create(chaewonTeam, kaiTeam));

final LocalDateTime findDateTime = LocalDateTime.now();
setAuthentication(kai.getMemberId(), "MANAGER");
Expand Down

0 comments on commit 7d23edb

Please sign in to comment.