Skip to content

Commit 22e7726

Browse files
authored
Merge pull request #4 from f-lab-edu/feature/user
Feature/user
2 parents c08f68f + a558739 commit 22e7726

File tree

14 files changed

+969
-6
lines changed

14 files changed

+969
-6
lines changed

app/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ dependencies {
1010

1111

1212
testImplementation 'org.springframework:spring-tx'
13+
testImplementation 'org.springframework.boot:spring-boot-starter-security'
1314
testRuntimeOnly 'com.h2database:h2'
1415

1516

16-
runtimeOnly 'com.mysql:mysql-connector-j'
17+
runtimeOnly 'org.postgresql:postgresql'
1718

1819
developmentOnly 'org.springframework.boot:spring-boot-devtools'
1920

app/src/main/resources/application.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ spring:
1313
# Database Configuration (MySQL)
1414
# ===================================================================
1515
datasource:
16-
driver-class-name: com.mysql.cj.jdbc.Driver
16+
driver-class-name: org.postgresql.Driver
1717

1818
hikari:
1919
maximum-pool-size: 10
@@ -40,7 +40,7 @@ spring:
4040
hibernate:
4141
format_sql: true
4242
highlight_sql: true
43-
dialect: org.hibernate.dialect.MySQLDialect
43+
dialect: org.hibernate.dialect.PostgreSQLDialect
4444

4545
jdbc:
4646
batch_size: 20
@@ -72,9 +72,15 @@ server:
7272
# ===================================================================
7373
springdoc:
7474
api-docs:
75-
path: /api-docs
75+
path: docs/openapi/api-spec.yaml
7676
swagger-ui:
7777
path: /swagger-ui.html
7878
display-request-duration: true
7979
groups-order: asc
8080
show-actuator: false
81+
82+
83+
jwt:
84+
secret: dev-secret-key-for-jwt-token-generation-minimum-256-bits-required-for-hs256-algorithm-do-not-use-in-production
85+
access-token-validity-in-seconds: 3600 # 1 hour
86+
refresh-token-validity-in-seconds: 604800 # 7 days
Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
package com.btg.e2e;
2+
3+
import com.btg.core.application.port.in.dailyprogress.GetDailyProgressUseCase;
4+
import com.btg.core.application.port.in.dailyprogress.UpdateDailyProgressUseCase;
5+
import com.btg.core.application.port.in.group.*;
6+
import com.btg.core.application.port.in.task.*;
7+
import com.btg.core.application.port.in.user.GetUserProfileUseCase;
8+
import com.btg.core.application.port.in.user.UpdateUserProfileUseCase;
9+
import com.btg.infrastructure.persistence.auth.repository.RefreshTokenJpaRepository;
10+
import com.btg.infrastructure.persistence.user.repository.UserJpaRepository;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.DisplayName;
13+
import org.junit.jupiter.api.Test;
14+
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.boot.test.context.SpringBootTest;
16+
import org.springframework.boot.test.mock.mockito.MockBean;
17+
import org.springframework.boot.test.web.client.TestRestTemplate;
18+
import org.springframework.http.HttpEntity;
19+
import org.springframework.http.HttpHeaders;
20+
import org.springframework.http.HttpStatus;
21+
import org.springframework.http.MediaType;
22+
import org.springframework.test.context.ActiveProfiles;
23+
24+
import static org.assertj.core.api.Assertions.assertThat;
25+
26+
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
27+
@ActiveProfiles("test")
28+
@DisplayName("Auth Controller E2E Tests (Real Server)")
29+
class AuthControllerE2ETest {
30+
31+
@Autowired
32+
private TestRestTemplate restTemplate;
33+
34+
@Autowired
35+
private UserJpaRepository userJpaRepository;
36+
37+
@Autowired
38+
private RefreshTokenJpaRepository refreshTokenJpaRepository;
39+
40+
@MockBean private GetDailyProgressUseCase getDailyProgressUseCase;
41+
@MockBean private UpdateDailyProgressUseCase updateDailyProgressUseCase;
42+
@MockBean private CreateTaskUseCase createTaskUseCase;
43+
@MockBean private GetTaskUseCase getTaskUseCase;
44+
@MockBean private UpdateTaskUseCase updateTaskUseCase;
45+
@MockBean private DeleteTaskUseCase deleteTaskUseCase;
46+
@MockBean private ListTasksUseCase listTasksUseCase;
47+
@MockBean private CreateGroupUseCase createGroupUseCase;
48+
@MockBean private GetGroupUseCase getGroupUseCase;
49+
@MockBean private UpdateGroupUseCase updateGroupUseCase;
50+
@MockBean private DeleteGroupUseCase deleteGroupUseCase;
51+
@MockBean private ListGroupsUseCase listGroupsUseCase;
52+
@MockBean private JoinGroupUseCase joinGroupUseCase;
53+
@MockBean private GetUserProfileUseCase getUserProfileUseCase;
54+
@MockBean private UpdateUserProfileUseCase updateUserProfileUseCase;
55+
56+
@BeforeEach
57+
void setUp() {
58+
refreshTokenJpaRepository.deleteAll();
59+
userJpaRepository.deleteAll();
60+
}
61+
62+
@Test
63+
@DisplayName("POST /auth/signup - Success with real database persistence")
64+
void signup_Success_WithDatabasePersistence() {
65+
// Given
66+
String requestBody = """
67+
{
68+
"email": "[email protected]",
69+
"password": "securePassword123",
70+
"name": "New User"
71+
}
72+
""";
73+
74+
HttpHeaders headers = new HttpHeaders();
75+
headers.setContentType(MediaType.APPLICATION_JSON);
76+
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
77+
78+
// When
79+
var response = restTemplate.postForEntity("/auth/signup", request, String.class);
80+
81+
// Then
82+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
83+
assertThat(response.getBody()).contains("[email protected]");
84+
assertThat(response.getBody()).contains("New User");
85+
86+
// Verify database persistence
87+
var savedUser = userJpaRepository.findByEmail("[email protected]");
88+
assertThat(savedUser).isPresent();
89+
assertThat(savedUser.get().getName()).isEqualTo("New User");
90+
// Password should be encrypted (not plain text)
91+
assertThat(savedUser.get().getPassword()).isNotEqualTo("securePassword123");
92+
assertThat(savedUser.get().getPassword()).startsWith("$2a$"); // BCrypt prefix
93+
}
94+
95+
@Test
96+
@DisplayName("POST /auth/signup - Duplicate email should fail")
97+
void signup_Fail_DuplicateEmail() {
98+
// Given: Create first user
99+
String firstUserRequest = """
100+
{
101+
"email": "[email protected]",
102+
"password": "password123",
103+
"name": "First User"
104+
}
105+
""";
106+
107+
HttpHeaders headers = new HttpHeaders();
108+
headers.setContentType(MediaType.APPLICATION_JSON);
109+
restTemplate.postForEntity("/auth/signup", new HttpEntity<>(firstUserRequest, headers), String.class);
110+
111+
// When: Try to create user with same email
112+
String duplicateRequest = """
113+
{
114+
"email": "[email protected]",
115+
"password": "differentPassword",
116+
"name": "Second User"
117+
}
118+
""";
119+
120+
var response = restTemplate.postForEntity("/auth/signup", new HttpEntity<>(duplicateRequest, headers), String.class);
121+
122+
// Then
123+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
124+
}
125+
126+
@Test
127+
@DisplayName("POST /auth/login - Success with real JWT token generation")
128+
void login_Success_WithRealJwtToken() {
129+
// Given: Create a user first
130+
String signupRequest = """
131+
{
132+
"email": "[email protected]",
133+
"password": "myPassword123",
134+
"name": "Login Test User"
135+
}
136+
""";
137+
138+
HttpHeaders headers = new HttpHeaders();
139+
headers.setContentType(MediaType.APPLICATION_JSON);
140+
restTemplate.postForEntity("/auth/signup", new HttpEntity<>(signupRequest, headers), String.class);
141+
142+
// When: Login
143+
String loginRequest = """
144+
{
145+
"email": "[email protected]",
146+
"password": "myPassword123"
147+
}
148+
""";
149+
150+
var response = restTemplate.postForEntity("/auth/login", new HttpEntity<>(loginRequest, headers), String.class);
151+
152+
// Then
153+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
154+
assertThat(response.getBody()).contains("accessToken");
155+
assertThat(response.getBody()).contains("refreshToken");
156+
assertThat(response.getBody()).contains("[email protected]");
157+
assertThat(response.getBody()).contains("Login Test User");
158+
159+
// Verify tokens are real JWT format (header.payload.signature)
160+
assertThat(response.getBody()).containsPattern("\"accessToken\":\"[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\"");
161+
assertThat(response.getBody()).containsPattern("\"refreshToken\":\"[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\"");
162+
163+
// Verify refresh token is stored in database
164+
String refreshToken = extractRefreshToken(response.getBody());
165+
var savedToken = refreshTokenJpaRepository.findByToken(refreshToken);
166+
assertThat(savedToken).isPresent();
167+
}
168+
169+
@Test
170+
@DisplayName("POST /auth/login - Wrong password should fail")
171+
void login_Fail_WrongPassword() {
172+
// Given: Create a user first
173+
String signupRequest = """
174+
{
175+
"email": "[email protected]",
176+
"password": "correctPassword",
177+
"name": "Test User"
178+
}
179+
""";
180+
181+
HttpHeaders headers = new HttpHeaders();
182+
headers.setContentType(MediaType.APPLICATION_JSON);
183+
restTemplate.postForEntity("/auth/signup", new HttpEntity<>(signupRequest, headers), String.class);
184+
185+
// When: Try to login with wrong password
186+
String loginRequest = """
187+
{
188+
"email": "[email protected]",
189+
"password": "wrongPassword"
190+
}
191+
""";
192+
193+
var response = restTemplate.postForEntity("/auth/login", new HttpEntity<>(loginRequest, headers), String.class);
194+
195+
// Then
196+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
197+
}
198+
199+
@Test
200+
@DisplayName("POST /auth/refresh - Success with new access token")
201+
void refreshToken_Success() {
202+
// Given: Signup and login to get refresh token
203+
String signupRequest = """
204+
{
205+
"email": "[email protected]",
206+
"password": "password123",
207+
"name": "Refresh User"
208+
}
209+
""";
210+
211+
HttpHeaders headers = new HttpHeaders();
212+
headers.setContentType(MediaType.APPLICATION_JSON);
213+
restTemplate.postForEntity("/auth/signup", new HttpEntity<>(signupRequest, headers), String.class);
214+
215+
String loginRequest = """
216+
{
217+
"email": "[email protected]",
218+
"password": "password123"
219+
}
220+
""";
221+
222+
var loginResponse = restTemplate.postForEntity("/auth/login", new HttpEntity<>(loginRequest, headers), String.class);
223+
String refreshToken = extractRefreshToken(loginResponse.getBody());
224+
225+
// When: Refresh access token
226+
String refreshRequest = String.format("""
227+
{
228+
"refreshToken": "%s"
229+
}
230+
""", refreshToken);
231+
232+
var response = restTemplate.postForEntity("/auth/refresh", new HttpEntity<>(refreshRequest, headers), String.class);
233+
234+
// Then
235+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
236+
assertThat(response.getBody()).contains("accessToken");
237+
// New access token should be different from original
238+
assertThat(response.getBody()).containsPattern("\"accessToken\":\"[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\"");
239+
}
240+
241+
@Test
242+
@DisplayName("POST /auth/logout - Success and token removed from database")
243+
void logout_Success_TokenRemovedFromDatabase() {
244+
// Given: Signup and login
245+
String signupRequest = """
246+
{
247+
"email": "[email protected]",
248+
"password": "password123",
249+
"name": "Logout User"
250+
}
251+
""";
252+
253+
HttpHeaders headers = new HttpHeaders();
254+
headers.setContentType(MediaType.APPLICATION_JSON);
255+
restTemplate.postForEntity("/auth/signup", new HttpEntity<>(signupRequest, headers), String.class);
256+
257+
String loginRequest = """
258+
{
259+
"email": "[email protected]",
260+
"password": "password123"
261+
}
262+
""";
263+
264+
var loginResponse = restTemplate.postForEntity("/auth/login", new HttpEntity<>(loginRequest, headers), String.class);
265+
String refreshToken = extractRefreshToken(loginResponse.getBody());
266+
267+
// Verify token exists before logout
268+
assertThat(refreshTokenJpaRepository.findByToken(refreshToken)).isPresent();
269+
270+
// When: Logout
271+
String logoutRequest = String.format("""
272+
{
273+
"refreshToken": "%s"
274+
}
275+
""", refreshToken);
276+
277+
var response = restTemplate.postForEntity("/auth/logout", new HttpEntity<>(logoutRequest, headers), String.class);
278+
279+
// Then
280+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT);
281+
282+
// Verify token is removed from database
283+
assertThat(refreshTokenJpaRepository.findByToken(refreshToken)).isEmpty();
284+
}
285+
286+
@Test
287+
@DisplayName("POST /auth/signup - Validation error for invalid email")
288+
void signup_ValidationError_InvalidEmail() {
289+
// Given
290+
String requestBody = """
291+
{
292+
"email": "not-an-email",
293+
"password": "password123",
294+
"name": "Test User"
295+
}
296+
""";
297+
298+
HttpHeaders headers = new HttpHeaders();
299+
headers.setContentType(MediaType.APPLICATION_JSON);
300+
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
301+
302+
// When
303+
var response = restTemplate.postForEntity("/auth/signup", request, String.class);
304+
305+
// Then
306+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
307+
}
308+
309+
@Test
310+
@DisplayName("POST /auth/signup - Validation error for short password")
311+
void signup_ValidationError_ShortPassword() {
312+
// Given
313+
String requestBody = """
314+
{
315+
"email": "[email protected]",
316+
"password": "short",
317+
"name": "Test User"
318+
}
319+
""";
320+
321+
HttpHeaders headers = new HttpHeaders();
322+
headers.setContentType(MediaType.APPLICATION_JSON);
323+
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
324+
325+
// When
326+
var response = restTemplate.postForEntity("/auth/signup", request, String.class);
327+
328+
// Then
329+
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
330+
}
331+
332+
// Helper method to extract refresh token from JSON response
333+
private String extractRefreshToken(String jsonResponse) {
334+
int startIndex = jsonResponse.indexOf("\"refreshToken\":\"") + 16;
335+
int endIndex = jsonResponse.indexOf("\"", startIndex);
336+
return jsonResponse.substring(startIndex, endIndex);
337+
}
338+
}

0 commit comments

Comments
 (0)