Skip to content
Merged
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI with Gradle

on:
pull_request:
branches: [ "develop", "release", "master" ]

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
checks: write

steps:
- name: Checkout the code
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

- name: Make Gradle wrapper executable
run: chmod +x ./gradlew

- name: Build with Gradle Wrapper
run: ./gradlew build

- name: Publish Test Report
uses: mikepenz/action-junit-report@v5
if: success() || failure()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,17 @@
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import java.util.stream.Collectors;

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

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

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

private final AppleOAuthClientProperties appleOAuthClientProperties;
private PrivateKey privateKey;

@PostConstruct
private void initPrivateKey() {
privateKey = readPrivateKey();
privateKey = loadPrivateKey();
}

public String generateClientSecret() {
Expand All @@ -57,16 +53,14 @@ public String generateClientSecret() {
.compact();
}

private PrivateKey readPrivateKey() {
try (InputStream is = getClass().getClassLoader().getResourceAsStream(SECRET_KEY_PATH);
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {

String secretKey = reader.lines().collect(Collectors.joining("\n"));
private PrivateKey loadPrivateKey() {
try {
String secretKey = appleOAuthClientProperties.secretKey();
byte[] encoded = Base64.decodeBase64(secretKey);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return keyFactory.generatePrivate(keySpec);
} catch (Exception e) {
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public record AppleOAuthClientProperties(
String publicKeyUrl,
String clientId,
String teamId,
String keyId
String keyId,
String secretKey
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.ZoneId;
import java.time.ZonedDateTime;

import static java.time.ZoneOffset.UTC;
import static java.time.temporal.ChronoUnit.MICROS;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
Expand All @@ -24,12 +26,12 @@ public abstract class BaseEntity {

@PrePersist
public void onPrePersist() {
this.createdAt = ZonedDateTime.now(ZoneId.of("UTC"));
this.createdAt = ZonedDateTime.now(UTC).truncatedTo(MICROS); // 나노초 6자리 까지만 저장
this.updatedAt = this.createdAt;
}

@PreUpdate
public void onPreUpdate() {
this.updatedAt = ZonedDateTime.now(ZoneId.of("UTC"));
this.updatedAt = ZonedDateTime.now(UTC).truncatedTo(MICROS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ActiveProfiles;

import java.sql.DatabaseMetaData;
import java.sql.SQLException;
Expand All @@ -20,7 +19,6 @@

@Disabled
@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY)
@ActiveProfiles("test")
@DataJpaTest
class DatabaseConnectionTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.ActiveProfiles;

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

@Disabled
@ActiveProfiles("test")
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class RedisConnectionTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public void setUpUserAndToken() {
ApplicationsResponse response = RestAssured.given().log().all()
.header("Authorization", "Bearer " + accessToken)
.when().log().all()
.get("/applications")
.get("/applications/competitors")
.then().log().all()
.statusCode(200)
.extract().as(ApplicationsResponse.class);
Expand All @@ -119,30 +119,24 @@ public void setUpUserAndToken() {
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
List.of(ApplicantResponse.of(나의_지원정보, true))),
UniversityApplicantsResponse.of(메이지대학_지원_정보,
List.of(ApplicantResponse.of(사용자2_지원정보, false))),
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보,
List.of(ApplicantResponse.of(사용자3_지원정보, false)))
UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보,
List.of())
));
assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of(
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
List.of(ApplicantResponse.of(나의_지원정보, true))),
List.of(ApplicantResponse.of(나의_지원정보, false))),
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
UniversityApplicantsResponse.of(메이지대학_지원_정보,
List.of(ApplicantResponse.of(사용자3_지원정보, false))),
UniversityApplicantsResponse.of(그라츠대학_지원_정보,
List.of(ApplicantResponse.of(사용자2_지원정보, false)))
List.of(ApplicantResponse.of(사용자1_지원정보, true))),
UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보,
List.of())
));
assertThat(thirdChoiceApplicants).containsAnyElementsOf(List.of(
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
List.of()),
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
List.of()),
UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보,
List.of(ApplicantResponse.of(나의_지원정보, true))),
UniversityApplicantsResponse.of(서던덴마크대학교_지원_정보,
List.of(ApplicantResponse.of(사용자2_지원정보, false))),
UniversityApplicantsResponse.of(그라츠공과대학_지원_정보,
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
UniversityApplicantsResponse.of(메이지대학_지원_정보,
List.of(ApplicantResponse.of(사용자3_지원정보, false)))
List.of(ApplicantResponse.of(나의_지원정보, true)))
));
}

Expand All @@ -151,7 +145,7 @@ public void setUpUserAndToken() {
ApplicationsResponse response = RestAssured.given().log().all()
.header("Authorization", "Bearer " + accessToken)
.when().log().all()
.get("/applications?region=" + 영미권.getCode())
.get("/applications/competitors?region=" + 영미권.getCode())
.then().log().all()
.statusCode(200)
.extract().as(ApplicationsResponse.class);
Expand All @@ -163,68 +157,22 @@ public void setUpUserAndToken() {
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
List.of(ApplicantResponse.of(사용자1_지원정보, false))),
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
List.of(ApplicantResponse.of(나의_지원정보, true))),
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보,
List.of(ApplicantResponse.of(사용자3_지원정보, false)))));
List.of(ApplicantResponse.of(나의_지원정보, true)))
));
assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of(
UniversityApplicantsResponse.of(괌대학_A_지원_정보,
List.of(ApplicantResponse.of(나의_지원정보, true))),
UniversityApplicantsResponse.of(괌대학_B_지원_정보,
List.of(ApplicantResponse.of(사용자1_지원정보, false)))));
}

@Test
void 대학_국문_이름으로_필터링해서_지원자를_조회한다() {
ApplicationsResponse response = RestAssured.given().log().all()
.header("Authorization", "Bearer " + accessToken)
.when().log().all()
.get("/applications?keyword=라")
.then().log().all()
.statusCode(200)
.extract().as(ApplicationsResponse.class);

List<UniversityApplicantsResponse> firstChoiceApplicants = response.firstChoice();
List<UniversityApplicantsResponse> secondChoiceApplicants = response.secondChoice();

assertThat(firstChoiceApplicants).containsExactlyInAnyOrder(
UniversityApplicantsResponse.of(그라츠대학_지원_정보, List.of()),
UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, List.of()),
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보,
List.of(ApplicantResponse.of(사용자3_지원정보, false))));
assertThat(secondChoiceApplicants).containsAnyElementsOf(List.of(
UniversityApplicantsResponse.of(그라츠대학_지원_정보,
List.of(ApplicantResponse.of(사용자2_지원정보, false))),
UniversityApplicantsResponse.of(그라츠공과대학_지원_정보,
List.of(ApplicantResponse.of(사용자3_지원정보, false))),
UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, List.of())));
}

@Test
void 국가_국문_이름으로_필터링해서_지원자를_조회한다() {
ApplicationsResponse response = RestAssured.given().log().all()
.header("Authorization", "Bearer " + accessToken)
.when().log().all()
.get("/applications?keyword=일본")
.then().log().all()
.statusCode(200)
.extract().as(ApplicationsResponse.class);

List<UniversityApplicantsResponse> firstChoiceApplicants = response.firstChoice();
List<UniversityApplicantsResponse> secondChoiceApplicants = response.secondChoice();

assertThat(firstChoiceApplicants).containsExactlyInAnyOrder(
UniversityApplicantsResponse.of(메이지대학_지원_정보,
List.of(ApplicantResponse.of(사용자2_지원정보, false))));
assertThat(secondChoiceApplicants).containsExactlyInAnyOrder(
UniversityApplicantsResponse.of(메이지대학_지원_정보, List.of()));
List.of(ApplicantResponse.of(사용자1_지원정보, false)))
));
}

@Test
void 지원자를_조회할_때_이전학기_지원자는_조회되지_않는다() {
ApplicationsResponse response = RestAssured.given().log().all()
.header("Authorization", "Bearer " + accessToken)
.when().log().all()
.get("/applications")
.get("/applications/competitors")
.then().log().all()
.statusCode(200)
.extract().as(ApplicationsResponse.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Objects;

@ActiveProfiles("test")
@Component
public class DatabaseCleaner {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,24 @@
package com.example.solidconnection.support;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;

import javax.sql.DataSource;
public class MySQLTestContainer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

@TestConfiguration
public class MySQLTestContainer {

@Container
private static final MySQLContainer<?> CONTAINER = new MySQLContainer<>("mysql:8.0");

@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(CONTAINER.getJdbcUrl())
.username(CONTAINER.getUsername())
.password(CONTAINER.getPassword())
.driverClassName(CONTAINER.getDriverClassName())
.build();
static {
CONTAINER.start();
}

@PostConstruct
void startContainer() {
if (!CONTAINER.isRunning()) {
CONTAINER.start();
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of(
"spring.datasource.url=" + CONTAINER.getJdbcUrl(),
"spring.datasource.username=" + CONTAINER.getUsername(),
"spring.datasource.password=" + CONTAINER.getPassword()
).applyTo(applicationContext.getEnvironment());
}
}
Loading
Loading