diff --git a/backend/src/main/java/net/pengcook/authentication/controller/LoginController.java b/backend/src/main/java/net/pengcook/authentication/controller/LoginController.java index b6eb6460..25812acc 100644 --- a/backend/src/main/java/net/pengcook/authentication/controller/LoginController.java +++ b/backend/src/main/java/net/pengcook/authentication/controller/LoginController.java @@ -6,6 +6,8 @@ import net.pengcook.authentication.dto.GoogleLoginResponse; import net.pengcook.authentication.dto.GoogleSignUpRequest; import net.pengcook.authentication.dto.GoogleSignUpResponse; +import net.pengcook.authentication.dto.TokenRefreshRequest; +import net.pengcook.authentication.dto.TokenRefreshResponse; import net.pengcook.authentication.service.LoginService; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PostMapping; @@ -29,4 +31,9 @@ public GoogleLoginResponse loginWithGoogle(@RequestBody @Valid GoogleLoginReques public GoogleSignUpResponse sighUpWithGoogle(@RequestBody @Valid GoogleSignUpRequest googleSignUpRequest) { return LoginService.signUpWithGoogle(googleSignUpRequest); } + + @PostMapping("/api/token/refresh") + public TokenRefreshResponse refresh(@RequestBody @Valid TokenRefreshRequest tokenRefreshRequest) { + return LoginService.refresh(tokenRefreshRequest.refreshToken()); + } } diff --git a/backend/src/main/java/net/pengcook/authentication/util/JwtTokenManager.java b/backend/src/main/java/net/pengcook/authentication/domain/JwtTokenManager.java similarity index 56% rename from backend/src/main/java/net/pengcook/authentication/util/JwtTokenManager.java rename to backend/src/main/java/net/pengcook/authentication/domain/JwtTokenManager.java index c6d0dcd8..3163ab0e 100644 --- a/backend/src/main/java/net/pengcook/authentication/util/JwtTokenManager.java +++ b/backend/src/main/java/net/pengcook/authentication/domain/JwtTokenManager.java @@ -1,39 +1,48 @@ -package net.pengcook.authentication.util; +package net.pengcook.authentication.domain; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; +import java.time.Duration; +import java.time.temporal.ChronoUnit; import java.util.Date; -import net.pengcook.authentication.dto.TokenPayload; import net.pengcook.authentication.exception.JwtTokenException; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.convert.DurationUnit; import org.springframework.stereotype.Component; @Component public class JwtTokenManager { private static final String CLAIM_EMAIL = "email"; + private static final String CLAIM_TOKEN_TYPE = "tokenType"; private final Algorithm secretAlgorithm; - private final long tokenExpirationMills; + @DurationUnit(ChronoUnit.DAYS) + private final Duration accessTokenExpirationDays; + @DurationUnit(ChronoUnit.DAYS) + private final Duration refreshTokenExpirationDays; public JwtTokenManager( @Value("${jwt.secret}") String secret, - @Value("${jwt.expire-in-millis}") long tokenExpirationMills + @Value("${jwt.access-token.expire-in-days}") Duration accessTokenExpirationDays, + @Value("${jwt.refresh-token.expire-in-days}") Duration refreshTokenExpirationDays ) { this.secretAlgorithm = Algorithm.HMAC512(secret); - this.tokenExpirationMills = tokenExpirationMills; + this.accessTokenExpirationDays = accessTokenExpirationDays; + this.refreshTokenExpirationDays = refreshTokenExpirationDays; } public String createToken(TokenPayload payload) { Date issuedAt = new Date(System.currentTimeMillis()); - Date expiresAt = new Date(System.currentTimeMillis() + tokenExpirationMills); + Date expiresAt = getExpiresAt(payload); return JWT.create() .withSubject(String.valueOf(payload.userId())) .withClaim(CLAIM_EMAIL, payload.email()) + .withClaim(CLAIM_TOKEN_TYPE, payload.tokenType().name()) .withIssuedAt(issuedAt) .withExpiresAt(expiresAt) .sign(secretAlgorithm); @@ -49,10 +58,18 @@ public TokenPayload extract(String token) { } } + private Date getExpiresAt(TokenPayload payload) { + if (payload.tokenType() == TokenType.REFRESH) { + return new Date(System.currentTimeMillis() + refreshTokenExpirationDays.toMillis()); + } + return new Date(System.currentTimeMillis() + accessTokenExpirationDays.toMillis()); + } + private TokenPayload getTokenPayload(DecodedJWT decodedJWT) { long userId = Long.parseLong(decodedJWT.getSubject()); String email = decodedJWT.getClaim(CLAIM_EMAIL).asString(); + TokenType tokenType = TokenType.valueOf(decodedJWT.getClaim(CLAIM_TOKEN_TYPE).asString()); - return new TokenPayload(userId, email); + return new TokenPayload(userId, email, tokenType); } } diff --git a/backend/src/main/java/net/pengcook/authentication/util/TokenExtractor.java b/backend/src/main/java/net/pengcook/authentication/domain/TokenExtractor.java similarity index 93% rename from backend/src/main/java/net/pengcook/authentication/util/TokenExtractor.java rename to backend/src/main/java/net/pengcook/authentication/domain/TokenExtractor.java index e5652b00..f4132f8f 100644 --- a/backend/src/main/java/net/pengcook/authentication/util/TokenExtractor.java +++ b/backend/src/main/java/net/pengcook/authentication/domain/TokenExtractor.java @@ -1,4 +1,4 @@ -package net.pengcook.authentication.util; +package net.pengcook.authentication.domain; import net.pengcook.authentication.exception.AuthorizationHeaderException; import org.springframework.stereotype.Component; diff --git a/backend/src/main/java/net/pengcook/authentication/domain/TokenPayload.java b/backend/src/main/java/net/pengcook/authentication/domain/TokenPayload.java new file mode 100644 index 00000000..d111e9c9 --- /dev/null +++ b/backend/src/main/java/net/pengcook/authentication/domain/TokenPayload.java @@ -0,0 +1,22 @@ +package net.pengcook.authentication.domain; + +import net.pengcook.authentication.exception.JwtTokenException; + +public record TokenPayload( + long userId, + String email, + TokenType tokenType +) { + + public void validateAccessToken(String message) { + if (tokenType != TokenType.ACCESS) { + throw new JwtTokenException(message); + } + } + + public void validateRefreshToken(String message) { + if (tokenType != TokenType.REFRESH) { + throw new JwtTokenException(message); + } + } +} diff --git a/backend/src/main/java/net/pengcook/authentication/domain/TokenType.java b/backend/src/main/java/net/pengcook/authentication/domain/TokenType.java new file mode 100644 index 00000000..5ca63f4a --- /dev/null +++ b/backend/src/main/java/net/pengcook/authentication/domain/TokenType.java @@ -0,0 +1,6 @@ +package net.pengcook.authentication.domain; + +public enum TokenType { + + ACCESS, REFRESH, +} diff --git a/backend/src/main/java/net/pengcook/authentication/dto/GoogleLoginResponse.java b/backend/src/main/java/net/pengcook/authentication/dto/GoogleLoginResponse.java index a2d2fca5..b9bd076f 100644 --- a/backend/src/main/java/net/pengcook/authentication/dto/GoogleLoginResponse.java +++ b/backend/src/main/java/net/pengcook/authentication/dto/GoogleLoginResponse.java @@ -2,6 +2,7 @@ public record GoogleLoginResponse( boolean registered, - String accessToken + String accessToken, + String refreshToken ) { } diff --git a/backend/src/main/java/net/pengcook/authentication/dto/GoogleSignUpResponse.java b/backend/src/main/java/net/pengcook/authentication/dto/GoogleSignUpResponse.java index a4de997b..87438c69 100644 --- a/backend/src/main/java/net/pengcook/authentication/dto/GoogleSignUpResponse.java +++ b/backend/src/main/java/net/pengcook/authentication/dto/GoogleSignUpResponse.java @@ -10,9 +10,10 @@ public record GoogleSignUpResponse( String image, String birthday, String country, - String accessToken + String accessToken, + String refreshToken ) { - public GoogleSignUpResponse(User user, String accessToken) { + public GoogleSignUpResponse(User user, String accessToken, String refreshToken) { this( user.getId(), user.getEmail(), @@ -21,7 +22,8 @@ public GoogleSignUpResponse(User user, String accessToken) { user.getImage(), user.getBirth().toString(), user.getRegion(), - accessToken + accessToken, + refreshToken ); } } diff --git a/backend/src/main/java/net/pengcook/authentication/dto/TokenPayload.java b/backend/src/main/java/net/pengcook/authentication/dto/TokenPayload.java deleted file mode 100644 index a6f3081d..00000000 --- a/backend/src/main/java/net/pengcook/authentication/dto/TokenPayload.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.pengcook.authentication.dto; - -public record TokenPayload( - long userId, - String email -) { -} diff --git a/backend/src/main/java/net/pengcook/authentication/dto/TokenRefreshRequest.java b/backend/src/main/java/net/pengcook/authentication/dto/TokenRefreshRequest.java new file mode 100644 index 00000000..4a985316 --- /dev/null +++ b/backend/src/main/java/net/pengcook/authentication/dto/TokenRefreshRequest.java @@ -0,0 +1,6 @@ +package net.pengcook.authentication.dto; + +import jakarta.validation.constraints.NotBlank; + +public record TokenRefreshRequest(@NotBlank String refreshToken) { +} diff --git a/backend/src/main/java/net/pengcook/authentication/dto/TokenRefreshResponse.java b/backend/src/main/java/net/pengcook/authentication/dto/TokenRefreshResponse.java new file mode 100644 index 00000000..5862fa4b --- /dev/null +++ b/backend/src/main/java/net/pengcook/authentication/dto/TokenRefreshResponse.java @@ -0,0 +1,7 @@ +package net.pengcook.authentication.dto; + +public record TokenRefreshResponse( + String accessToken, + String refreshToken +) { +} diff --git a/backend/src/main/java/net/pengcook/authentication/resolver/LoginUserArgumentResolver.java b/backend/src/main/java/net/pengcook/authentication/resolver/LoginUserArgumentResolver.java index 64f93d1d..54c663c5 100644 --- a/backend/src/main/java/net/pengcook/authentication/resolver/LoginUserArgumentResolver.java +++ b/backend/src/main/java/net/pengcook/authentication/resolver/LoginUserArgumentResolver.java @@ -1,10 +1,10 @@ package net.pengcook.authentication.resolver; import lombok.AllArgsConstructor; +import net.pengcook.authentication.domain.JwtTokenManager; +import net.pengcook.authentication.domain.TokenExtractor; +import net.pengcook.authentication.domain.TokenPayload; import net.pengcook.authentication.domain.UserInfo; -import net.pengcook.authentication.dto.TokenPayload; -import net.pengcook.authentication.util.JwtTokenManager; -import net.pengcook.authentication.util.TokenExtractor; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -27,7 +27,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument( + public UserInfo resolveArgument( MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @@ -38,6 +38,7 @@ public Object resolveArgument( String accessToken = tokenExtractor.extractToken(authorizationHeader); TokenPayload tokenPayload = jwtTokenManager.extract(accessToken); + tokenPayload.validateAccessToken("헤더에 토큰이 access token이 아닙니다."); return new UserInfo(tokenPayload.userId(), tokenPayload.email()); } diff --git a/backend/src/main/java/net/pengcook/authentication/service/LoginService.java b/backend/src/main/java/net/pengcook/authentication/service/LoginService.java index 4de0ea87..624de70f 100644 --- a/backend/src/main/java/net/pengcook/authentication/service/LoginService.java +++ b/backend/src/main/java/net/pengcook/authentication/service/LoginService.java @@ -4,14 +4,16 @@ import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseToken; import lombok.AllArgsConstructor; +import net.pengcook.authentication.domain.JwtTokenManager; +import net.pengcook.authentication.domain.TokenPayload; +import net.pengcook.authentication.domain.TokenType; import net.pengcook.authentication.dto.GoogleLoginRequest; import net.pengcook.authentication.dto.GoogleLoginResponse; import net.pengcook.authentication.dto.GoogleSignUpRequest; import net.pengcook.authentication.dto.GoogleSignUpResponse; -import net.pengcook.authentication.dto.TokenPayload; +import net.pengcook.authentication.dto.TokenRefreshResponse; import net.pengcook.authentication.exception.DuplicationException; import net.pengcook.authentication.exception.FirebaseTokenException; -import net.pengcook.authentication.util.JwtTokenManager; import net.pengcook.user.domain.User; import net.pengcook.user.repository.UserRepository; import org.springframework.stereotype.Service; @@ -31,11 +33,12 @@ public GoogleLoginResponse loginWithGoogle(GoogleLoginRequest googleLoginRequest User user = userRepository.findByEmail(email).orElse(null); if (user == null) { - return new GoogleLoginResponse(false, null); + return new GoogleLoginResponse(false, null, null); } + String accessToken = jwtTokenManager.createToken(new TokenPayload(user.getId(), user.getEmail(), TokenType.ACCESS)); + String refreshToken = jwtTokenManager.createToken(new TokenPayload(user.getId(), user.getEmail(), TokenType.REFRESH)); - String accessToken = jwtTokenManager.createToken(new TokenPayload(user.getId(), user.getEmail())); - return new GoogleLoginResponse(true, accessToken); + return new GoogleLoginResponse(true, accessToken, refreshToken); } public GoogleSignUpResponse signUpWithGoogle(GoogleSignUpRequest googleSignUpRequest) { @@ -46,9 +49,22 @@ public GoogleSignUpResponse signUpWithGoogle(GoogleSignUpRequest googleSignUpReq } User savedUser = userRepository.save(user); - String accessToken = jwtTokenManager.createToken(new TokenPayload(savedUser.getId(), savedUser.getEmail())); + String accessToken = jwtTokenManager.createToken( + new TokenPayload(savedUser.getId(), savedUser.getEmail(), TokenType.ACCESS)); + String refreshToken = jwtTokenManager.createToken( + new TokenPayload(savedUser.getId(), savedUser.getEmail(), TokenType.REFRESH)); - return new GoogleSignUpResponse(savedUser, accessToken); + return new GoogleSignUpResponse(savedUser, accessToken, refreshToken); + } + + public TokenRefreshResponse refresh(String refreshToken) { + TokenPayload tokenPayload = jwtTokenManager.extract(refreshToken); + tokenPayload.validateRefreshToken("refresh token이 아닙니다."); + + return new TokenRefreshResponse( + jwtTokenManager.createToken(new TokenPayload(tokenPayload.userId(), tokenPayload.email(), TokenType.ACCESS)), + jwtTokenManager.createToken(tokenPayload) + ); } private User createUser(GoogleSignUpRequest googleSignUpRequest) { diff --git a/backend/src/main/java/net/pengcook/user/controller/UserController.java b/backend/src/main/java/net/pengcook/user/controller/UserController.java index 164825d8..cd8a2312 100644 --- a/backend/src/main/java/net/pengcook/user/controller/UserController.java +++ b/backend/src/main/java/net/pengcook/user/controller/UserController.java @@ -1,11 +1,14 @@ package net.pengcook.user.controller; +import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; import net.pengcook.authentication.domain.UserInfo; import net.pengcook.authentication.resolver.LoginUser; import net.pengcook.user.dto.UserResponse; +import net.pengcook.user.dto.UsernameCheckResponse; import net.pengcook.user.service.UserService; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -15,7 +18,12 @@ public class UserController { private final UserService userService; @GetMapping("/api/user/me") - public UserResponse getUserById(@LoginUser UserInfo userInfo) { + public UserResponse getUserProfile(@LoginUser UserInfo userInfo) { return userService.getUserById(userInfo.getId()); } + + @GetMapping("/api/user/username/check") + public UsernameCheckResponse checkUsername(@RequestParam @NotBlank String username) { + return userService.checkUsername(username); + } } diff --git a/backend/src/main/java/net/pengcook/user/dto/UsernameCheckResponse.java b/backend/src/main/java/net/pengcook/user/dto/UsernameCheckResponse.java new file mode 100644 index 00000000..a18494fa --- /dev/null +++ b/backend/src/main/java/net/pengcook/user/dto/UsernameCheckResponse.java @@ -0,0 +1,4 @@ +package net.pengcook.user.dto; + +public record UsernameCheckResponse(boolean available) { +} diff --git a/backend/src/main/java/net/pengcook/user/repository/UserRepository.java b/backend/src/main/java/net/pengcook/user/repository/UserRepository.java index b8eda9fa..4b15a988 100644 --- a/backend/src/main/java/net/pengcook/user/repository/UserRepository.java +++ b/backend/src/main/java/net/pengcook/user/repository/UserRepository.java @@ -11,4 +11,6 @@ public interface UserRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); + + boolean existsByUsername(String username); } diff --git a/backend/src/main/java/net/pengcook/user/service/UserService.java b/backend/src/main/java/net/pengcook/user/service/UserService.java index 8f1ca693..5c4bc48b 100644 --- a/backend/src/main/java/net/pengcook/user/service/UserService.java +++ b/backend/src/main/java/net/pengcook/user/service/UserService.java @@ -3,6 +3,7 @@ import lombok.AllArgsConstructor; import net.pengcook.user.domain.User; import net.pengcook.user.dto.UserResponse; +import net.pengcook.user.dto.UsernameCheckResponse; import net.pengcook.user.repository.UserRepository; import org.springframework.stereotype.Service; @@ -16,4 +17,9 @@ public UserResponse getUserById(long id) { User user = userRepository.findById(id).orElseThrow(); return new UserResponse(user); } + + public UsernameCheckResponse checkUsername(String username) { + boolean userExists = userRepository.existsByUsername(username); + return new UsernameCheckResponse(!userExists); + } } diff --git a/backend/src/main/resources/application-dev.yaml b/backend/src/main/resources/application-dev.yaml index a1ec40ee..1b847230 100644 --- a/backend/src/main/resources/application-dev.yaml +++ b/backend/src/main/resources/application-dev.yaml @@ -12,7 +12,6 @@ spring: jwt: secret: ENC(+tXNIDNrGAWDx/vMv+AO5KkhPG5iY/mkjagusXWcO16RVHEBI9rq40GukA/RU6X7mEcBPYgWHhlmA5ql6XQU349AIhsJ6Hd3g9f4FD4zcvkHDmJtRV8CAvr9XaMQbtSvCRWMoipYKbTFs5SobwGO1xWntcFIX8LCWxM0wi58Hzz4eJvo7naZiSg3Hqp39zH0pAR8LRP5qbhfyFKH7pIX6AB7q2SqYJIre3T7JIeU5U4=) - expire-in-millis: 86400000 firebase: service-account: ENC(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) diff --git a/backend/src/main/resources/application-local.yaml b/backend/src/main/resources/application-local.yaml index f775fbf5..959df36e 100644 --- a/backend/src/main/resources/application-local.yaml +++ b/backend/src/main/resources/application-local.yaml @@ -17,7 +17,6 @@ spring: jwt: secret: ENC(PMp06UdOJEXk9e7bpjHZunoeLhFdeTqCRRrn/WZ2xBnGvd9+eIAe2TCCRBgBEH9pGwfDCIhnZeHeEs4ownlWCeGOKEhrl5wpMkJX8ycus7kqlR6XYREK8rDGDH2jPwQtda97ZQ6Mk6nJ00x9Sx6tZts4z0OnbRkgPDCPhQV6M3TJpdJHFeYmYq6b+5wGVcnBA/9CVm1FGa/d0LPbtg2fAJ/mVpIer0VhTl/hUmUvjPI=) - expire-in-millis: 86400000 firebase: service-account: ENC(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) diff --git a/backend/src/main/resources/application-test.yaml b/backend/src/main/resources/application-test.yaml index c39a812a..6ddcb048 100644 --- a/backend/src/main/resources/application-test.yaml +++ b/backend/src/main/resources/application-test.yaml @@ -17,7 +17,6 @@ spring: jwt: secret: ENC(s7RMs5p5vqjqalSV1nneb/LRNJ2OzFgKq5lx3Cs1xVS9c62jl3ffw3Gi6gsZzVHHbktbJk+JnL+wfhJs2zd331MhwqXAlfeI5vxpCLYJzMwo5QHwZZnU0G4UmkreUrYpa6teH/Sm58o4myn5r8Y+99wf4YsbBahn6NJCd7WfQ7J42wP0++qYwxBrP7EsvrIcjG2A6lhrTiMRbKAw4lQg4eKvBOztRKvNXHy0sE/Tpt0=) - expire-in-millis: 86400000 firebase: service-account: ENC(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) diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index b6500ec0..36b29aaa 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -7,6 +7,12 @@ jasypt: encryptor: password: ${JASYPT_PASSWORD} +jwt: + access-token: + expire-in-days: 1d + refresh-token: + expire-in-days: 30d + springdoc: enable-default-api-docs: false api-docs: diff --git a/backend/src/test/java/net/pengcook/authentication/controller/LoginControllerTest.java b/backend/src/test/java/net/pengcook/authentication/controller/LoginControllerTest.java index 6046a17f..d3acdfc1 100644 --- a/backend/src/test/java/net/pengcook/authentication/controller/LoginControllerTest.java +++ b/backend/src/test/java/net/pengcook/authentication/controller/LoginControllerTest.java @@ -1,9 +1,13 @@ package net.pengcook.authentication.controller; +import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +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 com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuthException; @@ -11,35 +15,35 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.time.LocalDate; +import java.util.regex.Pattern; +import net.pengcook.RestDocsSetting; +import net.pengcook.authentication.domain.JwtTokenManager; +import net.pengcook.authentication.domain.TokenPayload; +import net.pengcook.authentication.domain.TokenType; import net.pengcook.authentication.dto.GoogleLoginRequest; import net.pengcook.authentication.dto.GoogleLoginResponse; import net.pengcook.authentication.dto.GoogleSignUpRequest; import net.pengcook.authentication.dto.GoogleSignUpResponse; -import org.junit.jupiter.api.BeforeEach; +import net.pengcook.authentication.dto.TokenRefreshRequest; +import net.pengcook.authentication.dto.TokenRefreshResponse; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.jdbc.Sql; -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) @Sql("/data/users.sql") -class LoginControllerTest { +class LoginControllerTest extends RestDocsSetting { + + private static final Pattern JWT_PATTERN = Pattern.compile("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$"); - @LocalServerPort - int port; @MockBean private FirebaseAuth firebaseAuth; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } + @Autowired + private JwtTokenManager jwtTokenManager; @Test - @DisplayName("이미 가입된 계정으로 로그인하면 이미 가입되었다고 알리고 access token을 반환한다.") + @DisplayName("이미 가입된 계정으로 로그인하면 이미 가입되었다고 알리고 access token과 refresh token을 반환한다.") void loginWithGoogleWithEmailAlreadyRegistered() throws FirebaseAuthException { String email = "loki@pengcook.net"; String idToken = "test.id.token"; @@ -48,7 +52,19 @@ void loginWithGoogleWithEmailAlreadyRegistered() throws FirebaseAuthException { when(firebaseToken.getEmail()).thenReturn(email); when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken); - GoogleLoginResponse actual = RestAssured.given().log().all() + GoogleLoginResponse actual = RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "이미 가입된 계정으로 로그인하여 토큰을 반환합니다.", + "구글 로그인 API", + requestFields( + fieldWithPath("idToken").description("Google ID Token") + ), + responseFields( + fieldWithPath("accessToken").description("JWT Access Token"), + fieldWithPath("refreshToken").description("JWT Refresh Token"), + fieldWithPath("registered").description("사용자 등록 여부") + ) + )) .contentType(ContentType.JSON) .body(new GoogleLoginRequest(idToken)) .when().post("/api/oauth/google/login") @@ -58,13 +74,14 @@ void loginWithGoogleWithEmailAlreadyRegistered() throws FirebaseAuthException { .as(GoogleLoginResponse.class); assertAll( - () -> assertThat(actual.accessToken()).isNotNull(), + () -> assertThat(actual.accessToken()).matches(JWT_PATTERN), + () -> assertThat(actual.refreshToken()).matches(JWT_PATTERN), () -> assertThat(actual.registered()).isTrue() ); } @Test - @DisplayName("처음 로그인하면 가입되어 있지 않음을 알리고 access token을 반환하지 않는다.") + @DisplayName("처음 로그인하면 가입되어 있지 않음을 알리고 access token과 refresh token을 반환하지 않는다.") void loginWithGoogleWithEmailNotRegistered() throws FirebaseAuthException { String email = "new_face@pengcook.net"; String idToken = "test.id.token"; @@ -73,7 +90,19 @@ void loginWithGoogleWithEmailNotRegistered() throws FirebaseAuthException { when(firebaseToken.getEmail()).thenReturn(email); when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken); - GoogleLoginResponse actual = RestAssured.given().log().all() + GoogleLoginResponse actual = RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "처음 로그인하여 등록되지 않은 계정을 알립니다.", + "구글 로그인 API", + requestFields( + fieldWithPath("idToken").description("Google ID Token") + ), + responseFields( + fieldWithPath("accessToken").description("JWT Access Token").optional(), + fieldWithPath("refreshToken").description("JWT Refresh Token").optional(), + fieldWithPath("registered").description("사용자 등록 여부") + ) + )) .contentType(ContentType.JSON) .body(new GoogleLoginRequest(idToken)) .when().post("/api/oauth/google/login") @@ -84,6 +113,7 @@ void loginWithGoogleWithEmailNotRegistered() throws FirebaseAuthException { assertAll( () -> assertThat(actual.accessToken()).isNull(), + () -> assertThat(actual.refreshToken()).isNull(), () -> assertThat(actual.registered()).isFalse() ); } @@ -106,7 +136,29 @@ void signUpWithGoogle() throws FirebaseAuthException { when(firebaseToken.getPicture()).thenReturn("new_face.jpg"); when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken); - GoogleSignUpResponse actual = RestAssured.given().log().all() + GoogleSignUpResponse actual = RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "구글 계정으로 새로운 사용자를 등록합니다.", + "구글 회원가입 API", + requestFields( + fieldWithPath("idToken").description("Google ID Token"), + fieldWithPath("username").description("사용자 아이디"), + fieldWithPath("nickname").description("사용자 닉네임"), + fieldWithPath("birthday").description("생년월일"), + fieldWithPath("country").description("국가") + ), + responseFields( + fieldWithPath("id").description("사용자 ID"), + fieldWithPath("email").description("사용자 이메일"), + fieldWithPath("username").description("사용자 아이디"), + fieldWithPath("nickname").description("사용자 닉네임"), + fieldWithPath("image").description("사용자 프로필 이미지"), + fieldWithPath("birthday").description("사용자 생년월일"), + fieldWithPath("country").description("사용자 국가"), + fieldWithPath("accessToken").description("JWT Access Token"), + fieldWithPath("refreshToken").description("JWT Refresh Token") + ) + )) .contentType(ContentType.JSON) .body(request) .when().post("/api/oauth/google/sign-up") @@ -136,11 +188,53 @@ void signUpWithGoogleWhenEmailAleadyRegistered() throws FirebaseAuthException { when(firebaseToken.getPicture()).thenReturn("loki.jpg"); when(firebaseAuth.verifyIdToken(idToken)).thenReturn(firebaseToken); - RestAssured.given().log().all() + RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "이미 등록된 계정으로 회원가입 시도 시 예외를 발생합니다.", + "구글 회원가입 API", + requestFields( + fieldWithPath("idToken").description("Google ID Token"), + fieldWithPath("username").description("사용자 아이디"), + fieldWithPath("nickname").description("사용자 닉네임"), + fieldWithPath("birthday").description("생년월일"), + fieldWithPath("country").description("국가") + ) + )) .contentType(ContentType.JSON) .body(request) .when().post("/api/oauth/google/sign-up") .then().log().all() .statusCode(400); } + + @Test + @DisplayName("refresh token으로 access token을 재발급한다.") + void refresh() { + String refreshToken = jwtTokenManager.createToken(new TokenPayload(1L, "tester@pengcook.net", TokenType.REFRESH)); + + TokenRefreshResponse response = RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "Refresh token을 사용하여 새로운 access token과 refresh token을 발급합니다.", + "토큰 재발급 API", + requestFields( + fieldWithPath("refreshToken").description("JWT Refresh Token") + ), + responseFields( + fieldWithPath("accessToken").description("새로 발급된 JWT Access Token"), + fieldWithPath("refreshToken").description("새로 발급된 JWT Refresh Token") + ) + )) + .contentType(ContentType.JSON) + .body(new TokenRefreshRequest(refreshToken)) + .when().post("/api/token/refresh") + .then().log().all() + .statusCode(200) + .extract() + .as(TokenRefreshResponse.class); + + assertAll( + () -> assertThat(response.accessToken()).matches(JWT_PATTERN), + () -> assertThat(response.refreshToken()).isNotSameAs(refreshToken) + ); + } } diff --git a/backend/src/test/java/net/pengcook/authentication/extension/LoginExtension.java b/backend/src/test/java/net/pengcook/authentication/extension/LoginExtension.java index 4f62f63d..c9922f6f 100644 --- a/backend/src/test/java/net/pengcook/authentication/extension/LoginExtension.java +++ b/backend/src/test/java/net/pengcook/authentication/extension/LoginExtension.java @@ -1,23 +1,24 @@ package net.pengcook.authentication.extension; import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; import java.time.LocalDate; import net.pengcook.authentication.annotation.WithLoginUser; -import net.pengcook.authentication.dto.TokenPayload; -import net.pengcook.authentication.util.JwtTokenManager; +import net.pengcook.authentication.domain.JwtTokenManager; +import net.pengcook.authentication.domain.TokenPayload; +import net.pengcook.authentication.domain.TokenType; import net.pengcook.user.domain.User; import net.pengcook.user.repository.UserRepository; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; -import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; import org.springframework.context.ApplicationContext; import org.springframework.test.context.junit.jupiter.SpringExtension; -public class LoginExtension implements BeforeEachCallback, AfterTestExecutionCallback { +public class LoginExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { @Override - public void beforeEach(ExtensionContext context) { + public void beforeTestExecution(ExtensionContext context) { WithLoginUser annotation = context.getRequiredTestMethod().getAnnotation(WithLoginUser.class); if (annotation != null) { @@ -26,10 +27,9 @@ public void beforeEach(ExtensionContext context) { UserRepository userRepository = applicationContext.getBean(UserRepository.class); User user = findOrSaveUser(annotation, userRepository); - String accessToken = jwtTokenManager.createToken(new TokenPayload(user.getId(), user.getEmail())); + String accessToken = jwtTokenManager.createToken(new TokenPayload(user.getId(), user.getEmail(), TokenType.ACCESS)); - RestAssured.port = ((ServletWebServerApplicationContext) applicationContext).getWebServer().getPort(); - RestAssured.requestSpecification = RestAssured.given().header("Authorization", "Bearer " + accessToken); + RestAssured.requestSpecification = new RequestSpecBuilder().build().header("Authorization", "Bearer " + accessToken); } } diff --git a/backend/src/test/java/net/pengcook/authentication/resolver/LoginUserArgumentResolverTest.java b/backend/src/test/java/net/pengcook/authentication/resolver/LoginUserArgumentResolverTest.java new file mode 100644 index 00000000..bbd3adac --- /dev/null +++ b/backend/src/test/java/net/pengcook/authentication/resolver/LoginUserArgumentResolverTest.java @@ -0,0 +1,63 @@ +package net.pengcook.authentication.resolver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.restassured.RestAssured; +import net.pengcook.authentication.domain.JwtTokenManager; +import net.pengcook.authentication.domain.TokenExtractor; +import net.pengcook.authentication.domain.TokenPayload; +import net.pengcook.authentication.domain.TokenType; +import net.pengcook.authentication.domain.UserInfo; +import net.pengcook.authentication.exception.JwtTokenException; +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; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.web.context.request.NativeWebRequest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class LoginUserArgumentResolverTest { + + @LocalServerPort + int port; + @Autowired + private JwtTokenManager jwtTokenManager; + @Autowired + private TokenExtractor tokenExtractor; + + @BeforeEach + void setUp() { + RestAssured.port = port; + } + + @Test + @DisplayName("로그인한 사용자 정보를 추출한다.") + void resolveArgument() { + LoginUserArgumentResolver loginUserArgumentResolver = new LoginUserArgumentResolver(jwtTokenManager, tokenExtractor); + String accessToken = jwtTokenManager.createToken(new TokenPayload(1L, "tester@pengcook.net", TokenType.ACCESS)); + NativeWebRequest webRequest = mock(NativeWebRequest.class); + when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + accessToken); + + UserInfo actual = loginUserArgumentResolver.resolveArgument(null, null, webRequest, null); + + assertThat(actual.getEmail()).isEqualTo("tester@pengcook.net"); + } + + @Test + @DisplayName("Authorization 헤더에 refresh 토큰을 넣으면 예외가 발생한다.") + void resolveArgumentWhenAuthorizationHeaderRefreshToken() { + LoginUserArgumentResolver loginUserArgumentResolver = new LoginUserArgumentResolver(jwtTokenManager, tokenExtractor); + String refresh = jwtTokenManager.createToken(new TokenPayload(1L, "tester@pengcook.net", TokenType.REFRESH)); + NativeWebRequest webRequest = mock(NativeWebRequest.class); + when(webRequest.getHeader("Authorization")).thenReturn("Bearer " + refresh); + + assertThatThrownBy(() -> loginUserArgumentResolver.resolveArgument(null, null, webRequest, null)) + .isInstanceOf(JwtTokenException.class) + .hasMessage("헤더에 토큰이 access token이 아닙니다."); + } +} diff --git a/backend/src/test/java/net/pengcook/authentication/service/LoginServiceTest.java b/backend/src/test/java/net/pengcook/authentication/service/LoginServiceTest.java index 4183aaf0..1d075770 100644 --- a/backend/src/test/java/net/pengcook/authentication/service/LoginServiceTest.java +++ b/backend/src/test/java/net/pengcook/authentication/service/LoginServiceTest.java @@ -10,16 +10,20 @@ import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseToken; import java.time.LocalDate; +import java.util.regex.Pattern; +import net.pengcook.authentication.domain.JwtTokenManager; +import net.pengcook.authentication.domain.TokenPayload; +import net.pengcook.authentication.domain.TokenType; import net.pengcook.authentication.dto.GoogleLoginRequest; import net.pengcook.authentication.dto.GoogleLoginResponse; import net.pengcook.authentication.dto.GoogleSignUpRequest; import net.pengcook.authentication.dto.GoogleSignUpResponse; +import net.pengcook.authentication.dto.TokenRefreshResponse; import net.pengcook.authentication.exception.DuplicationException; -import net.pengcook.authentication.util.JwtTokenManager; +import net.pengcook.authentication.exception.JwtTokenException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; @@ -28,14 +32,17 @@ @DataJpaTest @Import({LoginService.class, JwtTokenManager.class}) -@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @Sql(scripts = "/data/users.sql") class LoginServiceTest { + private static final Pattern JWT_PATTERN = Pattern.compile("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$"); + @MockBean private FirebaseAuth firebaseAuth; @Autowired private LoginService loginService; + @Autowired + private JwtTokenManager jwtTokenManager; @Test @DisplayName("이미 가입된 계정으로 로그인하면 이미 가입되었다고 알리고 access token을 반환한다.") @@ -51,7 +58,7 @@ void loginWithGoogleWithEmailAlreadyRegistered() throws FirebaseAuthException { GoogleLoginResponse googleLoginResponse = loginService.loginWithGoogle(request); assertAll( - () -> assertThat(googleLoginResponse.accessToken()).isNotNull(), + () -> assertThat(googleLoginResponse.accessToken()).matches(JWT_PATTERN), () -> assertThat(googleLoginResponse.registered()).isTrue() ); } @@ -121,4 +128,37 @@ void signUpWithGoogleWhenEmailAleadyRegistered() throws FirebaseAuthException { .isInstanceOf(DuplicationException.class) .hasMessage("이미 가입된 이메일입니다."); } + + @Test + @DisplayName("refresh token을 이용해 access token을 재발급한다.") + void refresh() { + String refreshToken = jwtTokenManager.createToken(new TokenPayload(1L, "tester@pengcook.net", TokenType.REFRESH)); + + TokenRefreshResponse refresh = loginService.refresh(refreshToken); + + assertAll( + () -> assertThat(refresh.accessToken()).matches(JWT_PATTERN), + () -> assertThat(refresh.refreshToken()).matches(JWT_PATTERN).isNotSameAs(refreshToken) + ); + } + + @Test + @DisplayName("refresh token이 유효하지 않으면 예외가 발생한다.") + void refreshWhenWithAccessToken() { + String accessToken = jwtTokenManager.createToken(new TokenPayload(1L, "tester@pengcook.net", TokenType.ACCESS)); + + assertThatThrownBy(() -> loginService.refresh(accessToken)) + .isInstanceOf(JwtTokenException.class) + .hasMessage("refresh token이 아닙니다."); + } + + @Test + @DisplayName("refresh token이 유효하지 않으면 예외가 발생한다.") + void refreshWhenRefreshTokenInvalid() { + String refreshToken = "invalid.refresh.token"; + + assertThatThrownBy(() -> loginService.refresh(refreshToken)) + .isInstanceOf(JwtTokenException.class) + .hasMessage("유효하지 않은 토큰입니다."); + } } diff --git a/backend/src/test/java/net/pengcook/authentication/util/JwtTokenManagerTest.java b/backend/src/test/java/net/pengcook/authentication/util/JwtTokenManagerTest.java index 23300c38..89e4fefc 100644 --- a/backend/src/test/java/net/pengcook/authentication/util/JwtTokenManagerTest.java +++ b/backend/src/test/java/net/pengcook/authentication/util/JwtTokenManagerTest.java @@ -3,7 +3,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import net.pengcook.authentication.dto.TokenPayload; +import java.time.Duration; +import net.pengcook.authentication.domain.JwtTokenManager; +import net.pengcook.authentication.domain.TokenPayload; +import net.pengcook.authentication.domain.TokenType; import net.pengcook.authentication.exception.JwtTokenException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -11,12 +14,12 @@ class JwtTokenManagerTest { private static final String JWT_REGEX = "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$"; - private final JwtTokenManager jwtTokenManager = new JwtTokenManager("testSecretKey", 3600000L); + private final JwtTokenManager jwtTokenManager = new JwtTokenManager("testSecretKey", Duration.ofDays(1), Duration.ofDays(30)); @Test @DisplayName("access token을 생성한다.") void createToken() { - TokenPayload payload = new TokenPayload(1L, "test@pengcook.net"); + TokenPayload payload = new TokenPayload(1L, "test@pengcook.net", TokenType.ACCESS); String accessToken = jwtTokenManager.createToken(payload); @@ -26,8 +29,8 @@ void createToken() { @Test @DisplayName("access token의 정보를 추출한다.") void extract() { - TokenPayload expected = new TokenPayload(1L, "test@pengcook.net"); - TokenPayload payload = new TokenPayload(1L, "test@pengcook.net"); + TokenPayload expected = new TokenPayload(1L, "test@pengcook.net", TokenType.ACCESS); + TokenPayload payload = new TokenPayload(1L, "test@pengcook.net", TokenType.ACCESS); String accessToken = jwtTokenManager.createToken(payload); TokenPayload actual = jwtTokenManager.extract(accessToken); diff --git a/backend/src/test/java/net/pengcook/authentication/util/TokenExtractorTest.java b/backend/src/test/java/net/pengcook/authentication/util/TokenExtractorTest.java index adf04989..e81e5cc5 100644 --- a/backend/src/test/java/net/pengcook/authentication/util/TokenExtractorTest.java +++ b/backend/src/test/java/net/pengcook/authentication/util/TokenExtractorTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import net.pengcook.authentication.domain.TokenExtractor; import net.pengcook.authentication.exception.AuthorizationHeaderException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; diff --git a/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java b/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java index 52dcace8..4941e14d 100644 --- a/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java +++ b/backend/src/test/java/net/pengcook/user/controller/UserControllerTest.java @@ -1,43 +1,35 @@ package net.pengcook.user.controller; +import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import io.restassured.RestAssured; import io.restassured.http.ContentType; import java.time.LocalDate; -import net.pengcook.authentication.dto.TokenPayload; -import net.pengcook.authentication.util.JwtTokenManager; +import net.pengcook.RestDocsSetting; +import net.pengcook.authentication.annotation.WithLoginUser; +import net.pengcook.authentication.annotation.WithLoginUserTest; import net.pengcook.user.dto.UserResponse; -import org.junit.jupiter.api.BeforeEach; +import org.hamcrest.Matchers; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.context.jdbc.Sql; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@WithLoginUserTest @Sql("/data/users.sql") -class UserControllerTest { - - @LocalServerPort - int port; - @Autowired - private JwtTokenManager jwtTokenManager; - - @BeforeEach - void setUp() { - RestAssured.port = port; - } +class UserControllerTest extends RestDocsSetting { @Test + @WithLoginUser(email = "loki@pengcook.net") @DisplayName("id를 통해 사용자의 정보를 조회한다.") - void getUserById() { - long id = 1L; - String accessToken = jwtTokenManager.createToken(new TokenPayload(id, "loki@pengcook.net")); + void getUserProfile() { UserResponse expected = new UserResponse( - id, + 1L, "loki@pengcook.net", "loki", "로키", @@ -46,9 +38,21 @@ void getUserById() { "KOREA" ); - UserResponse actual = RestAssured.given().log().all() + UserResponse actual = RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "사용자 ID를 통해 사용자 정보를 조회합니다.", + "사용자 정보 조회 API", + responseFields( + fieldWithPath("id").description("사용자 ID"), + fieldWithPath("email").description("사용자 이메일"), + fieldWithPath("username").description("사용자 아이디"), + fieldWithPath("nickname").description("사용자 닉네임"), + fieldWithPath("image").description("사용자 프로필 이미지"), + fieldWithPath("birth").description("사용자 생년월일"), + fieldWithPath("region").description("사용자 국가") + ) + )) .contentType(ContentType.JSON) - .header("Authorization", "Bearer " + accessToken) .when().get("/api/user/me") .then().log().all() .statusCode(200) @@ -57,4 +61,42 @@ void getUserById() { assertThat(actual).isEqualTo(expected); } + + @Test + @DisplayName("username이 중복되지 않으면 사용 가능하다.") + void checkUsername() { + RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "username이 중복되면 사용 불가능하다.", + "사용자 이름 중복 체크 API", + responseFields( + fieldWithPath("available").description("사용 가능 여부") + ) + )) + .contentType(ContentType.JSON) + .queryParam("username", "new_face") + .when().get("/api/user/username/check") + .then().log().all() + .statusCode(200) + .body("available", Matchers.is(true)); + } + + @Test + @DisplayName("username이 중복되면 사용 불가능하다.") + void checkUsernameWhenDuplicateUsername() { + RestAssured.given(spec).log().all() + .filter(document(DEFAULT_RESTDOCS_PATH, + "username이 중복되면 사용 불가능하다.", + "사용자 이름 중복 체크 API", + responseFields( + fieldWithPath("available").description("사용 가능 여부") + ) + )) + .contentType(ContentType.JSON) + .queryParam("username", "loki") + .when().get("/api/user/username/check") + .then().log().all() + .statusCode(200) + .body("available", Matchers.is(false)); + } } diff --git a/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java b/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java index 56dc40fa..26d05957 100644 --- a/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java +++ b/backend/src/test/java/net/pengcook/user/service/UserServiceTest.java @@ -6,6 +6,7 @@ import java.time.LocalDate; import java.util.NoSuchElementException; import net.pengcook.user.dto.UserResponse; +import net.pengcook.user.dto.UsernameCheckResponse; import net.pengcook.user.repository.UserRepository; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -53,4 +54,24 @@ void getUserByIdWhenNotExistId() { assertThatThrownBy(() -> userService.getUserById(id)) .isInstanceOf(NoSuchElementException.class); } + + @Test + @DisplayName("중복되지 않은 username을 입력하면 사용 가능하다") + void checkUsername() { + String username = "new_face"; + + UsernameCheckResponse usernameCheckResponse = userService.checkUsername(username); + + assertThat(usernameCheckResponse.available()).isTrue(); + } + + @Test + @DisplayName("중복된 username을 입력하면 사용 불가능하다") + void checkUsernameWhenDuplicatedUsername() { + String username = "loki"; + + UsernameCheckResponse usernameCheckResponse = userService.checkUsername(username); + + assertThat(usernameCheckResponse.available()).isFalse(); + } }