Skip to content

Commit 6dfc6ed

Browse files
authored
chore: ci 스크립트 작성 및 ci 통과를 위한 코드 수정 (#266)
* chore: ci 스크립트 작성 * chore: 테스트용 application.yml 분리 * test: test profile 제거 * refactor: 애플 시크릿키를 yml로 이동 * refactor: 함수 이름 변경 * refactor: 오타 수정 * refactor: redis testcontainers 설정 수정 * refactor: ddl-auto 옵션 변경 - create-drop을 create로 변경 - 테스트 컨테이너를 실행해서 테스트하기 때문에, 테스트가 종료되면 당연히 테이블이 삭제된다. 따라서 create-drop을 하는것과 create를 하는 것이 동일하게 동작한다. 굳이 create로 바꾼 것은 삭제 시 FK 위반으로 WARN 로그가 불필요하게 백몇줄이 찍히는걸 막기 위해서. * test: e2e 테스트 수정 - 깨지는 테스트 수정 - 비지니스 로직 변경으로 더 이상 해당되지 않는 테스트 삭제 * chore: createAt과 updateAt의 나노초 길이 고정 * style: 주석 추가 * refactor: MySQL 테스트 컨테이너 설정 변경 - redis 테스트 컨테이너와 동일하게 설정하여 불필요한 인지 부하를 줄인다.
1 parent 1bce344 commit 6dfc6ed

File tree

13 files changed

+175
-143
lines changed

13 files changed

+175
-143
lines changed

.github/workflows/ci.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: CI with Gradle
2+
3+
on:
4+
pull_request:
5+
branches: [ "develop", "release", "master" ]
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: read
12+
checks: write
13+
14+
steps:
15+
- name: Checkout the code
16+
uses: actions/checkout@v4
17+
18+
- name: Set up JDK 17
19+
uses: actions/setup-java@v4
20+
with:
21+
java-version: '17'
22+
distribution: 'temurin'
23+
24+
- name: Setup Gradle
25+
uses: gradle/actions/setup-gradle@v4
26+
27+
- name: Make Gradle wrapper executable
28+
run: chmod +x ./gradlew
29+
30+
- name: Build with Gradle Wrapper
31+
run: ./gradlew build
32+
33+
- name: Publish Test Report
34+
uses: mikepenz/action-junit-report@v5
35+
if: success() || failure()
36+
with:
37+
report_paths: '**/build/test-results/test/TEST-*.xml'

src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,17 @@
99
import org.apache.tomcat.util.codec.binary.Base64;
1010
import org.springframework.stereotype.Component;
1111

12-
import java.io.BufferedReader;
13-
import java.io.InputStream;
14-
import java.io.InputStreamReader;
15-
import java.nio.charset.StandardCharsets;
1612
import java.security.KeyFactory;
13+
import java.security.NoSuchAlgorithmException;
1714
import java.security.PrivateKey;
15+
import java.security.spec.InvalidKeySpecException;
1816
import java.security.spec.PKCS8EncodedKeySpec;
1917
import java.util.Date;
20-
import java.util.stream.Collectors;
2118

2219
import static com.example.solidconnection.custom.exception.ErrorCode.FAILED_TO_READ_APPLE_PRIVATE_KEY;
2320

2421
/*
25-
* 애플 OAuth 에 필요하 클라이언트 시크릿은 매번 동적으로 생성해야 한다.
22+
* 애플 OAuth 에 필요한 클라이언트 시크릿은 매번 동적으로 생성해야 한다.
2623
* 클라이언트 시크릿은 애플 개발자 계정에서 발급받은 개인키(*.p8)를 사용하여 JWT 를 생성한다.
2724
* https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret
2825
* */
@@ -32,14 +29,13 @@ public class AppleOAuthClientSecretProvider {
3229

3330
private static final String KEY_ID_HEADER = "kid";
3431
private static final long TOKEN_DURATION = 1000 * 60 * 10; // 10min
35-
private static final String SECRET_KEY_PATH = "secret/AppleOAuthKey.p8";
3632

3733
private final AppleOAuthClientProperties appleOAuthClientProperties;
3834
private PrivateKey privateKey;
3935

4036
@PostConstruct
4137
private void initPrivateKey() {
42-
privateKey = readPrivateKey();
38+
privateKey = loadPrivateKey();
4339
}
4440

4541
public String generateClientSecret() {
@@ -57,16 +53,14 @@ public String generateClientSecret() {
5753
.compact();
5854
}
5955

60-
private PrivateKey readPrivateKey() {
61-
try (InputStream is = getClass().getClassLoader().getResourceAsStream(SECRET_KEY_PATH);
62-
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
63-
64-
String secretKey = reader.lines().collect(Collectors.joining("\n"));
56+
private PrivateKey loadPrivateKey() {
57+
try {
58+
String secretKey = appleOAuthClientProperties.secretKey();
6559
byte[] encoded = Base64.decodeBase64(secretKey);
6660
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
6761
KeyFactory keyFactory = KeyFactory.getInstance("EC");
6862
return keyFactory.generatePrivate(keySpec);
69-
} catch (Exception e) {
63+
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
7064
throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY);
7165
}
7266
}

src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public record AppleOAuthClientProperties(
1010
String publicKeyUrl,
1111
String clientId,
1212
String teamId,
13-
String keyId
13+
String keyId,
14+
String secretKey
1415
) {
1516
}

src/main/java/com/example/solidconnection/entity/common/BaseEntity.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
import org.hibernate.annotations.DynamicUpdate;
1010
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
1111

12-
import java.time.ZoneId;
1312
import java.time.ZonedDateTime;
1413

14+
import static java.time.ZoneOffset.UTC;
15+
import static java.time.temporal.ChronoUnit.MICROS;
16+
1517
@MappedSuperclass
1618
@EntityListeners(AuditingEntityListener.class)
1719
@Getter
@@ -24,12 +26,12 @@ public abstract class BaseEntity {
2426

2527
@PrePersist
2628
public void onPrePersist() {
27-
this.createdAt = ZonedDateTime.now(ZoneId.of("UTC"));
29+
this.createdAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); // 나노초 6자리 까지만 저장
2830
this.updatedAt = this.createdAt;
2931
}
3032

3133
@PreUpdate
3234
public void onPreUpdate() {
33-
this.updatedAt = ZonedDateTime.now(ZoneId.of("UTC"));
35+
this.updatedAt = ZonedDateTime.now(UTC).truncatedTo(MICROS);
3436
}
3537
}

src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
99
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
1010
import org.springframework.jdbc.core.JdbcTemplate;
11-
import org.springframework.test.context.ActiveProfiles;
1211

1312
import java.sql.DatabaseMetaData;
1413
import java.sql.SQLException;
@@ -20,7 +19,6 @@
2019

2120
@Disabled
2221
@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY)
23-
@ActiveProfiles("test")
2422
@DataJpaTest
2523
class DatabaseConnectionTest {
2624

src/test/java/com/example/solidconnection/database/RedisConnectionTest.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,10 @@
66
import org.springframework.beans.factory.annotation.Autowired;
77
import org.springframework.boot.test.context.SpringBootTest;
88
import org.springframework.data.redis.core.RedisTemplate;
9-
import org.springframework.test.context.ActiveProfiles;
109

1110
import static org.assertj.core.api.Assertions.assertThat;
1211

1312
@Disabled
14-
@ActiveProfiles("test")
1513
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
1614
class RedisConnectionTest {
1715

src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java

Lines changed: 18 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public void setUpUserAndToken() {
105105
ApplicationsResponse response = RestAssured.given().log().all()
106106
.header("Authorization", "Bearer " + accessToken)
107107
.when().log().all()
108-
.get("/applications")
108+
.get("/applications/competitors")
109109
.then().log().all()
110110
.statusCode(200)
111111
.extract().as(ApplicationsResponse.class);
@@ -119,30 +119,24 @@ public void setUpUserAndToken() {
119119
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
120120
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
121121
List.of(ApplicantResponse.of(나의_지원정보, true))),
122-
UniversityApplicantsResponse.of(메이지대학_지원_정보,
123-
List.of(ApplicantResponse.of(사용자2_지원정보, false))),
124-
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보,
125-
List.of(ApplicantResponse.of(사용자3_지원정보, false)))
122+
UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보,
123+
List.of())
126124
));
127125
assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of(
128126
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
129-
List.of(ApplicantResponse.of(나의_지원정보, true))),
127+
List.of(ApplicantResponse.of(나의_지원정보, false))),
130128
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
131-
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
132-
UniversityApplicantsResponse.of(메이지대학_지원_정보,
133-
List.of(ApplicantResponse.of(사용자3_지원정보, false))),
134-
UniversityApplicantsResponse.of(그라츠대학_지원_정보,
135-
List.of(ApplicantResponse.of(사용자2_지원정보, false)))
129+
List.of(ApplicantResponse.of(사용자1_지원정보, true))),
130+
UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보,
131+
List.of())
136132
));
137133
assertThat(thirdChoiceApplicants).containsAnyElementsOf(List.of(
134+
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
135+
List.of()),
136+
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
137+
List.of()),
138138
UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보,
139-
List.of(ApplicantResponse.of(나의_지원정보, true))),
140-
UniversityApplicantsResponse.of(서던덴마크대학교_지원_정보,
141-
List.of(ApplicantResponse.of(사용자2_지원정보, false))),
142-
UniversityApplicantsResponse.of(그라츠공과대학_지원_정보,
143-
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
144-
UniversityApplicantsResponse.of(메이지대학_지원_정보,
145-
List.of(ApplicantResponse.of(사용자3_지원정보, false)))
139+
List.of(ApplicantResponse.of(나의_지원정보, true)))
146140
));
147141
}
148142

@@ -151,7 +145,7 @@ public void setUpUserAndToken() {
151145
ApplicationsResponse response = RestAssured.given().log().all()
152146
.header("Authorization", "Bearer " + accessToken)
153147
.when().log().all()
154-
.get("/applications?region=" + 영미권.getCode())
148+
.get("/applications/competitors?region=" + 영미권.getCode())
155149
.then().log().all()
156150
.statusCode(200)
157151
.extract().as(ApplicationsResponse.class);
@@ -163,68 +157,22 @@ public void setUpUserAndToken() {
163157
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
164158
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
165159
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
166-
List.of(ApplicantResponse.of(나의_지원정보, true))),
167-
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보,
168-
List.of(ApplicantResponse.of(사용자3_지원정보, false)))));
160+
List.of(ApplicantResponse.of(나의_지원정보, true)))
161+
));
169162
assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of(
170163
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
171164
List.of(ApplicantResponse.of(나의_지원정보, true))),
172165
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
173-
List.of(ApplicantResponse.of(사용자1_지원정보, false)))));
174-
}
175-
176-
@Test
177-
void 대학_국문_이름으로_필터링해서_지원자를_조회한다() {
178-
ApplicationsResponse response = RestAssured.given().log().all()
179-
.header("Authorization", "Bearer " + accessToken)
180-
.when().log().all()
181-
.get("/applications?keyword=라")
182-
.then().log().all()
183-
.statusCode(200)
184-
.extract().as(ApplicationsResponse.class);
185-
186-
List<UniversityApplicantsResponse> firstChoiceApplicants = response.firstChoice();
187-
List<UniversityApplicantsResponse> secondChoiceApplicants = response.secondChoice();
188-
189-
assertThat(firstChoiceApplicants).containsExactlyInAnyOrder(
190-
UniversityApplicantsResponse.of(그라츠대학_지원_정보, List.of()),
191-
UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, List.of()),
192-
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보,
193-
List.of(ApplicantResponse.of(사용자3_지원정보, false))));
194-
assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of(
195-
UniversityApplicantsResponse.of(그라츠대학_지원_정보,
196-
List.of(ApplicantResponse.of(사용자2_지원정보, false))),
197-
UniversityApplicantsResponse.of(그라츠공과대학_지원_정보,
198-
List.of(ApplicantResponse.of(사용자3_지원정보, false))),
199-
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, List.of())));
200-
}
201-
202-
@Test
203-
void 국가_국문_이름으로_필터링해서_지원자를_조회한다() {
204-
ApplicationsResponse response = RestAssured.given().log().all()
205-
.header("Authorization", "Bearer " + accessToken)
206-
.when().log().all()
207-
.get("/applications?keyword=일본")
208-
.then().log().all()
209-
.statusCode(200)
210-
.extract().as(ApplicationsResponse.class);
211-
212-
List<UniversityApplicantsResponse> firstChoiceApplicants = response.firstChoice();
213-
List<UniversityApplicantsResponse> secondChoiceApplicants = response.secondChoice();
214-
215-
assertThat(firstChoiceApplicants).containsExactlyInAnyOrder(
216-
UniversityApplicantsResponse.of(메이지대학_지원_정보,
217-
List.of(ApplicantResponse.of(사용자2_지원정보, false))));
218-
assertThat(secondChoiceApplicants).containsExactlyInAnyOrder(
219-
UniversityApplicantsResponse.of(메이지대학_지원_정보, List.of()));
166+
List.of(ApplicantResponse.of(사용자1_지원정보, false)))
167+
));
220168
}
221169

222170
@Test
223171
void 지원자를_조회할_때_이전학기_지원자는_조회되지_않는다() {
224172
ApplicationsResponse response = RestAssured.given().log().all()
225173
.header("Authorization", "Bearer " + accessToken)
226174
.when().log().all()
227-
.get("/applications")
175+
.get("/applications/competitors")
228176
.then().log().all()
229177
.statusCode(200)
230178
.extract().as(ApplicationsResponse.class);

src/test/java/com/example/solidconnection/support/DatabaseCleaner.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
import org.springframework.beans.factory.annotation.Autowired;
66
import org.springframework.data.redis.core.RedisTemplate;
77
import org.springframework.stereotype.Component;
8-
import org.springframework.test.context.ActiveProfiles;
98
import org.springframework.transaction.annotation.Transactional;
109

1110
import java.util.List;
1211
import java.util.Objects;
1312

14-
@ActiveProfiles("test")
1513
@Component
1614
public class DatabaseCleaner {
1715

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,24 @@
11
package com.example.solidconnection.support;
22

3-
import jakarta.annotation.PostConstruct;
4-
import org.springframework.boot.jdbc.DataSourceBuilder;
5-
import org.springframework.boot.test.context.TestConfiguration;
6-
import org.springframework.context.annotation.Bean;
3+
import org.springframework.boot.test.util.TestPropertyValues;
4+
import org.springframework.context.ApplicationContextInitializer;
5+
import org.springframework.context.ConfigurableApplicationContext;
76
import org.testcontainers.containers.MySQLContainer;
8-
import org.testcontainers.junit.jupiter.Container;
97

10-
import javax.sql.DataSource;
8+
public class MySQLTestContainer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
119

12-
@TestConfiguration
13-
public class MySQLTestContainer {
14-
15-
@Container
1610
private static final MySQLContainer<?> CONTAINER = new MySQLContainer<>("mysql:8.0");
1711

18-
@Bean
19-
public DataSource dataSource() {
20-
return DataSourceBuilder.create()
21-
.url(CONTAINER.getJdbcUrl())
22-
.username(CONTAINER.getUsername())
23-
.password(CONTAINER.getPassword())
24-
.driverClassName(CONTAINER.getDriverClassName())
25-
.build();
12+
static {
13+
CONTAINER.start();
2614
}
2715

28-
@PostConstruct
29-
void startContainer() {
30-
if (!CONTAINER.isRunning()) {
31-
CONTAINER.start();
32-
}
16+
@Override
17+
public void initialize(ConfigurableApplicationContext applicationContext) {
18+
TestPropertyValues.of(
19+
"spring.datasource.url=" + CONTAINER.getJdbcUrl(),
20+
"spring.datasource.username=" + CONTAINER.getUsername(),
21+
"spring.datasource.password=" + CONTAINER.getPassword()
22+
).applyTo(applicationContext.getEnvironment());
3323
}
3424
}

0 commit comments

Comments
 (0)