Skip to content

Commit 950a9dd

Browse files
authored
Merge pull request #200 from prgrms-web-devcourse-final-project/test#179
[test] spring security test 작성
2 parents dc3f499 + ed4f8d7 commit 950a9dd

File tree

8 files changed

+256
-5
lines changed

8 files changed

+256
-5
lines changed

src/main/java/com/back/domain/cocktail/controller/CocktailShareController.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.back.domain.cocktail.repository.CocktailRepository;
44
import com.back.global.rsData.RsData;
55
import lombok.RequiredArgsConstructor;
6+
import org.springframework.beans.factory.annotation.Value;
67
import org.springframework.http.HttpStatus;
78
import org.springframework.http.ResponseEntity;
89
import org.springframework.web.bind.annotation.GetMapping;
@@ -18,13 +19,16 @@
1819
public class CocktailShareController {
1920
private final CocktailRepository cocktailRepository;
2021

22+
@Value("${custom.prod.frontUrl}")
23+
private String frontUrl;
24+
2125
@GetMapping("/{id}/share")
2226
public ResponseEntity<RsData<Map<String, String>>> getShareLink(@PathVariable Long id) {
2327
return cocktailRepository.findById(id)
2428
.map(cocktail -> {
2529
Map<String, String> response = Map.of(
2630
// 공유 URL
27-
"url", "https://www.ssoul.life/cocktails/" + cocktail.getId(),
31+
"url", frontUrl +"/cocktails/" + cocktail.getId(),
2832
// 공유 제목
2933
"title", cocktail.getCocktailName(),
3034
// 공유 이미지 (선택)

src/main/java/com/back/global/jwt/JwtUtil.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ public String generateAccessToken(Long userId, String email, String nickname) {
4545
.compact();
4646
}
4747

48+
// 테스트용: 커스텀 만료시간으로 토큰 생성
49+
public String generateAccessTokenWithExpiration(Long userId, String email, String nickname, long customExpirationMs) {
50+
Date now = new Date();
51+
Date expiration = new Date(now.getTime() + customExpirationMs);
52+
53+
return Jwts.builder()
54+
.setSubject(String.valueOf(userId))
55+
.claim("email", email)
56+
.claim("nickname", nickname)
57+
.setIssuedAt(now)
58+
.setExpiration(expiration)
59+
.signWith(secretKey)
60+
.compact();
61+
}
62+
4863

4964
public void addAccessTokenToCookie(HttpServletResponse response, String accessToken) {
5065
Cookie cookie = new Cookie(ACCESS_TOKEN_COOKIE_NAME, accessToken);

src/main/java/com/back/global/rq/Rq.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ public class Rq {
3131
@Value("${custom.cookie.same}")
3232
private String cookieSameSite;
3333

34+
@Value("${custom.site.cookieDomain}")
35+
private String cookieDomain;
36+
3437

3538
public User getActor() {
3639
return Optional.ofNullable(
@@ -95,6 +98,7 @@ public void setCrossDomainCookie(String name, String value, int maxAge) {
9598
.maxAge(maxAge)
9699
.secure(cookieSecure)
97100
.sameSite(cookieSameSite)
101+
.domain(cookieDomain)
98102
.httpOnly(true)
99103
.build();
100104
resp.addHeader("Set-Cookie", cookie.toString());

src/main/resources/application-dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ logging:
6060
# 쿠키 보안 설정 (HTTP 환경용)
6161
custom:
6262
cookie:
63-
secure: true
63+
secure: false
6464
same: "Lax"
6565

6666
# # AI 설정

src/main/resources/application-prod.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ spring:
1919
driver-class-name: com.mysql.cj.jdbc.Driver
2020
jpa:
2121
hibernate:
22-
ddl-auto: create # 삭제 재성성 과정에서 외래키 제약발생. create 변경 후 배포 테스트
22+
ddl-auto: update # 삭제 재성성 과정에서 외래키 제약발생. create 변경 후 배포 테스트
2323
properties:
2424
hibernate:
2525
show_sql: false
@@ -44,8 +44,8 @@ custom:
4444
cookie:
4545
secure: true
4646
same: "None"
47-
domain: ${custom.prod.cookieDomain}
4847
site:
48+
cookieDomain: "${BASE_URL}"
4949
frontUrl: "${custom.prod.frontUrl}"
5050
backUrl: "${custom.prod.backUrl}"
5151
name: ssoul
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package com.back.global.security;
2+
3+
import com.back.domain.user.entity.User;
4+
import com.back.domain.user.repository.UserRepository;
5+
import com.back.global.jwt.JwtUtil;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.springframework.beans.factory.annotation.Autowired;
11+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
12+
import org.springframework.boot.test.context.SpringBootTest;
13+
import org.springframework.http.MediaType;
14+
import org.springframework.test.context.TestPropertySource;
15+
import org.springframework.test.web.servlet.MockMvc;
16+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
17+
import org.springframework.transaction.annotation.Transactional;
18+
import org.springframework.web.context.WebApplicationContext;
19+
20+
import jakarta.servlet.http.Cookie;
21+
22+
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
23+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
24+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
25+
26+
@SpringBootTest
27+
@AutoConfigureWebMvc
28+
@TestPropertySource(locations = "classpath:application-test.yml")
29+
@Transactional
30+
class SecurityIntegrationTest {
31+
32+
@Autowired
33+
private WebApplicationContext context;
34+
35+
@Autowired
36+
private UserRepository userRepository;
37+
38+
@Autowired
39+
private JwtUtil jwtUtil;
40+
41+
@Autowired
42+
private ObjectMapper objectMapper;
43+
44+
private MockMvc mockMvc;
45+
private User testUser;
46+
47+
@BeforeEach
48+
void setUp() {
49+
mockMvc = MockMvcBuilders
50+
.webAppContextSetup(context)
51+
.apply(springSecurity())
52+
.build();
53+
54+
testUser = User.builder()
55+
.email("test@example.com")
56+
.nickname("테스트사용자")
57+
.oauthId("test_123456")
58+
.role("USER")
59+
.build();
60+
testUser = userRepository.save(testUser);
61+
}
62+
63+
@Test
64+
@DisplayName("JWT 토큰 없이 보호된 API 접근 - 통과 (현재 permitAll 설정)")
65+
void accessProtectedApiWithoutToken() throws Exception {
66+
mockMvc.perform(get("/api/test")
67+
.contentType(MediaType.APPLICATION_JSON))
68+
.andExpect(status().isNotFound()); // 404 (엔드포인트 없음) - 인증은 통과
69+
}
70+
71+
@Test
72+
@DisplayName("유효한 JWT 토큰으로 API 접근 - CustomAuthenticationFilter 동작 확인")
73+
void accessApiWithValidToken() throws Exception {
74+
// given - 실제 JWT 토큰 생성
75+
String accessToken = jwtUtil.generateAccessToken(testUser.getId(), testUser.getEmail(), testUser.getNickname());
76+
Cookie tokenCookie = new Cookie("accessToken", accessToken);
77+
78+
// when & then - 실제 필터 체인 동작 확인
79+
mockMvc.perform(get("/api/test")
80+
.cookie(tokenCookie)
81+
.contentType(MediaType.APPLICATION_JSON))
82+
.andExpect(status().isNotFound()); // 엔드포인트 없음이지만 인증은 성공
83+
}
84+
85+
@Test
86+
@DisplayName("만료된 JWT 토큰으로 API 접근")
87+
void accessApiWithExpiredToken() throws Exception {
88+
// given - 만료된 토큰 생성 (음수 만료시간)
89+
String expiredToken = jwtUtil.generateAccessTokenWithExpiration(
90+
testUser.getId(), testUser.getEmail(), testUser.getNickname(), -1000);
91+
Cookie tokenCookie = new Cookie("accessToken", expiredToken);
92+
93+
// when & then
94+
mockMvc.perform(get("/api/test")
95+
.cookie(tokenCookie)
96+
.contentType(MediaType.APPLICATION_JSON))
97+
.andExpect(status().isNotFound()); // 여전히 통과 (permitAll)
98+
}
99+
100+
@Test
101+
@DisplayName("잘못된 형식의 JWT 토큰으로 API 접근")
102+
void accessApiWithInvalidToken() throws Exception {
103+
// given
104+
Cookie invalidTokenCookie = new Cookie("accessToken", "invalid.jwt.token");
105+
106+
// when & then
107+
mockMvc.perform(get("/api/test")
108+
.cookie(invalidTokenCookie)
109+
.contentType(MediaType.APPLICATION_JSON))
110+
.andExpect(status().isNotFound()); // 여전히 통과 (permitAll)
111+
}
112+
113+
@Test
114+
@DisplayName("OAuth2 로그인 엔드포인트 접근 - 실제 리다이렉트 확인")
115+
void oAuth2LoginEndpoint() throws Exception {
116+
mockMvc.perform(get("/oauth2/authorization/naver"))
117+
.andExpect(status().is3xxRedirection())
118+
.andExpect(header().exists("Location"))
119+
.andExpect(header().string("Location",
120+
org.hamcrest.Matchers.containsString("nid.naver.com")));
121+
}
122+
123+
@Test
124+
@DisplayName("OAuth2 로그인 엔드포인트 - 카카오")
125+
void kakaoLoginEndpoint() throws Exception {
126+
mockMvc.perform(get("/oauth2/authorization/kakao"))
127+
.andExpect(status().is3xxRedirection())
128+
.andExpect(header().exists("Location"))
129+
.andExpect(header().string("Location",
130+
org.hamcrest.Matchers.containsString("kauth.kakao.com")));
131+
}
132+
133+
@Test
134+
@DisplayName("OAuth2 로그인 엔드포인트 - 구글")
135+
void googleLoginEndpoint() throws Exception {
136+
mockMvc.perform(get("/oauth2/authorization/google"))
137+
.andExpect(status().is3xxRedirection())
138+
.andExpect(header().exists("Location"))
139+
.andExpect(header().string("Location",
140+
org.hamcrest.Matchers.containsString("accounts.google.com")));
141+
}
142+
143+
@Test
144+
@DisplayName("CORS 헤더 확인")
145+
void checkCorsHeaders() throws Exception {
146+
mockMvc.perform(options("/api/test")
147+
.header("Origin", "http://localhost:3000")
148+
.header("Access-Control-Request-Method", "GET"))
149+
.andExpect(status().isOk())
150+
.andExpect(header().exists("Access-Control-Allow-Origin"))
151+
.andExpect(header().exists("Access-Control-Allow-Credentials"));
152+
}
153+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
spring:
2+
profiles:
3+
active: test
4+
5+
# H2 테스트 데이터베이스
6+
datasource:
7+
driver-class-name: org.h2.Driver
8+
url: jdbc:h2:mem:testdb;MODE=MySQL
9+
username: sa
10+
password:
11+
12+
jpa:
13+
hibernate:
14+
ddl-auto: create-drop
15+
properties:
16+
hibernate:
17+
show_sql: true
18+
format_sql: true
19+
20+
# OAuth2 테스트 설정 (실제 동작하지 않지만 필터는 동작)
21+
security:
22+
oauth2:
23+
client:
24+
registration:
25+
kakao:
26+
clientId: test-kakao-client-id
27+
scope: profile_nickname
28+
client-name: Kakao
29+
authorization-grant-type: authorization_code
30+
redirect-uri: 'http://localhost:8080/login/oauth2/code/kakao'
31+
google:
32+
client-id: test-google-client-id
33+
client-secret: test-google-client-secret
34+
redirect-uri: 'http://localhost:8080/login/oauth2/code/google'
35+
client-name: Google
36+
scope: profile
37+
naver:
38+
client-id: test-naver-client-id
39+
client-secret: test-naver-client-secret
40+
scope: profile_nickname
41+
client-name: Naver
42+
authorization-grant-type: authorization_code
43+
redirect-uri: 'http://localhost:8080/login/oauth2/code/naver'
44+
provider:
45+
kakao:
46+
authorization-uri: https://kauth.kakao.com/oauth/authorize
47+
token-uri: https://kauth.kakao.com/oauth/token
48+
user-info-uri: https://kapi.kakao.com/v2/user/me
49+
user-name-attribute: id
50+
naver:
51+
authorization-uri: https://nid.naver.com/oauth2.0/authorize
52+
token-uri: https://nid.naver.com/oauth2.0/token
53+
user-info-uri: https://openapi.naver.com/v1/nid/me
54+
user-name-attribute: response
55+
56+
57+
custom:
58+
dev:
59+
cookieDomain: localhost
60+
frontUrl: "http://localhost:3000"
61+
backUrl: "http://localhost:8080"
62+
site:
63+
cookieDomain: localhost
64+
frontUrl: "http://localhost:3000"
65+
backUrl: "http://localhost:8080"
66+
name: test
67+
cookie:
68+
secure: false
69+
same: "Lax"
70+
jwt:
71+
secretKey: "test-secret-key-for-jwt-token-generation-and-validation-purposes-only"
72+
accessToken:
73+
expirationSeconds: "#{60*15}"
74+
refreshToken:
75+
expirationSeconds: "#{60*60*24*30}"

terraform/variables.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ variable "prefix" {
1717

1818
variable "app_1_domain" {
1919
description = "app_1 domain"
20-
default = "api.ssoul.o-r.kr"
20+
default = "api.ssoul.life"
2121
}
2222

2323
variable "s3_bucket_name" {

0 commit comments

Comments
 (0)