diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..66a0cbfc5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +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 diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java index 2de0b7291..87b3ad6b1 100644 --- a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java @@ -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 * */ @@ -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() { @@ -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); } } diff --git a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java index 609e9ee89..c04908583 100644 --- a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java +++ b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java @@ -10,6 +10,7 @@ public record AppleOAuthClientProperties( String publicKeyUrl, String clientId, String teamId, - String keyId + String keyId, + String secretKey ) { } diff --git a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java index ee74bb90b..51abe506e 100644 --- a/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java @@ -71,8 +71,10 @@ class 댓글_조회_테스트 { () -> assertThat(response.parentId()).isNull(), () -> assertThat(response.content()).isEqualTo(parentComment.getContent()), () -> assertThat(response.isOwner()).isTrue(), - () -> assertThat(response.createdAt()).isEqualTo(parentComment.getCreatedAt()), - () -> assertThat(response.updatedAt()).isEqualTo(parentComment.getUpdatedAt()), + () -> assertThat(response.createdAt().withNano(0)) + .isEqualTo(parentComment.getCreatedAt().withNano(0)), + () -> assertThat(response.updatedAt().withNano(0)) + .isEqualTo(parentComment.getUpdatedAt().withNano(0)), () -> assertThat(response.postFindSiteUserResponse().id()) .isEqualTo(parentComment.getSiteUser().getId()), @@ -89,8 +91,10 @@ class 댓글_조회_테스트 { () -> assertThat(response.parentId()).isEqualTo(parentComment.getId()), () -> assertThat(response.content()).isEqualTo(childComment.getContent()), () -> assertThat(response.isOwner()).isFalse(), - () -> assertThat(response.createdAt()).isEqualTo(childComment.getCreatedAt()), - () -> assertThat(response.updatedAt()).isEqualTo(childComment.getUpdatedAt()), + () -> assertThat(response.createdAt().withNano(0)) + .isEqualTo(childComment.getCreatedAt().withNano(0)), + () -> assertThat(response.updatedAt().withNano(0)) + .isEqualTo(childComment.getUpdatedAt().withNano(0)), () -> assertThat(response.postFindSiteUserResponse().id()) .isEqualTo(childComment.getSiteUser().getId()), diff --git a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java index d156cf485..ca3c64c7a 100644 --- a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java @@ -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; @@ -20,7 +19,6 @@ @Disabled @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY) -@ActiveProfiles("test") @DataJpaTest class DatabaseConnectionTest { diff --git a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java index 69fcedaef..527ae7e07 100644 --- a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java @@ -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 { diff --git a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java index bb77f82f2..aee6a2bc6 100644 --- a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java +++ b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java @@ -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 { diff --git a/src/test/java/com/example/solidconnection/support/RedisTestContainer.java b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java index 39f35c2d5..212499361 100644 --- a/src/test/java/com/example/solidconnection/support/RedisTestContainer.java +++ b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java @@ -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 { - @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()); } } diff --git a/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java index 339672e60..2f09151d6 100644 --- a/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java +++ b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java @@ -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; @@ -13,7 +12,6 @@ @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) -@ActiveProfiles("test") @Testcontainers @Import(MySQLTestContainer.class) @Target(ElementType.TYPE) diff --git a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java index fe9b74f60..8d0d5d99a 100644 --- a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java +++ b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java @@ -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; @@ -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 { diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 000000000..e8db19044 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,73 @@ +spring: + +# db + data: + redis: + host: localhost + port: 6379 + jpa: + hibernate: + ddl-auto: create + generate-ddl: true + show-sql: true + database: mysql + properties: + hibernate: + format_sql: true + flyway: + enabled: false + +# cloud +cloud: + aws: + credentials: + access-key: access-key + secret-key: access-key + region: + static: ap-northeast-2 + stack: + auto: false + s3: + bucket: solid-connection-uploaded + url: + default: default-url + uploaded: uploaded-url + cloudFront: + url: + default: default-url + uploaded: uploaded-url + +# variable +view: + count: + scheduling: + delay: 3000 +oauth: + apple: + token-url: "https://appleid.apple.com/auth/token" + client-secret-audience-url: "https://appleid.apple.com" + public-key-url: "https://appleid.apple.com/auth/keys" + client-id: client-id + team-id: team-id + key-id: key-id + redirect-url: "https://localhost:8080/auth/apple" + secret-key: MEECAQAwEwYHKoZIzj0CAQYIKoZIzj0DAQcEJzAlAgEBBCAfGIQ3TtNYAZG7i3m72odmdhfymkM9wAFg2rEL2RKUEA== +kakao: + redirect-url: "http://localhost:8080/auth/kakao" + client-id: client-id + token-url: "https://kauth.kakao.com/oauth/token" + user-info_url: "https://kapi.kakao.com/v2/user/me" +sentry: + environment: test + dsn: "https://test-public-key@sentry.test-domain.io/123456" + send-default-pii: true + traces-sample-rate: 1.0 + exception-resolver-order: -2147483647 +university: + term: 2024-1 +jwt: + secret: + 1234567-1234-1234-1234-12345678901 +cors: + allowed-origins: + - "http://localhost:8080"