Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = generatePrivateKey();
}

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 generatePrivateKey() {
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 @@ -12,6 +12,8 @@
import java.time.ZoneId;
import java.time.ZonedDateTime;

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(ZoneId.of("UTC")).truncatedTo(MICROS); // 나노초 6자리 까지만 저장
this.updatedAt = this.createdAt;
}

@PreUpdate
public void onPreUpdate() {
this.updatedAt = ZonedDateTime.now(ZoneId.of("UTC"));
this.updatedAt = ZonedDateTime.now(ZoneId.of("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,28 +1,25 @@
package com.example.solidconnection.support;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;

@TestConfiguration
public class RedisTestContainer {
public class RedisTestContainer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

@Container
private static final GenericContainer<?> CONTAINER = new GenericContainer<>("redis:7.0");
private static final int ORIGINAL_PORT = 6379;
private static final GenericContainer<?> CONTAINER = new GenericContainer<>("redis:7.0")
.withExposedPorts(ORIGINAL_PORT);

@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.redis.host", CONTAINER::getHost);
registry.add("spring.redis.port", CONTAINER::getFirstMappedPort);
static {
CONTAINER.start();
}

@PostConstruct
void startContainer() {
if (!CONTAINER.isRunning()) {
CONTAINER.start();
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of(
"spring.data.redis.host=" + CONTAINER.getHost(),
"spring.data.redis.port=" + CONTAINER.getMappedPort(ORIGINAL_PORT)
).applyTo(applicationContext.getEnvironment());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.lang.annotation.ElementType;
Expand All @@ -13,7 +12,6 @@

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
@Testcontainers
@Import(MySQLTestContainer.class)
@Target(ElementType.TYPE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.lang.annotation.ElementType;
Expand All @@ -13,11 +13,11 @@
import java.lang.annotation.Target;

@ExtendWith({DatabaseClearExtension.class})
@ContextConfiguration(initializers = RedisTestContainer.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@ActiveProfiles("test")
@Testcontainers
@Import({MySQLTestContainer.class, RedisTestContainer.class})
@Import({MySQLTestContainer.class})
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestContainerSpringBootTest {
Expand Down
Loading
Loading