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
39 changes: 35 additions & 4 deletions .github/workflows/dev-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,32 @@ jobs:

runs-on: ubuntu-latest

# 테스트(contextLoads 등)는 실제 dev DB/Redis가 아니라 CI 안에서 띄우는
# 임시 컨테이너를 바라보게 한다 (원격 dev 환경은 GitHub Actions 러너에서 네트워크가 닿지 않음).
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: runnect
POSTGRES_USER: runnect
POSTGRES_PASSWORD: runnect_local_password
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
Expand All @@ -39,14 +65,19 @@ jobs:

# GitHub-Actions 에서 설정한 값을 application.yml 파일에 쓰기
echo "${{ secrets.RUNNECT_DEV_APPLICATION }}" >> ./application.yml

# application.yml 파일 확인
cat ./application.yml
shell: bash

# 이 워크플로우는 gradle build
- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build with Gradle # 실제 application build
run: ./gradlew build -PactiveProfiles=local -x test
run: ./gradlew build -PactiveProfiles=local
env:
SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/runnect
SPRING_DATASOURCE_USERNAME: runnect
SPRING_DATASOURCE_PASSWORD: runnect_local_password
SPRING_DATA_REDIS_HOST: localhost
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package org.runnect.server.common.resolver.userId;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.runnect.server.common.constant.TokenStatus;
import org.runnect.server.config.jwt.JwtService;
import org.runnect.server.user.exception.authException.InvalidAccessTokenException;
import org.runnect.server.user.exception.authException.NullAccessTokenException;
import org.runnect.server.user.exception.authException.TimeExpiredAccessTokenException;
import org.runnect.server.user.exception.userException.NotFoundUserException;
import org.slf4j.MDC;
import org.springframework.core.MethodParameter;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.context.request.NativeWebRequest;

class UserIdResolverTest {

private static final Long VISITOR_ID = 0L;

private JwtService jwtService;
private UserIdResolver userIdResolver;
private MethodParameter methodParameter;

@BeforeEach
void setUp() {
jwtService = mock(JwtService.class);
userIdResolver = new UserIdResolver(jwtService);
ReflectionTestUtils.setField(userIdResolver, "VISITOR_ID", VISITOR_ID);
ReflectionTestUtils.invokeMethod(userIdResolver, "setVISITOR_POSSIBLE_URLS", "/api/public-course");
methodParameter = mock(MethodParameter.class);
}

@AfterEach
void tearDown() {
MDC.clear();
}

private NativeWebRequest webRequestWith(String accessToken, String refreshToken, String method, String uri) {
HttpServletRequest servletRequest = mock(HttpServletRequest.class);
when(servletRequest.getHeader("accessToken")).thenReturn(accessToken);
when(servletRequest.getHeader("refreshToken")).thenReturn(refreshToken);
when(servletRequest.getMethod()).thenReturn(method);
when(servletRequest.getRequestURI()).thenReturn(uri);

NativeWebRequest webRequest = mock(NativeWebRequest.class);
when(webRequest.getNativeRequest()).thenReturn(servletRequest);
return webRequest;
}

@Test
void accessToken이_없으면_예외를_던진다() {
NativeWebRequest webRequest = webRequestWith(null, "refresh", "GET", "/api/user");

assertThatThrownBy(() -> userIdResolver.resolveArgument(methodParameter, null, webRequest, null))
.isInstanceOf(NullAccessTokenException.class);
}

@Test
void refreshToken이_없으면_예외를_던진다() {
NativeWebRequest webRequest = webRequestWith("access", null, "GET", "/api/user");

assertThatThrownBy(() -> userIdResolver.resolveArgument(methodParameter, null, webRequest, null))
.isInstanceOf(NullAccessTokenException.class);
}

@Test
void 방문자_모드_허용_URL이면_VISITOR_ID를_반환하고_MDC에_채운다() {
NativeWebRequest webRequest = webRequestWith("visitor", "visitor", "GET", "/api/public-course/123");

Object result = userIdResolver.resolveArgument(methodParameter, null, webRequest, null);

assertThat(result).isEqualTo(VISITOR_ID);
assertThat(MDC.get("userId")).isEqualTo(String.valueOf(VISITOR_ID));
}

@Test
void 만료된_토큰이면_예외를_던진다() {
when(jwtService.verifyToken("expired")).thenReturn(TokenStatus.TOKEN_EXPIRED);
NativeWebRequest webRequest = webRequestWith("expired", "refresh", "GET", "/api/user");

assertThatThrownBy(() -> userIdResolver.resolveArgument(methodParameter, null, webRequest, null))
.isInstanceOf(TimeExpiredAccessTokenException.class);
}

@Test
void 유효하지_않은_토큰이면_예외를_던진다() {
when(jwtService.verifyToken("invalid")).thenReturn(TokenStatus.TOKEN_INVALID);
NativeWebRequest webRequest = webRequestWith("invalid", "refresh", "GET", "/api/user");

assertThatThrownBy(() -> userIdResolver.resolveArgument(methodParameter, null, webRequest, null))
.isInstanceOf(InvalidAccessTokenException.class);
}

@Test
void 유효한_토큰이면_userId를_반환하고_MDC에_채운다() {
when(jwtService.verifyToken("valid")).thenReturn(TokenStatus.TOKEN_VALID);
when(jwtService.getJwtContents("valid")).thenReturn("42");
NativeWebRequest webRequest = webRequestWith("valid", "refresh", "GET", "/api/user");

Object result = userIdResolver.resolveArgument(methodParameter, null, webRequest, null);

assertThat(result).isEqualTo(42L);
assertThat(MDC.get("userId")).isEqualTo("42");
}

@Test
void 토큰의_userId_클레임이_숫자가_아니면_예외를_던진다() {
when(jwtService.verifyToken("valid")).thenReturn(TokenStatus.TOKEN_VALID);
when(jwtService.getJwtContents("valid")).thenReturn("not-a-number");
NativeWebRequest webRequest = webRequestWith("valid", "refresh", "GET", "/api/user");

assertThatThrownBy(() -> userIdResolver.resolveArgument(methodParameter, null, webRequest, null))
.isInstanceOf(NotFoundUserException.class);
}

@Test
void UserId_애노테이션과_Long_타입일_때만_지원한다() {
when(methodParameter.hasParameterAnnotation(UserId.class)).thenReturn(true);
when(methodParameter.getParameterType()).thenReturn((Class) Long.class);

assertThat(userIdResolver.supportsParameter(methodParameter)).isTrue();
}
}
57 changes: 57 additions & 0 deletions src/test/java/org/runnect/server/config/jwt/JwtServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.runnect.server.config.jwt;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.runnect.server.common.constant.TokenStatus;
import org.runnect.server.config.redis.RedisService;
import org.springframework.test.util.ReflectionTestUtils;

class JwtServiceTest {

private JwtService jwtService;

@BeforeEach
void setUp() {
RedisService redisService = mock(RedisService.class);
jwtService = new JwtService(redisService);
ReflectionTestUtils.setField(jwtService, "jwtSecret", "test-secret-key-for-jwt-service-unit-test-only");
ReflectionTestUtils.invokeMethod(jwtService, "init");
}

@Test
void 발급한_액세스_토큰은_검증에_성공한다() {
String accessToken = jwtService.issuedAccessToken(1L);

long status = jwtService.verifyToken(accessToken);

assertThat(status).isEqualTo(TokenStatus.TOKEN_VALID);
}

@Test
void 발급한_토큰에서_userId_클레임을_그대로_추출한다() {
String accessToken = jwtService.issuedAccessToken(42L);

String userId = jwtService.getJwtContents(accessToken);

assertThat(userId).isEqualTo("42");
}

@Test
void 형식이_깨진_토큰은_INVALID로_판정한다() {
long status = jwtService.verifyToken("not-a-real-jwt");

assertThat(status).isEqualTo(TokenStatus.TOKEN_INVALID);
}

@Test
void 이미_만료된_토큰은_EXPIRED로_판정한다() {
String expiredToken = jwtService.issuedToken("access_token", -1000L, "1");

long status = jwtService.verifyToken(expiredToken);

assertThat(status).isEqualTo(TokenStatus.TOKEN_EXPIRED);
}
}
Loading