Skip to content

Commit 982a79c

Browse files
committed
Raise backend coverage to 95 percent
1 parent 5d8a826 commit 982a79c

40 files changed

Lines changed: 4483 additions & 21 deletions

.github/workflows/test.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,21 @@ jobs:
6161
6262
6363
64-
# 4. Gradle 빌드 & JUnit 테스트 실행
64+
# 4. Gradle 검증 및 커버리지 검사 실행
6565
- name: Run Tests with Gradle
6666
env:
6767
SPRING_PROFILES_ACTIVE: test
6868
run: |
6969
cd ontime-back
70-
./gradlew test
70+
./gradlew check
71+
72+
- name: Upload JaCoCo Coverage Report
73+
if: always()
74+
uses: actions/upload-artifact@v4
75+
with:
76+
name: jacoco-coverage-report
77+
path: ontime-back/build/reports/jacoco/test/
78+
if-no-files-found: warn
7179

7280
- name: Verify Flyway Migrations
7381
run: |

ontime-back/build.gradle

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
plugins {
22
id 'java'
3+
id 'jacoco'
34
id 'org.springframework.boot' version '3.3.4'
45
id 'io.spring.dependency-management' version '1.1.6'
56
id 'org.flywaydb.flyway' version '9.22.1'
@@ -70,4 +71,30 @@ dependencies {
7071

7172
tasks.named('test') {
7273
useJUnitPlatform()
74+
finalizedBy tasks.named('jacocoTestReport')
75+
}
76+
77+
tasks.named('jacocoTestReport') {
78+
dependsOn tasks.named('test')
79+
80+
reports {
81+
xml.required = true
82+
html.required = true
83+
}
84+
}
85+
86+
tasks.named('jacocoTestCoverageVerification') {
87+
dependsOn tasks.named('jacocoTestReport')
88+
89+
violationRules {
90+
rule {
91+
limit {
92+
minimum = 0.95
93+
}
94+
}
95+
}
96+
}
97+
98+
tasks.named('check') {
99+
dependsOn tasks.named('jacocoTestCoverageVerification')
73100
}

ontime-back/src/main/java/devkor/ontime_back/global/oauth/apple/AppleLoginService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ public Authentication handleRegister(String appleRefreshToken, OAuthAppleUserDto
132132

133133
User savedUser = userRepository.save(newUser);
134134

135-
String accessToken = jwtTokenProvider.createAccessToken(newUser.getEmail(), newUser.getId());
135+
String accessToken = jwtTokenProvider.createAccessToken(savedUser.getEmail(), savedUser.getId());
136136
String refreshToken = jwtTokenProvider.createRefreshToken();
137137

138138
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);

ontime-back/src/main/java/devkor/ontime_back/global/oauth/google/GoogleLoginService.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import jakarta.servlet.http.HttpServletResponse;
1818
import lombok.RequiredArgsConstructor;
1919
import lombok.extern.slf4j.Slf4j;
20+
import org.springframework.beans.factory.annotation.Autowired;
2021
import org.springframework.beans.factory.annotation.Value;
2122
import org.springframework.http.HttpEntity;
2223
import org.springframework.http.HttpHeaders;
@@ -48,17 +49,31 @@ public class GoogleLoginService {
4849
private static final String GOOGLE_REVOKE_URL = "https://oauth2.googleapis.com/revoke?token=";
4950

5051
private final List<String> validClientIds;
52+
private final RestTemplate revokeRestTemplate;
5153

54+
@Autowired
5255
public GoogleLoginService(
5356
JwtTokenProvider jwtTokenProvider,
5457
UserRepository userRepository,
5558
UserAlarmSettingRepository userAlarmSettingRepository,
5659
@Value("${google.web.client-id}") String webClientId,
5760
@Value("${google.app.client-id}") String appClientId
61+
) {
62+
this(jwtTokenProvider, userRepository, userAlarmSettingRepository, webClientId, appClientId, createRevokeRestTemplate());
63+
}
64+
65+
GoogleLoginService(
66+
JwtTokenProvider jwtTokenProvider,
67+
UserRepository userRepository,
68+
UserAlarmSettingRepository userAlarmSettingRepository,
69+
String webClientId,
70+
String appClientId,
71+
RestTemplate revokeRestTemplate
5872
) {
5973
this.jwtTokenProvider = jwtTokenProvider;
6074
this.userRepository = userRepository;
6175
this.userAlarmSettingRepository = userAlarmSettingRepository;
76+
this.revokeRestTemplate = revokeRestTemplate;
6277
this.validClientIds = Stream.concat(
6378
Stream.of(webClientId),
6479
Stream.of(appClientId.split(","))
@@ -71,6 +86,13 @@ public GoogleLoginService(
7186
.toList());
7287
}
7388

89+
private static RestTemplate createRevokeRestTemplate() {
90+
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
91+
requestFactory.setConnectTimeout(3000);
92+
requestFactory.setReadTimeout(3000);
93+
return new RestTemplate(requestFactory);
94+
}
95+
7496

7597
public Authentication handleLogin(OAuthGoogleRequestDto oAuthGoogleRequestDto, User user, HttpServletResponse response) throws IOException {
7698
user.updateSocialLoginToken(oAuthGoogleRequestDto.getRefreshToken());
@@ -129,7 +151,7 @@ public Authentication handleRegister(OAuthGoogleRequestDto oAuthGoogleRequestDto
129151

130152
User savedUser = userRepository.save(newUser);
131153

132-
String accessToken = jwtTokenProvider.createAccessToken(newUser.getEmail(), newUser.getId());
154+
String accessToken = jwtTokenProvider.createAccessToken(savedUser.getEmail(), savedUser.getId());
133155
String refreshToken = jwtTokenProvider.createRefreshToken();
134156

135157
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
@@ -228,18 +250,14 @@ public boolean revokeToken(Long userId) {
228250

229251
String googleRefreshToken = user.getSocialLoginToken();
230252

231-
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
232-
requestFactory.setConnectTimeout(3000);
233-
requestFactory.setReadTimeout(3000);
234-
RestTemplate restTemplate = new RestTemplate(requestFactory);
235253
String revokeUrl = GOOGLE_REVOKE_URL + googleRefreshToken;
236254

237255
HttpHeaders headers = new HttpHeaders();
238256
headers.set("Content-Type", "application/x-www-form-urlencoded");
239257

240258
HttpEntity<String> request = new HttpEntity<>(headers);
241259

242-
ResponseEntity<String> response = restTemplate.exchange(
260+
ResponseEntity<String> response = revokeRestTemplate.exchange(
243261
revokeUrl, HttpMethod.POST, request, String.class);
244262

245263
return response.getStatusCode().is2xxSuccessful();

ontime-back/src/main/java/devkor/ontime_back/global/oauth/kakao/KakaoLoginFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private Authentication handleRegister(OAuthKakaoUserDto oAuthKakaoUserDto, HttpS
119119

120120
User savedUser = userRepository.save(newUser);
121121

122-
String accessToken = jwtTokenProvider.createAccessToken(newUser.getEmail(), newUser.getId());
122+
String accessToken = jwtTokenProvider.createAccessToken(savedUser.getEmail(), savedUser.getId());
123123
String refreshToken = jwtTokenProvider.createRefreshToken();
124124
jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
125125
savedUser.updateAccessToken(accessToken);

ontime-back/src/test/java/devkor/ontime_back/ControllerTestSupport.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
AlarmController.class,
2828
SocialAuthController.class,
2929
AccountDeletionPageController.class,
30-
PrivacyPolicyController.class
30+
PrivacyPolicyController.class,
31+
DocumentController.class
3132
}
3233
)
3334
public abstract class ControllerTestSupport {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package devkor.ontime_back;
2+
3+
import devkor.ontime_back.entity.ApiLog;
4+
import devkor.ontime_back.logging.RequestLogPolicy;
5+
import devkor.ontime_back.response.ErrorCode;
6+
import devkor.ontime_back.response.GeneralException;
7+
import devkor.ontime_back.service.ApiLogService;
8+
import org.aspectj.lang.ProceedingJoinPoint;
9+
import org.junit.jupiter.api.AfterEach;
10+
import org.junit.jupiter.api.BeforeEach;
11+
import org.junit.jupiter.api.Test;
12+
import org.mockito.ArgumentCaptor;
13+
import org.springframework.http.HttpStatus;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.mock.web.MockHttpServletRequest;
16+
import org.springframework.mock.web.MockHttpServletResponse;
17+
import org.springframework.security.authentication.TestingAuthenticationToken;
18+
import org.springframework.security.core.context.SecurityContextHolder;
19+
import org.springframework.web.context.request.RequestContextHolder;
20+
import org.springframework.web.context.request.ServletRequestAttributes;
21+
22+
import static org.assertj.core.api.Assertions.assertThat;
23+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
24+
import static org.mockito.Mockito.mock;
25+
import static org.mockito.Mockito.verify;
26+
import static org.mockito.Mockito.when;
27+
28+
class LoggingAspectTest {
29+
30+
private ApiLogService apiLogService;
31+
private LoggingAspect loggingAspect;
32+
private MockHttpServletResponse response;
33+
34+
@BeforeEach
35+
void setUp() {
36+
apiLogService = mock(ApiLogService.class);
37+
loggingAspect = new LoggingAspect(apiLogService);
38+
response = new MockHttpServletResponse();
39+
TestingAuthenticationToken authentication = new TestingAuthenticationToken("user-7", null);
40+
authentication.setAuthenticated(true);
41+
SecurityContextHolder.getContext().setAuthentication(authentication);
42+
}
43+
44+
@AfterEach
45+
void tearDown() {
46+
RequestContextHolder.resetRequestAttributes();
47+
SecurityContextHolder.clearContext();
48+
}
49+
50+
@Test
51+
void logRequestStoresSuccessfulResponseStatusAndExposesRequestId() throws Throwable {
52+
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/alarm/status");
53+
request.setRemoteAddr("203.0.113.10");
54+
request.addHeader(RequestLogPolicy.REQUEST_ID_HEADER, "request-1");
55+
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
56+
ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
57+
when(joinPoint.proceed()).thenReturn(ResponseEntity.status(HttpStatus.CREATED).body("ok"));
58+
59+
Object result = loggingAspect.logRequest(joinPoint);
60+
61+
assertThat(result).isInstanceOf(ResponseEntity.class);
62+
assertThat(response.getHeader(RequestLogPolicy.REQUEST_ID_HEADER)).isEqualTo("request-1");
63+
ArgumentCaptor<ApiLog> captor = ArgumentCaptor.forClass(ApiLog.class);
64+
verify(apiLogService).saveLog(captor.capture());
65+
ApiLog log = captor.getValue();
66+
assertThat(log.getRequestUrl()).isEqualTo("/alarm/status");
67+
assertThat(log.getRequestMethod()).isEqualTo("POST");
68+
assertThat(log.getUserId()).isEqualTo("user-7");
69+
assertThat(log.getClientIp()).isEqualTo("203.0.113.10");
70+
assertThat(log.getResponseStatus()).isEqualTo(201);
71+
assertThat(log.getTakenTime()).isNotNegative();
72+
}
73+
74+
@Test
75+
void logRequestStoresBusinessErrorStatusBeforeRethrowing() throws Throwable {
76+
MockHttpServletRequest request = new MockHttpServletRequest("PATCH", "/alarm/settings");
77+
request.setRemoteAddr("203.0.113.20");
78+
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
79+
ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
80+
when(joinPoint.proceed()).thenThrow(new GeneralException(ErrorCode.DEVICE_SESSION_NOT_ACTIVE));
81+
82+
assertThatThrownBy(() -> loggingAspect.logRequest(joinPoint))
83+
.isInstanceOf(GeneralException.class);
84+
85+
ArgumentCaptor<ApiLog> captor = ArgumentCaptor.forClass(ApiLog.class);
86+
verify(apiLogService).saveLog(captor.capture());
87+
assertThat(captor.getValue().getResponseStatus()).isEqualTo(ErrorCode.DEVICE_SESSION_NOT_ACTIVE.getCode());
88+
}
89+
90+
@Test
91+
void logRequestStoresServerErrorStatusForUnexpectedExceptions() throws Throwable {
92+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/users/me");
93+
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
94+
ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
95+
when(joinPoint.proceed()).thenThrow(new IllegalStateException("boom"));
96+
97+
assertThatThrownBy(() -> loggingAspect.logRequest(joinPoint))
98+
.isInstanceOf(IllegalStateException.class);
99+
100+
ArgumentCaptor<ApiLog> captor = ArgumentCaptor.forClass(ApiLog.class);
101+
verify(apiLogService).saveLog(captor.capture());
102+
assertThat(captor.getValue().getResponseStatus()).isEqualTo(500);
103+
}
104+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package devkor.ontime_back.config;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.io.TempDir;
5+
import org.springframework.test.util.ReflectionTestUtils;
6+
7+
import java.io.InputStream;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Path;
10+
import java.util.Base64;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class FirebaseInitializationTest {
15+
16+
@TempDir
17+
private Path tempDir;
18+
19+
@Test
20+
void resolveCredentialsPrefersBase64EncodedCredentials() throws Exception {
21+
FirebaseInitialization initialization = new FirebaseInitialization();
22+
ReflectionTestUtils.setField(
23+
initialization,
24+
"firebaseCredentialsBase64",
25+
Base64.getEncoder().encodeToString("base64-json".getBytes(StandardCharsets.UTF_8))
26+
);
27+
ReflectionTestUtils.setField(initialization, "firebaseCredentialsJson", "inline-json");
28+
29+
try (InputStream credentials = ReflectionTestUtils.invokeMethod(initialization, "resolveCredentials")) {
30+
assertThat(new String(credentials.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("base64-json");
31+
}
32+
}
33+
34+
@Test
35+
void resolveCredentialsFallsBackToInlineJson() throws Exception {
36+
FirebaseInitialization initialization = new FirebaseInitialization();
37+
ReflectionTestUtils.setField(initialization, "firebaseCredentialsJson", "inline-json");
38+
39+
try (InputStream credentials = ReflectionTestUtils.invokeMethod(initialization, "resolveCredentials")) {
40+
assertThat(new String(credentials.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("inline-json");
41+
}
42+
}
43+
44+
@Test
45+
void resolveCredentialsUsesConfiguredPathBeforeGoogleApplicationCredentials() throws Exception {
46+
Path configuredPath = tempDir.resolve("firebase.json");
47+
Path googlePath = tempDir.resolve("google.json");
48+
java.nio.file.Files.writeString(configuredPath, "configured-path-json");
49+
java.nio.file.Files.writeString(googlePath, "google-path-json");
50+
FirebaseInitialization initialization = new FirebaseInitialization();
51+
ReflectionTestUtils.setField(initialization, "firebaseCredentialsPath", configuredPath.toString());
52+
ReflectionTestUtils.setField(initialization, "googleApplicationCredentials", googlePath.toString());
53+
54+
try (InputStream credentials = ReflectionTestUtils.invokeMethod(initialization, "resolveCredentials")) {
55+
assertThat(new String(credentials.readAllBytes(), StandardCharsets.UTF_8)).isEqualTo("configured-path-json");
56+
}
57+
}
58+
59+
@Test
60+
void resolveCredentialsReturnsNullWhenNoCredentialsAreConfigured() throws Exception {
61+
FirebaseInitialization initialization = new FirebaseInitialization();
62+
63+
InputStream credentials = ReflectionTestUtils.invokeMethod(initialization, "resolveCredentials");
64+
65+
assertThat(credentials).isNull();
66+
}
67+
}

0 commit comments

Comments
 (0)