Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package me.chan99k.learningmanager.controller.auth;

import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -11,7 +12,9 @@
import jakarta.validation.Valid;
import me.chan99k.learningmanager.authentication.IssueToken;
import me.chan99k.learningmanager.authentication.RefreshAccessToken;
import me.chan99k.learningmanager.authentication.RevokeAllTokens;
import me.chan99k.learningmanager.authentication.RevokeToken;
import me.chan99k.learningmanager.security.CustomUserDetails;

@Tag(name = "Authentication", description = "์ธ์ฆ API")
@RestController
Expand All @@ -20,15 +23,18 @@ public class AuthController {
private final IssueToken issueToken;
private final RefreshAccessToken refreshAccessToken;
private final RevokeToken revokeToken;
private final RevokeAllTokens revokeAllTokens;

public AuthController(
IssueToken issueToken,
RefreshAccessToken refreshAccessToken,
RevokeToken revokeToken
RevokeToken revokeToken,
RevokeAllTokens revokeAllTokens
) {
this.issueToken = issueToken;
this.refreshAccessToken = refreshAccessToken;
this.revokeToken = revokeToken;
this.revokeAllTokens = revokeAllTokens;
}

@Operation(summary = "๋กœ๊ทธ์ธ", description = "์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ๋กœ๊ทธ์ธํ•˜์—ฌ Access Token๊ณผ Refresh Token์„ ๋ฐœ๊ธ‰๋ฐ›์Šต๋‹ˆ๋‹ค.")
Expand All @@ -55,4 +61,13 @@ public ResponseEntity<Void> revokeToken(
revokeToken.revoke(request);
return ResponseEntity.ok().build();
}

@Operation(summary = "์ „์ฒด ์„ธ์…˜ ๋กœ๊ทธ์•„์›ƒ", description = "๋ชจ๋“  ๊ธฐ๊ธฐ์—์„œ ๋กœ๊ทธ์•„์›ƒํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  Refresh Token์„ ํ๊ธฐํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/token/revoke-all")
public ResponseEntity<Void> revokeAllTokens(
@AuthenticationPrincipal CustomUserDetails user
) {
revokeAllTokens.revokeAll(user.getMemberId());
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
Expand All @@ -14,13 +15,16 @@
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.fasterxml.jackson.databind.ObjectMapper;

import me.chan99k.learningmanager.adapter.web.attendance.MockCustomUserDetailsArgumentResolver;
import me.chan99k.learningmanager.authentication.AuthProblemCode;
import me.chan99k.learningmanager.authentication.IssueToken;
import me.chan99k.learningmanager.authentication.JwtProvider;
import me.chan99k.learningmanager.authentication.RefreshAccessToken;
import me.chan99k.learningmanager.authentication.RevokeAllTokens;
import me.chan99k.learningmanager.authentication.RevokeToken;
import me.chan99k.learningmanager.controller.auth.AuthController;
import me.chan99k.learningmanager.exception.DomainException;
Expand Down Expand Up @@ -53,6 +57,9 @@ public class AuthControllerTest {
@MockBean
private RevokeToken revokeToken;

@MockBean
private RevokeAllTokens revokeAllTokens;

@MockBean
private JwtProvider jwtProvider;

Expand All @@ -63,7 +70,6 @@ class IssueTokenTest {
@Test
@DisplayName("[Success] ์œ ํšจํ•œ ์ž๊ฒฉ์ฆ๋ช…์œผ๋กœ ํ† ํฐ ๋ฐœ๊ธ‰ ์„ฑ๊ณต - 200 OK")
void issue_token_test_01() throws Exception {
// Given
IssueToken.Request request = new IssueToken.Request(TEST_EMAIL, TEST_PASSWORD);
IssueToken.Response response = IssueToken.Response.of(
TEST_ACCESS_TOKEN, TEST_REFRESH_TOKEN, EXPIRES_IN_SECONDS
Expand All @@ -72,7 +78,6 @@ void issue_token_test_01() throws Exception {
given(issueToken.issueToken(any(IssueToken.Request.class)))
.willReturn(response);

// When & Then
mockMvc.perform(post("/api/v1/auth/token")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -88,10 +93,8 @@ void issue_token_test_01() throws Exception {
@Test
@DisplayName("[Failure] ์ด๋ฉ”์ผ ๋ˆ„๋ฝ - 400 Bad Request")
void issue_token_test_02() throws Exception {
// Given
String requestJson = "{\"password\":\"" + TEST_PASSWORD + "\"}";

// When & Then
mockMvc.perform(post("/api/v1/auth/token")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson))
Expand All @@ -102,10 +105,8 @@ void issue_token_test_02() throws Exception {
@Test
@DisplayName("[Failure] ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ˆ„๋ฝ - 400 Bad Request")
void issue_token_test_03() throws Exception {
// Given
String requestJson = "{\"email\":\"" + TEST_EMAIL + "\"}";

// When & Then
mockMvc.perform(post("/api/v1/auth/token")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson))
Expand All @@ -116,13 +117,11 @@ void issue_token_test_03() throws Exception {
@Test
@DisplayName("[Failure] ์ž˜๋ชป๋œ ์ž๊ฒฉ์ฆ๋ช… - 401 Unauthorized")
void issue_token_test_04() throws Exception {
// Given
IssueToken.Request request = new IssueToken.Request(TEST_EMAIL, "wrongPassword");

given(issueToken.issueToken(any(IssueToken.Request.class)))
.willThrow(new DomainException(AuthProblemCode.INVALID_CREDENTIALS));

// When & Then
mockMvc.perform(post("/api/v1/auth/token")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -136,13 +135,11 @@ void issue_token_test_04() throws Exception {
@Test
@DisplayName("[Failure] ์˜ˆ๊ธฐ์น˜ ๋ชปํ•œ ์„œ๋ฒ„ ์˜ค๋ฅ˜ - 500 Internal Server Error")
void issue_token_test_05() throws Exception {
// Given
IssueToken.Request request = new IssueToken.Request(TEST_EMAIL, TEST_PASSWORD);

given(issueToken.issueToken(any(IssueToken.Request.class)))
.willThrow(new RuntimeException("๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์˜ค๋ฅ˜"));

// When & Then
mockMvc.perform(post("/api/v1/auth/token")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -161,7 +158,6 @@ class RefreshTokenTest {
@Test
@DisplayName("[Success] ์œ ํšจํ•œ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์œผ๋กœ ๊ฐฑ์‹  ์„ฑ๊ณต - 200 OK")
void refresh_token_test_01() throws Exception {
// Given
RefreshAccessToken.Request request = new RefreshAccessToken.Request(TEST_REFRESH_TOKEN);
String newAccessToken = "new-access-token";
String newRefreshToken = "new-refresh-token";
Expand All @@ -172,7 +168,6 @@ void refresh_token_test_01() throws Exception {
given(refreshAccessToken.refresh(any(RefreshAccessToken.Request.class)))
.willReturn(response);

// When & Then
mockMvc.perform(post("/api/v1/auth/token/refresh")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -188,10 +183,8 @@ void refresh_token_test_01() throws Exception {
@Test
@DisplayName("[Failure] ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ ๋ˆ„๋ฝ - 400 Bad Request")
void refresh_token_test_02() throws Exception {
// Given
String requestJson = "{}";

// When & Then
mockMvc.perform(post("/api/v1/auth/token/refresh")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson))
Expand All @@ -202,13 +195,11 @@ void refresh_token_test_02() throws Exception {
@Test
@DisplayName("[Failure] ๋งŒ๋ฃŒ๋œ ํ† ํฐ - 401 Unauthorized")
void refresh_token_test_03() throws Exception {
// Given
RefreshAccessToken.Request request = new RefreshAccessToken.Request("expired-token");

given(refreshAccessToken.refresh(any(RefreshAccessToken.Request.class)))
.willThrow(new DomainException(AuthProblemCode.EXPIRED_TOKEN));

// When & Then
mockMvc.perform(post("/api/v1/auth/token/refresh")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -222,13 +213,11 @@ void refresh_token_test_03() throws Exception {
@Test
@DisplayName("[Failure] ํ๊ธฐ๋œ ํ† ํฐ - 401 Unauthorized")
void refresh_token_test_04() throws Exception {
// Given
RefreshAccessToken.Request request = new RefreshAccessToken.Request("revoked-token");

given(refreshAccessToken.refresh(any(RefreshAccessToken.Request.class)))
.willThrow(new DomainException(AuthProblemCode.REVOKED_TOKEN));

// When & Then
mockMvc.perform(post("/api/v1/auth/token/refresh")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -242,13 +231,11 @@ void refresh_token_test_04() throws Exception {
@Test
@DisplayName("[Failure] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ† ํฐ - 404 Not Found")
void refresh_token_test_05() throws Exception {
// Given
RefreshAccessToken.Request request = new RefreshAccessToken.Request("not-found-token");

given(refreshAccessToken.refresh(any(RefreshAccessToken.Request.class)))
.willThrow(new DomainException(AuthProblemCode.TOKEN_NOT_FOUND));

// When & Then
mockMvc.perform(post("/api/v1/auth/token/refresh")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -267,12 +254,10 @@ class RevokeTokenTest {
@Test
@DisplayName("[Success] ํ† ํฐ ํ๊ธฐ ์„ฑ๊ณต - 200 OK")
void revoke_token_test_01() throws Exception {
// Given
RevokeToken.Request request = new RevokeToken.Request(TEST_REFRESH_TOKEN, "refresh_token");

doNothing().when(revokeToken).revoke(any(RevokeToken.Request.class));

// When & Then
mockMvc.perform(post("/api/v1/auth/token/revoke")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -283,12 +268,10 @@ void revoke_token_test_01() throws Exception {
@Test
@DisplayName("[Success] tokenTypeHint ์—†์ด ํ† ํฐ ํ๊ธฐ ์„ฑ๊ณต - 200 OK")
void revoke_token_test_02() throws Exception {
// Given
RevokeToken.Request request = new RevokeToken.Request(TEST_REFRESH_TOKEN);

doNothing().when(revokeToken).revoke(any(RevokeToken.Request.class));

// When & Then
mockMvc.perform(post("/api/v1/auth/token/revoke")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
Expand All @@ -299,10 +282,8 @@ void revoke_token_test_02() throws Exception {
@Test
@DisplayName("[Failure] ํ† ํฐ ๋ˆ„๋ฝ - 400 Bad Request")
void revoke_token_test_03() throws Exception {
// Given
String requestJson = "{}";

// When & Then
mockMvc.perform(post("/api/v1/auth/token/revoke")
.contentType(MediaType.APPLICATION_JSON)
.content(requestJson))
Expand All @@ -313,17 +294,54 @@ void revoke_token_test_03() throws Exception {
@Test
@DisplayName("[Success] ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ† ํฐ๋„ ์„ฑ๊ณต ์‘๋‹ต - 200 OK (RFC 7009 ๊ถŒ์žฅ)")
void revoke_token_test_04() throws Exception {
// Given - RFC 7009์— ๋”ฐ๋ฅด๋ฉด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ† ํฐ ํ๊ธฐ ์š”์ฒญ๋„ 200 OK ๋ฐ˜ํ™˜
// RFC 7009 ์— ๋”ฐ๋ฅด๋ฉด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ํ† ํฐ ํ๊ธฐ ์š”์ฒญ๋„ 200 OK ๋ฐ˜ํ™˜
RevokeToken.Request request = new RevokeToken.Request("not-found-token");

doNothing().when(revokeToken).revoke(any(RevokeToken.Request.class));

// When & Then
mockMvc.perform(post("/api/v1/auth/token/revoke")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andDo(print())
.andExpect(status().isOk());
}
}

@Nested
@DisplayName("์ „์ฒด ํ† ํฐ ํ๊ธฐ API ํ…Œ์ŠคํŠธ")
class RevokeAllTokensTest {

private MockMvc standaloneMockMvc;

@BeforeEach
void setUp() {
// Security๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋œ ์ƒํƒœ์—์„œ @AuthenticationPrincipal์„ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด
// standaloneSetup๊ณผ MockCustomUserDetailsArgumentResolver๋ฅผ ์‚ฌ์šฉ
standaloneMockMvc = MockMvcBuilders
.standaloneSetup(new AuthController(issueToken, refreshAccessToken, revokeToken, revokeAllTokens))
.setCustomArgumentResolvers(new MockCustomUserDetailsArgumentResolver())
.build();
}

@Test
@DisplayName("[Success] ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž์˜ ์ „์ฒด ํ† ํฐ ํ๊ธฐ ์„ฑ๊ณต - 200 OK")
void revoke_all_tokens_test_01() throws Exception {
doNothing().when(revokeAllTokens).revokeAll(anyLong());

standaloneMockMvc.perform(post("/api/v1/auth/token/revoke-all"))
.andDo(print())
.andExpect(status().isOk());

then(revokeAllTokens).should().revokeAll(123L); // MockCustomUserDetailsArgumentResolver์˜ ๊ธฐ๋ณธ memberId
}

@Test
@DisplayName("[Success] ์ „์ฒด ํ† ํฐ ํ๊ธฐ ์‹œ revokeAllTokens ์„œ๋น„์Šค๊ฐ€ ํ˜ธ์ถœ๋œ๋‹ค")
void revoke_all_tokens_test_02() throws Exception {
doNothing().when(revokeAllTokens).revokeAll(anyLong());
standaloneMockMvc.perform(post("/api/v1/auth/token/revoke-all"))
.andExpect(status().isOk());
then(revokeAllTokens).should().revokeAll(123L);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package me.chan99k.learningmanager.authentication;

public interface RevokeAllTokens {

void revokeAll(Long memberId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package me.chan99k.learningmanager.authentication;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class RevokeAllTokensService implements RevokeAllTokens {

private final RefreshTokenRepository refreshTokenRepository;

public RevokeAllTokensService(RefreshTokenRepository refreshTokenRepository) {
this.refreshTokenRepository = refreshTokenRepository;
}

@Override
public void revokeAll(Long memberId) {
refreshTokenRepository.revokeAllByMemberId(memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package me.chan99k.learningmanager.authentication;

import static org.mockito.BDDMockito.*;

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.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class RevokeAllTokensServiceTest {

private static final Long MEMBER_ID = 1L;

@Mock
RefreshTokenRepository refreshTokenRepository;

RevokeAllTokensService revokeAllTokensService;

@BeforeEach
void setUp() {
revokeAllTokensService = new RevokeAllTokensService(refreshTokenRepository);
}

@Nested
@DisplayName("revokeAll ๋ฉ”์„œ๋“œ")
class RevokeAllTest {

@Test
@DisplayName("ํšŒ์› ID๋กœ ์ „์ฒด ํ† ํฐ ํ๊ธฐ๋ฅผ ์š”์ฒญํ•˜๋ฉด ์ €์žฅ์†Œ์˜ revokeAllByMemberId๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค")
void calls_revoke_all_by_member_id_on_repository() {
revokeAllTokensService.revokeAll(MEMBER_ID);

then(refreshTokenRepository).should().revokeAllByMemberId(MEMBER_ID);
}
}
}
Loading