From f25968c1456978a39ed4bc24778b593d09a5dd7b Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Wed, 8 Jan 2025 04:43:51 +0900 Subject: [PATCH 01/23] =?UTF-8?q?refactor:=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: testcontainers 의존성 추가 * test: MySQL, Redis 테스트 컨테이너 추가 * test: 커스텀 어노테이션 생성 - 어노테이션으로 테스트 컨테이너를 사용할 수 있도록 * test: TestContainer 어노테이션 적용 * test: DatabaseCleaner에 MySQL 문법 적용 * test: 깨지는 테스트 수정 * test: 불필요한 테스트 disabled * chore: redis test container 의존성 제거 --- build.gradle | 13 +++++-- src/main/resources/secret | 2 +- .../PostLikeCountConcurrencyTest.java | 6 ++-- .../PostViewCountConcurrencyTest.java | 6 ++-- .../concurrency/ThunderingHerdTest.java | 6 ++-- .../database/DatabaseConnectionTest.java | 2 ++ .../database/RedisConnectionTest.java | 2 ++ .../solidconnection/e2e/BaseEndToEndTest.java | 6 ++-- .../solidconnection/e2e/SignUpTest.java | 2 +- .../e2e/UniversityDataSetUpEndToEndTest.java | 6 ++-- .../support/DatabaseCleaner.java | 9 ++--- .../support/MySQLTestContainer.java | 34 +++++++++++++++++++ .../support/RedisTestContainer.java | 28 +++++++++++++++ .../support/TestContainerDataJpaTest.java | 22 ++++++++++++ .../support/TestContainerSpringBootTest.java | 22 ++++++++++++ .../unit/repository/BoardRepositoryTest.java | 6 ++-- .../repository/CommentRepositoryTest.java | 7 ++-- .../repository/GpaScoreRepositoryTest.java | 6 ++-- .../LanguageTestScoreRepositoryTest.java | 6 ++-- .../repository/PostLikeRepositoryTest.java | 8 ++--- .../unit/repository/PostRepositoryTest.java | 10 +++--- 21 files changed, 152 insertions(+), 57 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/support/MySQLTestContainer.java create mode 100644 src/test/java/com/example/solidconnection/support/RedisTestContainer.java create mode 100644 src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java create mode 100644 src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java diff --git a/build.gradle b/build.gradle index ceba38c4b..24f5e41c4 100644 --- a/build.gradle +++ b/build.gradle @@ -37,17 +37,24 @@ dependencies {//todo: 안쓰는 의존성이나 deprecated된 의존성 제거 implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final' implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' implementation 'org.apache.commons:commons-lang3:3.12.0' - testImplementation 'org.mockito:mockito-core:3.3.3' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + // Lombok compileOnly 'org.projectlombok:lombok:1.18.26' annotationProcessor 'org.projectlombok:lombok' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'com.h2database:h2:2.2.224' + testImplementation 'org.mockito:mockito-core:3.3.3' testImplementation 'io.rest-assured:rest-assured:5.4.0' - implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + // Testcontainers + testImplementation 'org.testcontainers:testcontainers' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:mysql' + annotationProcessor( 'com.querydsl:querydsl-apt:5.0.0:jakarta', 'jakarta.persistence:jakarta.persistence-api:3.1.0', diff --git a/src/main/resources/secret b/src/main/resources/secret index b4f88d141..80a569b4c 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit b4f88d14185e2009e0793dfd16d22c2c3b9257ae +Subproject commit 80a569b4c023225c77874e140521c703010414eb diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index f07b6821c..e553eb4bb 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -7,6 +7,7 @@ import com.example.solidconnection.post.service.PostService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -16,8 +17,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -26,8 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -@SpringBootTest -@ActiveProfiles("test") +@TestContainerSpringBootTest @DisplayName("게시글 좋아요 동시성 테스트") class PostLikeCountConcurrencyTest { diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index c2213993d..dcd423168 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -7,6 +7,7 @@ import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -17,8 +18,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -28,8 +27,7 @@ import static com.example.solidconnection.type.RedisConstants.*; import static org.junit.jupiter.api.Assertions.assertEquals; -@SpringBootTest -@ActiveProfiles("test") +@TestContainerSpringBootTest @DisplayName("게시글 조회수 동시성 테스트") public class PostViewCountConcurrencyTest { diff --git a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java index 7ec6a511e..dce720610 100644 --- a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java @@ -3,6 +3,7 @@ import com.example.solidconnection.application.service.ApplicationQueryService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -10,9 +11,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; 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 java.util.Arrays; import java.util.Collections; @@ -22,8 +21,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -@SpringBootTest -@ActiveProfiles("test") +@TestContainerSpringBootTest @DisplayName("ThunderingHerd 테스트") public class ThunderingHerdTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java index a9d80afcc..d156cf485 100644 --- a/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/DatabaseConnectionTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.database; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -17,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; +@Disabled @AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2, replace = AutoConfigureTestDatabase.Replace.ANY) @ActiveProfiles("test") @DataJpaTest diff --git a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java index 6a7637ed5..69fcedaef 100644 --- a/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java +++ b/src/test/java/com/example/solidconnection/database/RedisConnectionTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.database; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -9,6 +10,7 @@ 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/e2e/BaseEndToEndTest.java b/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java index 9b23d230e..0b3ac3524 100644 --- a/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java +++ b/src/test/java/com/example/solidconnection/e2e/BaseEndToEndTest.java @@ -1,16 +1,14 @@ package com.example.solidconnection.e2e; import com.example.solidconnection.support.DatabaseClearExtension; +import com.example.solidconnection.support.TestContainerSpringBootTest; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.ActiveProfiles; +@TestContainerSpringBootTest @ExtendWith(DatabaseClearExtension.class) -@ActiveProfiles("test") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) abstract class BaseEndToEndTest { @LocalServerPort diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 2da99def8..eff3e54b5 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -105,7 +105,7 @@ class SignUpTest extends BaseEndToEndTest { assertAll( "관심 지역과 나라 정보를 저장한다.", () -> assertThat(interestedRegions).containsExactlyInAnyOrder(region), - () -> assertThat(interestedCountries).containsExactlyElementsOf(countries) + () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java index 9afecbbfd..20a0bbc6b 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDataSetUpEndToEndTest.java @@ -5,6 +5,7 @@ import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.support.DatabaseClearExtension; +import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.domain.LanguageRequirement; import com.example.solidconnection.university.domain.University; @@ -17,9 +18,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.test.context.ActiveProfiles; import java.util.HashSet; @@ -27,8 +26,7 @@ import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; @ExtendWith(DatabaseClearExtension.class) -@ActiveProfiles("test") -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestContainerSpringBootTest abstract class UniversityDataSetUpEndToEndTest { public static Region 영미권; diff --git a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java index 098a22c18..bb77f82f2 100644 --- a/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java +++ b/src/test/java/com/example/solidconnection/support/DatabaseCleaner.java @@ -32,17 +32,18 @@ public void clear() { } private void truncate() { - em.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 0").executeUpdate(); getTruncateQueries().forEach(query -> em.createNativeQuery(query).executeUpdate()); - em.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + em.createNativeQuery("SET FOREIGN_KEY_CHECKS = 1").executeUpdate(); } @SuppressWarnings("unchecked") private List getTruncateQueries() { String sql = """ - SELECT Concat('TRUNCATE TABLE ', TABLE_NAME, ' RESTART IDENTITY', ';') AS q + SELECT CONCAT('TRUNCATE TABLE ', TABLE_NAME, ';') AS q FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'PUBLIC' + WHERE TABLE_SCHEMA = (SELECT DATABASE()) + AND TABLE_TYPE = 'BASE TABLE' """; return em.createNativeQuery(sql).getResultList(); diff --git a/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java b/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java new file mode 100644 index 000000000..0256fec13 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/MySQLTestContainer.java @@ -0,0 +1,34 @@ +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.testcontainers.containers.MySQLContainer; +import org.testcontainers.junit.jupiter.Container; + +import javax.sql.DataSource; + +@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(); + } + + @PostConstruct + void startContainer() { + if (!CONTAINER.isRunning()) { + CONTAINER.start(); + } + } +} diff --git a/src/test/java/com/example/solidconnection/support/RedisTestContainer.java b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java new file mode 100644 index 000000000..39f35c2d5 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/RedisTestContainer.java @@ -0,0 +1,28 @@ +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.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; + +@TestConfiguration +public class RedisTestContainer { + + @Container + private static final GenericContainer CONTAINER = new GenericContainer<>("redis:7.0"); + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.redis.host", CONTAINER::getHost); + registry.add("spring.redis.port", CONTAINER::getFirstMappedPort); + } + + @PostConstruct + void startContainer() { + if (!CONTAINER.isRunning()) { + CONTAINER.start(); + } + } +} diff --git a/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java new file mode 100644 index 000000000..339672e60 --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/TestContainerDataJpaTest.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.support; + +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; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +@Testcontainers +@Import(MySQLTestContainer.class) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestContainerDataJpaTest { +} diff --git a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java new file mode 100644 index 000000000..bcb110c6b --- /dev/null +++ b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java @@ -0,0 +1,22 @@ +package com.example.solidconnection.support; + +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.testcontainers.junit.jupiter.Testcontainers; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +@Testcontainers +@Import({MySQLTestContainer.class, RedisTestContainer.class}) +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestContainerSpringBootTest { +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java index 9ea7ee0d9..17e74d140 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java @@ -7,6 +7,7 @@ import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -16,16 +17,13 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("게시판 레포지토리 테스트") class BoardRepositoryTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java index a53037346..b57288725 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java @@ -9,6 +9,7 @@ import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -17,8 +18,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -28,9 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; - -@SpringBootTest -@ActiveProfiles("dev") +@TestContainerDataJpaTest @DisplayName("댓글 레포지토리 테스트") class CommentRepositoryTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java index e3fa680c2..3ec59a5c2 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java @@ -5,6 +5,7 @@ import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -12,8 +13,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -21,8 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("학점 레포지토리 테스트") @Transactional public class GpaScoreRepositoryTest { diff --git a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java index 7369f20fa..0090088c1 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java @@ -5,6 +5,7 @@ import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.type.PreparationStatus; @@ -13,8 +14,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -22,8 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("어학성적 레포지토리 테스트") @Transactional public class LanguageTestScoreRepositoryTest { diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java index c39e28497..43ac210cb 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java @@ -3,12 +3,13 @@ import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -17,8 +18,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; @@ -26,8 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("게시글 좋아요 레포지토리 테스트") class PostLikeRepositoryTest { @Autowired diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index ecc2c4f6d..42da9de22 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -8,6 +8,7 @@ import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerDataJpaTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; @@ -16,20 +17,17 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; -@DataJpaTest -@ActiveProfiles("test") +@TestContainerDataJpaTest @DisplayName("게시글 레포지토리 테스트") class PostRepositoryTest { @Autowired From 6464568f6a93efb02393d0add0081f3f193dba96 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 16 Jan 2025 02:37:46 +0900 Subject: [PATCH 02/23] =?UTF-8?q?chore:=20=EB=A6=AC=ED=8C=A9=ED=84=B0?= =?UTF-8?q?=EB=A7=81=20=EC=9D=B4=EC=8A=88=20=ED=85=9C=ED=94=8C=EB=A6=BF=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#152)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/refactor_request.md | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/refactor_request.md diff --git a/.github/ISSUE_TEMPLATE/refactor_request.md b/.github/ISSUE_TEMPLATE/refactor_request.md new file mode 100644 index 000000000..38aa8ef03 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/refactor_request.md @@ -0,0 +1,28 @@ +--- +name: Refactor request +about: Suggest an refactor for this project +title: '' +labels: refactor +assignees: '' + +--- + +## 어떤 부분을 리팩터링하려 하나요? + +> 리팩터링하려는 부분에 대해 간결하게 설명해주세요 + +### AS-IS +- as-is +- as-is + +### TO-BE +- to-be +- to-be + +## 작업 상세 내용 + +- [ ] TODO +- [ ] TODO +- [ ] TODO + +## 참고할만한 자료(선택) From dc1bdcd5c404e60fbeb2202d63edde0f75178fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 16 Jan 2025 09:28:03 +0900 Subject: [PATCH 03/23] =?UTF-8?q?feat:=20=EC=B6=94=EC=B2=9C=20=EB=8C=80?= =?UTF-8?q?=ED=95=99=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B3=A0=20=EB=BF=90?= =?UTF-8?q?=EB=A7=8C=20=EC=95=84=EB=8B=88=EB=9D=BC=20background=20image?= =?UTF-8?q?=EB=8F=84=20=ED=8F=AC=ED=95=A8=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../university/dto/UniversityInfoForApplyPreviewResponse.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java index 93214b056..f6c2b4969 100644 --- a/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java +++ b/src/main/java/com/example/solidconnection/university/dto/UniversityInfoForApplyPreviewResponse.java @@ -12,6 +12,7 @@ public record UniversityInfoForApplyPreviewResponse( String region, String country, String logoImageUrl, + String backgroundImageUrl, int studentCapacity, List languageRequirements) { @@ -29,6 +30,7 @@ public static UniversityInfoForApplyPreviewResponse from(UniversityInfoForApply universityInfoForApply.getUniversity().getRegion().getKoreanName(), universityInfoForApply.getUniversity().getCountry().getKoreanName(), universityInfoForApply.getUniversity().getLogoImageUrl(), + universityInfoForApply.getUniversity().getBackgroundImageUrl(), universityInfoForApply.getStudentCapacity(), languageRequirementResponses ); From 428e72aac3cbe1c1b7c0f9892c6762f59928162b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 19 Jan 2025 23:08:53 +0900 Subject: [PATCH 04/23] =?UTF-8?q?test:=20=EB=8C=80=ED=95=99=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#148)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 대학교 상세조회 관련 통합테스트 코드 추가 * feat: 대학 통합테스트를 위한 데이터 셋업 추가 * refactor: 셋업된 데이터로 대학 조회 테스트하도록 변경 * feat: 대학교 검색 관련 통합테스트 코드 추가 * feat: 대학교 좋아요하기 관련 통합테스트 코드 추가 * refactor: 존재하지 않는 대학 상세정보 조회 시 id 변수명 수정 * refactor: *에서 구체적인 import 문으로 변경 * feat: 대학교 추천 관련 통합테스트 코드 추가 * refactor: 대학교 추천 서비스에 DisplayName 추가 * chore: 개행 컨벤션에 맞게 수정 * test: univseritySerivice에서 존재하지 않는 사용자에 대한 예외 검증 삭제 * refactor: UniversityService를 조회/좋아요 관련된 기능으로 분리 * test: 조회 및 좋아요 기능 테스트 코드 분리 * test: 대학교 조회 캐시 적용 테스트 개선 - CacheManager를 직접 검증하는 방식에서 SpyBean을 사용한 레포지토리 호출 횟수 검증으로 변경 * test: 예외처리 검증 테스트 한줄로 검증 * refactor: 좋아요 관련 응답 import *에서 구체적인 import 문으로 변경 * refactor: 대학 관련 서비스 dto *에서 구체적인 import 문으로 변경 * refactor: BDD Mockito 형식으로 변경 --- .../controller/UniversityController.java | 14 +- .../service/UniversityLikeService.java | 65 ++++ ...rvice.java => UniversityQueryService.java} | 48 +-- .../e2e/UniversityLikeTest.java | 4 +- .../UniversityDataSetUpIntegrationTest.java | 288 ++++++++++++++++++ .../service/UniversityLikeServiceTest.java | 136 +++++++++ .../service/UniversityQueryServiceTest.java | 205 +++++++++++++ .../UniversityRecommendServiceTest.java | 153 ++++++++++ 8 files changed, 858 insertions(+), 55 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java rename src/main/java/com/example/solidconnection/university/service/{UniversityService.java => UniversityQueryService.java} (55%) create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 2bab9da1a..1acfcb931 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -7,8 +7,9 @@ import com.example.solidconnection.university.dto.UniversityDetailResponse; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import com.example.solidconnection.university.service.UniversityLikeService; +import com.example.solidconnection.university.service.UniversityQueryService; import com.example.solidconnection.university.service.UniversityRecommendService; -import com.example.solidconnection.university.service.UniversityService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -26,7 +27,8 @@ @RestController public class UniversityController { - private final UniversityService universityService; + private final UniversityQueryService universityQueryService; + private final UniversityLikeService universityLikeService; private final UniversityRecommendService universityRecommendService; private final SiteUserService siteUserService; @@ -52,7 +54,7 @@ public ResponseEntity> getMyWishUniv public ResponseEntity getIsLiked( Principal principal, @PathVariable Long universityInfoForApplyId) { - IsLikeResponse isLiked = universityService.getIsLiked(principal.getName(), universityInfoForApplyId); + IsLikeResponse isLiked = universityLikeService.getIsLiked(principal.getName(), universityInfoForApplyId); return ResponseEntity.ok(isLiked); } @@ -60,7 +62,7 @@ public ResponseEntity getIsLiked( public ResponseEntity addWishUniversity( Principal principal, @PathVariable Long universityInfoForApplyId) { - LikeResultResponse likeResultResponse = universityService.likeUniversity(principal.getName(), universityInfoForApplyId); + LikeResultResponse likeResultResponse = universityLikeService.likeUniversity(principal.getName(), universityInfoForApplyId); return ResponseEntity .ok(likeResultResponse); } @@ -68,7 +70,7 @@ public ResponseEntity addWishUniversity( @GetMapping("/detail/{universityInfoForApplyId}") public ResponseEntity getUniversityDetails( @PathVariable Long universityInfoForApplyId) { - UniversityDetailResponse universityDetailResponse = universityService.getUniversityDetail(universityInfoForApplyId); + UniversityDetailResponse universityDetailResponse = universityQueryService.getUniversityDetail(universityInfoForApplyId); return ResponseEntity.ok(universityDetailResponse); } @@ -80,7 +82,7 @@ public ResponseEntity> searchUnivers @RequestParam(required = false, defaultValue = "") LanguageTestType testType, @RequestParam(required = false, defaultValue = "") String testScore) { List universityInfoForApplyPreviewResponse - = universityService.searchUniversity(region, keyword, testType, testScore).universityInfoForApplyPreviewResponses(); + = universityQueryService.searchUniversity(region, keyword, testType, testScore).universityInfoForApplyPreviewResponses(); return ResponseEntity.ok(universityInfoForApplyPreviewResponse); } } diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java new file mode 100644 index 000000000..4b15e5b8d --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java @@ -0,0 +1,65 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@RequiredArgsConstructor +@Service +public class UniversityLikeService { + + public static final String LIKE_SUCCESS_MESSAGE = "LIKE_SUCCESS"; + public static final String LIKE_CANCELED_MESSAGE = "LIKE_CANCELED"; + + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + private final LikedUniversityRepository likedUniversityRepository; + private final SiteUserRepository siteUserRepository; + + @Value("${university.term}") + public String term; + + /* + * 대학교를 '좋아요' 한다. + * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. + * */ + @Transactional + public LikeResultResponse likeUniversity(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); + + Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); + if (alreadyLikedUniversity.isPresent()) { + likedUniversityRepository.delete(alreadyLikedUniversity.get()); + return new LikeResultResponse(LIKE_CANCELED_MESSAGE); + } + + LikedUniversity likedUniversity = LikedUniversity.builder() + .universityInfoForApply(universityInfoForApply) + .siteUser(siteUser) + .build(); + likedUniversityRepository.save(likedUniversity); + return new LikeResultResponse(LIKE_SUCCESS_MESSAGE); + } + + /* + * '좋아요'한 대학교인지 확인한다. + * */ + @Transactional(readOnly = true) + public IsLikeResponse getIsLiked(String email, Long universityInfoForApplyId) { + SiteUser siteUser = siteUserRepository.getByEmail(email); + UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); + boolean isLike = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply).isPresent(); + return new IsLikeResponse(isLike); + } +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityService.java b/src/main/java/com/example/solidconnection/university/service/UniversityQueryService.java similarity index 55% rename from src/main/java/com/example/solidconnection/university/service/UniversityService.java rename to src/main/java/com/example/solidconnection/university/service/UniversityQueryService.java index 708374e96..f93f3ffae 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityQueryService.java @@ -1,15 +1,9 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; -import com.example.solidconnection.university.dto.IsLikeResponse; -import com.example.solidconnection.university.dto.LikeResultResponse; import com.example.solidconnection.university.dto.UniversityDetailResponse; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponses; @@ -21,19 +15,13 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Optional; @RequiredArgsConstructor @Service -public class UniversityService { - - public static final String LIKE_SUCCESS_MESSAGE = "LIKE_SUCCESS"; - public static final String LIKE_CANCELED_MESSAGE = "LIKE_CANCELED"; +public class UniversityQueryService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final LikedUniversityRepository likedUniversityRepository; private final UniversityFilterRepositoryImpl universityFilterRepository; - private final SiteUserRepository siteUserRepository; @Value("${university.term}") public String term; @@ -70,38 +58,4 @@ public UniversityInfoForApplyPreviewResponses searchUniversity( .map(UniversityInfoForApplyPreviewResponse::from) .toList()); } - - /* - * 대학교를 '좋아요' 한다. - * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. - * */ - @Transactional - public LikeResultResponse likeUniversity(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); - - Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); - if (alreadyLikedUniversity.isPresent()) { - likedUniversityRepository.delete(alreadyLikedUniversity.get()); - return new LikeResultResponse(LIKE_CANCELED_MESSAGE); - } - - LikedUniversity likedUniversity = LikedUniversity.builder() - .universityInfoForApply(universityInfoForApply) - .siteUser(siteUser) - .build(); - likedUniversityRepository.save(likedUniversity); - return new LikeResultResponse(LIKE_SUCCESS_MESSAGE); - } - - /* - * '좋아요'한 대학교인지 확인한다. - * */ - @Transactional(readOnly = true) - public IsLikeResponse getIsLiked(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); - boolean isLike = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply).isPresent(); - return new IsLikeResponse(isLike); - } } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index 37f922e4e..fb78cdffb 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -25,8 +25,8 @@ import static com.example.solidconnection.e2e.DynamicFixture.createLikedUniversity; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createUniversityForApply; -import static com.example.solidconnection.university.service.UniversityService.LIKE_CANCELED_MESSAGE; -import static com.example.solidconnection.university.service.UniversityService.LIKE_SUCCESS_MESSAGE; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java new file mode 100644 index 000000000..91518a09e --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java @@ -0,0 +1,288 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.Region; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.support.DatabaseClearExtension; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.domain.LanguageRequirement; +import com.example.solidconnection.university.domain.University; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.LanguageRequirementRepository; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.UniversityRepository; +import io.restassured.RestAssured; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.web.server.LocalServerPort; + +import java.util.HashSet; + +import static com.example.solidconnection.type.SemesterAvailableForDispatch.ONE_SEMESTER; +import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; + +@ExtendWith(DatabaseClearExtension.class) +@TestContainerSpringBootTest +abstract class UniversityDataSetUpIntegrationTest { + + public static Region 영미권; + public static Region 유럽; + public static Region 아시아; + public static Country 미국; + public static Country 캐나다; + public static Country 덴마크; + public static Country 오스트리아; + public static Country 일본; + + public static University 영미권_미국_괌대학; + public static University 영미권_미국_네바다주립대학_라스베이거스; + public static University 영미권_캐나다_메모리얼대학_세인트존스; + public static University 유럽_덴마크_서던덴마크대학교; + public static University 유럽_덴마크_코펜하겐IT대학; + public static University 유럽_오스트리아_그라츠대학; + public static University 유럽_오스트리아_그라츠공과대학; + public static University 유럽_오스트리아_린츠_카톨릭대학; + public static University 아시아_일본_메이지대학; + + public static UniversityInfoForApply 괌대학_A_지원_정보; + public static UniversityInfoForApply 괌대학_B_지원_정보; + public static UniversityInfoForApply 네바다주립대학_라스베이거스_지원_정보; + public static UniversityInfoForApply 메모리얼대학_세인트존스_A_지원_정보; + public static UniversityInfoForApply 서던덴마크대학교_지원_정보; + public static UniversityInfoForApply 코펜하겐IT대학_지원_정보; + public static UniversityInfoForApply 그라츠대학_지원_정보; + public static UniversityInfoForApply 그라츠공과대학_지원_정보; + public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; + public static UniversityInfoForApply 메이지대학_지원_정보; + + @Value("${university.term}") + public String term; + + @LocalServerPort + private int port; + + @Autowired + private RegionRepository regionRepository; + + @Autowired + private CountryRepository countryRepository; + + @Autowired + private UniversityRepository universityRepository; + + @Autowired + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Autowired + private LanguageRequirementRepository languageRequirementRepository; + + @BeforeEach + public void setUpBasicData() { + RestAssured.port = port; + + 영미권 = regionRepository.save(new Region("AMERICAS", "영미권")); + 유럽 = regionRepository.save(new Region("EUROPE", "유럽")); + 아시아 = regionRepository.save(new Region("ASIA", "아시아")); + + 미국 = countryRepository.save(new Country("US", "미국", 영미권)); + 캐나다 = countryRepository.save(new Country("CA", "캐나다", 영미권)); + 덴마크 = countryRepository.save(new Country("DK", "덴마크", 유럽)); + 오스트리아 = countryRepository.save(new Country("AT", "오스트리아", 유럽)); + 일본 = countryRepository.save(new Country("JP", "일본", 아시아)); + + 영미권_미국_괌대학 = universityRepository.save(new University( + null, "괌대학", "University of Guam", "university_of_guam", + "https://www.uog.edu/admissions/international-students", + "https://www.uog.edu/admissions/course-schedule", + "https://www.uog.edu/life-at-uog/residence-halls/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_guam/1.png", + null, 미국, 영미권 + )); + + 영미권_미국_네바다주립대학_라스베이거스 = universityRepository.save(new University( + null, "네바다주립대학 라스베이거스", "University of Nevada, Las Vegas", "university_of_nevada_las_vegas", + "https://www.unlv.edu/engineering/eip", + "https://www.unlv.edu/engineering/academic-programs", + "https://www.unlv.edu/housing", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_nevada_las_vegas/1.png", + null, 미국, 영미권 + )); + + 영미권_캐나다_메모리얼대학_세인트존스 = universityRepository.save(new University( + null, "메모리얼 대학 세인트존스", "Memorial University of Newfoundland St. John's", "memorial_university_of_newfoundland_st_johns", + "https://mun.ca/goabroad/visiting-students-inbound/", + "https://www.unlv.edu/engineering/academic-programs", + "https://www.mun.ca/residences/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/memorial_university_of_newfoundland_st_johns/1.png", + null, 캐나다, 영미권 + )); + + 유럽_덴마크_서던덴마크대학교 = universityRepository.save(new University( + null, "서던덴마크대학교", "University of Southern Denmark", "university_of_southern_denmark", + "https://www.sdu.dk/en", + "https://www.sdu.dk/en", + "https://www.sdu.dk/en/uddannelse/information_for_international_students/studenthousing", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_southern_denmark/1.png", + null, 덴마크, 유럽 + )); + + 유럽_덴마크_코펜하겐IT대학 = universityRepository.save(new University( + null, "코펜하겐 IT대학", "IT University of Copenhagen", "it_university_of_copenhagen", + "https://en.itu.dk/", null, + "https://en.itu.dk/Programmes/Student-Life/Practical-information-for-international-students", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/it_university_of_copenhagen/1.png", + null, 덴마크, 유럽 + )); + + 유럽_오스트리아_그라츠대학 = universityRepository.save(new University( + null, "그라츠 대학", "University of Graz", "university_of_graz", + "https://www.uni-graz.at/en/", + "https://static.uni-graz.at/fileadmin/veranstaltungen/orientation/documents/incstud_application-courses.pdf", + "https://orientation.uni-graz.at/de/planning-the-arrival/accommodation/", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/university_of_graz/1.png", + null, 오스트리아, 유럽 + )); + + 유럽_오스트리아_그라츠공과대학 = universityRepository.save(new University( + null, "그라츠공과대학", "Graz University of Technology", "graz_university_of_technology", + "https://www.tugraz.at/en/home", null, + "https://www.tugraz.at/en/studying-and-teaching/studying-internationally/incoming-students-exchange-at-tu-graz/your-stay-at-tu-graz/preparation#c75033", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/graz_university_of_technology/1.png", + null, 오스트리아, 유럽 + )); + + 유럽_오스트리아_린츠_카톨릭대학 = universityRepository.save(new University( + null, "린츠 카톨릭 대학교", "Catholic Private University Linz", "catholic_private_university_linz", + "https://ku-linz.at/en", null, + "https://ku-linz.at/en/ku_international/incomings/kulis", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/catholic_private_university_linz/1.png", + null, 오스트리아, 유럽 + )); + + 아시아_일본_메이지대학 = universityRepository.save(new University( + null, "메이지대학", "Meiji University", "meiji_university", + "https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf", null, + "https://www.meiji.ac.jp/cip/english/admissions/co7mm90000000461-att/co7mm900000004fa.pdf", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/logo.png", + "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png", + null, 일본, 아시아 + )); + + 괌대학_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "괌대학(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_괌대학 + )); + + 괌대학_B_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "괌대학(B형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_괌대학 + )); + + 네바다주립대학_라스베이거스_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "네바다주립대학 라스베이거스(B형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_미국_네바다주립대학_라스베이거스 + )); + + 메모리얼대학_세인트존스_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "메모리얼 대학 세인트존스(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 영미권_캐나다_메모리얼대학_세인트존스 + )); + + 서던덴마크대학교_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "서던덴마크대학교", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_덴마크_서던덴마크대학교 + )); + + 코펜하겐IT대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "코펜하겐 IT대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_덴마크_코펜하겐IT대학 + )); + + 그라츠대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "그라츠 대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_그라츠대학 + )); + + 그라츠공과대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "그라츠공과대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_그라츠공과대학 + )); + + 린츠_카톨릭대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "린츠 카톨릭 대학교", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 유럽_오스트리아_린츠_카톨릭대학 + )); + + 메이지대학_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( + null, term, "메이지대학", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, + "1", "detailsForLanguage", "gpaRequirement", + "gpaRequirementCriteria", "detailsForApply", "detailsForMajor", + "detailsForAccommodation", "detailsForEnglishCourse", "details", + new HashSet<>(), 아시아_일본_메이지대학 + )); + + saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEFL_IBT, "70"); + saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEIC, "900"); + saveLanguageTestRequirement(네바다주립대학_라스베이거스_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(메모리얼대학_세인트존스_A_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(서던덴마크대학교_지원_정보, LanguageTestType.TOEFL_IBT, "70"); + saveLanguageTestRequirement(코펜하겐IT대학_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(그라츠대학_지원_정보, LanguageTestType.TOEFL_IBT, "80"); + saveLanguageTestRequirement(그라츠공과대학_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(린츠_카톨릭대학_지원_정보, LanguageTestType.TOEIC, "800"); + saveLanguageTestRequirement(메이지대학_지원_정보, LanguageTestType.JLPT, "N2"); + } + + private void saveLanguageTestRequirement( + UniversityInfoForApply universityInfoForApply, LanguageTestType testType, String minScore) { + LanguageRequirement languageRequirement = new LanguageRequirement( + null, + testType, + minScore, + universityInfoForApply); + universityInfoForApply.addLanguageRequirements(languageRequirement); + universityInfoForApplyRepository.save(universityInfoForApply); + languageRequirementRepository.save(languageRequirement); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java new file mode 100644 index 000000000..50adf6839 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java @@ -0,0 +1,136 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.dto.IsLikeResponse; +import com.example.solidconnection.university.dto.LikeResultResponse; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; +import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; + +@DisplayName("대학교 좋아요 서비스 테스트") +class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { + + @Autowired + private UniversityLikeService universityLikeService; + + @Autowired + private LikedUniversityRepository likedUniversityRepository; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Test + void 대학_좋아요를_등록한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + LikeResultResponse response = universityLikeService.likeUniversity( + testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE); + assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( + testUser, 괌대학_A_지원_정보)).isPresent(); + } + + @Test + void 대학_좋아요를_취소한다() { + // given + SiteUser testUser = createSiteUser(); + saveLikedUniversity(testUser, 괌대학_A_지원_정보); + + // when + LikeResultResponse response = universityLikeService.likeUniversity( + testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE); + assertThat(likedUniversityRepository.findBySiteUserAndUniversityInfoForApply( + testUser, 괌대학_A_지원_정보)).isEmpty(); + } + + @Test + void 존재하지_않는_대학_좋아요_시도하면_예외를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + Long invalidUniversityId = 9999L; + + // when & then + assertThatCode(() -> universityLikeService.likeUniversity(testUser.getEmail(), invalidUniversityId)) + .isInstanceOf(CustomException.class) + .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); + } + + @Test + void 좋아요한_대학인지_확인한다() { + // given + SiteUser testUser = createSiteUser(); + saveLikedUniversity(testUser, 괌대학_A_지원_정보); + + // when + IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.isLike()).isTrue(); + } + + @Test + void 좋아요하지_않은_대학인지_확인한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + + // then + assertThat(response.isLike()).isFalse(); + } + + @Test + void 존재하지_않는_대학의_좋아요_여부_조회시_예외를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + Long invalidUniversityId = 9999L; + + // when & then + assertThatCode(() -> universityLikeService.getIsLiked(testUser.getEmail(), invalidUniversityId)) + .isInstanceOf(CustomException.class) + .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private void saveLikedUniversity(SiteUser siteUser, UniversityInfoForApply universityInfoForApply) { + LikedUniversity likedUniversity = LikedUniversity.builder() + .siteUser(siteUser) + .universityInfoForApply(universityInfoForApply) + .build(); + likedUniversityRepository.save(likedUniversity); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java new file mode 100644 index 000000000..54c235452 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java @@ -0,0 +1,205 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.university.dto.UniversityDetailResponse; +import com.example.solidconnection.university.dto.LanguageRequirementResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponses; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import com.example.solidconnection.university.repository.custom.UniversityFilterRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.SpyBean; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static com.example.solidconnection.custom.exception.ErrorCode.UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +@DisplayName("대학교 조회 서비스 테스트") +class UniversityQueryServiceTest extends UniversityDataSetUpIntegrationTest { + + @Autowired + private UniversityQueryService universityQueryService; + + @SpyBean + private UniversityFilterRepository universityFilterRepository; + + @SpyBean + private UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Test + void 대학_상세정보를_정상_조회한다() { + // given + Long universityId = 괌대학_A_지원_정보.getId(); + + // when + UniversityDetailResponse response = universityQueryService.getUniversityDetail(universityId); + + // then + Assertions.assertAll( + () -> assertThat(response.id()).isEqualTo(괌대학_A_지원_정보.getId()), + () -> assertThat(response.term()).isEqualTo(괌대학_A_지원_정보.getTerm()), + () -> assertThat(response.koreanName()).isEqualTo(괌대학_A_지원_정보.getKoreanName()), + () -> assertThat(response.englishName()).isEqualTo(영미권_미국_괌대학.getEnglishName()), + () -> assertThat(response.formatName()).isEqualTo(영미권_미국_괌대학.getFormatName()), + () -> assertThat(response.region()).isEqualTo(영미권.getKoreanName()), + () -> assertThat(response.country()).isEqualTo(미국.getKoreanName()), + () -> assertThat(response.homepageUrl()).isEqualTo(영미권_미국_괌대학.getHomepageUrl()), + () -> assertThat(response.logoImageUrl()).isEqualTo(영미권_미국_괌대학.getLogoImageUrl()), + () -> assertThat(response.backgroundImageUrl()).isEqualTo(영미권_미국_괌대학.getBackgroundImageUrl()), + () -> assertThat(response.detailsForLocal()).isEqualTo(영미권_미국_괌대학.getDetailsForLocal()), + () -> assertThat(response.studentCapacity()).isEqualTo(괌대학_A_지원_정보.getStudentCapacity()), + () -> assertThat(response.tuitionFeeType()).isEqualTo(괌대학_A_지원_정보.getTuitionFeeType().getKoreanName()), + () -> assertThat(response.semesterAvailableForDispatch()).isEqualTo(괌대학_A_지원_정보.getSemesterAvailableForDispatch().getKoreanName()), + () -> assertThat(response.languageRequirements()).containsOnlyOnceElementsOf( + 괌대학_A_지원_정보.getLanguageRequirements().stream() + .map(LanguageRequirementResponse::from) + .toList()), + () -> assertThat(response.detailsForLanguage()).isEqualTo(괌대학_A_지원_정보.getDetailsForLanguage()), + () -> assertThat(response.gpaRequirement()).isEqualTo(괌대학_A_지원_정보.getGpaRequirement()), + () -> assertThat(response.gpaRequirementCriteria()).isEqualTo(괌대학_A_지원_정보.getGpaRequirementCriteria()), + () -> assertThat(response.semesterRequirement()).isEqualTo(괌대학_A_지원_정보.getSemesterRequirement()), + () -> assertThat(response.detailsForApply()).isEqualTo(괌대학_A_지원_정보.getDetailsForApply()), + () -> assertThat(response.detailsForMajor()).isEqualTo(괌대학_A_지원_정보.getDetailsForMajor()), + () -> assertThat(response.detailsForAccommodation()).isEqualTo(괌대학_A_지원_정보.getDetailsForAccommodation()), + () -> assertThat(response.detailsForEnglishCourse()).isEqualTo(괌대학_A_지원_정보.getDetailsForEnglishCourse()), + () -> assertThat(response.details()).isEqualTo(괌대학_A_지원_정보.getDetails()), + () -> assertThat(response.accommodationUrl()).isEqualTo(괌대학_A_지원_정보.getUniversity().getAccommodationUrl()), + () -> assertThat(response.englishCourseUrl()).isEqualTo(괌대학_A_지원_정보.getUniversity().getEnglishCourseUrl()) + ); + } + + @Test + void 대학_상세정보_조회시_캐시가_적용된다() { + // given + Long universityId = 괌대학_A_지원_정보.getId(); + + // when + UniversityDetailResponse firstResponse = universityQueryService.getUniversityDetail(universityId); + UniversityDetailResponse secondResponse = universityQueryService.getUniversityDetail(universityId); + + // then + assertThat(firstResponse).isEqualTo(secondResponse); + then(universityInfoForApplyRepository).should(times(1)).getUniversityInfoForApplyById(universityId); + } + + @Test + void 존재하지_않는_대학_상세정보_조회시_예외_응답을_반환한다() { + // given + Long invalidUniversityInfoForApplyId = 9999L; + + // when & then + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> universityQueryService.getUniversityDetail(invalidUniversityInfoForApplyId)) + .havingRootCause() + .isInstanceOf(CustomException.class) + .withMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); + } + + @Test + void 전체_대학을_조회한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + null, List.of(), null, null); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠공과대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(린츠_카톨릭대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보) + ); + } + + @Test + void 대학_조회시_캐시가_적용된다() { + // given + String regionCode = 영미권.getCode(); + List keywords = List.of("괌"); + LanguageTestType testType = LanguageTestType.TOEFL_IBT; + String testScore = "70"; + String term = "2024-1"; + + // when + UniversityInfoForApplyPreviewResponses firstResponse = + universityQueryService.searchUniversity(regionCode, keywords, testType, testScore); + UniversityInfoForApplyPreviewResponses secondResponse = + universityQueryService.searchUniversity(regionCode, keywords, testType, testScore); + + // then + assertThat(firstResponse).isEqualTo(secondResponse); + then(universityFilterRepository).should(times(1)) + .findByRegionCodeAndKeywordsAndLanguageTestTypeAndTestScoreAndTerm( + regionCode, keywords, testType, testScore, term); + } + + @Test + void 지역으로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + 영미권.getCode(), List.of(), null, null); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보) + ); + } + + @Test + void 키워드로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + null, List.of("라", "일본"), null, null); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(그라츠공과대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보) + ); + } + + @Test + void 어학시험_조건으로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + null, List.of(), LanguageTestType.TOEFL_IBT, "70"); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보) + ); + } + + @Test + void 모든_조건으로_대학을_필터링한다() { + // when + UniversityInfoForApplyPreviewResponses response = universityQueryService.searchUniversity( + "EUROPE", List.of(), LanguageTestType.TOEFL_IBT, "70"); + + // then + assertThat(response.universityInfoForApplyPreviewResponses()).containsExactly(UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보)); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java new file mode 100644 index 000000000..1fee99033 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -0,0 +1,153 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import com.example.solidconnection.university.dto.UniversityRecommendsResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("대학교 추천 서비스 테스트") +class UniversityRecommendServiceTest extends UniversityDataSetUpIntegrationTest { + + @Autowired + private UniversityRecommendService universityRecommendService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private InterestedRegionRepository interestedRegionRepository; + + @Autowired + private InterestedCountyRepository interestedCountyRepository; + + @Autowired + private GeneralRecommendUniversities generalRecommendUniversities; + + @BeforeEach + void setUp() { + generalRecommendUniversities.init(); + } + + @Test + void 관심_지역_설정한_사용자의_맞춤_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + interestedRegionRepository.save(new InterestedRegion(testUser, 영미권)); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsAll(List.of( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보) + )); + } + + @Test + void 관심_국가_설정한_사용자의_맞춤_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsAll(List.of( + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + )); + } + + @Test + void 관심_지역과_국가_모두_설정한_사용자의_맞춤_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + interestedRegionRepository.save(new InterestedRegion(testUser, 영미권)); + interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsExactlyInAnyOrder( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(괌대학_B_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메모리얼대학_세인트존스_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(네바다주립대학_라스베이거스_지원_정보), + UniversityInfoForApplyPreviewResponse.from(서던덴마크대학교_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + ); + } + + @Test + void 관심사_미설정_사용자는_일반_추천_대학을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsExactlyInAnyOrderElementsOf( + generalRecommendUniversities.getRecommendUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList() + ); + } + + @Test + void 일반_추천_대학을_조회한다() { + // when + UniversityRecommendsResponse response = universityRecommendService.getGeneralRecommends(); + + // then + assertThat(response.recommendedUniversities()) + .hasSize(RECOMMEND_UNIVERSITY_NUM) + .containsExactlyInAnyOrderElementsOf( + generalRecommendUniversities.getRecommendUniversities().stream() + .map(UniversityInfoForApplyPreviewResponse::from) + .toList() + ); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } +} From f7dd92deadc90a3aa9cddd84a781a78fd8f7ba75 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 23 Jan 2025 02:12:40 +0900 Subject: [PATCH 05/23] =?UTF-8?q?refactor:=20=EC=8A=A4=ED=94=84=EB=A7=81?= =?UTF-8?q?=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 토큰 기능 제공 클래스 이름 변경 - TokenService > TokenProvider - 비지니스 로직이 아니라 기능만 제공하는 것이라 Provider가 더 적절하다고 판단함 * refactor: 토큰 접두사 추가 함수 이름 변경, static import 적용 * feat: subject 추출 함수 추가 * test: TokenProvider 테스트 작성 - 기존에 작성되지 않았던 것들도 작성함 * refactor: 예외 응답 함수 추출 * feat: 로그아웃 체크 필터 생성 - AS-IS: 액세스 토큰을 검증하면서 로그아웃했는지를 검증하고 있다. 이는 액세스 토큰의 검증부에 들어갈 것이 아니라, 더 이전 단계에서 처리되어야 한다. - TO-BE: 로그아웃 토큰을 필터에서 처리한다. 이전보다 더 빠르게 예외를 응답할 수 있다. * test: 로그아웃 체크 필터 테스트 작성 * refactor: 중복 코드 함수로 추출 * refactor: JWT 인증 필터 수정 - 가독성 개선 - 다른 객체들에 책임 분산 - permitAllEndpoint에 대한 설정 제거 * refactor: 사용하지 않는 코드,클래스 제거 * test: JWT 인증 필터 테스트 작성 * refactor: 스프링 시큐리티 설정 클래스 수정 - 가독성 향상 - 필터 추가 - 시큐리티 단에서 관리하는 인증 필요없는 uri 제거 * refactor: 중복 선언된 cors 설정 제거 * refactor: cors 관련 설정 ConfigurationProperties로 변경 * refactor: TokenType 패키지 이동 * refactor: TokenProvider, TokenValidator 패키지 이동 * refactor: JwtProperties 분리 * refactor: JwtUtils 분리 * refactor: ConfigurationPropertiesScan 적용 * refactor: 인스턴스화 방지 * test: 테스트 메서드 이름에 컨벤션 적용 --- .../SolidConnectionApplication.java | 2 + .../token => auth/domain}/TokenType.java | 10 +- .../auth/service/AuthService.java | 19 +-- .../auth/service/SignInService.java | 15 +-- .../auth/service/SignUpService.java | 14 +- .../auth/service/TokenProvider.java | 51 +++++++ .../service}/TokenValidator.java | 33 ++--- .../config/cors/CorsPropertiesConfig.java | 17 --- .../config/cors/WebConfig.java | 22 --- .../config/security/CorsProperties.java | 9 ++ .../config/security/JwtAuthentication.java | 29 ++++ .../security/JwtAuthenticationEntryPoint.java | 23 ++-- .../security/JwtAuthenticationFilter.java | 101 ++++---------- .../config/security/JwtProperties.java | 7 + .../security/JwtUserDetails.java} | 17 +-- .../security/SecurityConfiguration.java | 45 +++---- .../config/security/SignOutCheckFilter.java | 48 +++++++ .../config/token/TokenService.java | 70 ---------- .../custom/exception/ErrorCode.java | 3 +- .../userdetails/CustomUserDetailsService.java | 25 ---- .../solidconnection/util/JwtUtils.java | 51 +++++++ .../security/JwtAuthenticationFilterTest.java | 127 ++++++++++++++++++ .../security/SignOutCheckFilterTest.java | 106 +++++++++++++++ .../config/token/TokenProviderTest.java | 96 +++++++++++++ .../e2e/ApplicantsQueryTest.java | 24 ++-- .../solidconnection/e2e/MyPageTest.java | 12 +- .../solidconnection/e2e/MyPageUpdateTest.java | 12 +- .../solidconnection/e2e/SignInTest.java | 9 +- .../solidconnection/e2e/SignUpTest.java | 21 +-- .../e2e/UniversityDetailTest.java | 12 +- .../e2e/UniversityLikeTest.java | 12 +- .../e2e/UniversityRecommendTest.java | 12 +- .../e2e/UniversitySearchTest.java | 12 +- .../solidconnection/util/JwtUtilsTest.java | 120 +++++++++++++++++ 34 files changed, 808 insertions(+), 378 deletions(-) rename src/main/java/com/example/solidconnection/{config/token => auth/domain}/TokenType.java (52%) create mode 100644 src/main/java/com/example/solidconnection/auth/service/TokenProvider.java rename src/main/java/com/example/solidconnection/{config/token => auth/service}/TokenValidator.java (68%) delete mode 100644 src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java delete mode 100644 src/main/java/com/example/solidconnection/config/cors/WebConfig.java create mode 100644 src/main/java/com/example/solidconnection/config/security/CorsProperties.java create mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java create mode 100644 src/main/java/com/example/solidconnection/config/security/JwtProperties.java rename src/main/java/com/example/solidconnection/{custom/userdetails/CustomUserDetails.java => config/security/JwtUserDetails.java} (60%) create mode 100644 src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java delete mode 100644 src/main/java/com/example/solidconnection/config/token/TokenService.java delete mode 100644 src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java create mode 100644 src/main/java/com/example/solidconnection/util/JwtUtils.java create mode 100644 src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/util/JwtUtilsTest.java diff --git a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java index 670a3f0f7..a7f0554a3 100644 --- a/src/main/java/com/example/solidconnection/SolidConnectionApplication.java +++ b/src/main/java/com/example/solidconnection/SolidConnectionApplication.java @@ -2,10 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.cache.annotation.EnableCaching; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; import org.springframework.scheduling.annotation.EnableScheduling; +@ConfigurationPropertiesScan @EnableScheduling @EnableJpaAuditing @EnableCaching diff --git a/src/main/java/com/example/solidconnection/config/token/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java similarity index 52% rename from src/main/java/com/example/solidconnection/config/token/TokenType.java rename to src/main/java/com/example/solidconnection/auth/domain/TokenType.java index d5fc1717f..7fa6045f7 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -1,13 +1,13 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.domain; import lombok.Getter; @Getter public enum TokenType { - ACCESS("", 1000 * 60 * 60), - REFRESH("refresh:", 1000 * 60 * 60 * 24 * 7), - KAKAO_OAUTH("kakao:", 1000 * 60 * 60); + ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour + REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days + KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60); // 1hour private final String prefix; private final int expireTime; @@ -17,7 +17,7 @@ public enum TokenType { this.expireTime = expireTime; } - public String addTokenPrefixToSubject(String subject) { + public String addPrefixToSubject(String subject) { return prefix + subject; } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index caf78074d..29fb1b347 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -2,8 +2,6 @@ import com.example.solidconnection.auth.dto.ReissueResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -16,15 +14,18 @@ import java.time.LocalDate; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.config.token.TokenValidator.SIGN_OUT_VALUE; +import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @RequiredArgsConstructor @Service public class AuthService { + public static final String SIGN_OUT_VALUE = "signOut"; + private final RedisTemplate redisTemplate; - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; /* @@ -36,9 +37,9 @@ public class AuthService { * */ public void signOut(String email) { redisTemplate.opsForValue().set( - TokenType.REFRESH.addTokenPrefixToSubject(email), + REFRESH.addPrefixToSubject(email), SIGN_OUT_VALUE, - TokenType.REFRESH.getExpireTime(), + REFRESH.getExpireTime(), TimeUnit.MILLISECONDS ); } @@ -62,14 +63,14 @@ public void quit(String email) { * */ public ReissueResponse reissue(String email) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = TokenType.REFRESH.addTokenPrefixToSubject(email); + String refreshTokenKey = REFRESH.addPrefixToSubject(email); String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); if (ObjectUtils.isEmpty(refreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenService.generateToken(email, TokenType.ACCESS); - tokenService.saveToken(newAccessToken, TokenType.ACCESS); + String newAccessToken = tokenProvider.generateToken(email, ACCESS); + tokenProvider.saveToken(newAccessToken, ACCESS); return new ReissueResponse(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index f6adda20d..2cd356d73 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -6,8 +6,7 @@ import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; @@ -18,7 +17,7 @@ @Service public class SignInService { - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; private final KakaoOAuthClient kakaoOAuthClient; @@ -58,15 +57,15 @@ private void resetQuitedAt(String email) { } private SignInResponse getSignInInfo(String email) { - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignInResponse(true, accessToken, refreshToken); } private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { - String kakaoOauthToken = tokenService.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); - tokenService.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); + String kakaoOauthToken = tokenProvider.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); + tokenProvider.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index f10f40dbd..5cbd781eb 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -2,9 +2,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; -import com.example.solidconnection.config.token.TokenValidator; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; @@ -29,7 +27,7 @@ public class SignUpService { private final TokenValidator tokenValidator; - private final TokenService tokenService; + private final TokenProvider tokenProvider; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -51,7 +49,7 @@ public class SignUpService { public SignUpResponse signUp(SignUpRequest signUpRequest) { // 검증 tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); - String email = tokenService.getEmail(signUpRequest.kakaoOauthToken()); + String email = tokenProvider.getEmail(signUpRequest.kakaoOauthToken()); validateNicknameDuplicated(signUpRequest.nickname()); validateUserNotDuplicated(email); @@ -64,9 +62,9 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { saveInterestedCountry(signUpRequest, savedSiteUser); // 토큰 발급 - String accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignUpResponse(accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java new file mode 100644 index 000000000..693a968ea --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; + +@RequiredArgsConstructor +@Component +public class TokenProvider { + + private final RedisTemplate redisTemplate; + private final JwtProperties jwtProperties; + + public String generateToken(String email, TokenType tokenType) { + Claims claims = Jwts.claims().setSubject(email); + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) + .compact(); + } + + public String saveToken(String token, TokenType tokenType) { + String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + redisTemplate.opsForValue().set( + tokenType.addPrefixToSubject(subject), + token, + tokenType.getExpireTime(), + TimeUnit.MILLISECONDS + ); + return token; + } + + public String getEmail(String token) { + return parseSubject(token, jwtProperties.secret()); + } +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java similarity index 68% rename from src/main/java/com/example/solidconnection/config/token/TokenValidator.java rename to src/main/java/com/example/solidconnection/auth/service/TokenValidator.java index 9a63a21f5..8c17ad00c 100644 --- a/src/main/java/com/example/solidconnection/config/token/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java @@ -1,5 +1,6 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.service; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -12,18 +13,18 @@ import java.util.Date; import java.util.Objects; +import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.EMPTY_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; -import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; @Component @RequiredArgsConstructor public class TokenValidator { - public static final String SIGN_OUT_VALUE = "signOut"; - private final RedisTemplate redisTemplate; @Value("${jwt.secret}") @@ -31,20 +32,19 @@ public class TokenValidator { public void validateAccessToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, TokenType.ACCESS); - validateNotSignOut(token); + validateTokenNotExpired(token, ACCESS); validateRefreshToken(token); } public void validateKakaoToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, TokenType.KAKAO_OAUTH); + validateTokenNotExpired(token, KAKAO_OAUTH); validateKakaoTokenNotUsed(token); } private void validateTokenNotEmpty(String token) { if (!StringUtils.hasText(token)) { - throw new CustomException(INVALID_TOKEN); + throw new CustomException(EMPTY_TOKEN); } } @@ -52,32 +52,25 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { Date expiration = getClaim(token).getExpiration(); long now = new Date().getTime(); if ((expiration.getTime() - now) < 0) { - if (tokenType.equals(TokenType.ACCESS)) { + if (tokenType.equals(ACCESS)) { throw new CustomException(ACCESS_TOKEN_EXPIRED); } - if (token.equals(TokenType.KAKAO_OAUTH)) { + if (token.equals(KAKAO_OAUTH)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } } - private void validateNotSignOut(String token) { - String email = getClaim(token).getSubject(); - if (SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)))) { - throw new CustomException(USER_ALREADY_SIGN_OUT); - } - } - private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email)) == null) { + if (redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email)) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } private void validateKakaoTokenNotUsed(String token) { String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email)), token)) { + if (!Objects.equals(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email)), token)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } diff --git a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java b/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java deleted file mode 100644 index 68144d733..000000000 --- a/src/main/java/com/example/solidconnection/config/cors/CorsPropertiesConfig.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.solidconnection.config.cors; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Getter -@Setter -@ConfigurationProperties(prefix = "cors") -@Configuration -public class CorsPropertiesConfig { - - private List allowedOrigins; -} diff --git a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java b/src/main/java/com/example/solidconnection/config/cors/WebConfig.java deleted file mode 100644 index 00f3cf411..000000000 --- a/src/main/java/com/example/solidconnection/config/cors/WebConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.solidconnection.config.cors; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@RequiredArgsConstructor -public class WebConfig implements WebMvcConfigurer { - - private final CorsPropertiesConfig corsProperties; - - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0])) - .allowedMethods("*") - .allowedHeaders("*") - .allowCredentials(true); - } -} diff --git a/src/main/java/com/example/solidconnection/config/security/CorsProperties.java b/src/main/java/com/example/solidconnection/config/security/CorsProperties.java new file mode 100644 index 000000000..f851692c6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/CorsProperties.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.List; + +@ConfigurationProperties(prefix = "cors") +public record CorsProperties(List allowedOrigins) { +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java new file mode 100644 index 000000000..84692709a --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java @@ -0,0 +1,29 @@ +package com.example.solidconnection.config.security; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class JwtAuthentication extends AbstractAuthenticationToken { + + private final String token; + private final Object principal; + + public JwtAuthentication(Object principal, String token, Collection authorities) { + super(authorities); + this.token = token; + this.principal = principal; + setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return this.token; + } + + @Override + public Object getPrincipal() { + return this.principal; + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java index 69f5a2f2d..7487858be 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java @@ -12,7 +12,6 @@ import java.io.IOException; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; @Component @@ -25,24 +24,20 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + writeResponse(response, errorResponse); } - public void expiredCommence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(new CustomException(ACCESS_TOKEN_EXPIRED)); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setContentType("application/json"); - response.setCharacterEncoding("UTF-8"); - response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); + public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); + writeResponse(response, errorResponse); } - public void customCommence(HttpServletRequest request, HttpServletResponse response, - CustomException customException) throws IOException { + public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { ErrorResponse errorResponse = new ErrorResponse(customException); + writeResponse(response, errorResponse); + } + + private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index a618bec04..e01009be1 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -1,113 +1,60 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenValidator; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.JwtExpiredTokenException; -import io.jsonwebtoken.ExpiredJwtException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; -import org.springframework.util.AntPathMatcher; -import org.springframework.util.ObjectUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import java.util.HashSet; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { - public static final String TOKEN_HEADER = "Authorization"; - public static final String TOKEN_PREFIX = "Bearer "; + private static final String REISSUE_URI = "/auth/reissue"; + private static final String REISSUE_METHOD = "post"; - private final TokenService tokenService; - private final TokenValidator tokenValidator; + private final JwtProperties jwtProperties; private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - - // 인증 정보를 저장할 필요 없는 url - AntPathMatcher pathMatcher = new AntPathMatcher(); - for (String endpoint : getPermitAllEndpoints()) { - if (pathMatcher.match(endpoint, request.getRequestURI())) { - filterChain.doFilter(request, response); - return; - } + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null || isReissueRequest(request)) { + filterChain.doFilter(request, response); + return; } - // 토큰 검증 try { - String token = this.resolveAccessTokenFromRequest(request); // 웹 요청에서 토큰 추출 - if (token != null) { // 토큰이 있어야 검증 - 토큰 유무에 대한 다른 처리를 컨트롤러에서 할 수 있음 - try { - String requestURI = request.getRequestURI(); - if (requestURI.equals("/auth/reissue")) { - Authentication auth = this.tokenService.getAuthentication(token); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - return; - } - tokenValidator.validateAccessToken(token); // 액세스 토큰 검증 - 비어있는지, 유효한지, 리프레시 토큰, 로그아웃 - } catch (ExpiredJwtException e) { - throw new JwtExpiredTokenException(ACCESS_TOKEN_EXPIRED.getMessage()); - } - Authentication auth = this.tokenService.getAuthentication(token); // 토큰에서 인증 정보 가져옴 - SecurityContextHolder.getContext().setAuthentication(auth);// 인증 정보를 보안 컨텍스트에 설정 - } - } catch (JwtExpiredTokenException e) { - SecurityContextHolder.clearContext(); - jwtAuthenticationEntryPoint.expiredCommence(request, response, e); - return; + String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + UserDetails userDetails = new JwtUserDetails(subject); + Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + filterChain.doFilter(request, response); } catch (AuthenticationException e) { - SecurityContextHolder.clearContext(); jwtAuthenticationEntryPoint.commence(request, response, e); - return; } catch (CustomException e) { - jwtAuthenticationEntryPoint.customCommence(request, response, e); - return; - } - filterChain.doFilter(request, response); // 다음 필터로 요청과 응답 전달 - } - - private String resolveAccessTokenFromRequest(HttpServletRequest request) { - String token = request.getHeader(TOKEN_HEADER); - - if (!ObjectUtils.isEmpty(token) && token.startsWith(TOKEN_PREFIX)) { // 토큰이 비어 있지 않고, Bearer로 시작한다면 - return token.substring(TOKEN_PREFIX.length()); // Bearer 제외한 실제 토큰 부분 반환 + jwtAuthenticationEntryPoint.customCommence(response, e); + } catch (Exception e) { + jwtAuthenticationEntryPoint.generalCommence(response, e); } - return null; } - private HashSet getPermitAllEndpoints() { - var permitAllEndpoints = new HashSet(); - - // 서버 정상 작동 확인 - permitAllEndpoints.add("/"); - permitAllEndpoints.add("/index.html"); - permitAllEndpoints.add("/favicon.ico"); - - // 이미지 업로드 - permitAllEndpoints.add("/file/profile/pre"); - - // 토큰이 필요하지 않은 인증 - permitAllEndpoints.add("/auth/kakao"); - permitAllEndpoints.add("/auth/sign-up"); - - // 대학교 정보 - permitAllEndpoints.add("/university/search/**"); - - return permitAllEndpoints; + private boolean isReissueRequest(HttpServletRequest request) { + return REISSUE_URI.equals(request.getRequestURI()) && REISSUE_METHOD.equals(request.getMethod()); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtProperties.java b/src/main/java/com/example/solidconnection/config/security/JwtProperties.java new file mode 100644 index 000000000..e0c63da46 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/JwtProperties.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtProperties(String secret) { +} diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java b/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java similarity index 60% rename from src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java rename to src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java index 5d992adaf..b3bbda5fa 100644 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetails.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java @@ -1,26 +1,21 @@ -package com.example.solidconnection.custom.userdetails; +package com.example.solidconnection.config.security; -import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -public class CustomUserDetails implements UserDetails {//todo: principal 을 썼을 때 바로 SiteUser를 반환하게 하면 안되나?? +public class JwtUserDetails implements UserDetails { - private final SiteUser siteUser; + private final String userName; - public CustomUserDetails(SiteUser siteUser) { - this.siteUser = siteUser; - } - - public String getEmail() { - return siteUser.getEmail(); + public JwtUserDetails(String userName) { + this.userName = userName; } @Override public String getUsername() { - return siteUser.getEmail(); + return this.userName; } @Override diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 70bcf6c37..d28d883ca 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -1,65 +1,52 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.config.cors.CorsPropertiesConfig; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; - @Configuration @EnableWebSecurity -@EnableGlobalMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor public class SecurityConfiguration { + private final CorsProperties corsProperties; + private final SignOutCheckFilter signOutCheckFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; - private final CorsPropertiesConfig corsProperties; @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); - configuration.setAllowedOrigins(corsProperties.getAllowedOrigins()); - configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); - configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowedOrigins(corsProperties.allowedOrigins()); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); configuration.setAllowCredentials(true); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); + return source; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + return http .httpBasic(AbstractHttpConfigurer::disable) .csrf(AbstractHttpConfigurer::disable) - .sessionManagement((session) -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(authorizeRequest - -> authorizeRequest - .requestMatchers( - "/", "/index.html", "/favicon.ico", - "/file/profile/pre", - "/auth/kakao", "/auth/sign-up", "/auth/reissue", - "/university/detail/**", "/university/search/**", "/university/recommends", - "/actuator/**" - ) - .permitAll() - .anyRequest().authenticated()) - .addFilterBefore(this.jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .formLogin(AbstractHttpConfigurer::disable); - - return http.build(); + .formLogin(AbstractHttpConfigurer::disable) + .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) + .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .addFilterBefore(this.jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(this.signOutCheckFilter, JwtAuthenticationFilter.class) + .build(); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java new file mode 100644 index 000000000..3c1218d13 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -0,0 +1,48 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.service.AuthService.SIGN_OUT_VALUE; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; + +@Component +@RequiredArgsConstructor +public class SignOutCheckFilter extends OncePerRequestFilter { + + private final RedisTemplate redisTemplate; + private final JwtProperties jwtProperties; + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null || !isSignOut(token)) { + filterChain.doFilter(request, response); + return; + } + + jwtAuthenticationEntryPoint.customCommence(response, new CustomException(USER_ALREADY_SIGN_OUT)); + } + + private boolean isSignOut(String accessToken) { + String subject = parseSubject(accessToken, jwtProperties.secret()); + String refreshToken = REFRESH.addPrefixToSubject(subject); + return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); + } +} diff --git a/src/main/java/com/example/solidconnection/config/token/TokenService.java b/src/main/java/com/example/solidconnection/config/token/TokenService.java deleted file mode 100644 index fc9ccea31..000000000 --- a/src/main/java/com/example/solidconnection/config/token/TokenService.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.solidconnection.config.token; - -import com.example.solidconnection.custom.userdetails.CustomUserDetailsService; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; - -import java.util.Date; -import java.util.concurrent.TimeUnit; - -@RequiredArgsConstructor -@Component -public class TokenService { - - private final RedisTemplate redisTemplate; - private final CustomUserDetailsService customUserDetailsService; - - @Value("${jwt.secret}") - private String secretKey; - - public String generateToken(String email, TokenType tokenType) { - Claims claims = Jwts.claims().setSubject(email); - Date now = new Date(); - Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); - return Jwts.builder() - .setClaims(claims) - .setIssuedAt(now) - .setExpiration(expiredDate) - .signWith(SignatureAlgorithm.HS512, this.secretKey) - .compact(); - } - - public void saveToken(String token, TokenType tokenType) { - redisTemplate.opsForValue().set( - tokenType.addTokenPrefixToSubject(getClaim(token).getSubject()), - token, - tokenType.getExpireTime(), - TimeUnit.MILLISECONDS - ); - } - - public Authentication getAuthentication(String token) { - String email = getClaim(token).getSubject(); - UserDetails userDetails = customUserDetailsService.loadUserByUsername(email); - return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities()); - } - - public String getEmail(String token) { - return getClaim(token).getSubject(); - } - - private Claims getClaim(String token) { - try { - return Jwts.parser() - .setSigningKey(secretKey) - .parseClaimsJws(token) - .getBody(); - } catch (ExpiredJwtException e) { - return e.getClaims(); - } - } -} diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 765013303..8c3032284 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -29,7 +29,8 @@ public enum ErrorCode { // auth USER_ALREADY_SIGN_OUT(HttpStatus.UNAUTHORIZED.value(), "로그아웃 되었습니다."), - INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), + EMPTY_TOKEN(HttpStatus.UNAUTHORIZED.value(), "토큰이 필요한 경로에 빈 토큰으로 요청했습니다."), + INVALID_TOKEN(HttpStatus.UNAUTHORIZED.value(), "유효하지 않은 토큰입니다."), AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), diff --git a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java deleted file mode 100644 index c9f1b1606..000000000 --- a/src/main/java/com/example/solidconnection/custom/userdetails/CustomUserDetailsService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.solidconnection.custom.userdetails; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.stereotype.Service; - -import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; - -@Service -@RequiredArgsConstructor -public class CustomUserDetailsService implements UserDetailsService { - - private final SiteUserRepository siteUserRepository; - - @Override - public UserDetails loadUserByUsername(String username) { - SiteUser siteUser = siteUserRepository.findByEmail(username) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND, username)); - return new CustomUserDetails(siteUser); - } -} diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java new file mode 100644 index 000000000..a3775365d --- /dev/null +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jwts; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; + +@Component +public class JwtUtils { + + private static final String TOKEN_HEADER = "Authorization"; + private static final String TOKEN_PREFIX = "Bearer "; + + private JwtUtils() { + } + + public static String parseTokenFromRequest(HttpServletRequest request) { + String token = request.getHeader(TOKEN_HEADER); + if (token == null || token.isBlank() || !token.startsWith(TOKEN_PREFIX)) { + return null; + } + return token.substring(TOKEN_PREFIX.length()); + } + + public static String parseSubject(String token, String secretKey) { + try { + return extractSubject(token, secretKey); + } catch (ExpiredJwtException e) { + return e.getClaims().getSubject(); + } + } + + public static String parseSubjectOrElseThrow(String token, String secretKey) { + try { + return extractSubject(token, secretKey); + } catch (ExpiredJwtException e) { + throw new CustomException(INVALID_TOKEN); + } + } + + private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { + return Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java new file mode 100644 index 000000000..c0256f75a --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java @@ -0,0 +1,127 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("토큰 인증 필터 테스트") +class JwtAuthenticationFilterTest { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + private JwtProperties jwtProperties; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + SecurityContextHolder.clearContext(); + } + + @Test + public void 유효한_토큰에_대한_인증_정보를_저장한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(JwtAuthentication.class); + then(filterChain).should().doFilter(request, response); + } + + @Test + public void 토큰이_없으면_다음_필터로_진행한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + then(filterChain).should().doFilter(request, response); + } + + @Nested + class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { + + @Test + public void 만료된_토큰으로_인증하면_예외를_응답한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + then(filterChain).shouldHaveNoMoreInteractions(); + } + + @Test + public void 서명하지_않은_토큰으로_인증하면_예외를_응답한다() throws Exception { + // given + String token = Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, "wrongSecretKey") + .compact(); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + then(filterChain).shouldHaveNoMoreInteractions(); + } + } + + private HttpServletRequest createRequestWithToken(String token) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java new file mode 100644 index 000000000..13544152b --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java @@ -0,0 +1,106 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + +import java.util.Date; +import java.util.Objects; + +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("로그아웃 체크 필터 테스트") +class SignOutCheckFilterTest { + + @Autowired + private SignOutCheckFilter signOutCheckFilter; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + private final String subject = "subject"; + + @BeforeEach + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + Objects.requireNonNull(redisTemplate.getConnectionFactory()) + .getConnection() + .serverCommands() + .flushDb(); + } + + @Test + void 로그아웃한_토큰이면_예외를_응답한다() throws Exception { + // given + request = createTokenRequest(subject); + String refreshTokenKey = REFRESH.addPrefixToSubject(subject); + redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(USER_ALREADY_SIGN_OUT.getCode()); + then(filterChain).shouldHaveNoMoreInteractions(); + } + + @Test + void 토큰이_없으면_다음_필터로_전달한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + @Test + void 로그아웃하지_않은_토큰이면_다음_필터로_전달한다() throws Exception { + // given + request = createTokenRequest(subject); + + // when + signOutCheckFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + private HttpServletRequest createTokenRequest(String subject) { + String token = Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java b/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java new file mode 100644 index 000000000..d3992a33a --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java @@ -0,0 +1,96 @@ +package com.example.solidconnection.config.token; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +@TestContainerSpringBootTest +@DisplayName("TokenProvider 테스트") +class TokenProviderTest { + + @Autowired + private TokenProvider tokenProvider; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 토큰을_생성한다() { + // when + String subject = "subject123"; + String token = tokenProvider.generateToken(subject, TokenType.ACCESS); + + // then + String extractedSubject = Jwts.parser() + .setSigningKey(jwtProperties.secret()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + assertThat(subject).isEqualTo(extractedSubject); + } + + @Nested + class 토큰을_저장한다 { + + @Test + void 토큰이_유효하면_저장한다() { + // given + String subject = "subject321"; + String token = createValidToken(subject); + + // when + tokenProvider.saveToken(token, TokenType.ACCESS); + + // then + String savedToken = redisTemplate.opsForValue().get(TokenType.ACCESS.addPrefixToSubject(subject)); + assertThat(savedToken).isEqualTo(token); + } + + @Test + void 토큰이_유효하지않으면_예외가_발생한다() { + // given + String token = createInvalidToken(); + + // when & then + assertThatCode(() -> tokenProvider.saveToken(token, TokenType.REFRESH)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + private String createValidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private String createInvalidToken() { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 2f69d6cf7..6b739248b 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -7,8 +7,8 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -36,7 +36,7 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { ApplicationRepository applicationRepository; @Autowired - TokenService tokenService; + TokenProvider tokenProvider; private String accessToken; private String adminAccessToken; @@ -60,17 +60,17 @@ public void setUpUserAndToken() { SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); - adminAccessToken = tokenService.generateToken("email5", TokenType.ACCESS); - String adminRefreshToken = tokenService.generateToken("email5", TokenType.REFRESH); - tokenService.saveToken(adminRefreshToken, TokenType.REFRESH); + adminAccessToken = tokenProvider.generateToken("email5", TokenType.ACCESS); + String adminRefreshToken = tokenProvider.generateToken("email5", TokenType.REFRESH); + tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); - user6AccessToken = tokenService.generateToken("email6", TokenType.ACCESS); - String user6RefreshToken = tokenService.generateToken("email6", TokenType.REFRESH); - tokenService.saveToken(user6RefreshToken, TokenType.REFRESH); + user6AccessToken = tokenProvider.generateToken("email6", TokenType.ACCESS); + String user6RefreshToken = tokenProvider.generateToken("email6", TokenType.REFRESH); + tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); // setUp - 사용자 정보 저장 SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index 059e00cde..fb42216c9 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -23,7 +23,7 @@ class MyPageTest extends BaseEndToEndTest { @Autowired private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @BeforeEach @@ -32,9 +32,9 @@ public void setUpUserAndToken() { siteUserRepository.save(createSiteUserByEmail(email)); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index cb058fe3a..6d7f52032 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; @@ -31,7 +31,7 @@ class MyPageUpdateTest extends BaseEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @@ -46,9 +46,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index 8f1bd1018..efd5ad1d7 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -5,7 +5,6 @@ import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.config.token.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.restassured.RestAssured; @@ -19,6 +18,8 @@ import java.time.LocalDate; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.e2e.DynamicFixture.createKakaoUserInfoDtoByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.scheduler.UserRemovalScheduler.ACCOUNT_RECOVER_DURATION; @@ -64,7 +65,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), () -> assertThat(response.kakaoOauthToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.KAKAO_OAUTH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email))) .as("카카오 인증 토큰을 저장한다.") .isEqualTo(response.kakaoOauthToken()); } @@ -94,7 +95,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -128,7 +129,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull(), () -> assertThat(siteUserRepository.getByEmail(email).getQuitedAt()).isNull()); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index eff3e54b5..07dafb539 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -2,8 +2,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -27,6 +26,8 @@ import java.util.List; +import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; +import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; @@ -54,7 +55,7 @@ class SignUpTest extends BaseEndToEndTest { InterestedCountyRepository interestedCountyRepository; @Autowired - TokenService tokenService; + TokenProvider tokenProvider; @Autowired RedisTemplate redisTemplate; @@ -69,8 +70,8 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); @@ -108,7 +109,7 @@ class SignUpTest extends BaseEndToEndTest { () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); - assertThat(redisTemplate.opsForValue().get(TokenType.REFRESH.addTokenPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -122,8 +123,8 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenService.generateToken(email, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -148,8 +149,8 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = tokenService.generateToken(alreadyExistEmail, TokenType.KAKAO_OAUTH); - tokenService.saveToken(generatedKakaoToken, TokenType.KAKAO_OAUTH); + String generatedKakaoToken = tokenProvider.generateToken(alreadyExistEmail, KAKAO_OAUTH); + tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index dc8401700..947f44fd0 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.LanguageRequirementResponse; @@ -24,7 +24,7 @@ class UniversityDetailTest extends UniversityDataSetUpEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; @@ -36,9 +36,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index fb78cdffb..dccd1092f 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -45,7 +45,7 @@ class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; private SiteUser siteUser; @@ -57,9 +57,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index ee46733a1..00afbc8e3 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; import com.example.solidconnection.repositories.InterestedCountyRepository; @@ -38,7 +38,7 @@ class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { private InterestedCountyRepository interestedCountyRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; @Autowired private GeneralRecommendUniversities generalRecommendUniversities; @@ -54,9 +54,9 @@ void setUp() { generalRecommendUniversities.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 1187fb0ad..4859f9fe2 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -1,7 +1,7 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.config.token.TokenService; -import com.example.solidconnection.config.token.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -33,7 +33,7 @@ class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenService tokenService; + private TokenProvider tokenProvider; private String accessToken; private SiteUser siteUser; @@ -45,9 +45,9 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenService.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenService.generateToken(email, TokenType.REFRESH); - tokenService.saveToken(refreshToken, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @Test diff --git a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java new file mode 100644 index 000000000..4dfc11540 --- /dev/null +++ b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java @@ -0,0 +1,120 @@ +package com.example.solidconnection.util; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.util.Date; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("JwtUtils 테스트") +class JwtUtilsTest { + + private final String jwtSecretKey = "jwt-secret-key"; + + @Nested + class 요청으로부터_토큰을_추출한다 { + + @Test + void 토큰이_있으면_토큰을_반환한다() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + String token = "token"; + request.addHeader("Authorization", "Bearer " + token); + + // when + String extractedToken = parseTokenFromRequest(request); + + // then + assertThat(extractedToken).isEqualTo(token); + } + + @Test + void 토큰이_없으면_null_을_반환한다() { + // given + MockHttpServletRequest noHeader = new MockHttpServletRequest(); + MockHttpServletRequest wrongPrefix = new MockHttpServletRequest(); + wrongPrefix.addHeader("Authorization", "Wrong token"); + MockHttpServletRequest emptyToken = new MockHttpServletRequest(); + wrongPrefix.addHeader("Authorization", "Bearer "); + + // when & then + assertAll( + () -> assertThat(parseTokenFromRequest(noHeader)).isNull(), + () -> assertThat(parseTokenFromRequest(wrongPrefix)).isNull(), + () -> assertThat(parseTokenFromRequest(emptyToken)).isNull() + ); + } + } + + @Nested + class 토큰으로부터_subject_를_추출한다 { + + @Test + void 유효한_토큰의_subject_를_추출한다() { + // given + String subject = "subject000"; + String token = createValidToken(subject); + + // when + String extractedSubject = parseSubject(token, jwtSecretKey); + + // then + assertThat(extractedSubject).isEqualTo(subject); + } + + @Test + void 유효하지_않은_토큰의_subject_를_추출한다() { + // given + String subject = "subject999"; + String token = createInvalidToken(subject); + + // when + String extractedSubject = parseSubject(token, jwtSecretKey); + + // then + assertThat(extractedSubject).isEqualTo(subject); + } + + @Test + void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { + // given + String subject = "subject123"; + String token = createInvalidToken(subject); + + // when + assertThatCode(() -> parseSubjectOrElseThrow(token, jwtSecretKey)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + private String createValidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } + + private String createInvalidToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } +} From 35bc2ace6ee752ba04b7f05801e02ea44832a723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 23 Jan 2025 10:57:18 +0900 Subject: [PATCH 06/23] =?UTF-8?q?test:=20=EC=9C=A0=EC=A0=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20(#156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 통합 테스트 구조 개선 - BaseIntegrationTest 추가로 테스트 설정 공통화 - TestDataSetUpHelper 도입으로 테스트 데이터 관리 개선 * refactor: 대학교 통합 테스트 BaseIntegrationTest를 사용하는 것으로 변경 * test: 마이페이지 조회 관련 통합테스트 코드 추가 * test: 정보 수정을 위한 마이페이지 조회 관련 통합테스트 코드 추가 * test: 관심 대학교 목록 조회 관련 통합테스트 코드 추가 * test: 프로필 이미지를 수정 관련 통합테스트 코드 추가 * test: 닉네임 수정 관련 통합테스트 코드 추가 * chore: 예외 응답 테스트명 "~면_예외_응답을_반환한다"로 통일 * refactor: TestDataSetUpHelper 생성자 주입에서 필드 주입으로 변경 * style: 카멜케이스에 맞게 수정 * refactor: @Nested를 사용하여 프로필 이미지, 닉네임 수정 테스트 그룹화 * refactor: TestDataSetUpHelper를 BaseIntegrationTest로 통합 --- .../siteuser/service/SiteUserServiceTest.java | 309 ++++++++++++++++++ .../integration/BaseIntegrationTest.java} | 40 ++- .../service/UniversityLikeServiceTest.java | 7 +- .../service/UniversityQueryServiceTest.java | 5 +- .../UniversityRecommendServiceTest.java | 3 +- 5 files changed, 344 insertions(+), 20 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java rename src/test/java/com/example/solidconnection/{university/service/UniversityDataSetUpIntegrationTest.java => support/integration/BaseIntegrationTest.java} (96%) diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java new file mode 100644 index 000000000..8fdae031e --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -0,0 +1,309 @@ +package com.example.solidconnection.siteuser.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.MyPageResponse; +import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; +import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; +import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; +import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; +import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.university.domain.LikedUniversity; +import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_CHANGE_NICKNAME_YET; +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.PROFILE_IMAGE_NEEDED; +import static com.example.solidconnection.siteuser.service.SiteUserService.MIN_DAYS_BETWEEN_NICKNAME_CHANGES; +import static com.example.solidconnection.siteuser.service.SiteUserService.NICKNAME_LAST_CHANGE_DATE_FORMAT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.never; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.eq; + +@DisplayName("유저 서비스 테스트") +class SiteUserServiceTest extends BaseIntegrationTest { + + @Autowired + private SiteUserService siteUserService; + + @MockBean + private S3Service s3Service; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private LikedUniversityRepository likedUniversityRepository; + + @Test + void 마이페이지_정보를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + int likedUniversityCount = createLikedUniversities(testUser); + + // when + MyPageResponse response = siteUserService.getMyPageInfo(testUser.getEmail()); + + // then + Assertions.assertAll( + () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), + () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()), + () -> assertThat(response.role()).isEqualTo(testUser.getRole()), + () -> assertThat(response.birth()).isEqualTo(testUser.getBirth()), + () -> assertThat(response.email()).isEqualTo(testUser.getEmail()), + () -> assertThat(response.likedPostCount()).isEqualTo(testUser.getPostLikeList().size()), + () -> assertThat(response.likedUniversityCount()).isEqualTo(likedUniversityCount) + ); + } + + @Test + void 내_정보를_수정하기_위한_마이페이지_정보를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + MyPageUpdateResponse response = siteUserService.getMyPageInfoToUpdate(testUser.getEmail()); + + // then + Assertions.assertAll( + () -> assertThat(response.nickname()).isEqualTo(testUser.getNickname()), + () -> assertThat(response.profileImageUrl()).isEqualTo(testUser.getProfileImageUrl()) + ); + } + + @Test + void 관심_대학교_목록을_조회한다() { + // given + SiteUser testUser = createSiteUser(); + int likedUniversityCount = createLikedUniversities(testUser); + + // when + List response = siteUserService.getWishUniversity(testUser.getEmail()); + + // then + assertThat(response) + .hasSize(likedUniversityCount) + .usingRecursiveFieldByFieldElementComparatorIgnoringFields("id") + .containsAll(List.of( + UniversityInfoForApplyPreviewResponse.from(괌대학_A_지원_정보), + UniversityInfoForApplyPreviewResponse.from(메이지대학_지원_정보), + UniversityInfoForApplyPreviewResponse.from(코펜하겐IT대학_지원_정보) + )); + } + + @Nested + class 프로필_이미지_수정_테스트 { + + @Test + void 새로운_이미지로_성공적으로_업데이트한다() { + // given + SiteUser testUser = createSiteUser(); + String expectedUrl = "newProfileImageUrl"; + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse(expectedUrl)); + + // when + ProfileImageUpdateResponse response = siteUserService.updateProfileImage( + testUser.getEmail(), + imageFile + ); + + // then + assertThat(response.profileImageUrl()).isEqualTo(expectedUrl); + } + + @Test + void 프로필을_처음_수정하는_것이면_이전_이미지를_삭제하지_않는다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + + // when + siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + + // then + then(s3Service).should(never()).deleteExProfile(any()); + } + + @Test + void 프로필을_처음_수정하는_것이_아니라면_이전_이미지를_삭제한다() { + // given + SiteUser testUser = createSiteUserWithCustomProfile(); + MockMultipartFile imageFile = createValidImageFile(); + given(s3Service.uploadFile(any(), eq(ImgType.PROFILE))) + .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); + + // when + siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + + // then + then(s3Service).should().deleteExProfile(testUser.getEmail()); + } + + @Test + void 빈_이미지_파일로_프로필을_수정하면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + MockMultipartFile emptyFile = createEmptyImageFile(); + + // when & then + assertThatCode(() -> siteUserService.updateProfileImage(testUser.getEmail(), emptyFile)) + .isInstanceOf(CustomException.class) + .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); + } + } + + @Nested + class 닉네임_수정_테스트 { + + @Test + void 닉네임을_성공적으로_수정한다() { + // given + SiteUser testUser = createSiteUser(); + String newNickname = "newNickname"; + NicknameUpdateRequest request = new NicknameUpdateRequest(newNickname); + + // when + NicknameUpdateResponse response = siteUserService.updateNickname( + testUser.getEmail(), + request + ); + + // then + SiteUser updatedUser = siteUserRepository.getByEmail(testUser.getEmail()); + assertThat(updatedUser.getNicknameModifiedAt()).isNotNull(); + assertThat(response.nickname()).isEqualTo(newNickname); + } + + @Test + void 중복된_닉네임으로_변경하면_예외_응답을_반환한다() { + // given + createDuplicatedSiteUser(); + SiteUser testUser = createSiteUser(); + NicknameUpdateRequest request = new NicknameUpdateRequest("duplicatedNickname"); + + // when & then + assertThatCode(() -> siteUserService.updateNickname(testUser.getEmail(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); + } + + @Test + void 최소_대기기간이_지나지_않은_상태에서_변경하면_예외_응답을_반환한다() { + // given + SiteUser testUser = createSiteUser(); + LocalDateTime modifiedAt = LocalDateTime.now().minusDays(MIN_DAYS_BETWEEN_NICKNAME_CHANGES - 1); + testUser.setNicknameModifiedAt(modifiedAt); + siteUserRepository.save(testUser); + + NicknameUpdateRequest request = new NicknameUpdateRequest("newNickname"); + + // when & then + assertThatCode(() -> + siteUserService.updateNickname(testUser.getEmail(), request)) + .isInstanceOf(CustomException.class) + .hasMessage(createExpectedErrorMessage(modifiedAt)); + } + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private SiteUser createSiteUserWithCustomProfile() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profile/profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private void createDuplicatedSiteUser() { + SiteUser siteUser = new SiteUser( + "duplicated@example.com", + "duplicatedNickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + siteUserRepository.save(siteUser); + } + + private int createLikedUniversities(SiteUser testUser) { + LikedUniversity likedUniversity1 = new LikedUniversity(null, 괌대학_A_지원_정보, testUser); + LikedUniversity likedUniversity2 = new LikedUniversity(null, 메이지대학_지원_정보, testUser); + LikedUniversity likedUniversity3 = new LikedUniversity(null, 코펜하겐IT대학_지원_정보, testUser); + + likedUniversityRepository.save(likedUniversity1); + likedUniversityRepository.save(likedUniversity2); + likedUniversityRepository.save(likedUniversity3); + return likedUniversityRepository.countBySiteUser_Email(testUser.getEmail()); + } + + private MockMultipartFile createValidImageFile() { + return new MockMultipartFile( + "image", + "test.jpg", + "image/jpeg", + "test image content".getBytes() + ); + } + + private MockMultipartFile createEmptyImageFile() { + return new MockMultipartFile( + "image", + "empty.jpg", + "image/jpeg", + new byte[0] + ); + } + + private String createExpectedErrorMessage(LocalDateTime modifiedAt) { + String formatLastModifiedAt = String.format( + "(마지막 수정 시간 : %s)", + NICKNAME_LAST_CHANGE_DATE_FORMAT.format(modifiedAt) + ); + return CAN_NOT_CHANGE_NICKNAME_YET.getMessage() + " : " + formatLastModifiedAt; + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java rename to src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index 91518a09e..b1f7d9203 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityDataSetUpIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.university.service; +package com.example.solidconnection.support.integration; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.Region; @@ -13,21 +13,19 @@ import com.example.solidconnection.university.repository.LanguageRequirementRepository; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import com.example.solidconnection.university.repository.UniversityRepository; -import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.test.web.server.LocalServerPort; import java.util.HashSet; import static com.example.solidconnection.type.SemesterAvailableForDispatch.ONE_SEMESTER; import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; -@ExtendWith(DatabaseClearExtension.class) @TestContainerSpringBootTest -abstract class UniversityDataSetUpIntegrationTest { +@ExtendWith(DatabaseClearExtension.class) +public abstract class BaseIntegrationTest { public static Region 영미권; public static Region 유럽; @@ -59,12 +57,6 @@ abstract class UniversityDataSetUpIntegrationTest { public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; public static UniversityInfoForApply 메이지대학_지원_정보; - @Value("${university.term}") - public String term; - - @LocalServerPort - private int port; - @Autowired private RegionRepository regionRepository; @@ -80,20 +72,33 @@ abstract class UniversityDataSetUpIntegrationTest { @Autowired private LanguageRequirementRepository languageRequirementRepository; + @Value("${university.term}") + public String term; + @BeforeEach - public void setUpBasicData() { - RestAssured.port = port; + public void setUpBaseData() { + setUpRegions(); + setUpCountries(); + setUpUniversities(); + setUpUniversityInfos(); + setUpLanguageRequirements(); + } + private void setUpRegions() { 영미권 = regionRepository.save(new Region("AMERICAS", "영미권")); 유럽 = regionRepository.save(new Region("EUROPE", "유럽")); 아시아 = regionRepository.save(new Region("ASIA", "아시아")); + } + private void setUpCountries() { 미국 = countryRepository.save(new Country("US", "미국", 영미권)); 캐나다 = countryRepository.save(new Country("CA", "캐나다", 영미권)); 덴마크 = countryRepository.save(new Country("DK", "덴마크", 유럽)); 오스트리아 = countryRepository.save(new Country("AT", "오스트리아", 유럽)); 일본 = countryRepository.save(new Country("JP", "일본", 아시아)); + } + private void setUpUniversities() { 영미권_미국_괌대학 = universityRepository.save(new University( null, "괌대학", "University of Guam", "university_of_guam", "https://www.uog.edu/admissions/international-students", @@ -179,7 +184,9 @@ public void setUpBasicData() { "https://solid-connection.s3.ap-northeast-2.amazonaws.com/original/meiji_university/1.png", null, 일본, 아시아 )); + } + private void setUpUniversityInfos() { 괌대학_A_지원_정보 = universityInfoForApplyRepository.save(new UniversityInfoForApply( null, term, "괌대학(A형)", 1, HOME_UNIVERSITY_PAYMENT, ONE_SEMESTER, "1", "detailsForLanguage", "gpaRequirement", @@ -259,7 +266,9 @@ public void setUpBasicData() { "detailsForAccommodation", "detailsForEnglishCourse", "details", new HashSet<>(), 아시아_일본_메이지대학 )); + } + private void setUpLanguageRequirements() { saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEFL_IBT, "80"); saveLanguageTestRequirement(괌대학_A_지원_정보, LanguageTestType.TOEIC, "800"); saveLanguageTestRequirement(괌대학_B_지원_정보, LanguageTestType.TOEFL_IBT, "70"); @@ -275,7 +284,10 @@ public void setUpBasicData() { } private void saveLanguageTestRequirement( - UniversityInfoForApply universityInfoForApply, LanguageTestType testType, String minScore) { + UniversityInfoForApply universityInfoForApply, + LanguageTestType testType, + String minScore + ) { LanguageRequirement languageRequirement = new LanguageRequirement( null, testType, diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java index 50adf6839..14371486c 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java @@ -4,6 +4,7 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -22,7 +23,7 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; @DisplayName("대학교 좋아요 서비스 테스트") -class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { +class UniversityLikeServiceTest extends BaseIntegrationTest { @Autowired private UniversityLikeService universityLikeService; @@ -65,7 +66,7 @@ class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { } @Test - void 존재하지_않는_대학_좋아요_시도하면_예외를_반환한다() { + void 존재하지_않는_대학_좋아요_시도하면_예외_응답을_반환한다() { // given SiteUser testUser = createSiteUser(); Long invalidUniversityId = 9999L; @@ -102,7 +103,7 @@ class UniversityLikeServiceTest extends UniversityDataSetUpIntegrationTest { } @Test - void 존재하지_않는_대학의_좋아요_여부_조회시_예외를_반환한다() { + void 존재하지_않는_대학의_좋아요_여부를_조회하면_예외_응답을_반환한다() { // given SiteUser testUser = createSiteUser(); Long invalidUniversityId = 9999L; diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java index 54c235452..1cd0d755f 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityQueryServiceTest.java @@ -1,6 +1,7 @@ package com.example.solidconnection.university.service; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.dto.UniversityDetailResponse; import com.example.solidconnection.university.dto.LanguageRequirementResponse; @@ -23,7 +24,7 @@ import static org.mockito.Mockito.times; @DisplayName("대학교 조회 서비스 테스트") -class UniversityQueryServiceTest extends UniversityDataSetUpIntegrationTest { +class UniversityQueryServiceTest extends BaseIntegrationTest { @Autowired private UniversityQueryService universityQueryService; @@ -91,7 +92,7 @@ class UniversityQueryServiceTest extends UniversityDataSetUpIntegrationTest { } @Test - void 존재하지_않는_대학_상세정보_조회시_예외_응답을_반환한다() { + void 존재하지_않는_대학_상세정보를_조회하면_예외_응답을_반환한다() { // given Long invalidUniversityInfoForApplyId = 9999L; diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java index 1fee99033..cadd45aaf 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -6,6 +6,7 @@ import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; @@ -22,7 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DisplayName("대학교 추천 서비스 테스트") -class UniversityRecommendServiceTest extends UniversityDataSetUpIntegrationTest { +class UniversityRecommendServiceTest extends BaseIntegrationTest { @Autowired private UniversityRecommendService universityRecommendService; From d4e57c0076de24516b579a10e283a368d8240f00 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 28 Jan 2025 00:25:28 +0900 Subject: [PATCH 07/23] =?UTF-8?q?hotfix:=20=EB=8C=80=ED=95=99=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EA=B8=B0=EB=8A=A5=20=EC=A0=95=EC=83=81=ED=99=94=20?= =?UTF-8?q?(#159)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 기본 추천 대학 후보 추가 * chore: 기능 설명 주석 보충 --- .../university/service/GeneralRecommendUniversities.java | 3 ++- .../university/service/UniversityRecommendService.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java index 5c1c2e787..92054eee6 100644 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java @@ -31,7 +31,8 @@ public class GeneralRecommendUniversities { "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", "리스본대학 공과대학", "바덴뷔르템베르크 산학협력대학", "긴다이대학", "네바다주립대학 라스베이거스(B형)", "릴 가톨릭 대학", - "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교" + "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교", + "밀라노공과대학", "장물랭리옹3세대학교", "시드니대학", "아우크스부르크대학", "쳄니츠 공과대학", "북경외국어대학교 IBS" ); @Value("${university.term}") diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index cf9c112f8..6a6a43fbf 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -33,7 +33,7 @@ public class UniversityRecommendService { * 사용자 맞춤 추천 대학교를 불러온다. * - 회원가입 시 선택한 관심 지역과 관심 국가에 해당하는 대학 중, 이번 term 에 열리는 학교들을 불러온다. * - 불러온 맞춤 추천 대학교의 순서를 무작위로 섞는다. - * - 맞춤 추천 대학교의 수가 6개보다 적다면, 공통 추천 대학교를 부족한 수 만큼 불러온다. + * - 맞춤 추천 대학교의 수가 6개보다 적다면, 공통 추천 대학교 후보에서 이번 term 에 열리는 학교들을 부족한 수 만큼 불러온다. * */ @Transactional(readOnly = true) public UniversityRecommendsResponse getPersonalRecommends(String email) { From f26e75ccefd268acc180f0b9d82b6e5f151be312 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 28 Jan 2025 03:11:34 +0900 Subject: [PATCH 08/23] =?UTF-8?q?refactor:=20auth=20type=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD=20(#167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 사용자가 다양한 인증 유형을 가지도록 수정 * refactor: 함수 이름 변경 - 더 의미를 전달하도록 * test: 테스트 코드 패키지 이동 * refactor: EntryPoint 말고 필터로 필터에서 발생하는 모든 예외 처리 * feat: 토큰 만료 검사 함수 추가 * test: 깨지는 테스트 해결 * refactor: 로그아웃 로직 수정 * test: 로그아웃 필터 테스트 수정 --- .../auth/domain/TokenType.java | 4 +- .../auth/service/AuthService.java | 20 ++-- .../auth/service/TokenProvider.java | 6 +- ...Point.java => ExceptionHandlerFilter.java} | 33 ++++--- .../security/JwtAuthenticationFilter.java | 23 ++--- .../security/SecurityConfiguration.java | 6 +- .../config/security/SignOutCheckFilter.java | 20 ++-- .../siteuser/domain/AuthType.java | 9 ++ .../siteuser/domain/SiteUser.java | 34 ++++++- .../solidconnection/util/JwtUtils.java | 23 ++++- ...3__add_auth_type_column_and_unique_key.sql | 13 +++ .../service}/TokenProviderTest.java | 3 +- .../security/ExceptionHandlerFilterTest.java | 91 +++++++++++++++++++ .../security/JwtAuthenticationFilterTest.java | 23 +++-- .../security/SignOutCheckFilterTest.java | 38 ++++---- .../repository/SiteUserRepositoryTest.java | 62 +++++++++++++ .../support/TestContainerSpringBootTest.java | 2 + .../unit/repository/PostRepositoryTest.java | 10 +- .../solidconnection/util/JwtUtilsTest.java | 85 +++++++++++++++-- 19 files changed, 395 insertions(+), 110 deletions(-) rename src/main/java/com/example/solidconnection/config/security/{JwtAuthenticationEntryPoint.java => ExceptionHandlerFilter.java} (63%) create mode 100644 src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java create mode 100644 src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql rename src/test/java/com/example/solidconnection/{config/token => auth/service}/TokenProviderTest.java (96%) create mode 100644 src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java index 7fa6045f7..ad5607a27 100644 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -7,7 +7,9 @@ public enum TokenType { ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days - KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60); // 1hour + KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60), // 1hour + BLACKLIST("BLACKLIST:", ACCESS.expireTime) + ; private final String prefix; private final int expireTime; diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 29fb1b347..e16044e97 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -15,6 +15,7 @@ import java.util.concurrent.TimeUnit; import static com.example.solidconnection.auth.domain.TokenType.ACCESS; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @@ -30,16 +31,13 @@ public class AuthService { /* * 로그아웃 한다. - * - 리프레시 토큰을 무효화하기 위해 리프레시 토큰의 value 를 변경한다. - * - 어떤 사용자가 엑세스 토큰으로 인증이 필요한 기능을 사용하려 할 때, 로그아웃 검증이 진행되는데, - * - 이때 리프레시 토큰의 value 가 SIGN_OUT_VALUE 이면 예외 응답이 반환된다. - * - (TokenValidator.validateNotSignOut() 참고) + * - 엑세스 토큰을 블랙리스트에 추가한다. * */ - public void signOut(String email) { + public void signOut(String accessToken) { redisTemplate.opsForValue().set( - REFRESH.addPrefixToSubject(email), - SIGN_OUT_VALUE, - REFRESH.getExpireTime(), + BLACKLIST.addPrefixToSubject(accessToken), + accessToken, + BLACKLIST.getExpireTime(), TimeUnit.MILLISECONDS ); } @@ -61,15 +59,15 @@ public void quit(String email) { * - 리프레시 토큰이 만료되었거나, 존재하지 않는다면 예외 응답을 반환한다. * - 리프레시 토큰이 존재한다면, 액세스 토큰을 재발급한다. * */ - public ReissueResponse reissue(String email) { + public ReissueResponse reissue(String subject) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = REFRESH.addPrefixToSubject(email); + String refreshTokenKey = REFRESH.addPrefixToSubject(subject); String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); if (ObjectUtils.isEmpty(refreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenProvider.generateToken(email, ACCESS); + String newAccessToken = tokenProvider.generateToken(subject, ACCESS); tokenProvider.saveToken(newAccessToken, ACCESS); return new ReissueResponse(newAccessToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 693a968ea..9cba77c36 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -12,8 +12,8 @@ import java.util.Date; import java.util.concurrent.TimeUnit; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; @RequiredArgsConstructor @Component @@ -35,7 +35,7 @@ public String generateToken(String email, TokenType tokenType) { } public String saveToken(String token, TokenType tokenType) { - String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); + String subject = parseSubject(token, jwtProperties.secret()); redisTemplate.opsForValue().set( tokenType.addPrefixToSubject(subject), token, @@ -46,6 +46,6 @@ public String saveToken(String token, TokenType tokenType) { } public String getEmail(String token) { - return parseSubject(token, jwtProperties.secret()); + return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); } } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java b/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java similarity index 63% rename from src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java rename to src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java index 7487858be..59022c198 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationEntryPoint.java +++ b/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java @@ -3,12 +3,15 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -16,24 +19,32 @@ @Component @RequiredArgsConstructor -public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { +public class ExceptionHandlerFilter extends OncePerRequestFilter { private final ObjectMapper objectMapper; @Override - public void commence(HttpServletRequest request, HttpServletResponse response, - AuthenticationException authException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, authException.getMessage()); - writeResponse(response, errorResponse); + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + filterChain.doFilter(request, response); + } catch (CustomException e) { + customCommence(response, e); + } catch (Exception e) { + generalCommence(response, e); + } } - public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); + public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { + SecurityContextHolder.clearContext(); + ErrorResponse errorResponse = new ErrorResponse(customException); writeResponse(response, errorResponse); } - public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { - ErrorResponse errorResponse = new ErrorResponse(customException); + public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { + SecurityContextHolder.clearContext(); + ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); writeResponse(response, errorResponse); } diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java index e01009be1..5c7ab9f97 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java @@ -1,6 +1,5 @@ package com.example.solidconnection.config.security; -import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -8,7 +7,6 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; @@ -16,7 +14,7 @@ import java.io.IOException; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseSubject; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @@ -27,7 +25,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final String REISSUE_METHOD = "post"; private final JwtProperties jwtProperties; - private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @@ -39,19 +36,11 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, return; } - try { - String subject = parseSubjectOrElseThrow(token, jwtProperties.secret()); - UserDetails userDetails = new JwtUserDetails(subject); - Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - } catch (AuthenticationException e) { - jwtAuthenticationEntryPoint.commence(request, response, e); - } catch (CustomException e) { - jwtAuthenticationEntryPoint.customCommence(response, e); - } catch (Exception e) { - jwtAuthenticationEntryPoint.generalCommence(response, e); - } + String subject = parseSubject(token, jwtProperties.secret()); + UserDetails userDetails = new JwtUserDetails(subject); + Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + filterChain.doFilter(request, response); } private boolean isReissueRequest(HttpServletRequest request) { diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index d28d883ca..3f6307f8f 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -19,6 +19,7 @@ public class SecurityConfiguration { private final CorsProperties corsProperties; + private final ExceptionHandlerFilter exceptionHandlerFilter; private final SignOutCheckFilter signOutCheckFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; @@ -45,8 +46,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) - .addFilterBefore(this.jwtAuthenticationFilter, BasicAuthenticationFilter.class) - .addFilterBefore(this.signOutCheckFilter, JwtAuthenticationFilter.class) + .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) + .addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class) + .addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class) .build(); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java index 3c1218d13..c71252f1f 100644 --- a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java @@ -13,10 +13,8 @@ import java.io.IOException; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; -import static com.example.solidconnection.auth.service.AuthService.SIGN_OUT_VALUE; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; -import static com.example.solidconnection.util.JwtUtils.parseSubject; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @Component @@ -25,24 +23,20 @@ public class SignOutCheckFilter extends OncePerRequestFilter { private final RedisTemplate redisTemplate; private final JwtProperties jwtProperties; - private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException { String token = parseTokenFromRequest(request); - if (token == null || !isSignOut(token)) { - filterChain.doFilter(request, response); - return; + if (token != null && hasSignedOut(token)) { + throw new CustomException(USER_ALREADY_SIGN_OUT); } - - jwtAuthenticationEntryPoint.customCommence(response, new CustomException(USER_ALREADY_SIGN_OUT)); + filterChain.doFilter(request, response); } - private boolean isSignOut(String accessToken) { - String subject = parseSubject(accessToken, jwtProperties.secret()); - String refreshToken = REFRESH.addPrefixToSubject(subject); - return SIGN_OUT_VALUE.equals(redisTemplate.opsForValue().get(refreshToken)); + private boolean hasSignedOut(String accessToken) { + String blacklistKey = BLACKLIST.addPrefixToSubject(accessToken); + return redisTemplate.opsForValue().get(blacklistKey) != null; } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java new file mode 100644 index 000000000..d9d0b582c --- /dev/null +++ b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.siteuser.domain; + +public enum AuthType { + + KAKAO, + APPLE, + EMAIL, + ; +} diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index e518a5efb..2c2a5d8be 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -17,6 +17,8 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -32,15 +34,25 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @AllArgsConstructor +@Table(uniqueConstraints = { + @UniqueConstraint( + name = "uk_site_user_email_auth_type", + columnNames = {"email", "auth_type"} + ) +}) public class SiteUser { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(nullable = false, length = 100) + @Column(name = "email", nullable = false, length = 100) private String email; + @Column(name = "auth_type", nullable = false, length = 100) + @Enumerated(EnumType.STRING) + private AuthType authType; + @Setter @Column(nullable = false, length = 100) private String nickname; @@ -100,5 +112,25 @@ public SiteUser( this.preparationStage = preparationStage; this.role = role; this.gender = gender; + this.authType = AuthType.KAKAO; + } + + public SiteUser( + String email, + String nickname, + String profileImageUrl, + String birth, + PreparationStatus preparationStage, + Role role, + Gender gender, + AuthType authType) { + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.birth = birth; + this.preparationStage = preparationStage; + this.role = role; + this.gender = gender; + this.authType = authType; } } diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index a3775365d..3a1b58520 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -6,6 +6,8 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.stereotype.Component; +import java.util.Date; + import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; @Component @@ -25,22 +27,37 @@ public static String parseTokenFromRequest(HttpServletRequest request) { return token.substring(TOKEN_PREFIX.length()); } - public static String parseSubject(String token, String secretKey) { + public static String parseSubjectIgnoringExpiration(String token, String secretKey) { try { return extractSubject(token, secretKey); } catch (ExpiredJwtException e) { return e.getClaims().getSubject(); + } catch (Exception e) { + throw new CustomException(INVALID_TOKEN); } } - public static String parseSubjectOrElseThrow(String token, String secretKey) { + public static String parseSubject(String token, String secretKey) { try { return extractSubject(token, secretKey); - } catch (ExpiredJwtException e) { + } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } } + public static boolean isExpired(String token, String secretKey) { + try { + Date expiration = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody() + .getExpiration(); + return expiration.before(new Date()); + } catch (Exception e) { + return true; + } + } + private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { return Jwts.parser() .setSigningKey(secretKey) diff --git a/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql b/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql new file mode 100644 index 000000000..e89c4aa1b --- /dev/null +++ b/src/main/resources/db/migration/V3__add_auth_type_column_and_unique_key.sql @@ -0,0 +1,13 @@ +ALTER TABLE site_user +ADD COLUMN auth_type ENUM('KAKAO', 'APPLE', 'EMAIL'); + +UPDATE site_user +SET auth_type = 'KAKAO' +WHERE auth_type IS NULL; + +ALTER TABLE site_user +MODIFY COLUMN auth_type ENUM('KAKAO', 'APPLE', 'EMAIL') NOT NULL; + +ALTER TABLE site_user +ADD CONSTRAINT uk_site_user_email_auth_type +UNIQUE (email, auth_type); diff --git a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java rename to src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java index d3992a33a..8cc91e2c0 100644 --- a/src/test/java/com/example/solidconnection/config/token/TokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java @@ -1,7 +1,6 @@ -package com.example.solidconnection.config.token; +package com.example.solidconnection.auth.service; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; diff --git a/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java new file mode 100644 index 000000000..f4e8dc666 --- /dev/null +++ b/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java @@ -0,0 +1,91 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.BDDMockito.willThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +class ExceptionHandlerFilterTest { + + @Autowired + private ExceptionHandlerFilter exceptionHandlerFilter; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach() + void setUp() { + request = new MockHttpServletRequest(); + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + } + + @Test + void 필터_체인에서_예외가_발생하면_SecurityContext_를_초기화한다() throws Exception { + // given + Authentication authentication = mock(TestingAuthenticationToken.class); + SecurityContextHolder.getContext().setAuthentication(authentication); + willThrow(new RuntimeException()).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + } + + @Test + void 필터_체인에서_예외가_발생하지_않으면_다음_필터로_진행한다() throws Exception { + // given + willDoNothing().given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + then(filterChain).should().doFilter(request, response); + } + + @ParameterizedTest + @MethodSource("provideException") + void 필터_체인에서_예외가_발생하면_예외_응답을_반환한다(Throwable throwable) throws Exception { + // given + willThrow(throwable).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + private static Stream provideException() { + return Stream.of( + new RuntimeException(), + new CustomException(ErrorCode.INVALID_TOKEN) + ); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java index c0256f75a..16e3639f1 100644 --- a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java +++ b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -17,7 +18,9 @@ import java.util.Date; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; @@ -89,12 +92,10 @@ class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { .compact(); request = createRequestWithToken(token); - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + // when & then + assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_TOKEN.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } @@ -109,12 +110,10 @@ class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { .compact(); request = createRequestWithToken(token); - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + // when & then + assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_TOKEN.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } } diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java index 13544152b..a067bf9d9 100644 --- a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -17,9 +18,9 @@ import java.util.Date; import java.util.Objects; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.spy; @@ -55,15 +56,15 @@ void setUp() { @Test void 로그아웃한_토큰이면_예외를_응답한다() throws Exception { // given - request = createTokenRequest(subject); - String refreshTokenKey = REFRESH.addPrefixToSubject(subject); + String token = createToken(subject); + request = createRequest(token); + String refreshTokenKey = BLACKLIST.addPrefixToSubject(token); redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); - // when - signOutCheckFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(response.getStatus()).isEqualTo(USER_ALREADY_SIGN_OUT.getCode()); + // when & then + assertThatCode(() -> signOutCheckFilter.doFilterInternal(request, response, filterChain)) + .isInstanceOf(CustomException.class) + .hasMessage(USER_ALREADY_SIGN_OUT.getMessage()); then(filterChain).shouldHaveNoMoreInteractions(); } @@ -82,7 +83,8 @@ void setUp() { @Test void 로그아웃하지_않은_토큰이면_다음_필터로_전달한다() throws Exception { // given - request = createTokenRequest(subject); + String token = createToken(subject); + request = createRequest(token); // when signOutCheckFilter.doFilterInternal(request, response, filterChain); @@ -91,14 +93,16 @@ void setUp() { then(filterChain).should().doFilter(request, response); } - private HttpServletRequest createTokenRequest(String subject) { - String token = Jwts.builder() - .setSubject(subject) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); + private String createToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + private HttpServletRequest createRequest(String token) { MockHttpServletRequest request = new MockHttpServletRequest(); request.addHeader("Authorization", "Bearer " + token); return request; diff --git a/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java new file mode 100644 index 000000000..d3433937a --- /dev/null +++ b/src/test/java/com/example/solidconnection/siteuser/repository/SiteUserRepositoryTest.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.siteuser.repository; + +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.TestContainerDataJpaTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; + +import static org.assertj.core.api.Assertions.assertThatCode; + +@TestContainerDataJpaTest +class SiteUserRepositoryTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Nested + class 이메일과_인증_유형이_동일한_사용자는_저장할_수_없다 { + + @Test + void 이메일과_인증_유형이_동일한_사용자를_저장하면_예외_응답을_반환한다() { + // given + SiteUser user1 = createSiteUser("email", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", AuthType.KAKAO); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.save(user2)) + .isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + void 이메일이_같더라도_인증_유형이_다른_사용자는_정상_저장한다() { + // given + SiteUser user1 = createSiteUser("email", AuthType.KAKAO); + SiteUser user2 = createSiteUser("email", AuthType.APPLE); + siteUserRepository.save(user1); + + // when, then + assertThatCode(() -> siteUserRepository.save(user2)) + .doesNotThrowAnyException(); + } + } + + private SiteUser createSiteUser(String email, AuthType authType) { + return new SiteUser( + email, + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE, + authType + ); + } +} diff --git a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java index bcb110c6b..fe9b74f60 100644 --- a/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java +++ b/src/test/java/com/example/solidconnection/support/TestContainerSpringBootTest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.support; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; @@ -11,6 +12,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +@ExtendWith({DatabaseClearExtension.class}) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) @ActiveProfiles("test") diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java index 42da9de22..a37a0e6bf 100644 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java @@ -17,7 +17,6 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.List; @@ -30,10 +29,13 @@ @TestContainerDataJpaTest @DisplayName("게시글 레포지토리 테스트") class PostRepositoryTest { + @Autowired private PostRepository postRepository; + @Autowired private BoardRepository boardRepository; + @Autowired private SiteUserRepository siteUserRepository; @@ -89,7 +91,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. @@ -98,7 +99,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { // given Long invalidId = -1L; @@ -114,7 +114,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회한다() { Post foundPost = postRepository.getById(post.getId()); @@ -122,7 +121,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { Long invalidId = -1L; @@ -136,7 +134,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글_좋아요를_등록한다() { // given Long likeCount = post.getLikeCount(); @@ -150,7 +147,6 @@ private Post createPostWithImages(Board board, SiteUser siteUser) { } @Test - @Transactional void 게시글_좋아요를_삭제한다() { // given Long likeCount = post.getLikeCount(); diff --git a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java index 4dfc11540..95bdd5a52 100644 --- a/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java +++ b/src/test/java/com/example/solidconnection/util/JwtUtilsTest.java @@ -12,7 +12,7 @@ import java.util.Date; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectOrElseThrow; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; @@ -59,7 +59,7 @@ class 요청으로부터_토큰을_추출한다 { } @Nested - class 토큰으로부터_subject_를_추출한다 { + class 유효한_토큰으로부터_subject_를_추출한다 { @Test void 유효한_토큰의_subject_를_추출한다() { @@ -75,13 +75,29 @@ class 토큰으로부터_subject_를_추출한다 { } @Test - void 유효하지_않은_토큰의_subject_를_추출한다() { + void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { + // given + String subject = "subject123"; + String token = createExpiredToken(subject); + + // when + assertThatCode(() -> parseSubject(token, jwtSecretKey)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); + } + } + + @Nested + class 만료된_토큰으로부터_subject_를_추출한다 { + + @Test + void 만료된_토큰의_subject_를_예외를_발생시키지_않고_추출한다() { // given String subject = "subject999"; - String token = createInvalidToken(subject); + String token = createExpiredToken(subject); // when - String extractedSubject = parseSubject(token, jwtSecretKey); + String extractedSubject = parseSubjectIgnoringExpiration(token, jwtSecretKey); // then assertThat(extractedSubject).isEqualTo(subject); @@ -90,16 +106,56 @@ class 토큰으로부터_subject_를_추출한다 { @Test void 유효하지_않은_토큰의_subject_를_추출하면_예외_응답을_반환한다() { // given - String subject = "subject123"; - String token = createInvalidToken(subject); + String token = createExpiredUnsignedToken("hackers secret key"); - // when - assertThatCode(() -> parseSubjectOrElseThrow(token, jwtSecretKey)) + // when & then + assertThatCode(() -> parseSubjectIgnoringExpiration(token, jwtSecretKey)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); } } + + @Nested + class 토큰이_만료되었는지_확인한다 { + + @Test + void 서명된_토큰의_만료_여부를_반환한다() { + // given + String subject = "subject123"; + String validToken = createValidToken(subject); + String expiredToken = createExpiredToken(subject); + + // when + boolean isExpired1 = JwtUtils.isExpired(validToken, jwtSecretKey); + boolean isExpired2 = JwtUtils.isExpired(expiredToken, jwtSecretKey); + + // then + assertAll( + () -> assertThat(isExpired1).isFalse(), + () -> assertThat(isExpired2).isTrue() + ); + } + + @Test + void 서명되지_않은_토큰의_만료_여부를_반환한다() { + // given + String subject = "subject123"; + String validToken = createValidToken(subject); + String expiredToken = createExpiredToken(subject); + + // when + boolean isExpired1 = JwtUtils.isExpired(validToken, "wrong-secret-key"); + boolean isExpired2 = JwtUtils.isExpired(expiredToken, "wrong-secret-key"); + + // then + assertAll( + () -> assertThat(isExpired1).isTrue(), + () -> assertThat(isExpired2).isTrue() + ); + } + } + private String createValidToken(String subject) { return Jwts.builder() .setSubject(subject) @@ -109,7 +165,7 @@ private String createValidToken(String subject) { .compact(); } - private String createInvalidToken(String subject) { + private String createExpiredToken(String subject) { return Jwts.builder() .setSubject(subject) .setIssuedAt(new Date()) @@ -117,4 +173,13 @@ private String createInvalidToken(String subject) { .signWith(SignatureAlgorithm.HS256, jwtSecretKey) .compact(); } + + private String createExpiredUnsignedToken(String jwtSecretKey) { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtSecretKey) + .compact(); + } } From b37becd4a9a6de7a6ffefd8487bea74316e16b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:47:43 +0900 Subject: [PATCH 09/23] =?UTF-8?q?test:=20=EC=96=B4=ED=95=99=EC=A0=90?= =?UTF-8?q?=EC=88=98=20=EA=B4=80=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#165)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: GPA 등록 관련 통합테스트 코드 추가 * test: 어학 시험 점수 등록 관련 통합테스트 코드 추가 * test: GPA 조회 관련 통합테스트 코드 추가 * test: 어학시험 조회 관련 통합테스트 코드 추가 * test: GPA 점수 등록 테스트 시 VerifyStatus 검증 추가 * refactor: GPA 테스트 시 Long 타입 long으로 변경 * test: GpaScore 전체 url이 아닌 resource까지의 path로 변경 --- .../score/service/ScoreServiceTest.java | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java new file mode 100644 index 000000000..4a511d867 --- /dev/null +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -0,0 +1,204 @@ +package com.example.solidconnection.score.service; + +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.dto.GpaScoreRequest; +import com.example.solidconnection.score.dto.GpaScoreStatus; +import com.example.solidconnection.score.dto.GpaScoreStatusResponse; +import com.example.solidconnection.score.dto.LanguageTestScoreRequest; +import com.example.solidconnection.score.dto.LanguageTestScoreStatus; +import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.type.VerifyStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("점수 서비스 테스트") +class ScoreServiceTest extends BaseIntegrationTest { + + @Autowired + private ScoreService scoreService; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + + @Test + void GPA_점수_상태를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + List scores = List.of( + createGpaScore(testUser, 3.5, 4.5), + createGpaScore(testUser, 3.8, 4.5) + ); + + // when + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + + // then + assertThat(response.gpaScoreStatusList()) + .hasSize(scores.size()) + .containsExactlyInAnyOrder( + scores.stream() + .map(GpaScoreStatus::from) + .toArray(GpaScoreStatus[]::new) + ); + } + + @Test + void GPA_점수가_없는_경우_빈_리스트를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + + // then + assertThat(response.gpaScoreStatusList()).isEmpty(); + } + + @Test + void 어학_시험_점수_상태를_조회한다() { + // given + SiteUser testUser = createSiteUser(); + List scores = List.of( + createLanguageTestScore(testUser, LanguageTestType.TOEIC, "100"), + createLanguageTestScore(testUser, LanguageTestType.TOEFL_IBT, "7.5") + ); + + // when + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + + // then + assertThat(response.languageTestScoreStatusList()) + .hasSize(scores.size()) + .containsExactlyInAnyOrder( + scores.stream() + .map(LanguageTestScoreStatus::from) + .toArray(LanguageTestScoreStatus[]::new) + ); + } + + @Test + void 어학_시험_점수가_없는_경우_빈_리스트를_반환한다() { + // given + SiteUser testUser = createSiteUser(); + + // when + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + + // then + assertThat(response.languageTestScoreStatusList()).isEmpty(); + } + + @Test + void GPA_점수를_등록한다() { + // given + SiteUser testUser = createSiteUser(); + GpaScoreRequest request = createGpaScoreRequest(); + + // when + long scoreId = scoreService.submitGpaScore(testUser.getEmail(), request); + GpaScore savedScore = gpaScoreRepository.findById(scoreId).orElseThrow(); + + // then + assertAll( + () -> assertThat(savedScore.getId()).isEqualTo(scoreId), + () -> assertThat(savedScore.getGpa().getGpa()).isEqualTo(request.gpa()), + () -> assertThat(savedScore.getGpa().getGpaCriteria()).isEqualTo(request.gpaCriteria()), + () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), + () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) + ); + } + + @Test + void 어학_시험_점수를_등록한다() { + // given + SiteUser testUser = createSiteUser(); + LanguageTestScoreRequest request = createLanguageTestScoreRequest(); + + // when + long scoreId = scoreService.submitLanguageTestScore(testUser.getEmail(), request); + LanguageTestScore savedScore = languageTestScoreRepository.findById(scoreId).orElseThrow(); + + // then + assertAll( + () -> assertThat(savedScore.getId()).isEqualTo(scoreId), + () -> assertThat(savedScore.getLanguageTest().getLanguageTestType()).isEqualTo(request.languageTestType()), + () -> assertThat(savedScore.getLanguageTest().getLanguageTestScore()).isEqualTo(request.languageTestScore()), + () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), + () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) + ); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } + + private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteria) { + GpaScore gpaScore = new GpaScore( + new Gpa(gpa, gpaCriteria, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + return gpaScoreRepository.save(gpaScore); + } + + private LanguageTestScore createLanguageTestScore(SiteUser siteUser, LanguageTestType languageTestType, String score) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(languageTestType, score, "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + return languageTestScoreRepository.save(languageTestScore); + } + + private GpaScoreRequest createGpaScoreRequest() { + return new GpaScoreRequest( + 3.5, + 4.5, + LocalDate.now(), + "/gpa-report.pdf" + ); + } + + private LanguageTestScoreRequest createLanguageTestScoreRequest() { + return new LanguageTestScoreRequest( + LanguageTestType.TOEFL_IBT, + "100", + LocalDate.now(), + "/gpa-report.pdf" + ); + } +} From 68560a1826e0fe3e46cdfc05dade6bd29f206a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:31:46 +0900 Subject: [PATCH 10/23] =?UTF-8?q?refactor:=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20(#168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 게시글 생성 관련 통합테스트 코드 추가 * test: 게시글 수정 관련 통합테스트 코드 추가 * refactor: PostService Query, Command, Like Service로 분리 * test: 게시글 삭제 관련 통합테스트 코드 추가 * test: 게시글 조회 관련 통합테스트 코드 추가 * test: 게시글 좋아요 관련 통합테스트 코드 추가 * refactor: 유효하지 않은 코드 조회 시 에러 발생 테스트코드 삭제 * chore: 예외 응답 테스트명 "~면_예외_응답을_반환한다"로 통일 * style: 사용하지 않는 import문 제거 * refactor: 유저와 게시판 BaseIntegrationTest에서 미리 생성하도록 변경 * refactor: BaseIntegrationTest에서 생성한 유저와 게시판 사용하는 것으로 변경 - 기존 유저와 게시판 생성하는 private 함수 제거 * test: when 절 코드를 then 절로 이동 --- .../post/controller/PostController.java | 21 +- ...stService.java => PostCommandService.java} | 153 ++----- .../post/service/PostLikeService.java | 71 ++++ .../post/service/PostQueryService.java | 76 ++++ .../solidconnection/service/RedisService.java | 4 + .../PostLikeCountConcurrencyTest.java | 10 +- .../post/service/PostCommandServiceTest.java | 373 ++++++++++++++++++ .../post/service/PostLikeServiceTest.java | 136 +++++++ .../post/service/PostQueryServiceTest.java | 127 ++++++ .../integration/BaseIntegrationTest.java | 54 +++ .../unit/service/PostServiceTest.java | 132 ++++--- 11 files changed, 967 insertions(+), 190 deletions(-) rename src/main/java/com/example/solidconnection/post/service/{PostService.java => PostCommandService.java} (60%) create mode 100644 src/main/java/com/example/solidconnection/post/service/PostLikeService.java create mode 100644 src/main/java/com/example/solidconnection/post/service/PostQueryService.java create mode 100644 src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index 05cdfc574..c3ff3ce3a 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -8,7 +8,9 @@ import com.example.solidconnection.post.dto.PostLikeResponse; import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.post.service.PostCommandService; +import com.example.solidconnection.post.service.PostLikeService; +import com.example.solidconnection.post.service.PostQueryService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -32,7 +34,9 @@ @RequestMapping("/communities") public class PostController { - private final PostService postService; + private final PostQueryService postQueryService; + private final PostCommandService postCommandService; + private final PostLikeService postLikeService; @PostMapping(value = "/{code}/posts") public ResponseEntity createPost( @@ -44,7 +48,7 @@ public ResponseEntity createPost( if (imageFile == null) { imageFile = Collections.emptyList(); } - PostCreateResponse post = postService + PostCreateResponse post = postCommandService .createPost(principal.getName(), code, postCreateRequest, imageFile); return ResponseEntity.ok().body(post); } @@ -60,19 +64,18 @@ public ResponseEntity updatePost( if (imageFile == null) { imageFile = Collections.emptyList(); } - PostUpdateResponse postUpdateResponse = postService + PostUpdateResponse postUpdateResponse = postCommandService .updatePost(principal.getName(), code, postId, postUpdateRequest, imageFile); return ResponseEntity.ok().body(postUpdateResponse); } - @GetMapping("/{code}/posts/{post_id}") public ResponseEntity findPostById( Principal principal, @PathVariable("code") String code, @PathVariable("post_id") Long postId) { - PostFindResponse postFindResponse = postService + PostFindResponse postFindResponse = postQueryService .findPostById(principal.getName(), code, postId); return ResponseEntity.ok().body(postFindResponse); } @@ -83,7 +86,7 @@ public ResponseEntity deletePostById( @PathVariable("code") String code, @PathVariable("post_id") Long postId) { - PostDeleteResponse postDeleteResponse = postService.deletePostById(principal.getName(), code, postId); + PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(principal.getName(), code, postId); return ResponseEntity.ok().body(postDeleteResponse); } @@ -94,7 +97,7 @@ public ResponseEntity likePost( @PathVariable("post_id") Long postId ) { - PostLikeResponse postLikeResponse = postService.likePost(principal.getName(), code, postId); + PostLikeResponse postLikeResponse = postLikeService.likePost(principal.getName(), code, postId); return ResponseEntity.ok().body(postLikeResponse); } @@ -105,7 +108,7 @@ public ResponseEntity dislikePost( @PathVariable("post_id") Long postId ) { - PostDislikeResponse postDislikeResponse = postService.dislikePost(principal.getName(), code, postId); + PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(principal.getName(), code, postId); return ResponseEntity.ok().body(postDislikeResponse); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostService.java b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java similarity index 60% rename from src/main/java/com/example/solidconnection/post/service/PostService.java rename to src/main/java/com/example/solidconnection/post/service/PostCommandService.java index d31cfb97a..7b0c4f937 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java @@ -1,30 +1,20 @@ package com.example.solidconnection.post.service; import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.dto.PostFindBoardResponse; import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.service.CommentService; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; import com.example.solidconnection.post.dto.PostCreateRequest; import com.example.solidconnection.post.dto.PostCreateResponse; import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; import com.example.solidconnection.post.dto.PostUpdateRequest; import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.ImgType; @@ -33,7 +23,6 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -41,72 +30,24 @@ import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; -import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; @Service @RequiredArgsConstructor -public class PostService { +public class PostCommandService { private final PostRepository postRepository; private final SiteUserRepository siteUserRepository; private final BoardRepository boardRepository; private final S3Service s3Service; - private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; - private final PostLikeRepository postLikeRepository; - - private String validateCode(String code) { - try { - return String.valueOf(BoardCode.valueOf(code)); - } catch (IllegalArgumentException ex) { - throw new CustomException(INVALID_BOARD_CODE); - } - } - - private void validateOwnership(Post post, String email) { - if (!post.getSiteUser().getEmail().equals(email)) { - throw new CustomException(INVALID_POST_ACCESS); - } - } - - private void validateFileSize(List imageFile) { - if (imageFile.isEmpty()) { - return; - } - if (imageFile.size() > 5) { - throw new CustomException(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES); - } - } - - private void validateQuestion(Post post) { - if (post.getIsQuestion()) { - throw new CustomException(CAN_NOT_DELETE_OR_UPDATE_QUESTION); - } - } - - private void validatePostCategory(String category) { - if (!EnumUtils.isValidEnum(PostCategory.class, category) || category.equals(PostCategory.전체.toString())) { - throw new CustomException(INVALID_POST_CATEGORY); - } - } - - private Boolean getIsOwner(Post post, String email) { - return post.getSiteUser().getEmail().equals(email); - } - - private Boolean getIsLiked(Post post, SiteUser siteUser) { - return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) - .isPresent(); - } @Transactional public PostCreateResponse createPost(String email, String code, PostCreateRequest postCreateRequest, List imageFile) { - // 유효성 검증 String boardCode = validateCode(code); validatePostCategory(postCreateRequest.postCategory()); @@ -126,7 +67,6 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques @Transactional public PostUpdateResponse updatePost(String email, String code, Long postId, PostUpdateRequest postUpdateRequest, List imageFile) { - // 유효성 검증 String boardCode = validateCode(code); Post post = postRepository.getById(postId); @@ -155,40 +95,8 @@ private void savePostImages(List imageFile, Post post) { } } - private void removePostImages(Post post) { - for (PostImage postImage : post.getPostImageList()) { - s3Service.deletePostImage(postImage.getUrl()); - } - post.getPostImageList().clear(); - } - - @Transactional(readOnly = true) - public PostFindResponse findPostById(String email, String code, Long postId) { - - String boardCode = validateCode(code); - - Post post = postRepository.getByIdUsingEntityGraph(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); - Boolean isOwner = getIsOwner(post, email); - Boolean isLiked = getIsLiked(post, siteUser); - - PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); - PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); - List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); - List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); - - // caching && 어뷰징 방지 - if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { - redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); - } - - return PostFindResponse.from( - post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); - } - @Transactional public PostDeleteResponse deletePostById(String email, String code, Long postId) { - String boardCode = validateCode(code); Post post = postRepository.getById(postId); validateOwnership(post, email); @@ -203,40 +111,45 @@ public PostDeleteResponse deletePostById(String email, String code, Long postId) return new PostDeleteResponse(postId); } - @Transactional(isolation = Isolation.READ_COMMITTED) - public PostLikeResponse likePost(String email, String code, Long postId) { - - String boardCode = validateCode(code); - Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); - validateDuplicatePostLike(post, siteUser); - - PostLike postLike = new PostLike(); - postLike.setPostAndSiteUser(post, siteUser); - postLikeRepository.save(postLike); - postRepository.increaseLikeCount(post.getId()); - - return PostLikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(INVALID_BOARD_CODE); + } } - private void validateDuplicatePostLike(Post post, SiteUser siteUser) { - if (postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser).isPresent()) { - throw new CustomException(DUPLICATE_POST_LIKE); + private void validateOwnership(Post post, String email) { + if (!post.getSiteUser().getEmail().equals(email)) { + throw new CustomException(INVALID_POST_ACCESS); } } - @Transactional(isolation = Isolation.READ_COMMITTED) - public PostDislikeResponse dislikePost(String email, String code, Long postId) { + private void validateFileSize(List imageFile) { + if (imageFile.isEmpty()) { + return; + } + if (imageFile.size() > 5) { + throw new CustomException(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES); + } + } - String boardCode = validateCode(code); - Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); + private void validateQuestion(Post post) { + if (post.getIsQuestion()) { + throw new CustomException(CAN_NOT_DELETE_OR_UPDATE_QUESTION); + } + } - PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); - postLike.resetPostAndSiteUser(); - postLikeRepository.deleteById(postLike.getId()); - postRepository.decreaseLikeCount(post.getId()); + private void validatePostCategory(String category) { + if (!EnumUtils.isValidEnum(PostCategory.class, category) || category.equals(PostCategory.전체.toString())) { + throw new CustomException(INVALID_POST_CATEGORY); + } + } - return PostDislikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + private void removePostImages(Post post) { + for (PostImage postImage : post.getPostImageList()) { + s3Service.deletePostImage(postImage.getUrl()); + } + post.getPostImageList().clear(); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java new file mode 100644 index 000000000..8a72d5f9f --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java @@ -0,0 +1,71 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.post.dto.PostDislikeResponse; +import com.example.solidconnection.post.dto.PostLikeResponse; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.BoardCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; + +@Service +@RequiredArgsConstructor +public class PostLikeService { + + private final PostRepository postRepository; + private final SiteUserRepository siteUserRepository; + private final PostLikeRepository postLikeRepository; + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostLikeResponse likePost(String email, String code, Long postId) { + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + validateDuplicatePostLike(post, siteUser); + + PostLike postLike = new PostLike(); + postLike.setPostAndSiteUser(post, siteUser); + postLikeRepository.save(postLike); + postRepository.increaseLikeCount(post.getId()); + + return PostLikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } + + @Transactional(isolation = Isolation.READ_COMMITTED) + public PostDislikeResponse dislikePost(String email, String code, Long postId) { + String boardCode = validateCode(code); + Post post = postRepository.getById(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + + PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); + postLike.resetPostAndSiteUser(); + postLikeRepository.deleteById(postLike.getId()); + postRepository.decreaseLikeCount(post.getId()); + + return PostDislikeResponse.from(postRepository.getById(postId)); // 실시간성을 위한 재조회 + } + + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(INVALID_BOARD_CODE); + } + } + + private void validateDuplicatePostLike(Post post, SiteUser siteUser) { + if (postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser).isPresent()) { + throw new CustomException(DUPLICATE_POST_LIKE); + } + } +} diff --git a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java new file mode 100644 index 000000000..d53470124 --- /dev/null +++ b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java @@ -0,0 +1,76 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.dto.PostFindBoardResponse; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.post.dto.PostFindResponse; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.util.RedisUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; + +@Service +@RequiredArgsConstructor +public class PostQueryService { + + private final PostRepository postRepository; + private final SiteUserRepository siteUserRepository; + private final CommentService commentService; + private final RedisService redisService; + private final RedisUtils redisUtils; + private final PostLikeRepository postLikeRepository; + + @Transactional(readOnly = true) + public PostFindResponse findPostById(String email, String code, Long postId) { + String boardCode = validateCode(code); + + Post post = postRepository.getByIdUsingEntityGraph(postId); + SiteUser siteUser = siteUserRepository.getByEmail(email); + Boolean isOwner = getIsOwner(post, email); + Boolean isLiked = getIsLiked(post, siteUser); + + PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); + PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); + List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); + List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); + + // caching && 어뷰징 방지 + if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { + redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); + } + + return PostFindResponse.from( + post, isOwner, isLiked, boardPostFindResultDTO, siteUserPostFindResultDTO, commentFindResultDTOList, postImageFindResultDTOList); + } + + private String validateCode(String code) { + try { + return String.valueOf(BoardCode.valueOf(code)); + } catch (IllegalArgumentException ex) { + throw new CustomException(INVALID_BOARD_CODE); + } + } + + private Boolean getIsOwner(Post post, String email) { + return post.getSiteUser().getEmail().equals(email); + } + + private Boolean getIsLiked(Post post, SiteUser siteUser) { + return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) + .isPresent(); + } +} diff --git a/src/main/java/com/example/solidconnection/service/RedisService.java b/src/main/java/com/example/solidconnection/service/RedisService.java index 93a9de74f..36be7b66f 100644 --- a/src/main/java/com/example/solidconnection/service/RedisService.java +++ b/src/main/java/com/example/solidconnection/service/RedisService.java @@ -42,4 +42,8 @@ public boolean isPresent(String key) { return Boolean.TRUE.equals(redisTemplate.opsForValue() .setIfAbsent(key, "1", Long.parseLong(VALIDATE_VIEW_COUNT_TTL.getValue()), TimeUnit.SECONDS)); } + + public boolean isKeyExists(String key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } } diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index e553eb4bb..36bd91819 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -4,7 +4,8 @@ import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.post.service.PostCommandService; +import com.example.solidconnection.post.service.PostLikeService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; @@ -30,7 +31,7 @@ class PostLikeCountConcurrencyTest { @Autowired - private PostService postService; + private PostLikeService postLikeService; @Autowired private PostRepository postRepository; @Autowired @@ -118,8 +119,8 @@ private Post createPost(Board board, SiteUser siteUser) { String email = "email" + i; executorService.submit(() -> { try { - postService.likePost(email, board.getCode(), post.getId()); - postService.dislikePost(email, board.getCode(), post.getId()); + postLikeService.likePost(email, board.getCode(), post.getId()); + postLikeService.dislikePost(email, board.getCode(), post.getId()); } finally { doneSignal.countDown(); } @@ -135,5 +136,4 @@ private Post createPost(Board board, SiteUser siteUser) { assertEquals(likeCount, postRepository.getById(post.getId()).getLikeCount()); } - } diff --git a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java new file mode 100644 index 000000000..eb1b2b652 --- /dev/null +++ b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java @@ -0,0 +1,373 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostCreateRequest; +import com.example.solidconnection.post.dto.PostCreateResponse; +import com.example.solidconnection.post.dto.PostDeleteResponse; +import com.example.solidconnection.post.dto.PostUpdateRequest; +import com.example.solidconnection.post.dto.PostUpdateResponse; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.s3.S3Service; +import com.example.solidconnection.s3.UploadedFileUrlResponse; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.ImgType; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.util.RedisUtils; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.any; +import static org.mockito.BDDMockito.eq; +import static org.mockito.BDDMockito.then; + +@DisplayName("게시글 생성/수정/삭제 서비스 테스트") +class PostCommandServiceTest extends BaseIntegrationTest { + + @Autowired + private PostCommandService postCommandService; + + @MockBean + private S3Service s3Service; + + @Autowired + private RedisService redisService; + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostImageRepository postImageRepository; + + @Nested + class 게시글_생성_테스트 { + + @Test + @Transactional + void 게시글을_성공적으로_생성한다() { + // given + PostCreateRequest request = createPostCreateRequest(PostCategory.자유.name()); + List imageFiles = List.of(createImageFile()); + String expectedImageUrl = "test-image-url"; + given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); + + // when + PostCreateResponse response = postCommandService.createPost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + request, + imageFiles + ); + + // then + Post savedPost = postRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(response.id()).isEqualTo(savedPost.getId()), + () -> assertThat(savedPost.getTitle()).isEqualTo(request.title()), + () -> assertThat(savedPost.getContent()).isEqualTo(request.content()), + () -> assertThat(savedPost.getIsQuestion()).isEqualTo(request.isQuestion()), + () -> assertThat(savedPost.getCategory().name()).isEqualTo(request.postCategory()), + () -> assertThat(savedPost.getBoard().getCode()).isEqualTo(자유게시판.getCode()), + () -> assertThat(savedPost.getPostImageList()).hasSize(imageFiles.size()), + () -> assertThat(savedPost.getPostImageList()) + .extracting(PostImage::getUrl) + .containsExactly(expectedImageUrl) + ); + } + + @Test + void 전체_카테고리로_생성하면_예외_응답을_반환한다() { + // given + PostCreateRequest request = createPostCreateRequest(PostCategory.전체.name()); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_CATEGORY.getMessage()); + } + + @Test + void 존재하지_않는_카테고리로_생성하면_예외_응답을_반환한다() { + // given + PostCreateRequest request = createPostCreateRequest("INVALID_CATEGORY"); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_CATEGORY.getMessage()); + } + + @Test + void 이미지를_5개_초과하여_업로드하면_예외_응답을_반환한다() { + // given + PostCreateRequest request = createPostCreateRequest(PostCategory.자유.name()); + List imageFiles = createSixImageFiles(); + + // when & then + assertThatThrownBy(() -> + postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); + } + } + + @Nested + class 게시글_수정_테스트 { + + @Test + @Transactional + void 게시글을_성공적으로_수정한다() { + // given + String originImageUrl = "origin-image-url"; + String expectedImageUrl = "update-image-url"; + Post testPost = createPost(자유게시판, 테스트유저_1, originImageUrl); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = List.of(createImageFile()); + + given(s3Service.uploadFiles(any(), eq(ImgType.COMMUNITY))) + .willReturn(List.of(new UploadedFileUrlResponse(expectedImageUrl))); + + // when + PostUpdateResponse response = postCommandService.updatePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + ); + + // then + Post updatedPost = postRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(updatedPost.getTitle()).isEqualTo(request.title()), + () -> assertThat(updatedPost.getContent()).isEqualTo(request.content()), + () -> assertThat(updatedPost.getCategory().name()).isEqualTo(request.postCategory()), + () -> assertThat(updatedPost.getPostImageList()).hasSize(imageFiles.size()), + () -> assertThat(updatedPost.getPostImageList()) + .extracting(PostImage::getUrl) + .containsExactly(expectedImageUrl) + ); + then(s3Service).should().deletePostImage(originImageUrl); + } + + @Test + void 다른_사용자의_게시글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1, "origin-image-url"); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.updatePost( + 테스트유저_2.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + + @Test + void 질문_게시글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createQuestionPost(자유게시판, 테스트유저_1, "origin-image-url"); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = List.of(); + + // when & then + assertThatThrownBy(() -> + postCommandService.updatePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + } + + @Test + void 이미지를_5개_초과하여_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1, "origin-image-url"); + PostUpdateRequest request = createPostUpdateRequest(); + List imageFiles = createSixImageFiles(); + + // when & then + assertThatThrownBy(() -> + postCommandService.updatePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId(), + request, + imageFiles + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); + } + } + + @Nested + class 게시글_삭제_테스트 { + + @Test + void 게시글을_성공적으로_삭제한다() { + // given + String originImageUrl = "origin-image-url"; + Post testPost = createPost(자유게시판, 테스트유저_1, originImageUrl); + String viewCountKey = redisUtils.getPostViewCountRedisKey(testPost.getId()); + redisService.increaseViewCount(viewCountKey); + + // when + PostDeleteResponse response = postCommandService.deletePostById( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(testPost.getId()), + () -> assertThat(postRepository.findById(testPost.getId())).isEmpty(), + () -> assertThat(redisService.isKeyExists(viewCountKey)).isFalse() + ); + then(s3Service).should().deletePostImage(originImageUrl); + } + + @Test + void 다른_사용자의_게시글을_삭제하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1, "origin-image-url"); + + // when & then + assertThatThrownBy(() -> + postCommandService.deletePostById( + 테스트유저_2.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + + @Test + void 질문_게시글을_삭제하면_예외_응답을_반환한다() { + // given + Post testPost = createQuestionPost(자유게시판, 테스트유저_1, "origin-image-url"); + + // when & then + assertThatThrownBy(() -> + postCommandService.deletePostById( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + } + } + + private PostCreateRequest createPostCreateRequest(String category) { + return new PostCreateRequest( + category, + "테스트 제목", + "테스트 내용", + false + ); + } + + private MockMultipartFile createImageFile() { + return new MockMultipartFile( + "image", + "test.jpg", + "image/jpeg", + "test image content".getBytes() + ); + } + + private List createSixImageFiles() { + return List.of( + createImageFile(), + createImageFile(), + createImageFile(), + createImageFile(), + createImageFile(), + createImageFile() + ); + } + + private Post createPost(Board board, SiteUser siteUser, String originImageUrl) { + Post post = new Post( + "원본 제목", + "원본 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage(originImageUrl); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } + + private Post createQuestionPost(Board board, SiteUser siteUser, String originImageUrl) { + Post post = new Post( + "질문 제목", + "질문 내용", + true, + 0L, + 0L, + PostCategory.질문 + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage(originImageUrl); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } + + private PostUpdateRequest createPostUpdateRequest() { + return new PostUpdateRequest( + PostCategory.자유.name(), + "수정된 제목", + "수정된 내용" + ); + } +} diff --git a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java new file mode 100644 index 000000000..9fe6a2704 --- /dev/null +++ b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java @@ -0,0 +1,136 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostDislikeResponse; +import com.example.solidconnection.post.dto.PostLikeResponse; +import com.example.solidconnection.post.repository.PostLikeRepository; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.PostCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_POST_LIKE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("게시글 좋아요 서비스 테스트") +class PostLikeServiceTest extends BaseIntegrationTest { + + @Autowired + private PostLikeService postLikeService; + + @Autowired + private PostRepository postRepository; + + @Autowired + private PostLikeRepository postLikeRepository; + + @Nested + class 게시글_좋아요_테스트 { + + @Test + void 게시글을_성공적으로_좋아요한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + long beforeLikeCount = testPost.getLikeCount(); + + // when + PostLikeResponse response = postLikeService.likePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + Post likedPost = postRepository.findById(testPost.getId()).orElseThrow(); + assertAll( + () -> assertThat(response.likeCount()).isEqualTo(beforeLikeCount + 1), + () -> assertThat(response.isLiked()).isTrue(), + () -> assertThat(likedPost.getLikeCount()).isEqualTo(beforeLikeCount + 1), + () -> assertThat(postLikeRepository.findPostLikeByPostAndSiteUser(likedPost, 테스트유저_1)).isPresent() + ); + } + + @Test + void 이미_좋아요한_게시글을_다시_좋아요하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + + // when & then + assertThatThrownBy(() -> + postLikeService.likePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(DUPLICATE_POST_LIKE.getMessage()); + } + } + + @Nested + class 게시글_좋아요_취소_테스트 { + + @Test + void 게시글_좋아요를_성공적으로_취소한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + long beforeLikeCount = beforeResponse.likeCount(); + + // when + PostDislikeResponse response = postLikeService.dislikePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + Post unlikedPost = postRepository.findById(testPost.getId()).orElseThrow(); + assertAll( + () -> assertThat(response.likeCount()).isEqualTo(beforeLikeCount - 1), + () -> assertThat(response.isLiked()).isFalse(), + () -> assertThat(unlikedPost.getLikeCount()).isEqualTo(beforeLikeCount - 1), + () -> assertThat(postLikeRepository.findPostLikeByPostAndSiteUser(unlikedPost, 테스트유저_1)).isEmpty() + ); + } + + @Test + void 좋아요하지_않은_게시글을_좋아요_취소하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + + // when & then + assertThatThrownBy(() -> + postLikeService.dislikePost( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_LIKE.getMessage()); + } + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "테스트 제목", + "테스트 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + return postRepository.save(post); + } +} diff --git a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java new file mode 100644 index 000000000..7ec36b0df --- /dev/null +++ b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java @@ -0,0 +1,127 @@ +package com.example.solidconnection.post.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.post.dto.PostFindResponse; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.service.RedisService; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.PostCategory; +import com.example.solidconnection.util.RedisUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("게시글 조회 서비스 테스트") +class PostQueryServiceTest extends BaseIntegrationTest { + + @Autowired + private PostQueryService postQueryService; + + @Autowired + private RedisService redisService; + + @Autowired + private RedisUtils redisUtils; + + @Autowired + private PostRepository postRepository; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private PostImageRepository postImageRepository; + + @Test + void 게시글을_성공적으로_조회한다() { + // given + String expectedImageUrl = "test-image-url"; + List imageUrls = List.of(expectedImageUrl); + Post testPost = createPost(자유게시판, 테스트유저_1, expectedImageUrl); + List comments = createComments(testPost, 테스트유저_1, List.of("첫번째 댓글", "두번째 댓글")); + + String validateKey = redisUtils.getValidatePostViewCountRedisKey(테스트유저_1.getEmail(), testPost.getId()); + String viewCountKey = redisUtils.getPostViewCountRedisKey(testPost.getId()); + + // when + PostFindResponse response = postQueryService.findPostById( + 테스트유저_1.getEmail(), + 자유게시판.getCode(), + testPost.getId() + ); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(testPost.getId()), + () -> assertThat(response.title()).isEqualTo(testPost.getTitle()), + () -> assertThat(response.content()).isEqualTo(testPost.getContent()), + () -> assertThat(response.isQuestion()).isEqualTo(testPost.getIsQuestion()), + () -> assertThat(response.likeCount()).isEqualTo(testPost.getLikeCount()), + () -> assertThat(response.viewCount()).isEqualTo(testPost.getViewCount()), + () -> assertThat(response.postCategory()).isEqualTo(String.valueOf(testPost.getCategory())), + + () -> assertThat(response.postFindBoardResponse().code()).isEqualTo(자유게시판.getCode()), + () -> assertThat(response.postFindBoardResponse().koreanName()).isEqualTo(자유게시판.getKoreanName()), + + () -> assertThat(response.postFindSiteUserResponse().id()).isEqualTo(테스트유저_1.getId()), + () -> assertThat(response.postFindSiteUserResponse().nickname()).isEqualTo(테스트유저_1.getNickname()), + () -> assertThat(response.postFindSiteUserResponse().profileImageUrl()).isEqualTo(테스트유저_1.getProfileImageUrl()), + + () -> assertThat(response.postFindPostImageResponses()) + .hasSize(imageUrls.size()) + .extracting(PostFindPostImageResponse::url) + .containsExactlyElementsOf(imageUrls), + + () -> assertThat(response.postFindCommentResponses()) + .hasSize(comments.size()) + .extracting(PostFindCommentResponse::content) + .containsExactlyElementsOf(comments.stream().map(Comment::getContent).toList()), + + () -> assertThat(response.isOwner()).isTrue(), + () -> assertThat(response.isLiked()).isFalse(), + + () -> assertThat(redisService.isKeyExists(viewCountKey)).isTrue(), + () -> assertThat(redisService.isKeyExists(validateKey)).isTrue() + ); + } + + private Post createPost(Board board, SiteUser siteUser, String originImageUrl) { + Post post = new Post( + "원본 제목", + "원본 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage(originImageUrl); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } + + private List createComments(Post post, SiteUser siteUser, List contents) { + return contents.stream() + .map(content -> { + Comment comment = new Comment(content); + comment.setPostAndSiteUser(post, siteUser); + return commentRepository.save(comment); + }) + .toList(); + } +} diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index b1f7d9203..f588b87ae 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -1,12 +1,19 @@ package com.example.solidconnection.support.integration; +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.Region; import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.DatabaseClearExtension; import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; import com.example.solidconnection.university.domain.LanguageRequirement; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; @@ -20,6 +27,10 @@ import java.util.HashSet; +import static com.example.solidconnection.type.BoardCode.AMERICAS; +import static com.example.solidconnection.type.BoardCode.ASIA; +import static com.example.solidconnection.type.BoardCode.EUROPE; +import static com.example.solidconnection.type.BoardCode.FREE; import static com.example.solidconnection.type.SemesterAvailableForDispatch.ONE_SEMESTER; import static com.example.solidconnection.type.TuitionFeeType.HOME_UNIVERSITY_PAYMENT; @@ -27,6 +38,9 @@ @ExtendWith(DatabaseClearExtension.class) public abstract class BaseIntegrationTest { + public static SiteUser 테스트유저_1; + public static SiteUser 테스트유저_2; + public static Region 영미권; public static Region 유럽; public static Region 아시아; @@ -57,6 +71,14 @@ public abstract class BaseIntegrationTest { public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; public static UniversityInfoForApply 메이지대학_지원_정보; + public static Board 미주권; + public static Board 아시아권; + public static Board 유럽권; + public static Board 자유게시판; + + @Autowired + private SiteUserRepository siteUserRepository; + @Autowired private RegionRepository regionRepository; @@ -72,16 +94,41 @@ public abstract class BaseIntegrationTest { @Autowired private LanguageRequirementRepository languageRequirementRepository; + @Autowired + private BoardRepository boardRepository; + @Value("${university.term}") public String term; @BeforeEach public void setUpBaseData() { + setUpSiteUsers(); setUpRegions(); setUpCountries(); setUpUniversities(); setUpUniversityInfos(); setUpLanguageRequirements(); + setUpBoards(); + } + + private void setUpSiteUsers() { + 테스트유저_1 = siteUserRepository.save(new SiteUser( + "test1@example.com", + "nickname1", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); + + 테스트유저_2 = siteUserRepository.save(new SiteUser( + "test2@example.com", + "nickname2", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); } private void setUpRegions() { @@ -283,6 +330,13 @@ private void setUpLanguageRequirements() { saveLanguageTestRequirement(메이지대학_지원_정보, LanguageTestType.JLPT, "N2"); } + private void setUpBoards() { + 미주권 = boardRepository.save(new Board(AMERICAS.name(), "미주권")); + 아시아권 = boardRepository.save(new Board(ASIA.name(), "아시아권")); + 유럽권 = boardRepository.save(new Board(EUROPE.name(), "유럽권")); + 자유게시판 = boardRepository.save(new Board(FREE.name(), "자유게시판")); + } + private void saveLanguageTestRequirement( UniversityInfoForApply universityInfoForApply, LanguageTestType testType, diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java index 57c5916a9..afc899255 100644 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java @@ -14,7 +14,9 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.dto.*; import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostService; +import com.example.solidconnection.post.service.PostCommandService; +import com.example.solidconnection.post.service.PostLikeService; +import com.example.solidconnection.post.service.PostQueryService; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; @@ -33,21 +35,41 @@ import org.springframework.mock.web.MockMultipartFile; import org.springframework.web.multipart.MultipartFile; -import java.util.*; - -import static com.example.solidconnection.custom.exception.ErrorCode.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) @DisplayName("게시글 서비스 테스트") class PostServiceTest { + + @InjectMocks + PostQueryService postQueryService; + + @InjectMocks + PostCommandService postCommandService; + @InjectMocks - PostService postService; + PostLikeService postLikeService; + @Mock PostRepository postRepository; @Mock @@ -75,7 +97,6 @@ class PostServiceTest { private List imageFilesWithMoreThanFiveFiles; private List uploadedFileUrlResponseList; - @BeforeEach void setUp() { siteUser = createSiteUser(); @@ -206,7 +227,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.save(any(Post.class))).thenReturn(postWithImages); // When - PostCreateResponse postCreateResponse = postService.createPost( + PostCreateResponse postCreateResponse = postCommandService.createPost( siteUser.getEmail(), board.getCode(), postCreateRequest, imageFiles); // Then @@ -227,7 +248,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.save(postCreateRequest.toEntity(siteUser, board))).thenReturn(post); // When - PostCreateResponse postCreateResponse = postService.createPost( + PostCreateResponse postCreateResponse = postCommandService.createPost( siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList()); // Then @@ -245,7 +266,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { "자유", "title", "content", false); // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postService + CustomException exception = assertThrows(CustomException.class, () -> postCommandService .createPost(siteUser.getEmail(), invalidBoardCode, postCreateRequest, Collections.emptyList())); assertThat(exception.getMessage()) .isEqualTo(INVALID_BOARD_CODE.getMessage()); @@ -261,7 +282,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { invalidPostCategory, "title", "content", false); // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postService + CustomException exception = assertThrows(CustomException.class, () -> postCommandService .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList())); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_CATEGORY.getMessage()); @@ -276,7 +297,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { "자유", "title", "content", false); // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postService + CustomException exception = assertThrows(CustomException.class, () -> postCommandService .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, imageFilesWithMoreThanFiveFiles)); assertThat(exception.getMessage()) .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); @@ -294,7 +315,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.getById(post.getId())).thenReturn(post); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, Collections.emptyList()); // Then @@ -311,7 +332,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, Collections.emptyList()); // Then @@ -329,7 +350,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFiles); // Then @@ -347,7 +368,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); // When - PostUpdateResponse response = postService.updatePost( + PostUpdateResponse response = postCommandService.updatePost( siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, imageFiles); // Then @@ -365,11 +386,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), invalidBoardCode, post.getId(), postUpdateRequest, imageFiles)); + postCommandService.updatePost(siteUser.getEmail(), invalidBoardCode, post.getId(), postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -381,7 +402,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), board.getCode(), invalidPostId, postUpdateRequest, imageFiles)); + postCommandService.updatePost(siteUser.getEmail(), board.getCode(), invalidPostId, postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) @@ -397,7 +418,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(invalidEmail, board.getCode(), post.getId(), postUpdateRequest, imageFiles)); + postCommandService.updatePost(invalidEmail, board.getCode(), post.getId(), postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ACCESS.getMessage()); assertThat(exception.getCode()) @@ -412,14 +433,13 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), board.getCode(), questionPost.getId(), postUpdateRequest, imageFiles)); + postCommandService.updatePost(siteUser.getEmail(), board.getCode(), questionPost.getId(), postUpdateRequest, imageFiles)); assertThat(exception.getMessage()) .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); assertThat(exception.getCode()) .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); } - @Test void 게시글을_수정할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { // Given @@ -428,7 +448,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.updatePost(siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFilesWithMoreThanFiveFiles)); + postCommandService.updatePost(siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFilesWithMoreThanFiveFiles)); assertThat(exception.getMessage()) .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); assertThat(exception.getCode()) @@ -448,7 +468,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(commentService.findCommentsByPostId(siteUser.getEmail(), post.getId())).thenReturn(commentFindResultDTOList); // When - PostFindResponse response = postService.findPostById(siteUser.getEmail(), board.getCode(), post.getId()); + PostFindResponse response = postQueryService.findPostById(siteUser.getEmail(), board.getCode(), post.getId()); // Then PostFindResponse expectedResponse = PostFindResponse.from( @@ -474,11 +494,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.findPostById(siteUser.getEmail(), invalidBoardCode, post.getId())); + postQueryService.findPostById(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -489,7 +509,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.findPostById(siteUser.getEmail(), board.getCode(), invalidPostId)); + postQueryService.findPostById(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) @@ -505,7 +525,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postRepository.getById(post.getId())).thenReturn(post); // When - PostDeleteResponse postDeleteResponse = postService.deletePostById(siteUser.getEmail(), board.getCode(), post.getId()); + PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), post.getId()); // Then assertEquals(postDeleteResponse.id(), post.getId()); @@ -521,11 +541,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(siteUser.getEmail(), invalidBoardCode, post.getId())); + postCommandService.deletePostById(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -536,11 +556,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(siteUser.getEmail(), board.getCode(), invalidPostId)); + postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_POST_ID.getMessage()); + .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_POST_ID.getCode()); + .isEqualTo(INVALID_POST_ID.getCode()); } @Test @@ -551,7 +571,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(invalidEmail, board.getCode(), post.getId()) + postCommandService.deletePostById(invalidEmail, board.getCode(), post.getId()) ); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ACCESS.getMessage()); @@ -565,11 +585,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.deletePostById(siteUser.getEmail(), board.getCode(), questionPost.getId())); + postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), questionPost.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); + .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); + .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); } /** @@ -582,7 +602,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); // When - PostLikeResponse postLikeResponse = postService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); + PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); // Then assertEquals(postLikeResponse, PostLikeResponse.from(post)); @@ -597,7 +617,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); + postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); assertThat(exception.getMessage()) .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getMessage()); assertThat(exception.getCode()) @@ -611,11 +631,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + postLikeService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -626,7 +646,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + postLikeService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) @@ -642,7 +662,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenReturn(postLike); // When - PostDislikeResponse postDislikeResponse = postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); + PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); // Then assertEquals(postDislikeResponse, PostDislikeResponse.from(post)); @@ -657,11 +677,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); + postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_POST_LIKE.getMessage()); + .isEqualTo(INVALID_POST_LIKE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_POST_LIKE.getCode()); + .isEqualTo(INVALID_POST_LIKE.getCode()); } @Test @@ -671,11 +691,11 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); + postLikeService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); + .isEqualTo(INVALID_BOARD_CODE.getMessage()); assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); + .isEqualTo(INVALID_BOARD_CODE.getCode()); } @Test @@ -686,7 +706,7 @@ private List createMockImageFilesWithMoreThanFiveFiles() { // When & Then CustomException exception = assertThrows(CustomException.class, () -> - postService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); + postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); assertThat(exception.getMessage()) .isEqualTo(INVALID_POST_ID.getMessage()); assertThat(exception.getCode()) From b66e6106c6c0dc3df26a9097a2863ad3e3e75d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Wed, 29 Jan 2025 14:41:49 +0900 Subject: [PATCH 11/23] =?UTF-8?q?test:=20=EB=8C=93=EA=B8=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20=20(#173)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 댓글 조회 관련 통합테스트 코드 추가 * test: 댓글 생성 관련 통합테스트 코드 추가 * test: 댓글 수정 관련 통합테스트 코드 추가 * test: 댓글 삭제 관련 통합테스트 코드 추가 * test: 기존 단위테스트 코드 삭제 --- .../comment/service/CommentServiceTest.java | 420 ++++++++++ .../unit/repository/BoardRepositoryTest.java | 139 ---- .../repository/CommentRepositoryTest.java | 147 ---- .../repository/GpaScoreRepositoryTest.java | 91 --- .../LanguageTestScoreRepositoryTest.java | 96 --- .../repository/PostLikeRepositoryTest.java | 120 --- .../unit/repository/PostRepositoryTest.java | 162 ---- .../unit/service/ApplicationServiceTest.java | 259 ------- .../unit/service/BoardServiceTest.java | 152 ---- .../unit/service/CommentServiceTest.java | 483 ------------ .../unit/service/PostServiceTest.java | 715 ------------------ .../unit/service/ScoreServiceTest.java | 201 ----- .../unit/service/SiteUserServiceTest.java | 197 ----- 13 files changed, 420 insertions(+), 2762 deletions(-) create mode 100644 src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java diff --git a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java new file mode 100644 index 000000000..418a04d8c --- /dev/null +++ b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java @@ -0,0 +1,420 @@ +package com.example.solidconnection.comment.service; + +import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.comment.dto.CommentCreateRequest; +import com.example.solidconnection.comment.dto.CommentCreateResponse; +import com.example.solidconnection.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.comment.repository.CommentRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.PostCategory; +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPDATE_DEPRECATED_COMMENT; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_LEVEL; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("댓글 서비스 테스트") +class CommentServiceTest extends BaseIntegrationTest { + + @Autowired + private CommentService commentService; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private PostRepository postRepository; + + @Nested + class 댓글_조회_테스트 { + + @Test + void 게시글의_모든_댓글을_조회한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + List comments = List.of(parentComment, childComment); + + // when + List responses = commentService.findCommentsByPostId( + 테스트유저_1.getEmail(), + testPost.getId() + ); + + // then + assertAll( + () -> assertThat(responses).hasSize(comments.size()), + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(parentComment.getId())) + .singleElement() + .satisfies(response -> assertAll( + () -> assertThat(response.id()).isEqualTo(parentComment.getId()), + () -> 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.postFindSiteUserResponse().id()) + .isEqualTo(parentComment.getSiteUser().getId()), + () -> assertThat(response.postFindSiteUserResponse().nickname()) + .isEqualTo(parentComment.getSiteUser().getNickname()), + () -> assertThat(response.postFindSiteUserResponse().profileImageUrl()) + .isEqualTo(parentComment.getSiteUser().getProfileImageUrl()) + )), + () -> assertThat(responses) + .filteredOn(response -> response.id().equals(childComment.getId())) + .singleElement() + .satisfies(response -> assertAll( + () -> assertThat(response.id()).isEqualTo(childComment.getId()), + () -> 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.postFindSiteUserResponse().id()) + .isEqualTo(childComment.getSiteUser().getId()), + () -> assertThat(response.postFindSiteUserResponse().nickname()) + .isEqualTo(childComment.getSiteUser().getNickname()), + () -> assertThat(response.postFindSiteUserResponse().profileImageUrl()) + .isEqualTo(childComment.getSiteUser().getProfileImageUrl()) + )) + ); + } + } + + @Nested + class 댓글_생성_테스트 { + + @Test + void 댓글을_성공적으로_생성한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + CommentCreateRequest request = new CommentCreateRequest("테스트 댓글", null); + + // when + CommentCreateResponse response = commentService.createComment( + 테스트유저_1.getEmail(), + testPost.getId(), + request + ); + + // then + Comment savedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(savedComment.getId()).isEqualTo(response.id()), + () -> assertThat(savedComment.getContent()).isEqualTo(request.content()), + () -> assertThat(savedComment.getParentComment()).isNull(), + () -> assertThat(savedComment.getPost().getId()).isEqualTo(testPost.getId()), + () -> assertThat(savedComment.getSiteUser().getId()).isEqualTo(테스트유저_1.getId()) + ); + } + + @Test + void 대댓글을_성공적으로_생성한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + CommentCreateRequest request = new CommentCreateRequest("테스트 대댓글", parentComment.getId()); + + // when + CommentCreateResponse response = commentService.createComment( + 테스트유저_2.getEmail(), + testPost.getId(), + request + ); + + // then + Comment savedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(savedComment.getId()).isEqualTo(response.id()), + () -> assertThat(savedComment.getContent()).isEqualTo(request.content()), + () -> assertThat(savedComment.getParentComment().getId()).isEqualTo(parentComment.getId()), + () -> assertThat(savedComment.getPost().getId()).isEqualTo(testPost.getId()), + () -> assertThat(savedComment.getSiteUser().getId()).isEqualTo(테스트유저_2.getId()) + ); + } + + @Test + void 대대댓글_생성_시도하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + CommentCreateRequest request = new CommentCreateRequest("테스트 대대댓글", childComment.getId()); + + // when & then + assertThatThrownBy(() -> + commentService.createComment( + 테스트유저_1.getEmail(), + testPost.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_COMMENT_LEVEL.getMessage()); + } + + @Test + void 존재하지_않는_부모댓글로_대댓글_작성시_예외를_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + long invalidCommentId = 9999L; + CommentCreateRequest request = new CommentCreateRequest("테스트 대댓글", invalidCommentId); + + // when & then + assertThatThrownBy(() -> + commentService.createComment( + 테스트유저_1.getEmail(), + testPost.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_COMMENT_ID.getMessage()); + } + } + + @Nested + class 댓글_수정_테스트 { + + @Test + void 댓글을_성공적으로_수정한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "원본 댓글"); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글"); + + // when + CommentUpdateResponse response = commentService.updateComment( + 테스트유저_1.getEmail(), + testPost.getId(), + comment.getId(), + request + ); + + // then + Comment updatedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(updatedComment.getId()).isEqualTo(comment.getId()), + () -> assertThat(updatedComment.getContent()).isEqualTo(request.content()), + () -> assertThat(updatedComment.getParentComment()).isNull(), + () -> assertThat(updatedComment.getPost().getId()).isEqualTo(testPost.getId()), + () -> assertThat(updatedComment.getSiteUser().getId()).isEqualTo(테스트유저_1.getId()) + ); + } + + @Test + void 다른_사용자의_댓글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "원본 댓글"); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글"); + + // when & then + assertThatThrownBy(() -> + commentService.updateComment( + 테스트유저_2.getEmail(), + testPost.getId(), + comment.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + + @Test + void 삭제된_댓글을_수정하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, null); + CommentUpdateRequest request = new CommentUpdateRequest("수정된 댓글"); + + // when & then + assertThatThrownBy(() -> + commentService.updateComment( + 테스트유저_1.getEmail(), + testPost.getId(), + comment.getId(), + request + )) + .isInstanceOf(CustomException.class) + .hasMessage(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); + } + } + + @Nested + class 댓글_삭제_테스트 { + + @Test + @Transactional + void 대댓글이_없는_댓글을_삭제한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "테스트 댓글"); + List comments = testPost.getCommentList(); + int expectedCommentsCount = comments.size() - 1; + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_1.getEmail(), + testPost.getId(), + comment.getId() + ); + + // then + assertAll( + () -> assertThat(response.id()).isEqualTo(comment.getId()), + () -> assertThat(commentRepository.findById(comment.getId())).isEmpty(), + () -> assertThat(testPost.getCommentList()).hasSize(expectedCommentsCount) + ); + } + + @Test + @Transactional + void 대댓글이_있는_댓글을_삭제하면_내용만_삭제된다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + List comments = testPost.getCommentList(); + List childComments = parentComment.getCommentList(); + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_1.getEmail(), + testPost.getId(), + parentComment.getId() + ); + + // then + Comment deletedComment = commentRepository.findById(response.id()).orElseThrow(); + assertAll( + () -> assertThat(deletedComment.getContent()).isNull(), + () -> assertThat(deletedComment.getCommentList()) + .extracting(Comment::getId) + .containsExactlyInAnyOrder(childComment.getId()), + () -> assertThat(testPost.getCommentList()).hasSize(comments.size()), + () -> assertThat(deletedComment.getCommentList()).hasSize(childComments.size()) + ); + } + + @Test + @Transactional + void 대댓글을_삭제하면_부모댓글이_삭제되지_않는다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment1 = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글 1"); + Comment childComment2 = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글 2"); + List childComments = parentComment.getCommentList(); + int expectedChildCommentsCount = childComments.size() - 1; + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_2.getEmail(), + testPost.getId(), + childComment1.getId() + ); + + // then + Comment remainingParentComment = commentRepository.findById(parentComment.getId()).orElseThrow(); + List remainingChildComments = remainingParentComment.getCommentList(); + assertAll( + () -> assertThat(commentRepository.findById(response.id())).isEmpty(), + () -> assertThat(remainingParentComment.getContent()).isEqualTo(parentComment.getContent()), + () -> assertThat(remainingChildComments).hasSize(expectedChildCommentsCount), + () -> assertThat(remainingChildComments) + .extracting(Comment::getId) + .containsExactly(childComment2.getId()) + ); + } + + @Test + @Transactional + void 대댓글을_삭제하고_부모댓글이_삭제된_상태면_부모댓글도_삭제된다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment parentComment = createComment(testPost, 테스트유저_1, "부모 댓글"); + Comment childComment = createChildComment(testPost, 테스트유저_2, parentComment, "자식 댓글"); + List comments = testPost.getCommentList(); + int expectedCommentsCount = comments.size() - 2; + parentComment.deprecateComment(); + + // when + CommentDeleteResponse response = commentService.deleteCommentById( + 테스트유저_2.getEmail(), + testPost.getId(), + childComment.getId() + ); + + // then + assertAll( + () -> assertThat(commentRepository.findById(response.id())).isEmpty(), + () -> assertThat(commentRepository.findById(parentComment.getId())).isEmpty(), + () -> assertThat(testPost.getCommentList()).hasSize(expectedCommentsCount) + ); + } + + @Test + void 다른_사용자의_댓글을_삭제하면_예외_응답을_반환한다() { + // given + Post testPost = createPost(자유게시판, 테스트유저_1); + Comment comment = createComment(testPost, 테스트유저_1, "테스트 댓글"); + + // when & then + assertThatThrownBy(() -> + commentService.deleteCommentById( + 테스트유저_2.getEmail(), + testPost.getId(), + comment.getId() + )) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_POST_ACCESS.getMessage()); + } + } + + private Post createPost(Board board, SiteUser siteUser) { + Post post = new Post( + "테스트 제목", + "테스트 내용", + false, + 0L, + 0L, + PostCategory.자유 + ); + post.setBoardAndSiteUser(board, siteUser); + return postRepository.save(post); + } + + private Comment createComment(Post post, SiteUser siteUser, String content) { + Comment comment = new Comment(content); + comment.setPostAndSiteUser(post, siteUser); + return commentRepository.save(comment); + } + + private Comment createChildComment(Post post, SiteUser siteUser, Comment parentComment, String content) { + Comment comment = new Comment(content); + comment.setPostAndSiteUser(post, siteUser); + comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); + return commentRepository.save(comment); + } +} diff --git a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java deleted file mode 100644 index 17e74d140..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/BoardRepositoryTest.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; - -@TestContainerDataJpaTest -@DisplayName("게시판 레포지토리 테스트") -class BoardRepositoryTest { - @Autowired - private PostRepository postRepository; - @Autowired - private BoardRepository boardRepository; - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private EntityManager entityManager; - - private Board board; - private SiteUser siteUser; - private Post post; - - @BeforeEach - public void setUp() { - board = createBoard(); - boardRepository.save(board); - - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - - post = createPost(board, siteUser); - post = postRepository.save(post); - - entityManager.flush(); - entityManager.clear(); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - @Test - @Transactional - public void 게시판을_조회할_때_게시글은_즉시_로딩한다() { - // when - Board foundBoard = boardRepository.getByCodeUsingEntityGraph(board.getCode()); - foundBoard.getPostList().size(); // 추가쿼리 발생하지 않는다. - - // then - assertThat(foundBoard.getCode()).isEqualTo(board.getCode()); - } - - @Test - @Transactional - public void 게시판을_조회할_때_게시글은_즉시_로딩한다_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // given - String invalidCode = "INVALID_CODE"; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - boardRepository.getByCodeUsingEntityGraph(invalidCode); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - @Transactional - public void 게시판을_조회한다() { - // when - Board foundBoard = boardRepository.getByCode(board.getCode()); - - // then - assertEquals(board.getCode(), foundBoard.getCode()); - } - - @Test - @Transactional - public void 게시판을_조회할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // given - String invalidCode = "INVALID_CODE"; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - boardRepository.getByCode(invalidCode); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java deleted file mode 100644 index b57288725..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/CommentRepositoryTest.java +++ /dev/null @@ -1,147 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_COMMENT_ID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@TestContainerDataJpaTest -@DisplayName("댓글 레포지토리 테스트") -class CommentRepositoryTest { - @Autowired - private PostRepository postRepository; - @Autowired - private BoardRepository boardRepository; - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private CommentRepository commentRepository; - - private Board board; - private SiteUser siteUser; - private Post post; - private Comment parentComment; - private Comment childComment; - - @BeforeEach - public void setUp() { - board = createBoard(); - boardRepository.save(board); - - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - - post = createPost(board, siteUser); - post = postRepository.save(post); - - parentComment = createParentComment(); - childComment = createChildComment(); - commentRepository.save(parentComment); - commentRepository.save(childComment); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private Comment createParentComment() { - Comment comment = new Comment( - "parent" - ); - comment.setPostAndSiteUser(post, siteUser); - return comment; - } - - private Comment createChildComment() { - Comment comment = new Comment( - "child" - ); - comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); - return comment; - } - - @Test - @Transactional - public void 재귀쿼리로_댓글트리를_조회한다() { - // when - List commentTreeByPostId = commentRepository.findCommentTreeByPostId(post.getId()); - - // then - List expectedResponse = List.of(parentComment, childComment); - assertEquals(commentTreeByPostId, expectedResponse); - } - - @Test - @Transactional - public void 댓글을_조회한다() { - // when - Comment foundComment = commentRepository.getById(parentComment.getId()); - - // then - assertEquals(parentComment, foundComment); - } - - @Test - @Transactional - public void 댓글을_조회할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { - // given - Long invalidId = -1L; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - commentRepository.getById(invalidId); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java deleted file mode 100644 index 3ec59a5c2..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/GpaScoreRepositoryTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.score.domain.GpaScore; -import com.example.solidconnection.score.repository.GpaScoreRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@TestContainerDataJpaTest -@DisplayName("학점 레포지토리 테스트") -@Transactional -public class GpaScoreRepositoryTest { - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private GpaScoreRepository gpaScoreRepository; - - private SiteUser siteUser; - - @BeforeEach - public void setUp() { - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - @Test - public void 사용자의_학점을_조회한다_기존이력_없을_때() { - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUser(siteUser); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.empty()); - } - - @Test - public void 사용자의_학점을_조회한다_기존이력_있을_때() { - GpaScore gpaScore = new GpaScore( - new Gpa(4.5, 4.5, "http://example.com/gpa-report.pdf"), - siteUser, - LocalDate.of(2024, 10, 10) - ); - gpaScore.setSiteUser(siteUser); - gpaScoreRepository.save(gpaScore); - - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUser(siteUser); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.of(gpaScore)); - } - - @Test - public void 아이디와_사용자정보로_사용자의_학점을_조회한다_기존이력_없을_때() { - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, 1L); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.empty()); - } - - @Test - public void 아이디와_사용자정보로_사용자의_학점을_조회한다_기존이력_있을_때() { - GpaScore gpaScore = new GpaScore( - new Gpa(4.5, 4.5, "http://example.com/gpa-report.pdf"), - siteUser, - LocalDate.of(2024, 10, 10) - ); - gpaScore.setSiteUser(siteUser); - gpaScoreRepository.save(gpaScore); - - Optional gpaScoreBySiteUser = gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScore.getId()); - assertThat(gpaScoreBySiteUser).isEqualTo(Optional.of(gpaScore)); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java deleted file mode 100644 index 0090088c1..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/LanguageTestScoreRepositoryTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.score.repository.LanguageTestScoreRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDate; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; - -@TestContainerDataJpaTest -@DisplayName("어학성적 레포지토리 테스트") -@Transactional -public class LanguageTestScoreRepositoryTest { - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private LanguageTestScoreRepository languageTestScoreRepository; - - private SiteUser siteUser; - - @BeforeEach - public void setUp() { - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - @Test - public void 사용자의_어학성적을_조회한다_기존이력_없을_때() { - Optional languageTestScore = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(siteUser, LanguageTestType.TOEIC); - assertThat(languageTestScore).isEqualTo(Optional.empty()); - } - - @Test - public void 사용자의_어학성적을_조회한다_기존이력_있을_때() { - LanguageTestScore languageTestScore = new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "990", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 10), - siteUser - ); - languageTestScore.setSiteUser(siteUser); - languageTestScoreRepository.save(languageTestScore); - - Optional languageTestScore1 = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndLanguageTest_LanguageTestType(siteUser, LanguageTestType.TOEIC); - assertThat(languageTestScore1).isEqualTo(Optional.of(languageTestScore)); - } - - @Test - public void 아이디와_사용자정보로_사용자의_어학성적을_조회한다_기존이력_없을_때() { - Optional languageTestScore = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndId(siteUser, 1L); - assertThat(languageTestScore).isEqualTo(Optional.empty()); - } - - @Test - public void 아이디와_사용자정보로_사용자의_어학성적을_조회한다_기존이력_있을_때() { - LanguageTestScore languageTestScore = new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "990", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 10), - siteUser - ); - languageTestScore.setSiteUser(siteUser); - languageTestScoreRepository.save(languageTestScore); - - Optional languageTestScore1 = languageTestScoreRepository - .findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScore.getId()); - assertThat(languageTestScore1).isEqualTo(Optional.of(languageTestScore)); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java deleted file mode 100644 index 43ac210cb..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/PostLikeRepositoryTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@TestContainerDataJpaTest -@DisplayName("게시글 좋아요 레포지토리 테스트") -class PostLikeRepositoryTest { - @Autowired - private PostRepository postRepository; - @Autowired - private BoardRepository boardRepository; - @Autowired - private SiteUserRepository siteUserRepository; - @Autowired - private PostLikeRepository postLikeRepository; - - private Post post; - private Board board; - private SiteUser siteUser; - private PostLike postLike; - - - @BeforeEach - void setUp() { - board = createBoard(); - boardRepository.save(board); - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - post = createPost(board, siteUser); - post = postRepository.save(post); - postLike = createPostLike(post, siteUser); - postLikeRepository.save(postLike); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private PostLike createPostLike(Post post, SiteUser siteUser) { - PostLike postLike = new PostLike(); - postLike.setPostAndSiteUser(post, siteUser); - return postLike; - } - - @Test - @Transactional - void 게시글_좋아요를_조회한다() { - // when - PostLike foundPostLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); - - // then - assertEquals(foundPostLike, postLike); - } - - @Test - @Transactional - void 게시글_좋아요를_조회할_때_유효한_좋아요가_아니라면_예외_응답을_반환한다() { - // given - postLike.resetPostAndSiteUser(); - postLikeRepository.delete(postLike); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - postLikeRepository.getByPostAndSiteUser(post, siteUser); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_LIKE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_LIKE.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java b/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java deleted file mode 100644 index a37a0e6bf..000000000 --- a/src/test/java/com/example/solidconnection/unit/repository/PostRepositoryTest.java +++ /dev/null @@ -1,162 +0,0 @@ -package com.example.solidconnection.unit.repository; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.support.TestContainerDataJpaTest; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.PostCategory; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.ArrayList; -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -@TestContainerDataJpaTest -@DisplayName("게시글 레포지토리 테스트") -class PostRepositoryTest { - - @Autowired - private PostRepository postRepository; - - @Autowired - private BoardRepository boardRepository; - - @Autowired - private SiteUserRepository siteUserRepository; - - private Post post; - private Board board; - private SiteUser siteUser; - - @BeforeEach - void setUp() { - board = createBoard(); - boardRepository.save(board); - siteUser = createSiteUser(); - siteUserRepository.save(siteUser); - post = createPostWithImages(board, siteUser); - post = postRepository.save(post); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPostWithImages(Board board, SiteUser siteUser) { - Post postWithImages = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - postWithImages.setBoardAndSiteUser(board, siteUser); - - List postImageList = new ArrayList<>(); - postImageList.add(new PostImage("https://s3.example.com/test1.png")); - postImageList.add(new PostImage("https://s3.example.com/test2.png")); - for (PostImage postImage : postImageList) { - postImage.setPost(postWithImages); - } - return postWithImages; - } - - @Test - void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다() { - Post foundPost = postRepository.getByIdUsingEntityGraph(post.getId()); - foundPost.getPostImageList().size(); // 추가쿼리 발생하지 않는다. - - assertThat(foundPost).isEqualTo(post); - } - - @Test - void 게시글을_조회할_때_게시글_이미지는_즉시_로딩한다_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // given - Long invalidId = -1L; - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - postRepository.getByIdUsingEntityGraph(invalidId); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글을_조회한다() { - Post foundPost = postRepository.getById(post.getId()); - - assertEquals(post, foundPost); - } - - @Test - void 게시글을_조회할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - Long invalidId = -1L; - - CustomException exception = assertThrows(CustomException.class, () -> { - postRepository.getById(invalidId); - }); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글_좋아요를_등록한다() { - // given - Long likeCount = post.getLikeCount(); - - // when - postRepository.increaseLikeCount(post.getId()); - - // then - Post response = postRepository.getById(post.getId()); - assertEquals(response.getLikeCount(), likeCount + 1); - } - - @Test - void 게시글_좋아요를_삭제한다() { - // given - Long likeCount = post.getLikeCount(); - postRepository.increaseLikeCount(post.getId()); - - // when - postRepository.decreaseLikeCount(post.getId()); - - // then - Post response = postRepository.getById(post.getId()); - assertEquals(response.getLikeCount(), likeCount); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java deleted file mode 100644 index dd87a383f..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/ApplicationServiceTest.java +++ /dev/null @@ -1,259 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.application.domain.Application; -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.application.dto.ApplyRequest; -import com.example.solidconnection.application.dto.UniversityChoiceRequest; -import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.application.service.ApplicationSubmissionService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.score.domain.GpaScore; -import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.score.repository.GpaScoreRepository; -import com.example.solidconnection.score.repository.LanguageTestScoreRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.*; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.beans.factory.annotation.Value; - -import java.time.LocalDate; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("지원 서비스 테스트") -public class ApplicationServiceTest { - @InjectMocks - ApplicationSubmissionService applicationSubmissionService; - @Mock - ApplicationRepository applicationRepository; - @Mock - UniversityInfoForApplyRepository universityInfoForApplyRepository; - @Mock - SiteUserRepository siteUserRepository; - @Mock - GpaScoreRepository gpaScoreRepository; - @Mock - LanguageTestScoreRepository languageTestScoreRepository; - - @Value("${university.term}") - private String term; - private SiteUser siteUser; - private GpaScore gpaScore; - private LanguageTestScore languageTestScore; - private final long gpaScoreId = 1L; - private final long languageTestScoreId = 1L; - private final long firstChoiceUniversityId = 1L; - private final long secondChoiceUniversityId = 2L; - private final long thirdChoiceUniversityId = 3L; - - @BeforeEach - void setUp() { - siteUser = new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - gpaScore = new GpaScore( - new Gpa(4.3, 4.5, "gpaScoreUrl"), - siteUser, - LocalDate.of(2024, 10, 30) - ); - languageTestScore = new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "990", "languageTestScoreUrl"), - LocalDate.of(2024, 10, 30), - siteUser - ); - } - - @Test - void 지원한다_기존_이력_없음() { - // Given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.of(languageTestScore)); - when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.empty()); - - // When - boolean result = applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - - // Then - assertThat(result).isEqualTo(true); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(gpaScoreRepository, times(1)).findGpaScoreBySiteUserAndId(siteUser, gpaScoreId); - verify(languageTestScoreRepository, times(1)).findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId); - verify(applicationRepository, times(1)).save(any(Application.class)); - } - - @Test - void 지원한다_기존_이력_있음() { - // Given - Application beforeApplication = new Application( - siteUser, - new Gpa(4.5, 4.5, "beforeGpaScoreUrl"), - new LanguageTest(LanguageTestType.TOEIC, "900", "beforeLanguageTestUrl"), - term - ); - beforeApplication.setVerifyStatus(VerifyStatus.APPROVED); - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, 1L)).thenReturn(Optional.of(gpaScore)); - languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, 1L)).thenReturn(Optional.of(languageTestScore)); - when(applicationRepository.findBySiteUserAndTerm(siteUser, term)).thenReturn(Optional.of(beforeApplication)); - - // When - boolean result = applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - - // Then - assertThat(result).isEqualTo(true); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(gpaScoreRepository, times(1)).findGpaScoreBySiteUserAndId(siteUser, gpaScoreId); - verify(languageTestScoreRepository, times(1)).findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId); - verify(applicationRepository, times(1)).findBySiteUserAndTerm(siteUser, term); - verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(firstChoiceUniversityId, term); - verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(secondChoiceUniversityId, term); - verify(universityInfoForApplyRepository, times(1)).getUniversityInfoForApplyByIdAndTerm(thirdChoiceUniversityId, term); - verify(applicationRepository, times(1)).save(any(Application.class)); - } - - @Test - void 지원할_때_존재하지_않는_학점이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.empty()); - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE.getCode()); - } - - @Test - void 지원할_때_승인되지_않은_학점이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.REJECTED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE_STATUS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_GPA_SCORE_STATUS.getCode()); - } - - @Test - void 지원할_때_존재하지_않는_어학성적이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.empty()); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE.getCode()); - } - - @Test - void 지원할_때_승인되지_않은_어학성적이라면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, secondChoiceUniversityId, thirdChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - gpaScore.setVerifyStatus(VerifyStatus.APPROVED); - when(gpaScoreRepository.findGpaScoreBySiteUserAndId(siteUser, gpaScoreId)).thenReturn(Optional.of(gpaScore)); - languageTestScore.setVerifyStatus(VerifyStatus.REJECTED); - when(languageTestScoreRepository.findLanguageTestScoreBySiteUserAndId(siteUser, languageTestScoreId)).thenReturn(Optional.of(languageTestScore)); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS.getCode()); - } - - @Test - void 지원할_때_학교_선택이_중복되면_예외_응답을_반환한다() { - // given - ApplyRequest applyRequest = new ApplyRequest( - gpaScoreId, - languageTestScoreId, - new UniversityChoiceRequest(firstChoiceUniversityId, firstChoiceUniversityId, firstChoiceUniversityId) - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // when, then - CustomException exception = assertThrows(CustomException.class, () -> { - applicationSubmissionService.apply(siteUser.getEmail(), applyRequest); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java deleted file mode 100644 index 18c37b807..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/BoardServiceTest.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.board.service.BoardService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.BoardFindPostResponse; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.type.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - - -@ExtendWith(MockitoExtension.class) -@DisplayName("게시판 서비스 테스트") -class BoardServiceTest { - @InjectMocks - BoardService boardService; - @Mock - BoardRepository boardRepository; - - private SiteUser siteUser; - private Board board; - private List postList = new ArrayList<>(); - private List freePostList = new ArrayList<>(); - private List questionPostList = new ArrayList<>(); - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - board = createBoard("FREE", "자유게시판"); - - Post post_question_1 = createPost("질문", board, siteUser); - Post post_free_1 = createPost("자유", board, siteUser); - Post post_free_2 = createPost("자유", board, siteUser); - - postList.add(post_question_1); - postList.add(post_free_1); - postList.add(post_free_2); - questionPostList.add(post_question_1); - freePostList.add(post_free_1); - freePostList.add(post_free_2); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard(String code, String koreanName) { - return new Board(code, koreanName); - } - - private Post createPost(String postCategory, Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf(postCategory) - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - @Test - void 게시판을_조회할_때_게시판_코드와_게시글_카테고리에_따라서_조회한다() { - // Given - String category = "자유"; - when(boardRepository.getByCodeUsingEntityGraph(board.getCode())).thenReturn(board); - - // When - List responses = boardService.findPostsByCodeAndPostCategory(board.getCode(), category); - - // Then - List expectedResponses = freePostList.stream() - .map(BoardFindPostResponse::from) - .toList(); - assertIterableEquals(expectedResponses, responses); - verify(boardRepository, times(1)).getByCodeUsingEntityGraph(board.getCode()); - } - - @Test - void 게시판을_조회할_때_카테고리가_전체라면_해당_게시판의_모든_게시글을_조회한다() { - // Given - String category = "전체"; - when(boardRepository.getByCodeUsingEntityGraph(board.getCode())).thenReturn(board); - - // When - List responses = boardService.findPostsByCodeAndPostCategory(board.getCode(), category); - - // Then - List expectedResponses = postList.stream() - .map(BoardFindPostResponse::from) - .toList(); - assertIterableEquals(expectedResponses, responses); - verify(boardRepository, times(1)).getByCodeUsingEntityGraph(board.getCode()); - } - - @Test - void 게시판을_조회할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidCode = "INVALID_CODE"; - String category = "자유"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> { - boardService.findPostsByCodeAndPostCategory(invalidCode, category); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시판을_조회할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { - // Given - String invalidCategory = "INVALID_CATEGORY"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> { - boardService.findPostsByCodeAndPostCategory(board.getCode(), invalidCategory); - }); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.INVALID_POST_CATEGORY.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.INVALID_POST_CATEGORY.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java deleted file mode 100644 index 9ced8bcd8..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/CommentServiceTest.java +++ /dev/null @@ -1,483 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.*; -import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.comment.service.CommentService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.util.List; -import java.util.stream.Collectors; - -import static com.example.solidconnection.custom.exception.ErrorCode.*; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("댓글 서비스 테스트") -class CommentServiceTest { - @InjectMocks - CommentService commentService; - @Mock - PostRepository postRepository; - @Mock - SiteUserRepository siteUserRepository; - @Mock - CommentRepository commentRepository; - - private SiteUser siteUser; - private Board board; - private Post post; - private Comment parentComment; - private Comment parentCommentWithNullContent; - private Comment childComment; - private Comment childCommentOfNullContentParent; - - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - board = createBoard(); - post = createPost(board, siteUser); - parentComment = createParentComment("parent"); - parentCommentWithNullContent = createParentComment(null); - childComment = createChildComment(parentComment); - childCommentOfNullContentParent = createChildComment(parentCommentWithNullContent); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private Comment createParentComment(String content) { - Comment comment = new Comment( - content - ); - comment.setPostAndSiteUser(post, siteUser); - return comment; - } - - private Comment createChildComment(Comment parentComment) { - Comment comment = new Comment( - "child" - ); - comment.setParentCommentAndPostAndSiteUser(parentComment, post, siteUser); - return comment; - } - - /** - * 댓글 조회 - */ - - @Test - void 특정_게시글의_댓글들을_조회한다() { - // Given - List commentList = List.of(parentComment, childComment, parentCommentWithNullContent); - when(commentRepository.findCommentTreeByPostId(post.getId())).thenReturn(commentList); - - // When - List postFindCommentResponses = commentService.findCommentsByPostId( - siteUser.getEmail(), post.getId()); - - // Then - List expectedResponse = commentList.stream() - .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser.getEmail()), comment)) - .collect(Collectors.toList()); - assertEquals(postFindCommentResponses, expectedResponse); - } - - private Boolean isOwner(Comment comment, String email) { - return comment.getSiteUser().getEmail().equals(email); - } - - /** - * 댓글 등록 - */ - @Test - void 부모_댓글을_등록한다() { - // Given - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "parent", null - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.save(any(Comment.class))).thenReturn(parentComment); - - // When - CommentCreateResponse commentCreateResponse = commentService.createComment( - siteUser.getEmail(), post.getId(), commentCreateRequest); - - // Then - assertEquals(commentCreateResponse, CommentCreateResponse.from(parentComment)); - verify(commentRepository, times(0)) - .getById(any(Long.class)); - verify(commentRepository, times(1)) - .save(commentCreateRequest.toEntity(siteUser, post, parentComment)); - } - - @Test - void 자식_댓글을_등록한다() { - // Given - Long parentCommentId = 1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child", parentCommentId - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(parentCommentId)).thenReturn(parentComment); - when(commentRepository.save(any(Comment.class))).thenReturn(childComment); - - // When - CommentCreateResponse commentCreateResponse = commentService.createComment( - siteUser.getEmail(), post.getId(), commentCreateRequest); - - // Then - assertEquals(commentCreateResponse, CommentCreateResponse.from(childComment)); - verify(commentRepository, times(1)) - .getById(parentCommentId); - verify(commentRepository, times(1)) - .save(commentCreateRequest.toEntity(siteUser, post, parentComment)); - } - - - @Test - void 댓글을_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child", null - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.createComment(siteUser.getEmail(), invalidPostId, commentCreateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - verify(commentRepository, times(0)) - .save(any(Comment.class)); - } - - @Test - void 댓글을_등록할_때_유효한_부모_댓글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidParentCommentId = -1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child", invalidParentCommentId - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(invalidParentCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - verify(commentRepository, times(0)) - .save(any(Comment.class)); - } - - @Test - void 댓글을_등록할_때_대대댓글_부터는_예외_응답을_반환한다() { - // Given - Long childCommentId = 1L; - CommentCreateRequest commentCreateRequest = new CommentCreateRequest( - "child's child", childCommentId - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(childCommentId)).thenReturn(childComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.createComment(siteUser.getEmail(), post.getId(), commentCreateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_LEVEL.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_LEVEL.getCode()); - verify(commentRepository, times(0)) - .save(any(Comment.class)); - } - - /** - * 댓글 수정 - */ - @Test - void 댓글을_수정한다() { - // Given - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When - CommentUpdateResponse commentUpdateResponse = commentService.updateComment( - siteUser.getEmail(), post.getId(), parentComment.getId(), commentUpdateRequest); - - // Then - assertEquals(commentUpdateResponse.id(), parentComment.getId()); - } - - @Test - void 댓글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), invalidPostId, parentComment.getId(), commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 댓글을_수정할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidCommentId = -1L; - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), post.getId(), invalidCommentId, commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - } - - @Test - void 댓글을_수정할_때_이미_삭제된_댓글이라면_예외_응답을_반환한다() { - // Given - parentComment.deprecateComment(); - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(siteUser.getEmail(), post.getId(), parentComment.getId(), commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_UPDATE_DEPRECATED_COMMENT.getCode()); - } - - @Test - void 댓글을_수정할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@test.com"; - CommentUpdateRequest commentUpdateRequest = new CommentUpdateRequest( - "update" - ); - when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.updateComment(invalidEmail, post.getId(), parentComment.getId(), commentUpdateRequest) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } - - /** - * 댓글 삭제 - */ - - @Test - void 댓글을_삭제한다_자식댓글_있음() { - // Given - Long parentCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), parentCommentId); - - // Then - assertEquals(parentComment.getContent(), null); - assertEquals(commentDeleteResponse.id(), parentCommentId); - verify(commentRepository, times(0)).deleteById(parentCommentId); - } - - @Test - void 댓글을_삭제한다_자식댓글_없음() { - // Given - Long childCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(childComment); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), childCommentId); - - // Then - assertEquals(commentDeleteResponse.id(), childCommentId); - verify(commentRepository, times(1)).deleteById(childCommentId); - } - - @Test - void 대댓글을_삭제한다_부모댓글_유효() { - // Given - Long childCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(childComment); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), childCommentId); - - // Then - assertEquals(commentDeleteResponse.id(), childCommentId); - verify(commentRepository, times(1)).deleteById(childCommentId); - } - - @Test - void 대댓글을_삭제한다_부모댓글_유효하지_않음() { - // Given - - Long childCommentId = 1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(childCommentOfNullContentParent); - - // When - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById( - siteUser.getEmail(), post.getId(), childCommentId); - - // Then - assertEquals(commentDeleteResponse.id(), childCommentId); - verify(commentRepository, times(2)).deleteById(any()); - } - - @Test - void 댓글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(siteUser.getEmail(), invalidPostId, parentComment.getId()) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 댓글을_삭제할_때_유효한_댓글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidCommentId = -1L; - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(invalidCommentId)).thenThrow(new CustomException(INVALID_COMMENT_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(siteUser.getEmail(), post.getId(), invalidCommentId) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_COMMENT_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_COMMENT_ID.getCode()); - } - - @Test - void 댓글을_삭제할_때_자신의_댓글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@test.com"; - when(siteUserRepository.getByEmail(invalidEmail)).thenReturn(siteUser); - when(postRepository.getById(post.getId())).thenReturn(post); - when(commentRepository.getById(any())).thenReturn(parentComment); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - commentService.deleteCommentById(invalidEmail, post.getId(), parentComment.getId()) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java deleted file mode 100644 index afc899255..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/PostServiceTest.java +++ /dev/null @@ -1,715 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.dto.PostFindBoardResponse; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.service.CommentService; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.PostLike; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.*; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostCommandService; -import com.example.solidconnection.post.service.PostLikeService; -import com.example.solidconnection.post.service.PostQueryService; -import com.example.solidconnection.s3.S3Service; -import com.example.solidconnection.s3.UploadedFileUrlResponse; -import com.example.solidconnection.service.RedisService; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.*; -import com.example.solidconnection.util.RedisUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_DELETE_OR_UPDATE_QUESTION; -import static com.example.solidconnection.custom.exception.ErrorCode.CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ACCESS; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_ID; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_LIKE; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyList; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@DisplayName("게시글 서비스 테스트") -class PostServiceTest { - - @InjectMocks - PostQueryService postQueryService; - - @InjectMocks - PostCommandService postCommandService; - - @InjectMocks - PostLikeService postLikeService; - - @Mock - PostRepository postRepository; - @Mock - SiteUserRepository siteUserRepository; - @Mock - BoardRepository boardRepository; - @Mock - PostLikeRepository postLikeRepository; - @Mock - S3Service s3Service; - @Mock - CommentService commentService; - @Mock - RedisService redisService; - @Mock - RedisUtils redisUtils; - - private SiteUser siteUser; - private Board board; - private Post post; - private Post postWithImages; - private Post questionPost; - private PostLike postLike; - private List imageFiles; - private List imageFilesWithMoreThanFiveFiles; - private List uploadedFileUrlResponseList; - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - board = createBoard(); - imageFiles = createMockImageFiles(); - imageFilesWithMoreThanFiveFiles = createMockImageFilesWithMoreThanFiveFiles(); - uploadedFileUrlResponseList = createUploadedFileUrlResponses(); - post = createPost(board, siteUser); - postWithImages = createPostWithImages(board, siteUser); - questionPost = createQuestionPost(board, siteUser); - postLike = createPostLike(post, siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private Board createBoard() { - return new Board( - "FREE", "자유게시판"); - } - - private Post createPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - - return post; - } - - private Post createPostWithImages(Board board, SiteUser siteUser) { - Post postWithImages = new Post( - "title", - "content", - false, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - postWithImages.setBoardAndSiteUser(board, siteUser); - - List postImageList = new ArrayList<>(); - postImageList.add(new PostImage("https://s3.example.com/test1.png")); - postImageList.add(new PostImage("https://s3.example.com/test2.png")); - for (PostImage postImage : postImageList) { - postImage.setPost(postWithImages); - } - return postWithImages; - } - - private Post createQuestionPost(Board board, SiteUser siteUser) { - Post post = new Post( - "title", - "content", - true, - 0L, - 0L, - PostCategory.valueOf("자유") - ); - post.setBoardAndSiteUser(board, siteUser); - return post; - } - - private PostLike createPostLike(Post post, SiteUser siteUser) { - PostLike postLike = new PostLike(); - postLike.setPostAndSiteUser(post, siteUser); - return postLike; - } - - private List createMockImageFiles() { - List multipartFileList = new ArrayList<>(); - multipartFileList.add(new MockMultipartFile("file1", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file2", "test1.png", - "image/png", "test image content 1".getBytes())); - return multipartFileList; - } - - private List createUploadedFileUrlResponses() { - return Arrays.asList( - new UploadedFileUrlResponse("https://s3.example.com/test1.png"), - new UploadedFileUrlResponse("https://s3.example.com/test2.png") - ); - } - - private List createMockImageFilesWithMoreThanFiveFiles() { - List multipartFileList = new ArrayList<>(); - multipartFileList.add(new MockMultipartFile("file1", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file2", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file3", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file4", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file5", "test1.png", - "image/png", "test image content 1".getBytes())); - multipartFileList.add(new MockMultipartFile("file6", "test1.png", - "image/png", "test image content 1".getBytes())); - return multipartFileList; - } - - /** - * 게시글 등록 - */ - @Test - void 게시글을_등록한다_이미지_있음() { - // Given - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(boardRepository.getByCode(board.getCode())).thenReturn(board); - when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); - when(postRepository.save(any(Post.class))).thenReturn(postWithImages); - - // When - PostCreateResponse postCreateResponse = postCommandService.createPost( - siteUser.getEmail(), board.getCode(), postCreateRequest, imageFiles); - - // Then - assertEquals(postCreateResponse, PostCreateResponse.from(postWithImages)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(boardRepository, times(1)).getByCode(board.getCode()); - verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); - verify(postRepository, times(1)).save(any(Post.class)); - } - - @Test - void 게시글을_등록한다_이미지_없음() { - // Given - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(boardRepository.getByCode(board.getCode())).thenReturn(board); - when(postRepository.save(postCreateRequest.toEntity(siteUser, board))).thenReturn(post); - - // When - PostCreateResponse postCreateResponse = postCommandService.createPost( - siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList()); - - // Then - assertEquals(postCreateResponse, PostCreateResponse.from(post)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(boardRepository, times(1)).getByCode(board.getCode()); - verify(postRepository, times(1)).save(postCreateRequest.toEntity(siteUser, board)); - } - - @Test - void 게시글을_등록할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postCommandService - .createPost(siteUser.getEmail(), invalidBoardCode, postCreateRequest, Collections.emptyList())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_등록할_때_유효한_카테고리가_아니라면_예외_응답을_반환한다() { - // Given - String invalidPostCategory = "invalidPostCategory"; - PostCreateRequest postCreateRequest = new PostCreateRequest( - invalidPostCategory, "title", "content", false); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postCommandService - .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, Collections.emptyList())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_CATEGORY.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_CATEGORY.getCode()); - } - - @Test - void 게시글을_등록할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { - // Given - PostCreateRequest postCreateRequest = new PostCreateRequest( - "자유", "title", "content", false); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> postCommandService - .createPost(siteUser.getEmail(), board.getCode(), postCreateRequest, imageFilesWithMoreThanFiveFiles)); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getCode()); - } - - /** - * 게시글 수정 - */ - @Test - void 게시글을_수정한다_기존_사진_없음_수정_사진_없음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("질문", "updateTitle", "updateContent"); - when(postRepository.getById(post.getId())).thenReturn(post); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, Collections.emptyList()); - - // Then - assertEquals(response, PostUpdateResponse.from(post)); - verify(postRepository, times(1)).getById(post.getId()); - verify(s3Service, times(0)).deletePostImage(any(String.class)); - verify(s3Service, times(0)).uploadFiles(anyList(), any(ImgType.class)); - } - - @Test - void 게시글을_수정한다_기존_사진_있음_수정_사진_없음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); - when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, Collections.emptyList()); - - // Then - assertEquals(response, PostUpdateResponse.from(postWithImages)); - verify(postRepository, times(1)).getById(postWithImages.getId()); - verify(s3Service, times(imageFiles.size())).deletePostImage(any(String.class)); - verify(s3Service, times(0)).uploadFiles(anyList(), any(ImgType.class)); - } - - @Test - void 게시글을_수정한다_기존_사진_없음_수정_사진_있음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); - when(postRepository.getById(post.getId())).thenReturn(post); - when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFiles); - - // Then - assertEquals(response, PostUpdateResponse.from(post)); - verify(postRepository, times(1)).getById(post.getId()); - verify(s3Service, times(0)).deletePostImage(any(String.class)); - verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); - } - - @Test - void 게시글을_수정한다_기존_사진_있음_수정_사진_있음() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "updateTitle", "updateContent"); - when(postRepository.getById(postWithImages.getId())).thenReturn(postWithImages); - when(s3Service.uploadFiles(imageFiles, ImgType.COMMUNITY)).thenReturn(uploadedFileUrlResponseList); - - // When - PostUpdateResponse response = postCommandService.updatePost( - siteUser.getEmail(), board.getCode(), postWithImages.getId(), postUpdateRequest, imageFiles); - - // Then - assertEquals(response, PostUpdateResponse.from(postWithImages)); - verify(postRepository, times(1)).getById(postWithImages.getId()); - verify(s3Service, times(imageFiles.size())).deletePostImage(any(String.class)); - verify(s3Service, times(1)).uploadFiles(imageFiles, ImgType.COMMUNITY); - } - - @Test - void 게시글을_수정할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), invalidBoardCode, post.getId(), postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_수정할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), board.getCode(), invalidPostId, postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글을_수정할_때_본인의_게시글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@example.com"; - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(post.getId())).thenReturn(post); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(invalidEmail, board.getCode(), post.getId(), postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } - - @Test - void 게시글을_수정할_때_질문글_이라면_예외_응답을_반환한다() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(questionPost.getId())).thenReturn(questionPost); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), board.getCode(), questionPost.getId(), postUpdateRequest, imageFiles)); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); - } - - @Test - void 게시글을_수정할_때_파일_수가_5개를_넘는다면_예외_응답을_반환한다() { - // Given - PostUpdateRequest postUpdateRequest = new PostUpdateRequest("자유", "title", "content"); - when(postRepository.getById(post.getId())).thenReturn(post); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.updatePost(siteUser.getEmail(), board.getCode(), post.getId(), postUpdateRequest, imageFilesWithMoreThanFiveFiles)); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getCode()); - } - - /** - * 게시글 조회 - */ - @Test - void 게시글을_찾는다() { - // Given - List commentFindResultDTOList = new ArrayList<>(); - when(postRepository.getByIdUsingEntityGraph(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.empty()); - when(commentService.findCommentsByPostId(siteUser.getEmail(), post.getId())).thenReturn(commentFindResultDTOList); - - // When - PostFindResponse response = postQueryService.findPostById(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - PostFindResponse expectedResponse = PostFindResponse.from( - post, - true, - false, - PostFindBoardResponse.from(post.getBoard()), - PostFindSiteUserResponse.from(post.getSiteUser()), - commentFindResultDTOList, - PostFindPostImageResponse.from(post.getPostImageList()) - ); - assertEquals(expectedResponse, response); - verify(postRepository, times(1)).getByIdUsingEntityGraph(post.getId()); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(postLikeRepository, times(1)).findPostLikeByPostAndSiteUser(post, siteUser); - verify(commentService, times(1)).findCommentsByPostId(siteUser.getEmail(), post.getId()); - } - - @Test - void 게시글을_찾을_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postQueryService.findPostById(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_찾을_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getByIdUsingEntityGraph(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postQueryService.findPostById(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - /** - * 게시글 삭제 - */ - @Test - void 게시글을_삭제한다() { - // Give - when(postRepository.getById(post.getId())).thenReturn(post); - - // When - PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - assertEquals(postDeleteResponse.id(), post.getId()); - verify(postRepository, times(1)).getById(post.getId()); - verify(redisService, times(1)).deleteKey(redisUtils.getPostViewCountRedisKey(post.getId())); - verify(postRepository, times(1)).deleteById(post.getId()); - } - - @Test - void 게시글을_삭제할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글을_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글을_삭제할_때_자신의_게시글이_아니라면_예외_응답을_반환한다() { - // Given - String invalidEmail = "invalidEmail@example.com"; - when(postRepository.getById(post.getId())).thenReturn(post); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(invalidEmail, board.getCode(), post.getId()) - ); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ACCESS.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ACCESS.getCode()); - } - - @Test - void 게시글을_삭제할_때_질문글_이라면_예외_응답을_반환한다() { - when(postRepository.getById(questionPost.getId())).thenReturn(questionPost); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postCommandService.deletePostById(siteUser.getEmail(), board.getCode(), questionPost.getId())); - assertThat(exception.getMessage()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(CAN_NOT_DELETE_OR_UPDATE_QUESTION.getCode()); - } - - /** - * 게시글 좋아요 - */ - @Test - void 게시글_좋아요를_등록한다() { - // Given - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When - PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - assertEquals(postLikeResponse, PostLikeResponse.from(post)); - verify(postLikeRepository, times(1)).save(any(PostLike.class)); - } - - @Test - void 게시글_좋아요를_등록할_때_중복된_좋아요라면_예외_응답을_반환한다() { - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser)).thenReturn(Optional.of(postLike)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.likePost(siteUser.getEmail(), board.getCode(), post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(ErrorCode.DUPLICATE_POST_LIKE.getCode()); - } - - @Test - void 게시글_좋아요를_등록할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.likePost(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글_좋아요를_등록할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.likePost(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } - - @Test - void 게시글_좋아요를_삭제한다() { - // Given - Long likeCount = post.getLikeCount(); - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenReturn(postLike); - - // When - PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId()); - - // Then - assertEquals(postDislikeResponse, PostDislikeResponse.from(post)); - verify(postLikeRepository, times(1)).deleteById(post.getId()); - } - - @Test - void 게시글_좋아요를_삭제할_때_존재하지_않는_좋아요라면_예외_응답을_반환한다() { - when(postRepository.getById(post.getId())).thenReturn(post); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(postLikeRepository.getByPostAndSiteUser(post, siteUser)).thenThrow(new CustomException(INVALID_POST_LIKE)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_LIKE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_LIKE.getCode()); - } - - @Test - void 게시글_좋아요를_삭제할_때_유효한_게시판이_아니라면_예외_응답을_반환한다() { - // Given - String invalidBoardCode = "INVALID_CODE"; - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.dislikePost(siteUser.getEmail(), invalidBoardCode, post.getId())); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_BOARD_CODE.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_BOARD_CODE.getCode()); - } - - @Test - void 게시글_좋아요를_삭제할_때_유효한_게시글이_아니라면_예외_응답을_반환한다() { - // Given - Long invalidPostId = -1L; - when(postRepository.getById(invalidPostId)).thenThrow(new CustomException(INVALID_POST_ID)); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - postLikeService.dislikePost(siteUser.getEmail(), board.getCode(), invalidPostId)); - assertThat(exception.getMessage()) - .isEqualTo(INVALID_POST_ID.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(INVALID_POST_ID.getCode()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java deleted file mode 100644 index 39deadb54..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/ScoreServiceTest.java +++ /dev/null @@ -1,201 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.application.domain.Gpa; -import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.score.domain.GpaScore; -import com.example.solidconnection.score.domain.LanguageTestScore; -import com.example.solidconnection.score.dto.*; -import com.example.solidconnection.score.repository.GpaScoreRepository; -import com.example.solidconnection.score.repository.LanguageTestScoreRepository; -import com.example.solidconnection.score.service.ScoreService; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.LanguageTestType; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.LocalDate; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("점수 서비스 테스트") -public class ScoreServiceTest { - @InjectMocks - ScoreService scoreService; - @Mock - GpaScoreRepository gpaScoreRepository; - @Mock - LanguageTestScoreRepository languageTestScoreRepository; - @Mock - SiteUserRepository siteUserRepository; - - private SiteUser siteUser; - private GpaScore beforeGpaScore; - private GpaScore beforeGpaScore2; - private LanguageTestScore beforeLanguageTestScore; - private LanguageTestScore beforeLanguageTestScore2; - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - beforeGpaScore = createBeforeGpaScore(siteUser, 4.5); - beforeGpaScore2 = createBeforeGpaScore(siteUser, 4.3); - beforeLanguageTestScore = createBeforeLanguageTestScore(siteUser); - beforeLanguageTestScore2 = createBeforeLanguageTestScore2(siteUser); - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private GpaScore createBeforeGpaScore(SiteUser siteUser, Double gpa) { - return new GpaScore( - new Gpa(gpa, 4.5, "http://example.com/gpa-report.pdf"), - siteUser, - LocalDate.of(2024, 10, 20) - ); - } - - private LanguageTestScore createBeforeLanguageTestScore(SiteUser siteUser) { - return new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEIC, "900", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 30), - siteUser - ); - } - - private LanguageTestScore createBeforeLanguageTestScore2(SiteUser siteUser) { - return new LanguageTestScore( - new LanguageTest(LanguageTestType.TOEFL_IBT, "100", "http://example.com/gpa-report.pdf"), - LocalDate.of(2024, 10, 30), - siteUser - ); - } - - @Test - void 학점을_등록한다_기존이력이_없을_때() { - // Given - GpaScoreRequest gpaScoreRequest = new GpaScoreRequest( - 4.5, 4.5, LocalDate.of(2024, 10, 20), "http://example.com/gpa-report.pdf" - ); - GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(gpaScoreRepository.save(newGpaScore)).thenReturn(newGpaScore); - - // 새로운 gpa 저장하게된다. - scoreService.submitGpaScore(siteUser.getEmail(), gpaScoreRequest); - - // Then - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(gpaScoreRepository, times(1)).save(any(GpaScore.class)); - } - - @Test - void 어학성적을_등록한다_기존이력이_없을_때() { - // Given - LanguageTestScoreRequest languageTestScoreRequest = new LanguageTestScoreRequest( - LanguageTestType.TOEIC, "900", - LocalDate.of(2024, 10, 30), "http://example.com/gpa-report.pdf" - ); - LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); - LanguageTestScore languageTestScore = new LanguageTestScore(languageTest, LocalDate.of(2024, 10, 30), siteUser); - - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(languageTestScoreRepository.save(any(LanguageTestScore.class))).thenReturn(languageTestScore); - - //when - scoreService.submitLanguageTestScore(siteUser.getEmail(), languageTestScoreRequest); - - // Then - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(languageTestScoreRepository, times(1)).save(any(LanguageTestScore.class)); - } - - @Test - void 학점이력을_조회한다_제출이력이_있을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - beforeGpaScore.setSiteUser(siteUser); - beforeGpaScore2.setSiteUser(siteUser); - - // when - GpaScoreStatusResponse gpaScoreStatusResponse = scoreService.getGpaScoreStatus(siteUser.getEmail()); - - // Then - List expectedStatusList = List.of( - GpaScoreStatus.from(beforeGpaScore), - GpaScoreStatus.from(beforeGpaScore2) - ); - assertThat(gpaScoreStatusResponse.gpaScoreStatusList()) - .hasSize(2) - .containsExactlyElementsOf(expectedStatusList); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } - - @Test - void 학점이력을_조회한다_제출이력이_없을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // when - GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser.getEmail()); - - // Then - assertThat(gpaScoreStatus.gpaScoreStatusList()).isEmpty(); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } - - - @Test - void 어학이력을_조회한다_제출이력이_있을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - beforeLanguageTestScore.setSiteUser(siteUser); - beforeLanguageTestScore2.setSiteUser(siteUser); - - // when - LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser.getEmail()); - - // Then - List expectedStatusList = List.of( - LanguageTestScoreStatus.from(beforeLanguageTestScore), - LanguageTestScoreStatus.from(beforeLanguageTestScore2) - ); - assertThat(languageTestScoreStatus.languageTestScoreStatusList()) - .hasSize(2) - .containsExactlyElementsOf(expectedStatusList); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } - - @Test - void 어학이력을_조회한다_제출이력이_없을_때() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // when - LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser.getEmail()); - - // Then - assertThat(languageTestScoreStatus.languageTestScoreStatusList()).isEmpty(); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - } -} diff --git a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java deleted file mode 100644 index 860f76e11..000000000 --- a/src/test/java/com/example/solidconnection/unit/service/SiteUserServiceTest.java +++ /dev/null @@ -1,197 +0,0 @@ -package com.example.solidconnection.unit.service; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.s3.S3Service; -import com.example.solidconnection.s3.UploadedFileUrlResponse; -import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; -import com.example.solidconnection.siteuser.dto.NicknameUpdateResponse; -import com.example.solidconnection.siteuser.dto.ProfileImageUpdateResponse; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.siteuser.service.SiteUserService; -import com.example.solidconnection.type.Gender; -import com.example.solidconnection.type.ImgType; -import com.example.solidconnection.type.PreparationStatus; -import com.example.solidconnection.type.Role; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import java.time.LocalDateTime; - -import static com.example.solidconnection.custom.exception.ErrorCode.*; -import static com.example.solidconnection.siteuser.service.SiteUserService.NICKNAME_LAST_CHANGE_DATE_FORMAT; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -@DisplayName("유저 서비스 테스트") -public class SiteUserServiceTest { - @InjectMocks - SiteUserService siteUserService; - @Mock - SiteUserRepository siteUserRepository; - @Mock - LikedUniversityRepository likedUniversityRepository; - @Mock - S3Service s3Service; - - private SiteUser siteUser; - private MultipartFile imageFile; - private UploadedFileUrlResponse uploadedFileUrlResponse; - private final String defaultProfileImageUrl = "http://k.kakaocdn.net/dn/o2c5A/btsASaNh2Lr/Xum5kRyuErD8LIuLQEWfC0/img_640x640.jpg"; - - @BeforeEach - void setUp() { - siteUser = createSiteUser(); - imageFile = createMockImageFile(); - uploadedFileUrlResponse = createUploadedFileUrlResponse(); - - } - - private SiteUser createSiteUser() { - return new SiteUser( - "test@example.com", - "nickname", - "profile/fajwoiejoiewjfoi", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - } - - private MultipartFile createMockImageFile() { - return new MockMultipartFile("file1", "test1.png", - "image/png", "test image content 1".getBytes()); - - } - - private UploadedFileUrlResponse createUploadedFileUrlResponse() { - return new UploadedFileUrlResponse("profile/fajwoiejoiewjfoi"); - } - - @Test - void 초기_프로필_이미지를_수정한다_kakao() { - siteUser.setProfileImageUrl(defaultProfileImageUrl); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); - - // When - ProfileImageUpdateResponse profileImageUpdateResponse = - siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); - // Then - assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(s3Service, times(0)).deleteExProfile(siteUser.getEmail()); - verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 초기_프로필_이미지를_수정한다_null() { - siteUser.setProfileImageUrl(null); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); - - // When - ProfileImageUpdateResponse profileImageUpdateResponse = - siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); - // Then - assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(s3Service, times(0)).deleteExProfile(siteUser.getEmail()); - verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 프로필_이미지를_수정한다() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(s3Service.uploadFile(imageFile, ImgType.PROFILE)).thenReturn(uploadedFileUrlResponse); - - // When - ProfileImageUpdateResponse profileImageUpdateResponse = - siteUserService.updateProfileImage(siteUser.getEmail(), imageFile); - // Then - assertEquals(profileImageUpdateResponse, ProfileImageUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(s3Service, times(1)).deleteExProfile(siteUser.getEmail()); - verify(s3Service, times(1)).uploadFile(imageFile, ImgType.PROFILE); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 프로필_이미지를_수정할_때_이미지가_없다면_예외_응답을_반환한다() { - // Given - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - siteUserService.updateProfileImage(siteUser.getEmail(), null)); - assertThat(exception.getMessage()) - .isEqualTo(PROFILE_IMAGE_NEEDED.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(PROFILE_IMAGE_NEEDED.getCode()); - } - - @Test - void 닉네임을_수정한다() { - // Given - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When - NicknameUpdateResponse nicknameUpdateResponse - = siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest); - // Then - assertEquals( nicknameUpdateResponse, NicknameUpdateResponse.from(siteUser)); - verify(siteUserRepository, times(1)).getByEmail(siteUser.getEmail()); - verify(siteUserRepository, times(1)).save(any(SiteUser.class)); - } - - @Test - void 닉네임을_수정할_때_중복된_닉네임이라면_예외_응답을_반환한다() { - // Given - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - when(siteUserRepository.existsByNickname(nicknameUpdateRequest.nickname())).thenReturn(true); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest)); - assertThat(exception.getMessage()) - .isEqualTo(NICKNAME_ALREADY_EXISTED.getMessage()); - assertThat(exception.getCode()) - .isEqualTo(NICKNAME_ALREADY_EXISTED.getCode()); - } - - @Test - void 닉네임을_수정할_때_변경_가능_기한이_지나지_않았다면_예외_응답을_반환한다() { - // Given - NicknameUpdateRequest nicknameUpdateRequest = new NicknameUpdateRequest("newNickname"); - siteUser.setNicknameModifiedAt(LocalDateTime.now()); - when(siteUserRepository.getByEmail(siteUser.getEmail())).thenReturn(siteUser); - - // When & Then - CustomException exception = assertThrows(CustomException.class, () -> - siteUserService.updateNickname(siteUser.getEmail(), nicknameUpdateRequest)); - - String formatLastModifiedAt - = String.format("(마지막 수정 시간 : %s)", NICKNAME_LAST_CHANGE_DATE_FORMAT.format(siteUser.getNicknameModifiedAt())); - CustomException expectedException = new CustomException(CAN_NOT_CHANGE_NICKNAME_YET, formatLastModifiedAt); - assertThat(exception.getMessage()).isEqualTo(expectedException.getMessage()); - assertThat(exception.getCode()).isEqualTo(expectedException.getCode()); - } -} From 16b907f74e4ae6bd3a613f4a974706a5b36b68f4 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 30 Jan 2025 05:33:16 +0900 Subject: [PATCH 12/23] =?UTF-8?q?refactor:=20=EA=B3=B5=ED=86=B5=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=EB=8C=80=ED=95=99=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 공통 추천 대학 로직 변경 - 해당 학기에 열리는 대학들을 랜덤으로 가져오도록 * refactor: 불필요한 셔플 제거 - 랜덤으로 가져오는 것을 다시 셔플할 필요는 없으므로 * refactor: 클래스 이름 변경 * style: 필드 선언 순서 변경 - final 이 위로 가도록 * refactor: native query 를 사용하도록 --- .../UniversityInfoForApplyRepository.java | 8 +++ .../service/GeneralRecommendUniversities.java | 50 ------------------- .../GeneralUniversityRecommendService.java | 35 +++++++++++++ .../service/UniversityRecommendService.java | 7 ++- .../e2e/UniversityRecommendTest.java | 10 ++-- ...GeneralUniversityRecommendServiceTest.java | 41 +++++++++++++++ .../UniversityRecommendServiceTest.java | 8 +-- 7 files changed, 96 insertions(+), 63 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java create mode 100644 src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java create mode 100644 src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java diff --git a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java index 4adc0d718..60474c13d 100644 --- a/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java +++ b/src/main/java/com/example/solidconnection/university/repository/UniversityInfoForApplyRepository.java @@ -45,6 +45,14 @@ OR u.region.code IN ( """) List findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(@Param("siteUser") SiteUser siteUser, @Param("term") String term); + @Query(value = """ + SELECT * + FROM university_info_for_apply + WHERE term = :term + ORDER BY RAND() LIMIT :limitNum + """, nativeQuery = true) + List findRandomByTerm(@Param("term") String term, @Param("limitNum") int limitNum); + default UniversityInfoForApply getUniversityInfoForApplyById(Long id) { return findById(id) .orElseThrow(() -> new CustomException(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND)); diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java b/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java deleted file mode 100644 index 92054eee6..000000000 --- a/src/main/java/com/example/solidconnection/university/service/GeneralRecommendUniversities.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.solidconnection.university.service; - -import com.example.solidconnection.repositories.CountryRepository; -import com.example.solidconnection.university.domain.UniversityInfoForApply; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.context.event.ApplicationReadyEvent; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Component; - -import java.util.List; - -import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; - -@RequiredArgsConstructor -@Component -public class GeneralRecommendUniversities { - - /* - * 매 선발 시기(term) 마다 지원할 수 있는 대학교가 달라지므르, 추천 대학교도 달라져야 한다. - * 하지만 매번 추천 대학교를 바꾸기에는 번거롭다. - * 따라서 '추천 대학교 후보'들을 설정하고, DB 에서 현재 term 에 대해 찾아지는 대학교만 추천 대학교로 지정한다. - * */ - @Getter - private final List recommendUniversities; - private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final CountryRepository countryRepository; - private final List candidates = List.of( - "오스트라바 대학", "RMIT멜버른공과대학(A형)", "알브슈타트 지그마링엔 대학", - "뉴저지시티대학(A형)", "도요대학", "템플대학(A형)", "빈 공과대학교", - "리스본대학 공과대학", "바덴뷔르템베르크 산학협력대학", "긴다이대학", "네바다주립대학 라스베이거스(B형)", "릴 가톨릭 대학", - "그라츠공과대학", "그라츠 대학", "코펜하겐 IT대학", "메이지대학", "분쿄가쿠인대학", "린츠 카톨릭 대학교", - "밀라노공과대학", "장물랭리옹3세대학교", "시드니대학", "아우크스부르크대학", "쳄니츠 공과대학", "북경외국어대학교 IBS" - ); - - @Value("${university.term}") - public String term; - - @EventListener(ApplicationReadyEvent.class) - public void init() { - int i = 0; - while (recommendUniversities.size() < RECOMMEND_UNIVERSITY_NUM && i < candidates.size()) { - universityInfoForApplyRepository.findFirstByKoreanNameAndTerm(candidates.get(i), term) - .ifPresent(recommendUniversities::add); - i++; - } - } -} diff --git a/src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java new file mode 100644 index 000000000..d39fee1ec --- /dev/null +++ b/src/main/java/com/example/solidconnection/university/service/GeneralUniversityRecommendService.java @@ -0,0 +1,35 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; + +import java.util.List; + +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; + +@Service +@RequiredArgsConstructor +public class GeneralUniversityRecommendService { + + /* + * 해당 시기에 열리는 대학교들 중 랜덤으로 선택해서 목록을 구성한다. + * */ + private final UniversityInfoForApplyRepository universityInfoForApplyRepository; + + @Getter + private List recommendUniversities; + + @Value("${university.term}") + public String term; + + @EventListener(ApplicationReadyEvent.class) + public void init() { + recommendUniversities = universityInfoForApplyRepository.findRandomByTerm(term, RECOMMEND_UNIVERSITY_NUM); + } +} diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index 6a6a43fbf..654b08390 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -23,7 +23,7 @@ public class UniversityRecommendService { public static final int RECOMMEND_UNIVERSITY_NUM = 6; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final GeneralRecommendUniversities generalRecommendUniversities; + private final GeneralUniversityRecommendService generalUniversityRecommendService; private final SiteUserRepository siteUserRepository; @Value("${university.term}") @@ -56,7 +56,7 @@ public UniversityRecommendsResponse getPersonalRecommends(String email) { } private List getGeneralRecommendsExcludingSelected(List alreadyPicked) { - List generalRecommend = new ArrayList<>(generalRecommendUniversities.getRecommendUniversities()); + List generalRecommend = new ArrayList<>(generalUniversityRecommendService.getRecommendUniversities()); generalRecommend.removeAll(alreadyPicked); Collections.shuffle(generalRecommend); return generalRecommend.subList(0, RECOMMEND_UNIVERSITY_NUM - alreadyPicked.size()); @@ -68,8 +68,7 @@ private List getGeneralRecommendsExcludingSelected(List< @Transactional(readOnly = true) @ThunderingHerdCaching(key = "university:recommend:general", cacheManager = "customCacheManager", ttlSec = 86400) public UniversityRecommendsResponse getGeneralRecommends() { - List generalRecommends = new ArrayList<>(generalRecommendUniversities.getRecommendUniversities()); - Collections.shuffle(generalRecommends); + List generalRecommends = new ArrayList<>(generalUniversityRecommendService.getRecommendUniversities()); return new UniversityRecommendsResponse(generalRecommends.stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList()); diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index 00afbc8e3..4f3bd3042 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -10,7 +10,7 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityRecommendsResponse; -import com.example.solidconnection.university.service.GeneralRecommendUniversities; +import com.example.solidconnection.university.service.GeneralUniversityRecommendService; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -41,7 +41,7 @@ class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { private TokenProvider tokenProvider; @Autowired - private GeneralRecommendUniversities generalRecommendUniversities; + private GeneralUniversityRecommendService generalUniversityRecommendService; private SiteUser siteUser; private String accessToken; @@ -51,7 +51,7 @@ void setUp() { // setUp - 회원 정보 저장 String email = "email@email.com"; siteUser = siteUserRepository.save(createSiteUserByEmail(email)); - generalRecommendUniversities.init(); + generalUniversityRecommendService.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); @@ -156,7 +156,7 @@ void setUp() { .extract().as(UniversityRecommendsResponse.class); List generalRecommendUniversities - = this.generalRecommendUniversities.getRecommendUniversities().stream() + = this.generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList(); assertAll( @@ -179,7 +179,7 @@ void setUp() { .extract().as(UniversityRecommendsResponse.class); List generalRecommendUniversities - = this.generalRecommendUniversities.getRecommendUniversities().stream() + = this.generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList(); assertAll( diff --git a/src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java new file mode 100644 index 000000000..d93765a44 --- /dev/null +++ b/src/test/java/com/example/solidconnection/university/service/GeneralUniversityRecommendServiceTest.java @@ -0,0 +1,41 @@ +package com.example.solidconnection.university.service; + +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.university.domain.UniversityInfoForApply; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; + +import java.util.List; + +import static com.example.solidconnection.university.service.UniversityRecommendService.RECOMMEND_UNIVERSITY_NUM; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("공통 추천 대학 서비스 테스트") +@TestContainerSpringBootTest +class GeneralUniversityRecommendServiceTest extends BaseIntegrationTest { + + @Autowired + private GeneralUniversityRecommendService generalUniversityRecommendService; + + @Value("${university.term}") + private String term; + + @Test + void 모집_시기의_대학들_중에서_랜덤하게_N개를_추천_목록으로_구성한다() { + // given + generalUniversityRecommendService.init(); + List universities = generalUniversityRecommendService.getRecommendUniversities(); + + // when & then + assertAll( + () -> assertThat(universities) + .extracting("term") + .allMatch(term::equals), + () -> assertThat(universities).hasSize(RECOMMEND_UNIVERSITY_NUM) + ); + } +} diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java index cadd45aaf..17d951614 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -38,11 +38,11 @@ class UniversityRecommendServiceTest extends BaseIntegrationTest { private InterestedCountyRepository interestedCountyRepository; @Autowired - private GeneralRecommendUniversities generalRecommendUniversities; + private GeneralUniversityRecommendService generalUniversityRecommendService; @BeforeEach void setUp() { - generalRecommendUniversities.init(); + generalUniversityRecommendService.init(); } @Test @@ -118,7 +118,7 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIVERSITY_NUM) .containsExactlyInAnyOrderElementsOf( - generalRecommendUniversities.getRecommendUniversities().stream() + generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList() ); @@ -133,7 +133,7 @@ void setUp() { assertThat(response.recommendedUniversities()) .hasSize(RECOMMEND_UNIVERSITY_NUM) .containsExactlyInAnyOrderElementsOf( - generalRecommendUniversities.getRecommendUniversities().stream() + generalUniversityRecommendService.getRecommendUniversities().stream() .map(UniversityInfoForApplyPreviewResponse::from) .toList() ); From 96547704692263eb90e29e50533abb5f296c58b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:57:40 +0900 Subject: [PATCH 13/23] =?UTF-8?q?test:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/service/BoardServiceTest.java | 72 +++++++++++++++++++ .../integration/BaseIntegrationTest.java | 49 +++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java diff --git a/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java new file mode 100644 index 000000000..98c2b28fa --- /dev/null +++ b/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java @@ -0,0 +1,72 @@ +package com.example.solidconnection.board.service; + +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.dto.BoardFindPostResponse; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.type.PostCategory; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.ZonedDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("게시판 서비스 테스트") +class BoardServiceTest extends BaseIntegrationTest { + + @Autowired + private BoardService boardService; + + @Test + void 게시판_코드와_카테고리로_게시글_목록을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getCategory().equals(PostCategory.자유) && post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = BoardFindPostResponse.from(expectedPosts); + + // when + List actualResponses = boardService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.자유.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } + + @Test + void 전체_카테고리로_조회시_해당_게시판의_모든_게시글을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = BoardFindPostResponse.from(expectedPosts); + + // when + List actualResponses = boardService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.전체.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } +} diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index f588b87ae..054cf6851 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -3,8 +3,12 @@ import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; +import com.example.solidconnection.entity.PostImage; import com.example.solidconnection.entity.Region; +import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.PostImageRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -12,6 +16,7 @@ import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; import com.example.solidconnection.university.domain.LanguageRequirement; @@ -76,6 +81,15 @@ public abstract class BaseIntegrationTest { public static Board 유럽권; public static Board 자유게시판; + public static Post 미주권_자유게시글; + public static Post 아시아권_자유게시글; + public static Post 유럽권_자유게시글; + public static Post 자유게시판_자유게시글; + public static Post 미주권_질문게시글; + public static Post 아시아권_질문게시글; + public static Post 유럽권_질문게시글; + public static Post 자유게시판_질문게시글; + @Autowired private SiteUserRepository siteUserRepository; @@ -97,6 +111,12 @@ public abstract class BaseIntegrationTest { @Autowired private BoardRepository boardRepository; + @Autowired + private PostRepository postRepository; + + @Autowired + private PostImageRepository postImageRepository; + @Value("${university.term}") public String term; @@ -109,6 +129,7 @@ public void setUpBaseData() { setUpUniversityInfos(); setUpLanguageRequirements(); setUpBoards(); + setUpPosts(); } private void setUpSiteUsers() { @@ -337,6 +358,17 @@ private void setUpBoards() { 자유게시판 = boardRepository.save(new Board(FREE.name(), "자유게시판")); } + private void setUpPosts() { + 미주권_자유게시글 = createPost(미주권, 테스트유저_1, "미주권 자유게시글", "미주권 자유게시글 내용", PostCategory.자유); + 아시아권_자유게시글 = createPost(아시아권, 테스트유저_2, "아시아권 자유게시글", "아시아권 자유게시글 내용", PostCategory.자유); + 유럽권_자유게시글 = createPost(유럽권, 테스트유저_1, "유럽권 자유게시글", "유럽권 자유게시글 내용", PostCategory.자유); + 자유게시판_자유게시글 = createPost(자유게시판, 테스트유저_2, "자유게시판 자유게시글", "자유게시판 자유게시글 내용", PostCategory.자유); + 미주권_질문게시글 = createPost(미주권, 테스트유저_1, "미주권 질문게시글", "미주권 질문게시글 내용", PostCategory.질문); + 아시아권_질문게시글 = createPost(아시아권, 테스트유저_2, "아시아권 질문게시글", "아시아권 질문게시글 내용", PostCategory.질문); + 유럽권_질문게시글 = createPost(유럽권, 테스트유저_1, "유럽권 질문게시글", "유럽권 질문게시글 내용", PostCategory.질문); + 자유게시판_질문게시글 = createPost(자유게시판, 테스트유저_2, "자유게시판 질문게시글", "자유게시판 질문게시글 내용", PostCategory.질문); + } + private void saveLanguageTestRequirement( UniversityInfoForApply universityInfoForApply, LanguageTestType testType, @@ -351,4 +383,21 @@ private void saveLanguageTestRequirement( universityInfoForApplyRepository.save(universityInfoForApply); languageRequirementRepository.save(languageRequirement); } + + private Post createPost(Board board, SiteUser siteUser, String title, String content, PostCategory category) { + Post post = new Post( + title, + content, + false, + 0L, + 0L, + category + ); + post.setBoardAndSiteUser(board, siteUser); + Post savedPost = postRepository.save(post); + PostImage postImage = new PostImage("imageUrl"); + postImage.setPost(savedPost); + postImageRepository.save(postImage); + return savedPost; + } } From 89eca057716d882486a18e19d517673dbc97abe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Tue, 4 Feb 2025 09:07:41 +0900 Subject: [PATCH 14/23] =?UTF-8?q?test:=20=EC=A7=80=EC=9B=90=EC=84=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=86=B5=ED=95=A9=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 지원서 제출 관련 통합테스트 코드 추가 * test: 지원자 목록 조회 관련 통합테스트 코드 추가 * test: 경쟁자 목록 조회 관련 통합테스트 코드 추가 * test: 이번학기 지원자 대학국문 필터링 조회 시 secondChoice 검증 추가 * style: 불필요한 개행 삭제 * refactor: 테스트 메서드 이름에 컨벤션 적용 * test: UniversityChoiceRequest 생성 시 불필요한 인자 제거 * test: firstChoice 중복 검증 제외 * test: cntainsAll로 검증하는 것으로 변경 * test: 테스트유저2로 검증하는 것으로 변경 --- .../service/ApplicationQueryServiceTest.java | 214 ++++++++++++++++++ .../ApplicationSubmissionServiceTest.java | 202 +++++++++++++++++ .../integration/BaseIntegrationTest.java | 146 +++++++++++- 3 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java create mode 100644 src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java new file mode 100644 index 000000000..b8f5cd283 --- /dev/null +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java @@ -0,0 +1,214 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.dto.ApplicantResponse; +import com.example.solidconnection.application.dto.ApplicationsResponse; +import com.example.solidconnection.application.dto.UniversityApplicantsResponse; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("지원서 조회 서비스 테스트") +class ApplicationQueryServiceTest extends BaseIntegrationTest { + + @Autowired + private ApplicationQueryService applicationQueryService; + + @Nested + class 지원자_목록_조회_테스트 { + + @Test + void 이번_학기_전체_지원자를_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_2.getEmail(), + "", + "" + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))), + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_7_코펜하겐IT대학_X_X_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(그라츠대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + )); + + assertThat(response.thirdChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(서던덴마크대학교_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))), + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + )); + } + + @Test + void 이번_학기_특정_지역_지원자를_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_2.getEmail(), + 영미권.getCode(), + "" + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))) + )); + } + + @Test + void 이번_학기_지원자를_대학_국문_이름으로_필터링해서_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_2.getEmail(), + null, + "일본" + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(메이지대학_지원_정보, List.of()) + )); + + assertThat(response.thirdChoice()).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, false))) + ); + } + + @Test + void 이전_학기_지원자는_조회되지_않는다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicants( + 테스트유저_1.getEmail(), + "", + "" + ); + + // then + assertThat(response.firstChoice()).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(네바다주립대학_라스베이거스_지원_정보, + List.of(ApplicantResponse.of(이전학기_지원서, false))) + )); + assertThat(response.secondChoice()).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(그라츠공과대학_지원_정보, + List.of(ApplicantResponse.of(이전학기_지원서, false))) + )); + assertThat(response.thirdChoice()).doesNotContainAnyElementsOf(List.of( + UniversityApplicantsResponse.of(메이지대학_지원_정보, + List.of(ApplicantResponse.of(이전학기_지원서, false))) + )); + } + } + + @Nested + class 경쟁자_목록_조회_테스트 { + + @Test + void 이번_학기_지원한_대학의_경쟁자_목록을_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( + 테스트유저_2.getEmail() + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))) + )); + + assertThat(response.secondChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(괌대학_A_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))), + UniversityApplicantsResponse.of(괌대학_B_지원_정보, + List.of(ApplicantResponse.of(테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, false))) + )); + + assertThat(response.thirdChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(린츠_카톨릭대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, true))) + )); + } + + @Test + void 이번_학기_지원한_대학_중_미선택이_있을_때_경쟁자_목록을_조회한다() { + // when + ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( + 테스트유저_7.getEmail() + ); + + // then + assertThat(response.firstChoice()).containsAll(List.of( + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, + List.of(ApplicantResponse.of(테스트유저_7_코펜하겐IT대학_X_X_지원서, true))) + )); + + assertThat(response.secondChoice()).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, List.of()) + ); + + assertThat(response.thirdChoice()).containsExactlyInAnyOrder( + UniversityApplicantsResponse.of(코펜하겐IT대학_지원_정보, List.of()) + ); + } + + @Test + void 이번_학기_지원한_대학이_모두_미선택일_때_경쟁자_목록을_조회한다() { + //when + ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( + 테스트유저_6.getEmail() + ); + + // then + assertThat(response.firstChoice()).isEmpty(); + assertThat(response.secondChoice()).isEmpty(); + assertThat(response.thirdChoice()).isEmpty(); + } + } +} diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java new file mode 100644 index 000000000..84f130d54 --- /dev/null +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -0,0 +1,202 @@ +package com.example.solidconnection.application.service; + +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.dto.ApplyRequest; +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.application.repository.ApplicationRepository; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.LanguageTestType; +import com.example.solidconnection.type.VerifyStatus; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; + +import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; +import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_GPA_SCORE_STATUS; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_LANGUAGE_TEST_SCORE_STATUS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("지원서 제출 서비스 테스트") +class ApplicationSubmissionServiceTest extends BaseIntegrationTest { + + @Autowired + private ApplicationSubmissionService applicationSubmissionService; + + @Autowired + private ApplicationRepository applicationRepository; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + + @Test + void 정상적으로_지원서를_제출한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createApprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + 네바다주립대학_라스베이거스_지원_정보.getId(), + 메모리얼대학_세인트존스_A_지원_정보.getId() + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when + boolean result = applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + + // then + Application savedApplication = applicationRepository.findBySiteUserAndTerm(테스트유저_1, term).orElseThrow(); + assertAll( + () -> assertThat(result).isTrue(), + () -> assertThat(savedApplication.getGpa()).isEqualTo(gpaScore.getGpa()), + () -> assertThat(savedApplication.getLanguageTest()).isEqualTo(languageTestScore.getLanguageTest()), + () -> assertThat(savedApplication.getVerifyStatus()).isEqualTo(VerifyStatus.APPROVED), + () -> assertThat(savedApplication.getNicknameForApply()).isNotNull(), + () -> assertThat(savedApplication.getUpdateCount()).isZero(), + () -> assertThat(savedApplication.getTerm()).isEqualTo(term), + () -> assertThat(savedApplication.isDelete()).isFalse(), + () -> assertThat(savedApplication.getFirstChoiceUniversity().getId()).isEqualTo(괌대학_A_지원_정보.getId()), + () -> assertThat(savedApplication.getSecondChoiceUniversity().getId()).isEqualTo(네바다주립대학_라스베이거스_지원_정보.getId()), + () -> assertThat(savedApplication.getThirdChoiceUniversity().getId()).isEqualTo(메모리얼대학_세인트존스_A_지원_정보.getId()), + () -> assertThat(savedApplication.getSiteUser().getId()).isEqualTo(테스트유저_1.getId()) + ); + } + + @Test + void 미승인된_GPA_성적으로_지원하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createUnapprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createApprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + null, + null + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_GPA_SCORE_STATUS.getMessage()); + } + + @Test + void 미승인된_어학성적으로_지원하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createUnapprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + null, + null + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); + } + + @Test + void 동일한_대학을_중복_선택하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createUnapprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + 괌대학_A_지원_정보.getId(), + 메모리얼대학_세인트존스_A_지원_정보.getId() + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); + } + + @Test + void 지원서_수정_횟수를_초과하면_예외_응답을_반환한다() { + // given + GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); + LanguageTestScore languageTestScore = createApprovedLanguageTestScore(테스트유저_1); + UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( + 괌대학_A_지원_정보.getId(), + null, + null + ); + ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); + + for (int i = 0; i < APPLICATION_UPDATE_COUNT_LIMIT + 1; i++) { + applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + } + + // when & then + assertThatCode(() -> + applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + ) + .isInstanceOf(CustomException.class) + .hasMessage(APPLY_UPDATE_LIMIT_EXCEED.getMessage()); + } + + private GpaScore createUnapprovedGpaScore(SiteUser siteUser) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + return gpaScoreRepository.save(gpaScore); + } + + private GpaScore createApprovedGpaScore(SiteUser siteUser) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + return gpaScoreRepository.save(gpaScore); + } + + private LanguageTestScore createUnapprovedLanguageTestScore(SiteUser siteUser) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + return languageTestScoreRepository.save(languageTestScore); + } + + private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); + return languageTestScoreRepository.save(languageTestScore); + } +} diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index 054cf6851..ec29b8499 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -1,5 +1,9 @@ package com.example.solidconnection.support.integration; +import com.example.solidconnection.application.domain.Application; +import com.example.solidconnection.application.domain.Gpa; +import com.example.solidconnection.application.domain.LanguageTest; +import com.example.solidconnection.application.repository.ApplicationRepository; import com.example.solidconnection.board.domain.Board; import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; @@ -10,6 +14,10 @@ import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.PostImageRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.score.domain.GpaScore; +import com.example.solidconnection.score.domain.LanguageTestScore; +import com.example.solidconnection.score.repository.GpaScoreRepository; +import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.DatabaseClearExtension; @@ -19,6 +27,7 @@ import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.type.PreparationStatus; import com.example.solidconnection.type.Role; +import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.LanguageRequirement; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; @@ -30,7 +39,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import java.time.LocalDate; import java.util.HashSet; +import java.util.List; import static com.example.solidconnection.type.BoardCode.AMERICAS; import static com.example.solidconnection.type.BoardCode.ASIA; @@ -45,6 +56,12 @@ public abstract class BaseIntegrationTest { public static SiteUser 테스트유저_1; public static SiteUser 테스트유저_2; + public static SiteUser 테스트유저_3; + public static SiteUser 테스트유저_4; + public static SiteUser 테스트유저_5; + public static SiteUser 테스트유저_6; + public static SiteUser 테스트유저_7; + public static SiteUser 이전학기_지원자; public static Region 영미권; public static Region 유럽; @@ -76,6 +93,14 @@ public abstract class BaseIntegrationTest { public static UniversityInfoForApply 린츠_카톨릭대학_지원_정보; public static UniversityInfoForApply 메이지대학_지원_정보; + public static Application 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서; + public static Application 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서; + public static Application 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서; + public static Application 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서; + public static Application 테스트유저_6_X_X_X_지원서; + public static Application 테스트유저_7_코펜하겐IT대학_X_X_지원서; + public static Application 이전학기_지원서; + public static Board 미주권; public static Board 아시아권; public static Board 유럽권; @@ -108,6 +133,15 @@ public abstract class BaseIntegrationTest { @Autowired private LanguageRequirementRepository languageRequirementRepository; + @Autowired + private ApplicationRepository applicationRepository; + + @Autowired + private GpaScoreRepository gpaScoreRepository; + + @Autowired + private LanguageTestScoreRepository languageTestScoreRepository; + @Autowired private BoardRepository boardRepository; @@ -128,6 +162,7 @@ public void setUpBaseData() { setUpUniversities(); setUpUniversityInfos(); setUpLanguageRequirements(); + setUpApplications(); setUpBoards(); setUpPosts(); } @@ -150,6 +185,60 @@ private void setUpSiteUsers() { PreparationStatus.CONSIDERING, Role.MENTEE, Gender.FEMALE)); + + 테스트유저_3 = siteUserRepository.save(new SiteUser( + "test3@example.com", + "nickname3", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); + + 테스트유저_4 = siteUserRepository.save(new SiteUser( + "test4@example.com", + "nickname4", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); + + 테스트유저_5 = siteUserRepository.save(new SiteUser( + "test5@example.com", + "nickname5", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); + + 테스트유저_6 = siteUserRepository.save(new SiteUser( + "test6@example.com", + "nickname6", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); + + 테스트유저_7 = siteUserRepository.save(new SiteUser( + "test7@example.com", + "nickname7", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.FEMALE)); + + 이전학기_지원자 = siteUserRepository.save(new SiteUser( + "old@example.com", + "oldNickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE)); } private void setUpRegions() { @@ -351,6 +440,41 @@ private void setUpLanguageRequirements() { saveLanguageTestRequirement(메이지대학_지원_정보, LanguageTestType.JLPT, "N2"); } + private void setUpApplications() { + 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서 = new Application(테스트유저_2, createApprovedGpaScore(테스트유저_2).getGpa(), createApprovedLanguageTestScore(테스트유저_2).getLanguageTest(), + term, 괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "user2_nickname"); + + 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서 = new Application(테스트유저_3, createApprovedGpaScore(테스트유저_3).getGpa(), createApprovedLanguageTestScore(테스트유저_3).getLanguageTest(), + term, 괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "user3_nickname"); + + 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서 = new Application(테스트유저_4, createApprovedGpaScore(테스트유저_4).getGpa(), createApprovedLanguageTestScore(테스트유저_4).getLanguageTest(), + term, 메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "user4_nickname"); + + 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서 = new Application(테스트유저_5, createApprovedGpaScore(테스트유저_5).getGpa(), createApprovedLanguageTestScore(테스트유저_5).getLanguageTest(), + term, 네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "user5_nickname"); + + 테스트유저_6_X_X_X_지원서 = new Application(테스트유저_6, createApprovedGpaScore(테스트유저_6).getGpa(), createApprovedLanguageTestScore(테스트유저_6).getLanguageTest(), + term, null, null, null, "user6_nickname"); + + 테스트유저_7_코펜하겐IT대학_X_X_지원서 = new Application(테스트유저_7, createApprovedGpaScore(테스트유저_7).getGpa(), createApprovedLanguageTestScore(테스트유저_7).getLanguageTest(), + term, 코펜하겐IT대학_지원_정보, null, null, "user7_nickname"); + + 이전학기_지원서 = new Application(이전학기_지원자, createApprovedGpaScore(이전학기_지원자).getGpa(), createApprovedLanguageTestScore(이전학기_지원자).getLanguageTest(), + "1988-1", 네바다주립대학_라스베이거스_지원_정보, 그라츠공과대학_지원_정보, 메이지대학_지원_정보, "old_nickname"); + + 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_6_X_X_X_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 테스트유저_7_코펜하겐IT대학_X_X_지원서.setVerifyStatus(VerifyStatus.APPROVED); + 이전학기_지원서.setVerifyStatus(VerifyStatus.APPROVED); + + applicationRepository.saveAll(List.of( + 테스트유저_2_괌대학_B_괌대학_A_린츠_카톨릭대학_지원서, 테스트유저_3_괌대학_A_괌대학_B_그라츠공과대학_지원서, 테스트유저_4_메이지대학_그라츠대학_서던덴마크대학_지원서, 테스트유저_5_네바다주립대학_그라츠공과대학_메이지대학_지원서, + 테스트유저_6_X_X_X_지원서, 테스트유저_7_코펜하겐IT대학_X_X_지원서, 이전학기_지원서)); + } + private void setUpBoards() { 미주권 = boardRepository.save(new Board(AMERICAS.name(), "미주권")); 아시아권 = boardRepository.save(new Board(ASIA.name(), "아시아권")); @@ -384,7 +508,27 @@ private void saveLanguageTestRequirement( languageRequirementRepository.save(languageRequirement); } - private Post createPost(Board board, SiteUser siteUser, String title, String content, PostCategory category) { + private GpaScore createApprovedGpaScore(SiteUser siteUser) { + GpaScore gpaScore = new GpaScore( + new Gpa(4.0, 4.5, "/gpa-report.pdf"), + siteUser, + LocalDate.now() + ); + gpaScore.setVerifyStatus(VerifyStatus.APPROVED); + return gpaScoreRepository.save(gpaScore); + } + + private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { + LanguageTestScore languageTestScore = new LanguageTestScore( + new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), + LocalDate.now(), + siteUser + ); + languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); + return languageTestScoreRepository.save(languageTestScore); + } + + private Post createPost (Board board, SiteUser siteUser, String title, String content, PostCategory category){ Post post = new Post( title, content, From 2fc9d85f33e51fe46cc12041bcf63b1773953ccf Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 6 Feb 2025 07:48:15 +0900 Subject: [PATCH 15/23] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=EC=9D=98?= =?UTF-8?q?=20=EC=9D=B8=EC=A6=9D=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=EC=97=90=EC=84=9C=20=EC=9B=90?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=ED=98=95=ED=83=9C=EB=A1=9C=20=EB=B0=9B?= =?UTF-8?q?=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20(#171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: manager, provider, userDetails 적용 - 클래스 자체는 많아졌지만, 이 코드를 처음 보는 개발자도 쉽게 이해할 수 있도록, 스프링 시큐리티의 표준 스펙에 맞게 개발하였음 * refactor: 패키지 이동 - 설정(configuration)인 것들과 아닌 것들 분리 * test: Authentication 테스트 작성 * test: userDetailsService 테스트 작성 * test: AuthenticationProvider 테스트 작성 * feat: 인증 정보를 객체로 바꾸는 resolver 구현 - 서비스 코드가 더이상 어떤 정보가 token 의 subject인지 몰라도 되도록 결합도를 낮춤 * test: resolver 테스트 작성 * refactor: 탈퇴한 사용자인지 검증 추가, 함수 분리 * test: 탈퇴한 사용자 검증 테스트 작성 * style: 불필요한 개행 삭제 * refactor: 리졸버 이름 변경 * feat: 리졸버 활성화 * refactor: 지원서 관련 코드에 리졸버 적용 * refactor: 인증 관련 코드에 리졸버 적용 * refactor: 댓글 관련 코드에 리졸버 적용 * refactor: 사용자 관련 코드에 리졸버 적용 * refactor: 게시글 관련 코드에 리졸버 적용 * refactor: 대학 관련 코드에 리졸버 적용 * refactor: 성적 관련 코드에 리졸버 적용 * refactor: S3 관련 코드에 리졸버 적용 * test: 사용자와 성적이 양방향 관계가 되도록 수정 - 이전에는 siteUser.email로 조회를 해와서 DB에 있는 내용이 바로 반영된 SiteUser를 대상으로 했기 때문에 siteUser.getGpaScoreList 나 siteUser.getLanguageTestScoreList를 했을 때 조회가 되었었다. - 하지만 SiteUser 객체 자체를 넘기도록 테스트가 바뀌었고, 테스트 코드에서 Transactional 사용하는 것을 지양하기 위해서 양방향을 명시적으로 걸어주도록 코드를 수정했다. --- .../controller/ApplicationController.java | 27 +-- .../service/ApplicationQueryService.java | 13 +- .../service/ApplicationSubmissionService.java | 4 +- .../auth/controller/AuthController.java | 32 ++-- .../auth/service/AuthService.java | 7 +- .../auth/service/SignInService.java | 21 ++- .../auth/service/SignUpService.java | 8 +- .../auth/service/TokenProvider.java | 12 +- .../comment/controller/CommentController.java | 25 ++- .../comment/service/CommentService.java | 29 ++-- .../security/AuthenticationManagerConfig.java | 25 +++ .../config/security/JwtAuthentication.java | 29 ---- .../security/JwtAuthenticationFilter.java | 49 ------ .../security/SecurityConfiguration.java | 3 + .../config/web/WebMvcConfig.java | 27 +++ .../custom/resolver/AuthorizedUser.java | 11 ++ .../resolver/AuthorizedUserResolver.java | 38 +++++ .../custom/resolver/ExpiredToken.java | 11 ++ .../custom/resolver/ExpiredTokenResolver.java | 34 ++++ .../ExpiredTokenAuthentication.java | 18 ++ .../authentication/JwtAuthentication.java | 30 ++++ .../SiteUserAuthentication.java | 16 ++ .../filter}/ExceptionHandlerFilter.java | 2 +- .../filter/JwtAuthenticationFilter.java | 55 ++++++ .../security/filter}/SignOutCheckFilter.java | 3 +- .../ExpiredTokenAuthenticationProvider.java | 34 ++++ .../SiteUserAuthenticationProvider.java | 37 ++++ .../userdetails/SiteUserDetails.java} | 15 +- .../userdetails/SiteUserDetailsService.java | 48 ++++++ .../post/controller/PostController.java | 50 +++--- .../post/service/PostCommandService.java | 17 +- .../post/service/PostLikeService.java | 8 +- .../post/service/PostQueryService.java | 15 +- .../solidconnection/s3/S3Controller.java | 19 ++- .../example/solidconnection/s3/S3Service.java | 9 +- .../score/controller/ScoreController.java | 30 ++-- .../score/service/ScoreService.java | 15 +- .../controller/SiteUserController.java | 36 ++-- .../repository/LikedUniversityRepository.java | 4 +- .../repository/SiteUserRepository.java | 13 +- .../siteuser/service/SiteUserService.java | 22 +-- .../controller/UniversityController.java | 43 ++--- .../service/UniversityLikeService.java | 8 +- .../service/UniversityRecommendService.java | 5 +- .../solidconnection/util/RedisUtils.java | 4 +- .../service/ApplicationQueryServiceTest.java | 14 +- .../ApplicationSubmissionServiceTest.java | 12 +- .../comment/service/CommentServiceTest.java | 26 +-- .../PostLikeCountConcurrencyTest.java | 24 +-- .../PostViewCountConcurrencyTest.java | 8 +- .../concurrency/ThunderingHerdTest.java | 6 +- .../security/JwtAuthenticationFilterTest.java | 126 -------------- .../resolver/AuthorizedUserResolverTest.java | 67 ++++++++ .../resolver/ExpiredTokenResolverTest.java | 43 +++++ .../ExpiredTokenAuthenticationTest.java | 64 +++++++ .../SiteUserAuthenticationTest.java | 73 ++++++++ .../filter}/ExceptionHandlerFilterTest.java | 2 +- .../filter/JwtAuthenticationFilterTest.java | 116 +++++++++++++ .../filter}/SignOutCheckFilterTest.java | 3 +- ...xpiredTokenAuthenticationProviderTest.java | 80 +++++++++ .../SiteUserAuthenticationProviderTest.java | 159 ++++++++++++++++++ .../SiteUserDetailsServiceTest.java | 104 ++++++++++++ .../e2e/ApplicantsQueryTest.java | 43 +++-- .../solidconnection/e2e/MyPageTest.java | 20 ++- .../solidconnection/e2e/MyPageUpdateTest.java | 17 +- .../solidconnection/e2e/SignInTest.java | 11 +- .../solidconnection/e2e/SignUpTest.java | 5 +- .../e2e/UniversityDetailTest.java | 4 +- .../e2e/UniversityLikeTest.java | 12 +- .../e2e/UniversityRecommendTest.java | 4 +- .../e2e/UniversitySearchTest.java | 18 +- .../post/service/PostCommandServiceTest.java | 22 +-- .../post/service/PostLikeServiceTest.java | 12 +- .../post/service/PostQueryServiceTest.java | 4 +- .../score/service/ScoreServiceTest.java | 15 +- .../siteuser/service/SiteUserServiceTest.java | 26 +-- .../service/UniversityLikeServiceTest.java | 14 +- .../UniversityRecommendServiceTest.java | 8 +- 78 files changed, 1485 insertions(+), 638 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java delete mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java delete mode 100644 src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java create mode 100644 src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java rename src/main/java/com/example/solidconnection/{config/security => custom/security/filter}/ExceptionHandlerFilter.java (97%) create mode 100644 src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java rename src/main/java/com/example/solidconnection/{config/security => custom/security/filter}/SignOutCheckFilter.java (92%) create mode 100644 src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java create mode 100644 src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java rename src/main/java/com/example/solidconnection/{config/security/JwtUserDetails.java => custom/security/userdetails/SiteUserDetails.java} (63%) create mode 100644 src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java delete mode 100644 src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java rename src/test/java/com/example/solidconnection/{config/security => custom/security/filter}/ExceptionHandlerFilterTest.java (98%) create mode 100644 src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java rename src/test/java/com/example/solidconnection/{config/security => custom/security/filter}/SignOutCheckFilterTest.java (96%) create mode 100644 src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java diff --git a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java index dce62235f..6d8c45fbf 100644 --- a/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java +++ b/src/main/java/com/example/solidconnection/application/controller/ApplicationController.java @@ -5,6 +5,8 @@ import com.example.solidconnection.application.dto.ApplyRequest; import com.example.solidconnection.application.service.ApplicationQueryService; import com.example.solidconnection.application.service.ApplicationSubmissionService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -16,8 +18,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/application") @RestController @@ -29,9 +29,10 @@ public class ApplicationController { // 지원서 제출하기 api @PostMapping() public ResponseEntity apply( - Principal principal, - @Valid @RequestBody ApplyRequest applyRequest) { - boolean result = applicationSubmissionService.apply(principal.getName(), applyRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody ApplyRequest applyRequest + ) { + boolean result = applicationSubmissionService.apply(siteUser, applyRequest); return ResponseEntity .status(HttpStatus.OK) .body(new ApplicationSubmissionResponse(result)); @@ -39,20 +40,22 @@ public ResponseEntity apply( @GetMapping public ResponseEntity getApplicants( - Principal principal, + @AuthorizedUser SiteUser siteUser, @RequestParam(required = false, defaultValue = "") String region, - @RequestParam(required = false, defaultValue = "") String keyword) { - applicationQueryService.validateSiteUserCanViewApplicants(principal.getName()); - ApplicationsResponse result = applicationQueryService.getApplicants(principal.getName(), region, keyword); + @RequestParam(required = false, defaultValue = "") String keyword + ) { + applicationQueryService.validateSiteUserCanViewApplicants(siteUser); + ApplicationsResponse result = applicationQueryService.getApplicants(siteUser, region, keyword); return ResponseEntity .ok(result); } @GetMapping("/competitors") public ResponseEntity getApplicantsForUserCompetitors( - Principal principal) { - applicationQueryService.validateSiteUserCanViewApplicants(principal.getName()); - ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(principal.getName()); + @AuthorizedUser SiteUser siteUser + ) { + applicationQueryService.validateSiteUserCanViewApplicants(siteUser); + ApplicationsResponse result = applicationQueryService.getApplicantsByUserApplications(siteUser); return ResponseEntity .ok(result); } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java index 68cf9c0aa..170d7cf13 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationQueryService.java @@ -8,7 +8,6 @@ import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.University; import com.example.solidconnection.university.domain.UniversityInfoForApply; @@ -34,7 +33,6 @@ public class ApplicationQueryService { private final ApplicationRepository applicationRepository; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final SiteUserRepository siteUserRepository; private final UniversityFilterRepositoryImpl universityFilterRepository; @Value("${university.term}") @@ -49,9 +47,7 @@ public class ApplicationQueryService { * */ @Transactional(readOnly = true) @ThunderingHerdCaching(key = "application:query:{1}:{2}", cacheManager = "customCacheManager", ttlSec = 86400) - public ApplicationsResponse getApplicants(String email, String regionCode, String keyword) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public ApplicationsResponse getApplicants(SiteUser siteUser, String regionCode, String keyword) { // 국가와 키워드와 지역을 통해 대학을 필터링한다. List universities = universityFilterRepository.findByRegionCodeAndKeywords(regionCode, List.of(keyword)); @@ -64,9 +60,7 @@ public ApplicationsResponse getApplicants(String email, String regionCode, Strin } @Transactional(readOnly = true) - public ApplicationsResponse getApplicantsByUserApplications(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public ApplicationsResponse getApplicantsByUserApplications(SiteUser siteUser) { Application userLatestApplication = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term); List userAppliedUniversities = Arrays.asList( Optional.ofNullable(userLatestApplication.getFirstChoiceUniversity()) @@ -91,8 +85,7 @@ public ApplicationsResponse getApplicantsByUserApplications(String email) { // 학기별로 상태가 관리된다. // 금학기에 지원이력이 있는 사용자만 지원정보를 확인할 수 있도록 한다. @Transactional(readOnly = true) - public void validateSiteUserCanViewApplicants(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public void validateSiteUserCanViewApplicants(SiteUser siteUser) { VerifyStatus verifyStatus = applicationRepository.getApplicationBySiteUserAndTerm(siteUser, term).getVerifyStatus(); if (verifyStatus != VerifyStatus.APPROVED) { throw new CustomException(APPLICATION_NOT_APPROVED); diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index f82e9ad76..beb2f0cb0 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -38,7 +38,6 @@ public class ApplicationSubmissionService { private final ApplicationRepository applicationRepository; private final UniversityInfoForApplyRepository universityInfoForApplyRepository; - private final SiteUserRepository siteUserRepository; private final GpaScoreRepository gpaScoreRepository; private final LanguageTestScoreRepository languageTestScoreRepository; @@ -48,8 +47,7 @@ public class ApplicationSubmissionService { // 학점 및 어학성적이 모두 유효한 경우에만 지원서 등록이 가능하다. // 기존에 있던 status field 우선 APRROVED로 입력시킨다. @Transactional - public boolean apply(String email, ApplyRequest applyRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest(); validateUniversityChoices(universityChoiceRequest); diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 5f48124dc..1f6415157 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -8,6 +8,10 @@ import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.SignInService; import com.example.solidconnection.auth.service.SignUpService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.custom.resolver.ExpiredToken; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,8 +21,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/auth") @RestController @@ -29,32 +31,42 @@ public class AuthController { private final SignInService signInService; @PostMapping("/kakao") - public ResponseEntity processKakaoOauth(@RequestBody KakaoCodeRequest kakaoCodeRequest) { + public ResponseEntity processKakaoOauth( + @RequestBody KakaoCodeRequest kakaoCodeRequest + ) { KakaoOauthResponse kakaoOauthResponse = signInService.signIn(kakaoCodeRequest); return ResponseEntity.ok(kakaoOauthResponse); } @PostMapping("/sign-up") - public ResponseEntity signUp(@Valid @RequestBody SignUpRequest signUpRequest) { + public ResponseEntity signUp( + @Valid @RequestBody SignUpRequest signUpRequest + ) { SignUpResponse signUpResponseDto = signUpService.signUp(signUpRequest); return ResponseEntity.ok(signUpResponseDto); } @PostMapping("/sign-out") - public ResponseEntity signOut(Principal principal) { - authService.signOut(principal.getName()); + public ResponseEntity signOut( + @ExpiredToken ExpiredTokenAuthentication expiredToken + ) { + authService.signOut(expiredToken.getToken()); return ResponseEntity.ok().build(); } @PatchMapping("/quit") - public ResponseEntity quit(Principal principal) { - authService.quit(principal.getName()); + public ResponseEntity quit( + @AuthorizedUser SiteUser siteUser + ) { + authService.quit(siteUser); return ResponseEntity.ok().build(); } @PostMapping("/reissue") - public ResponseEntity reissueToken(Principal principal) { - ReissueResponse reissueResponse = authService.reissue(principal.getName()); + public ResponseEntity reissueToken( + @ExpiredToken ExpiredTokenAuthentication expiredToken + ) { + ReissueResponse reissueResponse = authService.reissue(expiredToken.getSubject()); return ResponseEntity.ok(reissueResponse); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index e16044e97..aed6f922f 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -4,7 +4,6 @@ import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; @@ -23,11 +22,8 @@ @Service public class AuthService { - public static final String SIGN_OUT_VALUE = "signOut"; - private final RedisTemplate redisTemplate; private final TokenProvider tokenProvider; - private final SiteUserRepository siteUserRepository; /* * 로그아웃 한다. @@ -48,8 +44,7 @@ public void signOut(String accessToken) { * - e.g. 2024-01-01 18:00 탈퇴 시, 2024-01-02 00:00 가 탈퇴일이 된다. * */ @Transactional - public void quit(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public void quit(SiteUser siteUser) { LocalDate tomorrow = LocalDate.now().plusDays(1); siteUser.setQuitedAt(tomorrow); } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index 2cd356d73..ae4947596 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -7,12 +7,15 @@ import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Optional; + @RequiredArgsConstructor @Service public class SignInService { @@ -35,11 +38,12 @@ public class SignInService { public KakaoOauthResponse signIn(KakaoCodeRequest kakaoCodeRequest) { KakaoUserInfoDto kakaoUserInfoDto = kakaoOAuthClient.processOauth(kakaoCodeRequest.code()); String email = kakaoUserInfoDto.kakaoAccountDto().email(); - boolean isAlreadyRegistered = siteUserRepository.existsByEmail(email); + Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO); - if (isAlreadyRegistered) { - resetQuitedAt(email); - return getSignInInfo(email); + if (optionalSiteUser.isPresent()) { + SiteUser siteUser = optionalSiteUser.get(); + resetQuitedAt(siteUser); + return getSignInInfo(siteUser); } return getFirstAccessInfo(kakaoUserInfoDto); @@ -47,8 +51,7 @@ public KakaoOauthResponse signIn(KakaoCodeRequest kakaoCodeRequest) { // 계적 복구 기한이 지난 회원은 자정마다 삭제된다. (UserRemovalScheduler 참고) // 따라서 DB 에서 조회되었다면 아직 기한이 지나지 않았다는 뜻이므로, 탈퇴 날짜를 초기화한다. - private void resetQuitedAt(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + private void resetQuitedAt(SiteUser siteUser) { if (siteUser.getQuitedAt() == null) { return; } @@ -56,9 +59,9 @@ private void resetQuitedAt(String email) { siteUser.setQuitedAt(null); } - private SignInResponse getSignInInfo(String email) { - String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + private SignInResponse getSignInInfo(SiteUser siteUser) { + String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignInResponse(true, accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index 5cbd781eb..697cdbdc0 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -10,6 +10,7 @@ import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.Role; @@ -45,6 +46,7 @@ public class SignUpService { * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. * */ + // todo: 여러가지 가입 방법 적용해야 함 @Transactional public SignUpResponse signUp(SignUpRequest signUpRequest) { // 검증 @@ -62,14 +64,14 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { saveInterestedCountry(signUpRequest, savedSiteUser); // 토큰 발급 - String accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); return new SignUpResponse(accessToken, refreshToken); } private void validateUserNotDuplicated(String email) { - if (siteUserRepository.existsByEmail(email)) { + if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.KAKAO)) { throw new CustomException(USER_ALREADY_EXISTED); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 9cba77c36..2dbf288ad 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -2,6 +2,7 @@ import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -12,8 +13,8 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; import static com.example.solidconnection.util.JwtUtils.parseSubject; +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; @RequiredArgsConstructor @Component @@ -22,8 +23,13 @@ public class TokenProvider { private final RedisTemplate redisTemplate; private final JwtProperties jwtProperties; - public String generateToken(String email, TokenType tokenType) { - Claims claims = Jwts.claims().setSubject(email); + public String generateToken(SiteUser siteUser, TokenType tokenType) { + String subject = siteUser.getId().toString(); + return generateToken(subject, tokenType); + } + + public String generateToken(String string, TokenType tokenType) { + Claims claims = Jwts.claims().setSubject(string); Date now = new Date(); Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); return Jwts.builder() diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java index a7eaab252..fda360b4a 100644 --- a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java +++ b/src/main/java/com/example/solidconnection/comment/controller/CommentController.java @@ -6,6 +6,8 @@ import com.example.solidconnection.comment.dto.CommentUpdateRequest; import com.example.solidconnection.comment.dto.CommentUpdateResponse; import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -17,8 +19,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RestController @RequiredArgsConstructor @RequestMapping("/posts") @@ -28,35 +28,32 @@ public class CommentController { @PostMapping("/{post_id}/comments") public ResponseEntity createComment( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("post_id") Long postId, @Valid @RequestBody CommentCreateRequest commentCreateRequest ) { - CommentCreateResponse commentCreateResponse = commentService.createComment( - principal.getName(), postId, commentCreateRequest); - return ResponseEntity.ok().body(commentCreateResponse); + CommentCreateResponse response = commentService.createComment(siteUser, postId, commentCreateRequest); + return ResponseEntity.ok().body(response); } @PatchMapping("/{post_id}/comments/{comment_id}") public ResponseEntity updateComment( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("post_id") Long postId, @PathVariable("comment_id") Long commentId, @Valid @RequestBody CommentUpdateRequest commentUpdateRequest ) { - CommentUpdateResponse commentUpdateResponse = commentService.updateComment( - principal.getName(), postId, commentId, commentUpdateRequest - ); - return ResponseEntity.ok().body(commentUpdateResponse); + CommentUpdateResponse response = commentService.updateComment(siteUser, postId, commentId, commentUpdateRequest); + return ResponseEntity.ok().body(response); } @DeleteMapping("/{post_id}/comments/{comment_id}") public ResponseEntity deleteCommentById( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("post_id") Long postId, @PathVariable("comment_id") Long commentId ) { - CommentDeleteResponse commentDeleteResponse = commentService.deleteCommentById(principal.getName(), postId, commentId); - return ResponseEntity.ok().body(commentDeleteResponse); + CommentDeleteResponse response = commentService.deleteCommentById(siteUser, postId, commentId); + return ResponseEntity.ok().body(response); } } diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/comment/service/CommentService.java index 7d25ee5f6..b7c1c6068 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/comment/service/CommentService.java @@ -12,7 +12,6 @@ import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -29,25 +28,22 @@ public class CommentService { private final CommentRepository commentRepository; - private final SiteUserRepository siteUserRepository; private final PostRepository postRepository; @Transactional(readOnly = true) - public List findCommentsByPostId(String email, Long postId) { + public List findCommentsByPostId(SiteUser siteUser, Long postId) { return commentRepository.findCommentTreeByPostId(postId) .stream() - .map(comment -> PostFindCommentResponse.from(isOwner(comment, email), comment)) + .map(comment -> PostFindCommentResponse.from(isOwner(comment, siteUser), comment)) .collect(Collectors.toList()); } - private Boolean isOwner(Comment comment, String email) { - return comment.getSiteUser().getEmail().equals(email); + private Boolean isOwner(Comment comment, SiteUser siteUser) { + return comment.getSiteUser().getId().equals(siteUser.getId()); } @Transactional - public CommentCreateResponse createComment(String email, Long postId, CommentCreateRequest commentCreateRequest) { - - SiteUser siteUser = siteUserRepository.getByEmail(email); + public CommentCreateResponse createComment(SiteUser siteUser, Long postId, CommentCreateRequest commentCreateRequest) { Post post = postRepository.getById(postId); Comment parentComment = null; @@ -68,13 +64,11 @@ private void validateCommentDepth(Comment parentComment) { } @Transactional - public CommentUpdateResponse updateComment(String email, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { - - SiteUser siteUser = siteUserRepository.getByEmail(email); + public CommentUpdateResponse updateComment(SiteUser siteUser, Long postId, Long commentId, CommentUpdateRequest commentUpdateRequest) { Post post = postRepository.getById(postId); Comment comment = commentRepository.getById(commentId); validateDeprecated(comment); - validateOwnership(comment, email); + validateOwnership(comment, siteUser); comment.updateContent(commentUpdateRequest.content()); @@ -88,11 +82,10 @@ private void validateDeprecated(Comment comment) { } @Transactional - public CommentDeleteResponse deleteCommentById(String email, Long postId, Long commentId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public CommentDeleteResponse deleteCommentById(SiteUser siteUser, Long postId, Long commentId) { Post post = postRepository.getById(postId); Comment comment = commentRepository.getById(commentId); - validateOwnership(comment, email); + validateOwnership(comment, siteUser); if (comment.getParentComment() != null) { // 대댓글인 경우 @@ -119,8 +112,8 @@ public CommentDeleteResponse deleteCommentById(String email, Long postId, Long c return new CommentDeleteResponse(commentId); } - private void validateOwnership(Comment comment, String email) { - if (!comment.getSiteUser().getEmail().equals(email)) { + private void validateOwnership(Comment comment, SiteUser siteUser) { + if (!comment.getSiteUser().getId().equals(siteUser.getId())) { throw new CustomException(INVALID_POST_ACCESS); } } diff --git a/src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java b/src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java new file mode 100644 index 000000000..785283d7d --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/security/AuthenticationManagerConfig.java @@ -0,0 +1,25 @@ +package com.example.solidconnection.config.security; + +import com.example.solidconnection.custom.security.provider.ExpiredTokenAuthenticationProvider; +import com.example.solidconnection.custom.security.provider.SiteUserAuthenticationProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; + +@RequiredArgsConstructor +@Configuration +public class AuthenticationManagerConfig { + + private final SiteUserAuthenticationProvider siteUserAuthenticationProvider; + private final ExpiredTokenAuthenticationProvider expiredTokenAuthenticationProvider; + + @Bean + public AuthenticationManager authenticationManager() { + return new ProviderManager( + siteUserAuthenticationProvider, + expiredTokenAuthenticationProvider + ); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java deleted file mode 100644 index 84692709a..000000000 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthentication.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.solidconnection.config.security; - -import org.springframework.security.authentication.AbstractAuthenticationToken; -import org.springframework.security.core.GrantedAuthority; - -import java.util.Collection; - -public class JwtAuthentication extends AbstractAuthenticationToken { - - private final String token; - private final Object principal; - - public JwtAuthentication(Object principal, String token, Collection authorities) { - super(authorities); - this.token = token; - this.principal = principal; - setAuthenticated(true); - } - - @Override - public Object getCredentials() { - return this.token; - } - - @Override - public Object getPrincipal() { - return this.principal; - } -} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java deleted file mode 100644 index 5c7ab9f97..000000000 --- a/src/main/java/com/example/solidconnection/config/security/JwtAuthenticationFilter.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.solidconnection.config.security; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; - -@Component -@RequiredArgsConstructor -public class JwtAuthenticationFilter extends OncePerRequestFilter { - - private static final String REISSUE_URI = "/auth/reissue"; - private static final String REISSUE_METHOD = "post"; - - private final JwtProperties jwtProperties; - - @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, - @NonNull HttpServletResponse response, - @NonNull FilterChain filterChain) throws ServletException, IOException { - String token = parseTokenFromRequest(request); - if (token == null || isReissueRequest(request)) { - filterChain.doFilter(request, response); - return; - } - - String subject = parseSubject(token, jwtProperties.secret()); - UserDetails userDetails = new JwtUserDetails(subject); - Authentication auth = new JwtAuthentication(userDetails, token, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(auth); - filterChain.doFilter(request, response); - } - - private boolean isReissueRequest(HttpServletRequest request) { - return REISSUE_URI.equals(request.getRequestURI()) && REISSUE_METHOD.equals(request.getMethod()); - } -} diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 3f6307f8f..6851b3e8c 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -1,5 +1,8 @@ package com.example.solidconnection.config.security; +import com.example.solidconnection.custom.security.filter.ExceptionHandlerFilter; +import com.example.solidconnection.custom.security.filter.JwtAuthenticationFilter; +import com.example.solidconnection.custom.security.filter.SignOutCheckFilter; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java b/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java new file mode 100644 index 000000000..10e468f56 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/web/WebMvcConfig.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.config.web; + + +import com.example.solidconnection.custom.resolver.AuthorizedUserResolver; +import com.example.solidconnection.custom.resolver.ExpiredTokenResolver; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.List; + +@Configuration +@RequiredArgsConstructor +public class WebMvcConfig implements WebMvcConfigurer { + + private final AuthorizedUserResolver authorizedUserResolver; + private final ExpiredTokenResolver expiredTokenResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.addAll(List.of( + authorizedUserResolver, + expiredTokenResolver + )); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java new file mode 100644 index 000000000..b14d80994 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUser.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.custom.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthorizedUser { +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java new file mode 100644 index 000000000..93707b007 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolver.java @@ -0,0 +1,38 @@ +package com.example.solidconnection.custom.resolver; + +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class AuthorizedUserResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthorizedUser.class) + && parameter.getParameterType().equals(SiteUser.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + try { + SiteUserDetails principal = (SiteUserDetails) SecurityContextHolder.getContext() + .getAuthentication() + .getPrincipal(); + return principal.getSiteUser(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java new file mode 100644 index 000000000..61abff98c --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredToken.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.custom.resolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ExpiredToken { +} diff --git a/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java new file mode 100644 index 000000000..691136438 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolver.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.custom.resolver; + +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class ExpiredTokenResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(ExpiredToken.class) + && parameter.getParameterType().equals(ExpiredTokenAuthentication.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) throws Exception { + try { + return SecurityContextHolder.getContext().getAuthentication(); + } catch (Exception e) { + return null; + } + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java new file mode 100644 index 000000000..811ea6a1b --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthentication.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.custom.security.authentication; + +public class ExpiredTokenAuthentication extends JwtAuthentication { + + public ExpiredTokenAuthentication(String token) { + super(token, null); + setAuthenticated(false); + } + + public ExpiredTokenAuthentication(String token, String subject) { + super(token, subject); + setAuthenticated(false); + } + + public String getSubject() { + return (String) getPrincipal(); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java new file mode 100644 index 000000000..ba195caff --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.custom.security.authentication; + +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public abstract class JwtAuthentication extends AbstractAuthenticationToken { + + private final String credentials; + + private final Object principal; + + public JwtAuthentication(String token, Object principal) { + super(null); + this.credentials = token; + this.principal = principal; + } + + @Override + public Object getCredentials() { + return this.credentials; + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + public final String getToken() { + return (String) getCredentials(); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java new file mode 100644 index 000000000..3387cee55 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthentication.java @@ -0,0 +1,16 @@ +package com.example.solidconnection.custom.security.authentication; + +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; + +public class SiteUserAuthentication extends JwtAuthentication { + + public SiteUserAuthentication(String token) { + super(token, null); + setAuthenticated(false); + } + + public SiteUserAuthentication(String token, SiteUserDetails principal) { + super(token, principal); + setAuthenticated(true); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java similarity index 97% rename from src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java rename to src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java index 59022c198..8d09bfada 100644 --- a/src/main/java/com/example/solidconnection/config/security/ExceptionHandlerFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.response.ErrorResponse; diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 000000000..3f5bce556 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,55 @@ +package com.example.solidconnection.custom.security.filter; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.custom.security.authentication.JwtAuthentication; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +import static com.example.solidconnection.util.JwtUtils.isExpired; +import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; + + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtProperties jwtProperties; + private final AuthenticationManager authenticationManager; + + @Override + public void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + String token = parseTokenFromRequest(request); + if (token == null) { + filterChain.doFilter(request, response); + return; + } + + JwtAuthentication authToken = createAuthentication(token); + Authentication auth = authenticationManager.authenticate(authToken); + SecurityContextHolder.getContext().setAuthentication(auth); + + filterChain.doFilter(request, response); + } + + private JwtAuthentication createAuthentication(String token) { + if (isExpired(token, jwtProperties.secret())) { + return new ExpiredTokenAuthentication(token); + } + return new SiteUserAuthentication(token); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java similarity index 92% rename from src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java rename to src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java index c71252f1f..90fb6866e 100644 --- a/src/main/java/com/example/solidconnection/config/security/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java @@ -1,5 +1,6 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; +import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java new file mode 100644 index 000000000..d7461a0e6 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProvider.java @@ -0,0 +1,34 @@ +package com.example.solidconnection.custom.security.provider; + + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.custom.security.authentication.JwtAuthentication; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; + +@Component +@RequiredArgsConstructor +public class ExpiredTokenAuthenticationProvider implements AuthenticationProvider { + + private final JwtProperties jwtProperties; + + @Override + public Authentication authenticate(Authentication auth) throws AuthenticationException { + JwtAuthentication jwtAuth = (JwtAuthentication) auth; + String token = jwtAuth.getToken(); + String subject = parseSubjectIgnoringExpiration(token, jwtProperties.secret()); + + return new ExpiredTokenAuthentication(token, subject); + } + + @Override + public boolean supports(Class authentication) { + return ExpiredTokenAuthentication.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java b/src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java new file mode 100644 index 000000000..25f211710 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProvider.java @@ -0,0 +1,37 @@ +package com.example.solidconnection.custom.security.provider; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetailsService; +import com.example.solidconnection.custom.security.authentication.JwtAuthentication; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.util.JwtUtils.parseSubject; + +@Component +@RequiredArgsConstructor +public class SiteUserAuthenticationProvider implements AuthenticationProvider { + + private final JwtProperties jwtProperties; + private final SiteUserDetailsService siteUserDetailsService; + + @Override + public Authentication authenticate(Authentication auth) throws AuthenticationException { + JwtAuthentication jwtAuth = (JwtAuthentication) auth; + String token = jwtAuth.getToken(); + + String username = parseSubject(token, jwtProperties.secret()); + SiteUserDetails userDetails = (SiteUserDetails) siteUserDetailsService.loadUserByUsername(username); + return new SiteUserAuthentication(token, userDetails); + } + + @Override + public boolean supports(Class authentication) { + return SiteUserAuthentication.class.isAssignableFrom(authentication); + } +} diff --git a/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java similarity index 63% rename from src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java rename to src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java index b3bbda5fa..36a0b815a 100644 --- a/src/main/java/com/example/solidconnection/config/security/JwtUserDetails.java +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java @@ -1,16 +1,23 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.userdetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import lombok.Getter; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection; -public class JwtUserDetails implements UserDetails { +public class SiteUserDetails implements UserDetails { + // userDetails 에서 userName 은 사용자 식별자를 의미함 private final String userName; - public JwtUserDetails(String userName) { - this.userName = userName; + @Getter + private final SiteUser siteUser; + + public SiteUserDetails(SiteUser siteUser) { + this.siteUser = siteUser; + this.userName = String.valueOf(siteUser.getId()); } @Override diff --git a/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java new file mode 100644 index 000000000..fd23fa899 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsService.java @@ -0,0 +1,48 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; + +@Service +@RequiredArgsConstructor +public class SiteUserDetailsService implements UserDetailsService { + + private final SiteUserRepository siteUserRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + long siteUserId = getSiteUserId(username); + SiteUser siteUser = getSiteUser(siteUserId); + validateNotQuit(siteUser); + + return new SiteUserDetails(siteUser); + } + + private long getSiteUserId(String username) { + try { + return Long.parseLong(username); + } catch (NumberFormatException e) { + throw new CustomException(INVALID_TOKEN, "인증 정보가 지정된 형식과 일치하지 않습니다."); + } + } + + private SiteUser getSiteUser(long siteUserId) { + return siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(AUTHENTICATION_FAILED, "인증 정보에 해당하는 사용자를 찾을 수 없습니다.")); + } + + private void validateNotQuit(SiteUser siteUser) { + if (siteUser.getQuitedAt() != null) { + throw new CustomException(AUTHENTICATION_FAILED, "탈퇴한 사용자입니다."); + } + } +} diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/post/controller/PostController.java index c3ff3ce3a..bc3f9d123 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/post/controller/PostController.java @@ -1,5 +1,6 @@ package com.example.solidconnection.post.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.post.dto.PostCreateRequest; import com.example.solidconnection.post.dto.PostCreateResponse; import com.example.solidconnection.post.dto.PostDeleteResponse; @@ -11,6 +12,7 @@ import com.example.solidconnection.post.service.PostCommandService; import com.example.solidconnection.post.service.PostLikeService; import com.example.solidconnection.post.service.PostQueryService; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -25,7 +27,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; import java.util.Collections; import java.util.List; @@ -40,75 +41,72 @@ public class PostController { @PostMapping(value = "/{code}/posts") public ResponseEntity createPost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @Valid @RequestPart("postCreateRequest") PostCreateRequest postCreateRequest, - @RequestParam(value = "file", required = false) List imageFile) { - + @RequestParam(value = "file", required = false) List imageFile + ) { if (imageFile == null) { imageFile = Collections.emptyList(); } - PostCreateResponse post = postCommandService - .createPost(principal.getName(), code, postCreateRequest, imageFile); + PostCreateResponse post = postCommandService.createPost(siteUser, code, postCreateRequest, imageFile); return ResponseEntity.ok().body(post); } @PatchMapping(value = "/{code}/posts/{post_id}") public ResponseEntity updatePost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @PathVariable("post_id") Long postId, @Valid @RequestPart("postUpdateRequest") PostUpdateRequest postUpdateRequest, - @RequestParam(value = "file", required = false) List imageFile) { - + @RequestParam(value = "file", required = false) List imageFile + ) { if (imageFile == null) { imageFile = Collections.emptyList(); } - PostUpdateResponse postUpdateResponse = postCommandService - .updatePost(principal.getName(), code, postId, postUpdateRequest, imageFile); + PostUpdateResponse postUpdateResponse = postCommandService.updatePost( + siteUser, code, postId, postUpdateRequest, imageFile + ); return ResponseEntity.ok().body(postUpdateResponse); } @GetMapping("/{code}/posts/{post_id}") public ResponseEntity findPostById( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, - @PathVariable("post_id") Long postId) { - - PostFindResponse postFindResponse = postQueryService - .findPostById(principal.getName(), code, postId); + @PathVariable("post_id") Long postId + ) { + PostFindResponse postFindResponse = postQueryService.findPostById(siteUser, code, postId); return ResponseEntity.ok().body(postFindResponse); } @DeleteMapping(value = "/{code}/posts/{post_id}") public ResponseEntity deletePostById( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, - @PathVariable("post_id") Long postId) { - - PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(principal.getName(), code, postId); + @PathVariable("post_id") Long postId + ) { + PostDeleteResponse postDeleteResponse = postCommandService.deletePostById(siteUser, code, postId); return ResponseEntity.ok().body(postDeleteResponse); } @PostMapping(value = "/{code}/posts/{post_id}/like") public ResponseEntity likePost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - - PostLikeResponse postLikeResponse = postLikeService.likePost(principal.getName(), code, postId); + PostLikeResponse postLikeResponse = postLikeService.likePost(siteUser, code, postId); return ResponseEntity.ok().body(postLikeResponse); } @DeleteMapping(value = "/{code}/posts/{post_id}/like") public ResponseEntity dislikePost( - Principal principal, + @AuthorizedUser SiteUser siteUser, @PathVariable("code") String code, @PathVariable("post_id") Long postId ) { - - PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(principal.getName(), code, postId); + PostDislikeResponse postDislikeResponse = postLikeService.dislikePost(siteUser, code, postId); return ResponseEntity.ok().body(postDislikeResponse); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java index 7b0c4f937..74eb86310 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostCommandService.java @@ -15,7 +15,6 @@ import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.ImgType; import com.example.solidconnection.type.PostCategory; @@ -39,14 +38,13 @@ public class PostCommandService { private final PostRepository postRepository; - private final SiteUserRepository siteUserRepository; private final BoardRepository boardRepository; private final S3Service s3Service; private final RedisService redisService; private final RedisUtils redisUtils; @Transactional - public PostCreateResponse createPost(String email, String code, PostCreateRequest postCreateRequest, + public PostCreateResponse createPost(SiteUser siteUser, String code, PostCreateRequest postCreateRequest, List imageFile) { // 유효성 검증 String boardCode = validateCode(code); @@ -54,7 +52,6 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques validateFileSize(imageFile); // 객체 생성 - SiteUser siteUser = siteUserRepository.getByEmail(email); Board board = boardRepository.getByCode(boardCode); Post post = postCreateRequest.toEntity(siteUser, board); // 이미지 처리 @@ -65,12 +62,12 @@ public PostCreateResponse createPost(String email, String code, PostCreateReques } @Transactional - public PostUpdateResponse updatePost(String email, String code, Long postId, PostUpdateRequest postUpdateRequest, + public PostUpdateResponse updatePost(SiteUser siteUser, String code, Long postId, PostUpdateRequest postUpdateRequest, List imageFile) { // 유효성 검증 String boardCode = validateCode(code); Post post = postRepository.getById(postId); - validateOwnership(post, email); + validateOwnership(post, siteUser); validateQuestion(post); validateFileSize(imageFile); @@ -96,10 +93,10 @@ private void savePostImages(List imageFile, Post post) { } @Transactional - public PostDeleteResponse deletePostById(String email, String code, Long postId) { + public PostDeleteResponse deletePostById(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getById(postId); - validateOwnership(post, email); + validateOwnership(post, siteUser); validateQuestion(post); removePostImages(post); @@ -119,8 +116,8 @@ private String validateCode(String code) { } } - private void validateOwnership(Post post, String email) { - if (!post.getSiteUser().getEmail().equals(email)) { + private void validateOwnership(Post post, SiteUser siteUser) { + if (!post.getSiteUser().getId().equals(siteUser.getId())) { throw new CustomException(INVALID_POST_ACCESS); } } diff --git a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java index 8a72d5f9f..5aaf994c7 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostLikeService.java @@ -8,7 +8,6 @@ import com.example.solidconnection.post.repository.PostLikeRepository; import com.example.solidconnection.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -23,14 +22,12 @@ public class PostLikeService { private final PostRepository postRepository; - private final SiteUserRepository siteUserRepository; private final PostLikeRepository postLikeRepository; @Transactional(isolation = Isolation.READ_COMMITTED) - public PostLikeResponse likePost(String email, String code, Long postId) { + public PostLikeResponse likePost(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); validateDuplicatePostLike(post, siteUser); PostLike postLike = new PostLike(); @@ -42,10 +39,9 @@ public PostLikeResponse likePost(String email, String code, Long postId) { } @Transactional(isolation = Isolation.READ_COMMITTED) - public PostDislikeResponse dislikePost(String email, String code, Long postId) { + public PostDislikeResponse dislikePost(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getById(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); PostLike postLike = postLikeRepository.getByPostAndSiteUser(post, siteUser); postLike.resetPostAndSiteUser(); diff --git a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java index d53470124..a45ca3968 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/post/service/PostQueryService.java @@ -12,7 +12,6 @@ import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; @@ -28,28 +27,26 @@ public class PostQueryService { private final PostRepository postRepository; - private final SiteUserRepository siteUserRepository; private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; private final PostLikeRepository postLikeRepository; @Transactional(readOnly = true) - public PostFindResponse findPostById(String email, String code, Long postId) { + public PostFindResponse findPostById(SiteUser siteUser, String code, Long postId) { String boardCode = validateCode(code); Post post = postRepository.getByIdUsingEntityGraph(postId); - SiteUser siteUser = siteUserRepository.getByEmail(email); - Boolean isOwner = getIsOwner(post, email); + Boolean isOwner = getIsOwner(post, siteUser); Boolean isLiked = getIsLiked(post, siteUser); PostFindBoardResponse boardPostFindResultDTO = PostFindBoardResponse.from(post.getBoard()); PostFindSiteUserResponse siteUserPostFindResultDTO = PostFindSiteUserResponse.from(post.getSiteUser()); List postImageFindResultDTOList = PostFindPostImageResponse.from(post.getPostImageList()); - List commentFindResultDTOList = commentService.findCommentsByPostId(email, postId); + List commentFindResultDTOList = commentService.findCommentsByPostId(siteUser, postId); // caching && 어뷰징 방지 - if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(email, postId))) { + if (redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), postId))) { redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(postId)); } @@ -65,8 +62,8 @@ private String validateCode(String code) { } } - private Boolean getIsOwner(Post post, String email) { - return post.getSiteUser().getEmail().equals(email); + private Boolean getIsOwner(Post post, SiteUser siteUser) { + return post.getSiteUser().getId().equals(siteUser.getId()); } private Boolean getIsLiked(Post post, SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/s3/S3Controller.java b/src/main/java/com/example/solidconnection/s3/S3Controller.java index 0f32a4ab6..26f9160c0 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Controller.java +++ b/src/main/java/com/example/solidconnection/s3/S3Controller.java @@ -1,5 +1,7 @@ package com.example.solidconnection.s3; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.ImgType; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -11,8 +13,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/file") @RestController @@ -34,29 +34,34 @@ public class S3Controller { @PostMapping("/profile/pre") public ResponseEntity uploadPreProfileImage( - @RequestParam("file") MultipartFile imageFile) { + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/profile/post") public ResponseEntity uploadPostProfileImage( - @RequestParam("file") MultipartFile imageFile, Principal principal) { + @AuthorizedUser SiteUser siteUser, + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.PROFILE); - s3Service.deleteExProfile(principal.getName()); + s3Service.deleteExProfile(siteUser); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/gpa") public ResponseEntity uploadGpaImage( - @RequestParam("file") MultipartFile imageFile) { + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.GPA); return ResponseEntity.ok(profileImageUrl); } @PostMapping("/language-test") public ResponseEntity uploadLanguageImage( - @RequestParam("file") MultipartFile imageFile) { + @RequestParam("file") MultipartFile imageFile + ) { UploadedFileUrlResponse profileImageUrl = s3Service.uploadFile(imageFile, ImgType.LANGUAGE_TEST); return ResponseEntity.ok(profileImageUrl); } diff --git a/src/main/java/com/example/solidconnection/s3/S3Service.java b/src/main/java/com/example/solidconnection/s3/S3Service.java index 049be9fa3..2f3c633dd 100644 --- a/src/main/java/com/example/solidconnection/s3/S3Service.java +++ b/src/main/java/com/example/solidconnection/s3/S3Service.java @@ -109,8 +109,8 @@ private String getFileExtension(String fileName) { * - 기존 파일의 key(S3파일명)를 찾는다. * - S3에서 파일을 삭제한다. * */ - public void deleteExProfile(String email) { - String key = getExProfileImageUrl(email); + public void deleteExProfile(SiteUser siteUser) { + String key = siteUser.getProfileImageUrl(); deleteFile(key); } @@ -129,9 +129,4 @@ private void deleteFile(String fileName) { throw new CustomException(S3_CLIENT_EXCEPTION); } } - - private String getExProfileImageUrl(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - return siteUser.getProfileImageUrl(); - } } diff --git a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java index 42ee7b009..6c54ab5fe 100644 --- a/src/main/java/com/example/solidconnection/score/controller/ScoreController.java +++ b/src/main/java/com/example/solidconnection/score/controller/ScoreController.java @@ -1,10 +1,12 @@ package com.example.solidconnection.score.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.score.dto.GpaScoreRequest; import com.example.solidconnection.score.dto.GpaScoreStatusResponse; import com.example.solidconnection.score.dto.LanguageTestScoreRequest; import com.example.solidconnection.score.dto.LanguageTestScoreStatusResponse; import com.example.solidconnection.score.service.ScoreService; +import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -14,8 +16,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; - @RestController @RequestMapping("/score") @RequiredArgsConstructor @@ -26,32 +26,38 @@ public class ScoreController { // 학점을 등록하는 api @PostMapping("/gpa") public ResponseEntity submitGpaScore( - Principal principal, - @Valid @RequestBody GpaScoreRequest gpaScoreRequest) { - Long id = scoreService.submitGpaScore(principal.getName(), gpaScoreRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody GpaScoreRequest gpaScoreRequest + ) { + Long id = scoreService.submitGpaScore(siteUser, gpaScoreRequest); return ResponseEntity.ok(id); } // 어학성적을 등록하는 api @PostMapping("/languageTest") public ResponseEntity submitLanguageTestScore( - Principal principal, - @Valid @RequestBody LanguageTestScoreRequest languageTestScoreRequest) { - Long id = scoreService.submitLanguageTestScore(principal.getName(), languageTestScoreRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody LanguageTestScoreRequest languageTestScoreRequest + ) { + Long id = scoreService.submitLanguageTestScore(siteUser, languageTestScoreRequest); return ResponseEntity.ok(id); } // 학점 상태를 확인하는 api @GetMapping("/gpa") - public ResponseEntity getGpaScoreStatus(Principal principal) { - GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(principal.getName()); + public ResponseEntity getGpaScoreStatus( + @AuthorizedUser SiteUser siteUser + ) { + GpaScoreStatusResponse gpaScoreStatus = scoreService.getGpaScoreStatus(siteUser); return ResponseEntity.ok(gpaScoreStatus); } // 어학 성적 상태를 확인하는 api @GetMapping("/languageTest") - public ResponseEntity getLanguageTestScoreStatus(Principal principal) { - LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(principal.getName()); + public ResponseEntity getLanguageTestScoreStatus( + @AuthorizedUser SiteUser siteUser + ) { + LanguageTestScoreStatusResponse languageTestScoreStatus = scoreService.getLanguageTestScoreStatus(siteUser); return ResponseEntity.ok(languageTestScoreStatus); } } diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index d09038fa5..e6c9d5c6e 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -12,7 +12,6 @@ import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -28,12 +27,9 @@ public class ScoreService { private final GpaScoreRepository gpaScoreRepository; private final LanguageTestScoreRepository languageTestScoreRepository; - private final SiteUserRepository siteUserRepository; @Transactional - public Long submitGpaScore(String email, GpaScoreRequest gpaScoreRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public Long submitGpaScore(SiteUser siteUser, GpaScoreRequest gpaScoreRequest) { GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); newGpaScore.setSiteUser(siteUser); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); // 저장 후 반환된 객체 @@ -41,8 +37,7 @@ public Long submitGpaScore(String email, GpaScoreRequest gpaScoreRequest) { } @Transactional - public Long submitLanguageTestScore(String email, LanguageTestScoreRequest languageTestScoreRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public Long submitLanguageTestScore(SiteUser siteUser, LanguageTestScoreRequest languageTestScoreRequest) { LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); LanguageTestScore newScore = new LanguageTestScore( @@ -53,8 +48,7 @@ public Long submitLanguageTestScore(String email, LanguageTestScoreRequest langu } @Transactional(readOnly = true) - public GpaScoreStatusResponse getGpaScoreStatus(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public GpaScoreStatusResponse getGpaScoreStatus(SiteUser siteUser) { List gpaScoreStatusList = Optional.ofNullable(siteUser.getGpaScoreList()) .map(scores -> scores.stream() @@ -65,8 +59,7 @@ public GpaScoreStatusResponse getGpaScoreStatus(String email) { } @Transactional(readOnly = true) - public LanguageTestScoreStatusResponse getLanguageTestScoreStatus(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public LanguageTestScoreStatusResponse getLanguageTestScoreStatus(SiteUser siteUser) { List languageTestScoreStatusList = Optional.ofNullable(siteUser.getLanguageTestScoreList()) .map(scores -> scores.stream() diff --git a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java index c0d58356f..11c154243 100644 --- a/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java +++ b/src/main/java/com/example/solidconnection/siteuser/controller/SiteUserController.java @@ -1,5 +1,7 @@ package com.example.solidconnection.siteuser.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; import com.example.solidconnection.siteuser.dto.NicknameUpdateRequest; @@ -17,8 +19,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; -import java.security.Principal; - @RequiredArgsConstructor @RequestMapping("/my-page") @RestController @@ -27,32 +27,36 @@ class SiteUserController { private final SiteUserService siteUserService; @GetMapping - public ResponseEntity getMyPageInfo(Principal principal) { - MyPageResponse myPageResponse = siteUserService.getMyPageInfo(principal.getName()); - return ResponseEntity - .ok(myPageResponse); + public ResponseEntity getMyPageInfo( + @AuthorizedUser SiteUser siteUser + ) { + MyPageResponse myPageResponse = siteUserService.getMyPageInfo(siteUser); + return ResponseEntity.ok(myPageResponse); } @GetMapping("/update") - public ResponseEntity getMyPageInfoToUpdate(Principal principal) { - MyPageUpdateResponse myPageUpdateDto = siteUserService.getMyPageInfoToUpdate(principal.getName()); - return ResponseEntity - .ok(myPageUpdateDto); + public ResponseEntity getMyPageInfoToUpdate( + @AuthorizedUser SiteUser siteUser + ) { + MyPageUpdateResponse myPageUpdateDto = siteUserService.getMyPageInfoToUpdate(siteUser); + return ResponseEntity.ok(myPageUpdateDto); } @PatchMapping("/update/profileImage") public ResponseEntity updateProfileImage( - Principal principal, - @RequestParam(value = "file", required = false) MultipartFile imageFile) { - ProfileImageUpdateResponse profileImageUpdateResponse = siteUserService.updateProfileImage(principal.getName(), imageFile); + @AuthorizedUser SiteUser siteUser, + @RequestParam(value = "file", required = false) MultipartFile imageFile + ) { + ProfileImageUpdateResponse profileImageUpdateResponse = siteUserService.updateProfileImage(siteUser, imageFile); return ResponseEntity.ok().body(profileImageUpdateResponse); } @PatchMapping("/update/nickname") public ResponseEntity updateNickname( - Principal principal, - @Valid @RequestBody NicknameUpdateRequest nicknameUpdateRequest) { - NicknameUpdateResponse nicknameUpdateResponse = siteUserService.updateNickname(principal.getName(), nicknameUpdateRequest); + @AuthorizedUser SiteUser siteUser, + @Valid @RequestBody NicknameUpdateRequest nicknameUpdateRequest + ) { + NicknameUpdateResponse nicknameUpdateResponse = siteUserService.updateNickname(siteUser, nicknameUpdateRequest); return ResponseEntity.ok().body(nicknameUpdateResponse); } } diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java index c3793eb06..d15949723 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/LikedUniversityRepository.java @@ -10,9 +10,9 @@ public interface LikedUniversityRepository extends JpaRepository { - List findAllBySiteUser_Email(String email); + List findAllBySiteUser_Id(long siteUserId); - int countBySiteUser_Email(String email); + int countBySiteUser_Id(long siteUserId); Optional findBySiteUserAndUniversityInfoForApply(SiteUser siteUser, UniversityInfoForApply universityInfoForApply); } diff --git a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java index 6b77252c5..e0617f046 100644 --- a/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java +++ b/src/main/java/com/example/solidconnection/siteuser/repository/SiteUserRepository.java @@ -1,6 +1,6 @@ package com.example.solidconnection.siteuser.repository; -import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; @@ -11,22 +11,15 @@ import java.util.List; import java.util.Optional; -import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; - @Repository public interface SiteUserRepository extends JpaRepository { - Optional findByEmail(String email); + Optional findByEmailAndAuthType(String email, AuthType authType); - boolean existsByEmail(String email); + boolean existsByEmailAndAuthType(String email, AuthType authType); boolean existsByNickname(String nickname); @Query("SELECT u FROM SiteUser u WHERE u.quitedAt <= :cutoffDate") List findUsersToBeRemoved(@Param("cutoffDate") LocalDate cutoffDate); - - default SiteUser getByEmail(String email) { - return findByEmail(email) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - } } diff --git a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java index a7a2e5d71..c181c2809 100644 --- a/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java +++ b/src/main/java/com/example/solidconnection/siteuser/service/SiteUserService.java @@ -42,9 +42,8 @@ public class SiteUserService { * 마이페이지 정보를 조회한다. * */ @Transactional(readOnly = true) - public MyPageResponse getMyPageInfo(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - int likedUniversityCount = likedUniversityRepository.countBySiteUser_Email(email); + public MyPageResponse getMyPageInfo(SiteUser siteUser) { + int likedUniversityCount = likedUniversityRepository.countBySiteUser_Id(siteUser.getId()); return MyPageResponse.of(siteUser, likedUniversityCount); } @@ -52,8 +51,7 @@ public MyPageResponse getMyPageInfo(String email) { * 내 정보를 수정하기 위한 마이페이지 정보를 조회한다. (닉네임, 프로필 사진) * */ @Transactional(readOnly = true) - public MyPageUpdateResponse getMyPageInfoToUpdate(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public MyPageUpdateResponse getMyPageInfoToUpdate(SiteUser siteUser) { return MyPageUpdateResponse.from(siteUser); } @@ -61,9 +59,8 @@ public MyPageUpdateResponse getMyPageInfoToUpdate(String email) { * 관심 대학교 목록을 조회한다. * */ @Transactional(readOnly = true) - public List getWishUniversity(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - List likedUniversities = likedUniversityRepository.findAllBySiteUser_Email(siteUser.getEmail()); + public List getWishUniversity(SiteUser siteUser) { + List likedUniversities = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()); return likedUniversities.stream() .map(likedUniversity -> UniversityInfoForApplyPreviewResponse.from(likedUniversity.getUniversityInfoForApply())) .toList(); @@ -73,13 +70,12 @@ public List getWishUniversity(String emai * 프로필 이미지를 수정한다. * */ @Transactional - public ProfileImageUpdateResponse updateProfileImage(String email, MultipartFile imageFile) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public ProfileImageUpdateResponse updateProfileImage(SiteUser siteUser, MultipartFile imageFile) { validateProfileImage(imageFile); // 프로필 이미지를 처음 수정하는 경우에는 deleteExProfile 수행하지 않음 if (!isDefaultProfileImage(siteUser.getProfileImageUrl())) { - s3Service.deleteExProfile(email); + s3Service.deleteExProfile(siteUser); } UploadedFileUrlResponse uploadedFileUrlResponse = s3Service.uploadFile(imageFile, ImgType.PROFILE); siteUser.setProfileImageUrl(uploadedFileUrlResponse.fileUrl()); @@ -102,9 +98,7 @@ private boolean isDefaultProfileImage(String profileImageUrl) { * 닉네임을 수정한다. * */ @Transactional - public NicknameUpdateResponse updateNickname(String email, NicknameUpdateRequest nicknameUpdateRequest) { - SiteUser siteUser = siteUserRepository.getByEmail(email); - + public NicknameUpdateResponse updateNickname(SiteUser siteUser, NicknameUpdateRequest nicknameUpdateRequest) { validateNicknameDuplicated(nicknameUpdateRequest.nickname()); validateNicknameNotChangedRecently(siteUser.getNicknameModifiedAt()); diff --git a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java index 1acfcb931..505bfe072 100644 --- a/src/main/java/com/example/solidconnection/university/controller/UniversityController.java +++ b/src/main/java/com/example/solidconnection/university/controller/UniversityController.java @@ -1,5 +1,7 @@ package com.example.solidconnection.university.controller; +import com.example.solidconnection.custom.resolver.AuthorizedUser; +import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.service.SiteUserService; import com.example.solidconnection.type.LanguageTestType; import com.example.solidconnection.university.dto.IsLikeResponse; @@ -19,7 +21,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import java.security.Principal; import java.util.List; @RequiredArgsConstructor @@ -34,42 +35,45 @@ public class UniversityController { @GetMapping("/recommends") public ResponseEntity getUniversityRecommends( - Principal principal) { - if (principal == null) { + @AuthorizedUser SiteUser siteUser + ) { + if (siteUser == null) { return ResponseEntity.ok(universityRecommendService.getGeneralRecommends()); } else { - return ResponseEntity.ok(universityRecommendService.getPersonalRecommends(principal.getName())); + return ResponseEntity.ok(universityRecommendService.getPersonalRecommends(siteUser)); } } @GetMapping("/like") - public ResponseEntity> getMyWishUniversity(Principal principal) { - List wishUniversities - = siteUserService.getWishUniversity(principal.getName()); - return ResponseEntity - .ok(wishUniversities); + public ResponseEntity> getMyWishUniversity( + @AuthorizedUser SiteUser siteUser + ) { + List wishUniversities = siteUserService.getWishUniversity(siteUser); + return ResponseEntity.ok(wishUniversities); } @GetMapping("/{universityInfoForApplyId}/like") public ResponseEntity getIsLiked( - Principal principal, - @PathVariable Long universityInfoForApplyId) { - IsLikeResponse isLiked = universityLikeService.getIsLiked(principal.getName(), universityInfoForApplyId); + @AuthorizedUser SiteUser siteUser, + @PathVariable Long universityInfoForApplyId + ) { + IsLikeResponse isLiked = universityLikeService.getIsLiked(siteUser, universityInfoForApplyId); return ResponseEntity.ok(isLiked); } @PostMapping("/{universityInfoForApplyId}/like") public ResponseEntity addWishUniversity( - Principal principal, - @PathVariable Long universityInfoForApplyId) { - LikeResultResponse likeResultResponse = universityLikeService.likeUniversity(principal.getName(), universityInfoForApplyId); - return ResponseEntity - .ok(likeResultResponse); + @AuthorizedUser SiteUser siteUser, + @PathVariable Long universityInfoForApplyId + ) { + LikeResultResponse likeResultResponse = universityLikeService.likeUniversity(siteUser, universityInfoForApplyId); + return ResponseEntity.ok(likeResultResponse); } @GetMapping("/detail/{universityInfoForApplyId}") public ResponseEntity getUniversityDetails( - @PathVariable Long universityInfoForApplyId) { + @PathVariable Long universityInfoForApplyId + ) { UniversityDetailResponse universityDetailResponse = universityQueryService.getUniversityDetail(universityInfoForApplyId); return ResponseEntity.ok(universityDetailResponse); } @@ -80,7 +84,8 @@ public ResponseEntity> searchUnivers @RequestParam(required = false, defaultValue = "") String region, @RequestParam(required = false, defaultValue = "") List keyword, @RequestParam(required = false, defaultValue = "") LanguageTestType testType, - @RequestParam(required = false, defaultValue = "") String testScore) { + @RequestParam(required = false, defaultValue = "") String testScore + ) { List universityInfoForApplyPreviewResponse = universityQueryService.searchUniversity(region, keyword, testType, testScore).universityInfoForApplyPreviewResponses(); return ResponseEntity.ok(universityInfoForApplyPreviewResponse); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java index 4b15e5b8d..d926bc516 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityLikeService.java @@ -2,7 +2,6 @@ import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.domain.LikedUniversity; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.dto.IsLikeResponse; @@ -24,7 +23,6 @@ public class UniversityLikeService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final LikedUniversityRepository likedUniversityRepository; - private final SiteUserRepository siteUserRepository; @Value("${university.term}") public String term; @@ -34,8 +32,7 @@ public class UniversityLikeService { * - 이미 좋아요가 눌러져있다면, 좋아요를 취소한다. * */ @Transactional - public LikeResultResponse likeUniversity(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public LikeResultResponse likeUniversity(SiteUser siteUser, Long universityInfoForApplyId) { UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); Optional alreadyLikedUniversity = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply); @@ -56,8 +53,7 @@ public LikeResultResponse likeUniversity(String email, Long universityInfoForApp * '좋아요'한 대학교인지 확인한다. * */ @Transactional(readOnly = true) - public IsLikeResponse getIsLiked(String email, Long universityInfoForApplyId) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public IsLikeResponse getIsLiked(SiteUser siteUser, Long universityInfoForApplyId) { UniversityInfoForApply universityInfoForApply = universityInfoForApplyRepository.getUniversityInfoForApplyById(universityInfoForApplyId); boolean isLike = likedUniversityRepository.findBySiteUserAndUniversityInfoForApply(siteUser, universityInfoForApply).isPresent(); return new IsLikeResponse(isLike); diff --git a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java index 654b08390..4d9ab6242 100644 --- a/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java +++ b/src/main/java/com/example/solidconnection/university/service/UniversityRecommendService.java @@ -2,7 +2,6 @@ import com.example.solidconnection.cache.annotation.ThunderingHerdCaching; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; import com.example.solidconnection.university.dto.UniversityRecommendsResponse; @@ -24,7 +23,6 @@ public class UniversityRecommendService { private final UniversityInfoForApplyRepository universityInfoForApplyRepository; private final GeneralUniversityRecommendService generalUniversityRecommendService; - private final SiteUserRepository siteUserRepository; @Value("${university.term}") private String term; @@ -36,8 +34,7 @@ public class UniversityRecommendService { * - 맞춤 추천 대학교의 수가 6개보다 적다면, 공통 추천 대학교 후보에서 이번 term 에 열리는 학교들을 부족한 수 만큼 불러온다. * */ @Transactional(readOnly = true) - public UniversityRecommendsResponse getPersonalRecommends(String email) { - SiteUser siteUser = siteUserRepository.getByEmail(email); + public UniversityRecommendsResponse getPersonalRecommends(SiteUser siteUser) { // 맞춤 추천 대학교를 불러온다. List personalRecommends = universityInfoForApplyRepository .findUniversityInfoForAppliesBySiteUsersInterestedCountryOrRegionAndTerm(siteUser, term); diff --git a/src/main/java/com/example/solidconnection/util/RedisUtils.java b/src/main/java/com/example/solidconnection/util/RedisUtils.java index 6c56fa73f..ed67acac0 100644 --- a/src/main/java/com/example/solidconnection/util/RedisUtils.java +++ b/src/main/java/com/example/solidconnection/util/RedisUtils.java @@ -44,8 +44,8 @@ public String getPostViewCountRedisKey(Long postId) { return VIEW_COUNT_KEY_PREFIX.getValue() + postId; } - public String getValidatePostViewCountRedisKey(String email, Long postId) { - return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + email; + public String getValidatePostViewCountRedisKey(long siteUserId, Long postId) { + return VALIDATE_VIEW_COUNT_KEY_PREFIX.getValue() + postId + ":" + siteUserId; } public Long getPostIdFromPostViewCountRedisKey(String key) { diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java index b8f5cd283..f06116ebb 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationQueryServiceTest.java @@ -26,7 +26,7 @@ class 지원자_목록_조회_테스트 { void 이번_학기_전체_지원자를_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_2.getEmail(), + 테스트유저_2, "", "" ); @@ -72,7 +72,7 @@ class 지원자_목록_조회_테스트 { void 이번_학기_특정_지역_지원자를_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_2.getEmail(), + 테스트유저_2, 영미권.getCode(), "" ); @@ -99,7 +99,7 @@ class 지원자_목록_조회_테스트 { void 이번_학기_지원자를_대학_국문_이름으로_필터링해서_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_2.getEmail(), + 테스트유저_2, null, "일본" ); @@ -124,7 +124,7 @@ class 지원자_목록_조회_테스트 { void 이전_학기_지원자는_조회되지_않는다() { // when ApplicationsResponse response = applicationQueryService.getApplicants( - 테스트유저_1.getEmail(), + 테스트유저_1, "", "" ); @@ -152,7 +152,7 @@ class 경쟁자_목록_조회_테스트 { void 이번_학기_지원한_대학의_경쟁자_목록을_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( - 테스트유저_2.getEmail() + 테스트유저_2 ); // then @@ -180,7 +180,7 @@ class 경쟁자_목록_조회_테스트 { void 이번_학기_지원한_대학_중_미선택이_있을_때_경쟁자_목록을_조회한다() { // when ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( - 테스트유저_7.getEmail() + 테스트유저_7 ); // then @@ -202,7 +202,7 @@ class 경쟁자_목록_조회_테스트 { void 이번_학기_지원한_대학이_모두_미선택일_때_경쟁자_목록을_조회한다() { //when ApplicationsResponse response = applicationQueryService.getApplicantsByUserApplications( - 테스트유저_6.getEmail() + 테스트유저_6 ); // then diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index 84f130d54..911172bfd 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -58,7 +58,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); // when - boolean result = applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + boolean result = applicationSubmissionService.apply(테스트유저_1, request); // then Application savedApplication = applicationRepository.findBySiteUserAndTerm(테스트유저_1, term).orElseThrow(); @@ -92,7 +92,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(INVALID_GPA_SCORE_STATUS.getMessage()); @@ -112,7 +112,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); @@ -132,7 +132,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); @@ -151,12 +151,12 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); for (int i = 0; i < APPLICATION_UPDATE_COUNT_LIMIT + 1; i++) { - applicationSubmissionService.apply(테스트유저_1.getEmail(), request); + applicationSubmissionService.apply(테스트유저_1, request); } // when & then assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1.getEmail(), request) + applicationSubmissionService.apply(테스트유저_1, request) ) .isInstanceOf(CustomException.class) .hasMessage(APPLY_UPDATE_LIMIT_EXCEED.getMessage()); diff --git a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java index 418a04d8c..d38463dcb 100644 --- a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java @@ -56,7 +56,7 @@ class 댓글_조회_테스트 { // when List responses = commentService.findCommentsByPostId( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId() ); @@ -114,7 +114,7 @@ class 댓글_생성_테스트 { // when CommentCreateResponse response = commentService.createComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), request ); @@ -139,7 +139,7 @@ class 댓글_생성_테스트 { // when CommentCreateResponse response = commentService.createComment( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), request ); @@ -166,7 +166,7 @@ class 댓글_생성_테스트 { // when & then assertThatThrownBy(() -> commentService.createComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), request )) @@ -184,7 +184,7 @@ class 댓글_생성_테스트 { // when & then assertThatThrownBy(() -> commentService.createComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), request )) @@ -205,7 +205,7 @@ class 댓글_수정_테스트 { // when CommentUpdateResponse response = commentService.updateComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), comment.getId(), request @@ -232,7 +232,7 @@ class 댓글_수정_테스트 { // when & then assertThatThrownBy(() -> commentService.updateComment( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), comment.getId(), request @@ -251,7 +251,7 @@ class 댓글_수정_테스트 { // when & then assertThatThrownBy(() -> commentService.updateComment( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), comment.getId(), request @@ -275,7 +275,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), comment.getId() ); @@ -300,7 +300,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_1.getEmail(), + 테스트유저_1, testPost.getId(), parentComment.getId() ); @@ -330,7 +330,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), childComment1.getId() ); @@ -361,7 +361,7 @@ class 댓글_삭제_테스트 { // when CommentDeleteResponse response = commentService.deleteCommentById( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), childComment.getId() ); @@ -383,7 +383,7 @@ class 댓글_삭제_테스트 { // when & then assertThatThrownBy(() -> commentService.deleteCommentById( - 테스트유저_2.getEmail(), + 테스트유저_2, testPost.getId(), comment.getId() )) diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index 36bd91819..544b31b4c 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -4,7 +4,6 @@ import com.example.solidconnection.board.repository.BoardRepository; import com.example.solidconnection.post.domain.Post; import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostCommandService; import com.example.solidconnection.post.service.PostLikeService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -24,6 +23,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static org.junit.jupiter.api.Assertions.assertEquals; @TestContainerSpringBootTest @@ -57,7 +57,6 @@ void setUp() { siteUserRepository.save(siteUser); post = createPost(board, siteUser); postRepository.save(post); - createSiteUsers(); } private SiteUser createSiteUser() { @@ -72,22 +71,6 @@ private SiteUser createSiteUser() { ); } - private void createSiteUsers() { - for (int i = 0; i < 1000; i++) { - - SiteUser siteUser = new SiteUser( - "email" + i, - "nickname", - "profileImageUrl", - "1999-01-01", - PreparationStatus.CONSIDERING, - Role.MENTEE, - Gender.MALE - ); - siteUserRepository.save(siteUser); - } - } - private Board createBoard() { return new Board( "FREE", "자유게시판"); @@ -117,10 +100,11 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { String email = "email" + i; + SiteUser tmpSiteUser = siteUserRepository.save(createSiteUserByEmail(email)); executorService.submit(() -> { try { - postLikeService.likePost(email, board.getCode(), post.getId()); - postLikeService.dislikePost(email, board.getCode(), post.getId()); + postLikeService.likePost(tmpSiteUser, board.getCode(), post.getId()); + postLikeService.dislikePost(tmpSiteUser, board.getCode(), post.getId()); } finally { doneSignal.countDown(); } diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index dcd423168..678e2b084 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -96,7 +96,7 @@ private Post createPost(Board board, SiteUser siteUser) { @Test public void 게시글을_조회할_때_조회수_동시성_문제를_해결한다() throws InterruptedException { - redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); @@ -126,7 +126,7 @@ private Post createPost(Board board, SiteUser siteUser) { @Test public void 게시글을_조회할_때_조회수_조작_문제를_해결한다() throws InterruptedException { - redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + redisService.deleteKey(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); ExecutorService executorService = Executors.newFixedThreadPool(THREAD_POOL_SIZE); CountDownLatch doneSignal = new CountDownLatch(THREAD_NUMS); @@ -134,7 +134,7 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); if (isFirstTime) { redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } @@ -147,7 +147,7 @@ private Post createPost(Board board, SiteUser siteUser) { for (int i = 0; i < THREAD_NUMS; i++) { executorService.submit(() -> { try { - boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getEmail(), post.getId())); + boolean isFirstTime = redisService.isPresent(redisUtils.getValidatePostViewCountRedisKey(siteUser.getId(), post.getId())); if (isFirstTime) { redisService.increaseViewCount(redisUtils.getPostViewCountRedisKey(post.getId())); } diff --git a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java index dce720610..35ab993f5 100644 --- a/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/ThunderingHerdTest.java @@ -66,9 +66,9 @@ private SiteUser createSiteUser() { executorService.submit(() -> { try { List tasks = Arrays.asList( - () -> applicationQueryService.getApplicants(siteUser.getEmail(), "", ""), - () -> applicationQueryService.getApplicants(siteUser.getEmail(), "ASIA", ""), - () -> applicationQueryService.getApplicants(siteUser.getEmail(), "", "추오") + () -> applicationQueryService.getApplicants(siteUser, "", ""), + () -> applicationQueryService.getApplicants(siteUser, "ASIA", ""), + () -> applicationQueryService.getApplicants(siteUser, "", "추오") ); Collections.shuffle(tasks); tasks.forEach(Runnable::run); diff --git a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java deleted file mode 100644 index 16e3639f1..000000000 --- a/src/test/java/com/example/solidconnection/config/security/JwtAuthenticationFilterTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.example.solidconnection.config.security; - -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.support.TestContainerSpringBootTest; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpServletResponse; -import org.springframework.security.core.context.SecurityContextHolder; - -import java.util.Date; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.mockito.BDDMockito.then; -import static org.mockito.Mockito.spy; - -@TestContainerSpringBootTest -@DisplayName("토큰 인증 필터 테스트") -class JwtAuthenticationFilterTest { - - @Autowired - private JwtAuthenticationFilter jwtAuthenticationFilter; - - @Autowired - private JwtProperties jwtProperties; - - private HttpServletRequest request; - private HttpServletResponse response; - private FilterChain filterChain; - - @BeforeEach - void setUp() { - response = new MockHttpServletResponse(); - filterChain = spy(FilterChain.class); - SecurityContextHolder.clearContext(); - } - - @Test - public void 유효한_토큰에_대한_인증_정보를_저장한다() throws Exception { - // given - String token = Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - request = createRequestWithToken(token); - - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()) - .isExactlyInstanceOf(JwtAuthentication.class); - then(filterChain).should().doFilter(request, response); - } - - @Test - public void 토큰이_없으면_다음_필터로_진행한다() throws Exception { - // given - request = new MockHttpServletRequest(); - - // when - jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); - - // then - assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); - then(filterChain).should().doFilter(request, response); - } - - @Nested - class 유효하지_않은_토큰으로_인증하면_예외를_응답한다 { - - @Test - public void 만료된_토큰으로_인증하면_예외를_응답한다() throws Exception { - // given - String token = Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() - 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - request = createRequestWithToken(token); - - // when & then - assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) - .isInstanceOf(CustomException.class) - .hasMessage(INVALID_TOKEN.getMessage()); - then(filterChain).shouldHaveNoMoreInteractions(); - } - - @Test - public void 서명하지_않은_토큰으로_인증하면_예외를_응답한다() throws Exception { - // given - String token = Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() - 1000)) - .signWith(SignatureAlgorithm.HS256, "wrongSecretKey") - .compact(); - request = createRequestWithToken(token); - - // when & then - assertThatCode(() -> jwtAuthenticationFilter.doFilterInternal(request, response, filterChain)) - .isInstanceOf(CustomException.class) - .hasMessage(INVALID_TOKEN.getMessage()); - then(filterChain).shouldHaveNoMoreInteractions(); - } - } - - private HttpServletRequest createRequestWithToken(String token) { - MockHttpServletRequest request = new MockHttpServletRequest(); - request.addHeader("Authorization", "Bearer " + token); - return request; - } -} diff --git a/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java new file mode 100644 index 000000000..763fdf101 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/resolver/AuthorizedUserResolverTest.java @@ -0,0 +1,67 @@ +package com.example.solidconnection.custom.resolver; + + +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestContainerSpringBootTest +@DisplayName("인증된 사용자 argument resolver 테스트") +class AuthorizedUserResolverTest { + + @Autowired + private AuthorizedUserResolver authorizedUserResolver; + + @Autowired + private SiteUserRepository siteUserRepository; + + @BeforeEach + void setUp() { + SecurityContextHolder.clearContext(); + } + + @Test + void security_context_에_저장된_인증된_사용자를_반환한다() throws Exception { + // given + SiteUser siteUser = siteUserRepository.save(createSiteUser()); + SiteUserDetails userDetails = new SiteUserDetails(siteUser); + SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // when + SiteUser resolveSiteUser = (SiteUser) authorizedUserResolver.resolveArgument(null, null, null, null); + + // then + assertThat(resolveSiteUser).isEqualTo(siteUser); + } + + @Test + void security_context_에_저장된_사용자가_없으면_null_을_반환한다() throws Exception { + // when, then + assertThat(authorizedUserResolver.resolveArgument(null, null, null, null)).isNull(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java b/src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java new file mode 100644 index 000000000..a0393dbc7 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/resolver/ExpiredTokenResolverTest.java @@ -0,0 +1,43 @@ +package com.example.solidconnection.custom.resolver; + +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestContainerSpringBootTest +@DisplayName("만료된 토큰 argument resolver 테스트") +class ExpiredTokenResolverTest { + + @BeforeEach + void setUp() { + SecurityContextHolder.clearContext(); + } + + @Autowired + private ExpiredTokenResolver expiredTokenResolver; + + @Test + void security_context_에_저장된_만료시간을_검증하지_않는_토큰을_반환한다() throws Exception { + // given + ExpiredTokenAuthentication authentication = new ExpiredTokenAuthentication("token"); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // when + ExpiredTokenAuthentication expiredTokenAuthentication = (ExpiredTokenAuthentication) expiredTokenResolver.resolveArgument(null, null, null, null); + + // then + assertThat(expiredTokenAuthentication.getToken()).isEqualTo("token"); + } + + @Test + void security_context_에_저장된_만료시간을_검증하지_않는_토큰이_없으면_null_을_반환한다() throws Exception { + // when, then + assertThat(expiredTokenResolver.resolveArgument(null, null, null, null)).isNull(); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java b/src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java new file mode 100644 index 000000000..9ef78d0c7 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/authentication/ExpiredTokenAuthenticationTest.java @@ -0,0 +1,64 @@ +package com.example.solidconnection.custom.security.authentication; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("만료된 토큰 인증 정보 테스트") +class ExpiredTokenAuthenticationTest { + + @Test + void 인증_정보에_저장된_토큰을_반환한다() { + // given + String token = "token123"; + ExpiredTokenAuthentication auth = new ExpiredTokenAuthentication(token); + + // when + String result = auth.getToken(); + + // then + assertThat(result).isEqualTo(token); + } + + @Test + void 인증_정보에_저장된_토큰의_subject_를_반환한다() { + // given + String subject = "subject321"; + String token = createToken(subject); + ExpiredTokenAuthentication auth = new ExpiredTokenAuthentication(token, subject); + + // when + String result = auth.getSubject(); + + // then + assertThat(result).isEqualTo(subject); + } + + @Test + void 항상_isAuthenticated_는_false_를_반환한다() { + // given + ExpiredTokenAuthentication auth1 = new ExpiredTokenAuthentication("token"); + ExpiredTokenAuthentication auth2 = new ExpiredTokenAuthentication("token", "subject"); + + // when & then + assertAll( + () -> assertThat(auth1.isAuthenticated()).isFalse(), + () -> assertThat(auth2.isAuthenticated()).isFalse() + ); + } + + private String createToken(String subject) { + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, "secret") + .compact(); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java b/src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java new file mode 100644 index 000000000..6932fcd28 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/authentication/SiteUserAuthenticationTest.java @@ -0,0 +1,73 @@ +package com.example.solidconnection.custom.security.authentication; + +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class SiteUserAuthenticationTest { + + @Test + void 인증_정보에_저장된_토큰을_반환한다() { + // given + String token = "token"; + SiteUserAuthentication authentication = new SiteUserAuthentication(token); + + // when + String result = authentication.getToken(); + + // then + assertThat(result).isEqualTo(token); + } + + @Test + void 인증_정보에_저장된_사용자를_반환한다() { + // given + SiteUserDetails userDetails = new SiteUserDetails(createSiteUser()); + SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + + // when & then + SiteUserDetails actual = (SiteUserDetails) authentication.getPrincipal(); + + // then + assertThat(actual) + .extracting("siteUser") + .extracting("id") + .isEqualTo(userDetails.getSiteUser().getId()); + } + + @Test + void 인증_전에_생성되면_isAuthenticated_는_false_를_반환한다() { + // given + SiteUserAuthentication authentication = new SiteUserAuthentication("token"); + + // when & then + assertThat(authentication.isAuthenticated()).isFalse(); + } + + @Test + void 인증_후에_생성되면_isAuthenticated_는_true_를_반환한다() { + // given + SiteUserDetails userDetails = new SiteUserDetails(createSiteUser()); + SiteUserAuthentication authentication = new SiteUserAuthentication("token", userDetails); + + // when & then + assertThat(authentication.isAuthenticated()).isTrue(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java similarity index 98% rename from src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java rename to src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java index f4e8dc666..091a75eb8 100644 --- a/src/test/java/com/example/solidconnection/config/security/ExceptionHandlerFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java new file mode 100644 index 000000000..cbca9c5f2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/filter/JwtAuthenticationFilterTest.java @@ -0,0 +1,116 @@ +package com.example.solidconnection.custom.security.filter; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetailsService; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.spy; + +@TestContainerSpringBootTest +@DisplayName("토큰 인증 필터 테스트") +class JwtAuthenticationFilterTest { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + private JwtProperties jwtProperties; + + @MockBean + private SiteUserDetailsService siteUserDetailsService; + + private HttpServletRequest request; + private HttpServletResponse response; + private FilterChain filterChain; + + @BeforeEach() + void setUp() { + response = new MockHttpServletResponse(); + filterChain = spy(FilterChain.class); + SecurityContextHolder.clearContext(); + } + + @Test + public void 토큰이_없으면_다음_필터로_진행한다() throws Exception { + // given + request = new MockHttpServletRequest(); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()).isNull(); + then(filterChain).should().doFilter(request, response); + } + + @Nested + class 토큰이_있으면_컨텍스트에_저장한다 { + + @Test + void 유효한_토큰을_컨텍스트에_저장한다() throws Exception { + // given + Date validExpiration = new Date(System.currentTimeMillis() + 1000); + String token = createTokenWithExpiration(validExpiration); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(SiteUserAuthentication.class); + then(filterChain).should().doFilter(request, response); + } + + @Test + void 만료된_토큰을_컨텍스트에_저장한다() throws Exception { + // given + Date invalidExpiration = new Date(System.currentTimeMillis() - 1000); + String token = createTokenWithExpiration(invalidExpiration); + request = createRequestWithToken(token); + + // when + jwtAuthenticationFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(SecurityContextHolder.getContext().getAuthentication()) + .isExactlyInstanceOf(ExpiredTokenAuthentication.class); + then(filterChain).should().doFilter(request, response); + } + } + + private String createTokenWithExpiration(Date expiration) { + return Jwts.builder() + .setSubject("1") + .setIssuedAt(new Date()) + .setExpiration(expiration) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private HttpServletRequest createRequestWithToken(String token) { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("Authorization", "Bearer " + token); + return request; + } +} diff --git a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java similarity index 96% rename from src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java rename to src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java index a067bf9d9..7eac22c71 100644 --- a/src/test/java/com/example/solidconnection/config/security/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java @@ -1,5 +1,6 @@ -package com.example.solidconnection.config.security; +package com.example.solidconnection.custom.security.filter; +import com.example.solidconnection.config.security.JwtProperties; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.support.TestContainerSpringBootTest; import io.jsonwebtoken.Jwts; diff --git a/src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java b/src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java new file mode 100644 index 000000000..ad6053359 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/provider/ExpiredTokenAuthenticationProviderTest.java @@ -0,0 +1,80 @@ +package com.example.solidconnection.custom.security.provider; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; + +import java.net.PasswordAuthentication; +import java.util.Date; + +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.*; + +@TestContainerSpringBootTest +@DisplayName("만료된 토큰 provider 테스트") +class ExpiredTokenAuthenticationProviderTest { + + @Autowired + private ExpiredTokenAuthenticationProvider expiredTokenAuthenticationProvider; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 처리할_수_있는_타입인지를_반환한다() { + // given + Class supportedType = ExpiredTokenAuthentication.class; + Class notSupportedType = PasswordAuthentication.class; + + // when & then + assertAll( + () -> assertTrue(expiredTokenAuthenticationProvider.supports(supportedType)), + () -> assertFalse(expiredTokenAuthenticationProvider.supports(notSupportedType)) + ); + } + + @Test + void 만료된_토큰의_인증_정보를_반환한다() { + // given + String expiredToken = createExpiredToken(); + ExpiredTokenAuthentication ExpiredTokenAuthentication = new ExpiredTokenAuthentication(expiredToken); + + // when + Authentication result = expiredTokenAuthenticationProvider.authenticate(ExpiredTokenAuthentication); + + // then + assertAll( + () -> assertThat(result).isInstanceOf(ExpiredTokenAuthentication.class), + () -> assertThat(result.isAuthenticated()).isFalse() + ); + } + + @Test + void 유효하지_않은_토큰이면_예외_응답을_반환한다() { + // given + ExpiredTokenAuthentication ExpiredTokenAuthentication = new ExpiredTokenAuthentication("invalid token"); + + // when & then + assertThatCode(() -> expiredTokenAuthenticationProvider.authenticate(ExpiredTokenAuthentication)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + private String createExpiredToken() { + return Jwts.builder() + .setSubject("1") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java b/src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java new file mode 100644 index 000000000..46d7498a2 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/provider/SiteUserAuthenticationProviderTest.java @@ -0,0 +1,159 @@ +package com.example.solidconnection.custom.security.provider; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.security.authentication.SiteUserAuthentication; +import com.example.solidconnection.custom.security.userdetails.SiteUserDetails; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; + +import java.net.PasswordAuthentication; +import java.util.Date; + +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("사용자 인증정보 provider 테스트") +class SiteUserAuthenticationProviderTest { + + @Autowired + private SiteUserAuthenticationProvider siteUserAuthenticationProvider; + + @Autowired + private JwtProperties jwtProperties; + + @Autowired + private SiteUserRepository siteUserRepository; + + private SiteUser siteUser; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + } + + @Test + void 처리할_수_있는_타입인지를_반환한다() { + // given + Class supportedType = SiteUserAuthentication.class; + Class notSupportedType = PasswordAuthentication.class; + + // when & then + assertAll( + () -> assertThat(siteUserAuthenticationProvider.supports(supportedType)).isTrue(), + () -> assertThat(siteUserAuthenticationProvider.supports(notSupportedType)).isFalse() + ); + } + + @Test + void 유효한_토큰이면_정상적으로_인증_정보를_반환한다() { + // given + String token = createValidToken(siteUser.getId()); + SiteUserAuthentication auth = new SiteUserAuthentication(token); + + // when + Authentication result = siteUserAuthenticationProvider.authenticate(auth); + + // then + assertThat(result).isNotNull(); + assertAll( + () -> assertThat(result.getCredentials()).isEqualTo(token), + () -> assertThat(result.getPrincipal().getClass()).isEqualTo(SiteUserDetails.class) + ); + } + + @Nested + class 예외_응답을_반환하다 { + + @Test + void 유효하지_않은_토큰이면_예외_응답을_반환한다() { + // given + SiteUserAuthentication expiredAuth = new SiteUserAuthentication(createExpiredToken()); + + // when & then + assertThatCode(() -> siteUserAuthenticationProvider.authenticate(expiredAuth)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + @Test + void 사용자_정보의_형식이_다르면_예외_응답을_반환한다() { + // given + SiteUserAuthentication wrongSubjectTypeAuth = new SiteUserAuthentication(createWrongSubjectTypeToken()); + + // when & then + assertThatCode(() -> siteUserAuthenticationProvider.authenticate(wrongSubjectTypeAuth)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + @Test + void 유효한_토큰이지만_해당되는_사용자가_없으면_예외_응답을_반환한다() { + // given + long notExistingUserId = siteUser.getId() + 100; + String token = createValidToken(notExistingUserId); + SiteUserAuthentication auth = new SiteUserAuthentication(token); + + // when & then + assertThatCode(() -> siteUserAuthenticationProvider.authenticate(auth)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + } + + private String createValidToken(long id) { + return Jwts.builder() + .setSubject(String.valueOf(id)) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private String createExpiredToken() { + return Jwts.builder() + .setSubject(String.valueOf(siteUser.getId())) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private String createWrongSubjectTypeToken() { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java new file mode 100644 index 000000000..731f840f3 --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java @@ -0,0 +1,104 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; + +import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("사용자 인증 정보 서비스 테스트") +@TestContainerSpringBootTest +class SiteUserDetailsServiceTest { + + @Autowired + private SiteUserDetailsService userDetailsService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Test + void 사용자_인증_정보를_반환한다() { + // given + SiteUser siteUser = siteUserRepository.save(createSiteUser()); + String username = getUserName(siteUser); + + // when + SiteUserDetails userDetails = (SiteUserDetails) userDetailsService.loadUserByUsername(username); + + // then + assertAll( + () -> assertThat(userDetails.getUsername()).isEqualTo(username), + () -> assertThat(userDetails.getSiteUser()).extracting("id").isEqualTo(siteUser.getId()) + ); + } + + @Nested + class 예외_응답을_반환한다 { + + @Test + void 지정되지_않은_형식의_식별자가_주어지면_예외_응답을_반환한다() { + // given + String username = "notNumber"; + + // when & then + assertThatCode(() -> userDetailsService.loadUserByUsername(username)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(INVALID_TOKEN.getMessage()); + } + + @Test + void 식별자에_해당하는_사용자가_없으면_예외_응답을_반환한다() { + // given + String username = "1234"; + + // when & then + assertThatCode(() -> userDetailsService.loadUserByUsername(username)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + + @Test + void 탈퇴한_사용자이면_예외_응답을_반환한다() { + // given + SiteUser siteUser = createSiteUser(); + siteUser.setQuitedAt(LocalDate.now()); + siteUserRepository.save(siteUser); + String username = getUserName(siteUser); + + // when & then + assertThatCode(() -> userDetailsService.loadUserByUsername(username)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(AUTHENTICATION_FAILED.getMessage()); + } + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } + + private String getUserName(SiteUser siteUser) { + return siteUser.getId().toString(); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 6b739248b..40f39e646 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -7,8 +7,8 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -30,13 +30,13 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { @Autowired - SiteUserRepository siteUserRepository; + private SiteUserRepository siteUserRepository; @Autowired - ApplicationRepository applicationRepository; + private ApplicationRepository applicationRepository; @Autowired - TokenProvider tokenProvider; + private TokenProvider tokenProvider; private String accessToken; private String adminAccessToken; @@ -55,24 +55,8 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { @BeforeEach public void setUpUserAndToken() { - // setUp - 회원 정보 저장 - String email = "email@email.com"; - SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); - - // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); - - adminAccessToken = tokenProvider.generateToken("email5", TokenType.ACCESS); - String adminRefreshToken = tokenProvider.generateToken("email5", TokenType.REFRESH); - tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); - - user6AccessToken = tokenProvider.generateToken("email6", TokenType.ACCESS); - String user6RefreshToken = tokenProvider.generateToken("email6", TokenType.REFRESH); - tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); - // setUp - 사용자 정보 저장 + SiteUser 나 = siteUserRepository.save(createSiteUserByEmail("my-email")); SiteUser 사용자1 = siteUserRepository.save(createSiteUserByEmail("email1")); SiteUser 사용자2 = siteUserRepository.save(createSiteUserByEmail("email2")); SiteUser 사용자3 = siteUserRepository.save(createSiteUserByEmail("email3")); @@ -80,16 +64,30 @@ public void setUpUserAndToken() { SiteUser 사용자5_관리자 = siteUserRepository.save(createSiteUserByEmail("email5")); SiteUser 사용자6 = siteUserRepository.save(createSiteUserByEmail("email6")); + // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 + accessToken = tokenProvider.generateToken(나, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(나, TokenType.REFRESH); + tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + + adminAccessToken = tokenProvider.generateToken(사용자5_관리자, TokenType.ACCESS); + String adminRefreshToken = tokenProvider.generateToken(사용자5_관리자, TokenType.REFRESH); + tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); + + user6AccessToken = tokenProvider.generateToken(사용자6, TokenType.ACCESS); + String user6RefreshToken = tokenProvider.generateToken(사용자6, TokenType.REFRESH); + tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); + // setUp - 지원 정보 저장 Gpa gpa = createDummyGpa(); LanguageTest languageTest = createDummyLanguageTest(); - 나의_지원정보 = new Application(siteUser, gpa, languageTest, term); + 나의_지원정보 = new Application(나, gpa, languageTest, term); 사용자1_지원정보 = new Application(사용자1, gpa, languageTest, term); 사용자2_지원정보 = new Application(사용자2, gpa, languageTest, term); 사용자3_지원정보 = new Application(사용자3, gpa, languageTest, term); 사용자4_이전학기_지원정보 = new Application(사용자4_이전학기_지원자, gpa, languageTest, beforeTerm); 사용자5_관리자_지원정보 = new Application(사용자5_관리자, gpa, languageTest, term); 사용자6_지원정보 = new Application(사용자6, gpa, languageTest, term); + 나의_지원정보.updateUniversityChoice(괌대학_B_지원_정보, 괌대학_A_지원_정보, 린츠_카톨릭대학_지원_정보, "0"); 사용자1_지원정보.updateUniversityChoice(괌대학_A_지원_정보, 괌대학_B_지원_정보, 그라츠공과대학_지원_정보, "1"); 사용자2_지원정보.updateUniversityChoice(메이지대학_지원_정보, 그라츠대학_지원_정보, 서던덴마크대학교_지원_정보, "2"); @@ -337,5 +335,4 @@ public void setUpUserAndToken() { assertThat(secondChoiceApplicants.size()).isEqualTo(choicedUniversityCount); assertThat(thirdChoiceApplicants.size()).isEqualTo(choicedUniversityCount); } - } diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index fb42216c9..567b1016d 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -19,21 +19,24 @@ @DisplayName("마이페이지 테스트") class MyPageTest extends BaseEndToEndTest { - private final String email = "email@email.com"; + private SiteUser siteUser; + @Autowired private SiteUserRepository siteUserRepository; + @Autowired private TokenProvider tokenProvider; + private String accessToken; @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUserRepository.save(createSiteUserByEmail(email)); + siteUser = siteUserRepository.save(createSiteUserByEmail("email")); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @@ -48,11 +51,10 @@ public void setUpUserAndToken() { .statusCode(HttpStatus.OK.value()) .extract().as(MyPageResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", - () -> assertThat(myPageResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), - () -> assertThat(myPageResponse.birth()).isEqualTo(savedSiteUser.getBirth()), - () -> assertThat(myPageResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl()), - () -> assertThat(myPageResponse.email()).isEqualTo(savedSiteUser.getEmail())); + () -> assertThat(myPageResponse.nickname()).isEqualTo(siteUser.getNickname()), + () -> assertThat(myPageResponse.birth()).isEqualTo(siteUser.getBirth()), + () -> assertThat(myPageResponse.profileImageUrl()).isEqualTo(siteUser.getProfileImageUrl()), + () -> assertThat(myPageResponse.email()).isEqualTo(siteUser.getEmail())); } } diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index 6d7f52032..025ddb7d7 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -37,17 +37,15 @@ class MyPageUpdateTest extends BaseEndToEndTest { private SiteUser siteUser; - private final String email = "email@email.com"; - @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUser = createSiteUserByEmail(email); + siteUser = createSiteUserByEmail("email"); siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @@ -62,10 +60,9 @@ public void setUpUserAndToken() { .statusCode(HttpStatus.OK.value()) .extract().as(MyPageUpdateResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); assertAll("불러온 마이 페이지 정보가 DB의 정보와 일치한다.", - () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname()), - () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(savedSiteUser.getProfileImageUrl())); + () -> assertThat(myPageUpdateResponse.nickname()).isEqualTo(siteUser.getNickname()), + () -> assertThat(myPageUpdateResponse.profileImageUrl()).isEqualTo(siteUser.getProfileImageUrl())); } @Test @@ -82,9 +79,9 @@ public void setUpUserAndToken() { .statusCode(HttpStatus.OK.value()) .extract().as(NicknameUpdateResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + SiteUser updatedSiteUser = siteUserRepository.findById(siteUser.getId()).get(); assertAll("마이 페이지 정보가 수정된다.", - () -> assertThat(nicknameUpdateResponse.nickname()).isEqualTo(savedSiteUser.getNickname())); + () -> assertThat(nicknameUpdateResponse.nickname()).isEqualTo(updatedSiteUser.getNickname())); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index efd5ad1d7..26eba657a 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -79,7 +79,7 @@ class SignInTest extends BaseEndToEndTest { .willReturn(createKakaoUserInfoDtoByEmail(email)); // setUp - 사용자 정보 저장 - siteUserRepository.save(createSiteUserByEmail(email)); + SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); // request - body 생성 및 요청 KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); @@ -95,7 +95,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -112,7 +112,7 @@ class SignInTest extends BaseEndToEndTest { SiteUser siteUserFixture = createSiteUserByEmail(email); LocalDate justBeforeRemoval = LocalDate.now().minusDays(ACCOUNT_RECOVER_DURATION - 1); siteUserFixture.setQuitedAt(justBeforeRemoval); - siteUserRepository.save(siteUserFixture); + SiteUser siteUser = siteUserRepository.save(siteUserFixture); // request - body 생성 및 요청 KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); @@ -124,12 +124,13 @@ class SignInTest extends BaseEndToEndTest { .statusCode(HttpStatus.OK.value()) .extract().as(SignInResponse.class); + SiteUser updatedSiteUser = siteUserRepository.findById(siteUser.getId()).get(); assertAll("리프레스 토큰과 엑세스 토큰을 응답하고, 탈퇴 날짜를 초기화한다.", () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull(), - () -> assertThat(siteUserRepository.getByEmail(email).getQuitedAt()).isNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) + () -> assertThat(updatedSiteUser.getQuitedAt()).isNull()); + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 07dafb539..1eb152387 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -12,6 +12,7 @@ import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.Gender; @@ -86,7 +87,7 @@ class SignUpTest extends BaseEndToEndTest { .statusCode(HttpStatus.OK.value()) .extract().as(SignUpResponse.class); - SiteUser savedSiteUser = siteUserRepository.getByEmail(email); + SiteUser savedSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO).get(); assertAll( "회원 정보를 저장한다.", () -> assertThat(savedSiteUser.getId()).isNotNull(), @@ -109,7 +110,7 @@ class SignUpTest extends BaseEndToEndTest { () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(savedSiteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index 947f44fd0..b7e112d00 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -36,8 +36,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index dccd1092f..301b373c4 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -33,8 +33,6 @@ @DisplayName("대학교 좋아요 테스트") class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { - private final String email = "email@email.com"; - @Autowired private SiteUserRepository siteUserRepository; @@ -53,12 +51,12 @@ class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUser = createSiteUserByEmail(email); + siteUser = createSiteUserByEmail("email@email.com"); siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } @@ -102,7 +100,7 @@ public void setUpUserAndToken() { .extract().as(LikeResultResponse.class); Optional likedUniversity - = likedUniversityRepository.findAllBySiteUser_Email(email).stream().findFirst(); + = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()).stream().findFirst(); assertAll("좋아요 누른 대학교를 저장하고 좋아요 성공 응답을 반환한다.", () -> assertThat(likedUniversity).isPresent(), () -> assertThat(likedUniversity.get().getId()).isEqualTo(괌대학_A_지원_정보.getId()), @@ -125,7 +123,7 @@ public void setUpUserAndToken() { .extract().as(LikeResultResponse.class); Optional likedUniversity - = likedUniversityRepository.findAllBySiteUser_Email(email).stream().findFirst(); + = likedUniversityRepository.findAllBySiteUser_Id(siteUser.getId()).stream().findFirst(); assertAll("좋아요 누른 대학교를 삭제하고, 좋아요 취소 응답을 반환한다.", () -> assertThat(likedUniversity).isEmpty(), () -> assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE) diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index 4f3bd3042..358f779cd 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -54,8 +54,8 @@ void setUp() { generalUniversityRecommendService.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 4859f9fe2..22abbfb53 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -1,12 +1,10 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; -import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; import io.restassured.RestAssured; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -21,17 +19,9 @@ @DisplayName("대학교 검색 테스트") class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { - private final String email = "email@email.com"; - @Autowired private SiteUserRepository siteUserRepository; - @Autowired - private UniversityInfoForApplyRepository universityInfoForApplyRepository; - - @Autowired - private LikedUniversityRepository likedUniversityRepository; - @Autowired private TokenProvider tokenProvider; @@ -41,12 +31,12 @@ class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { @BeforeEach public void setUpUserAndToken() { // setUp - 회원 정보 저장 - siteUser = createSiteUserByEmail(email); + siteUser = createSiteUserByEmail("email@email.com"); siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(email, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(email, TokenType.REFRESH); + accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); + String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); tokenProvider.saveToken(refreshToken, TokenType.REFRESH); } diff --git a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java index eb1b2b652..3cdc5a40c 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java @@ -78,7 +78,7 @@ class 게시글_생성_테스트 { // when PostCreateResponse response = postCommandService.createPost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), request, imageFiles @@ -108,7 +108,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(INVALID_POST_CATEGORY.getMessage()); } @@ -121,7 +121,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(INVALID_POST_CATEGORY.getMessage()); } @@ -134,7 +134,7 @@ class 게시글_생성_테스트 { // when & then assertThatThrownBy(() -> - postCommandService.createPost(테스트유저_1.getEmail(), 자유게시판.getCode(), request, imageFiles)) + postCommandService.createPost(테스트유저_1, 자유게시판.getCode(), request, imageFiles)) .isInstanceOf(CustomException.class) .hasMessage(CAN_NOT_UPLOAD_MORE_THAN_FIVE_IMAGES.getMessage()); } @@ -158,7 +158,7 @@ class 게시글_수정_테스트 { // when PostUpdateResponse response = postCommandService.updatePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId(), request, @@ -189,7 +189,7 @@ class 게시글_수정_테스트 { // when & then assertThatThrownBy(() -> postCommandService.updatePost( - 테스트유저_2.getEmail(), + 테스트유저_2, 자유게시판.getCode(), testPost.getId(), request, @@ -209,7 +209,7 @@ class 게시글_수정_테스트 { // when & then assertThatThrownBy(() -> postCommandService.updatePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId(), request, @@ -229,7 +229,7 @@ class 게시글_수정_테스트 { // when & then assertThatThrownBy(() -> postCommandService.updatePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId(), request, @@ -253,7 +253,7 @@ class 게시글_삭제_테스트 { // when PostDeleteResponse response = postCommandService.deletePostById( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); @@ -275,7 +275,7 @@ class 게시글_삭제_테스트 { // when & then assertThatThrownBy(() -> postCommandService.deletePostById( - 테스트유저_2.getEmail(), + 테스트유저_2, 자유게시판.getCode(), testPost.getId() )) @@ -291,7 +291,7 @@ class 게시글_삭제_테스트 { // when & then assertThatThrownBy(() -> postCommandService.deletePostById( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() )) diff --git a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java index 9fe6a2704..460b9a15b 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java @@ -44,7 +44,7 @@ class 게시글_좋아요_테스트 { // when PostLikeResponse response = postLikeService.likePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); @@ -63,12 +63,12 @@ class 게시글_좋아요_테스트 { void 이미_좋아요한_게시글을_다시_좋아요하면_예외_응답을_반환한다() { // given Post testPost = createPost(자유게시판, 테스트유저_1); - postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + postLikeService.likePost(테스트유저_1, 자유게시판.getCode(), testPost.getId()); // when & then assertThatThrownBy(() -> postLikeService.likePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() )) @@ -84,12 +84,12 @@ class 게시글_좋아요_취소_테스트 { void 게시글_좋아요를_성공적으로_취소한다() { // given Post testPost = createPost(자유게시판, 테스트유저_1); - PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1.getEmail(), 자유게시판.getCode(), testPost.getId()); + PostLikeResponse beforeResponse = postLikeService.likePost(테스트유저_1, 자유게시판.getCode(), testPost.getId()); long beforeLikeCount = beforeResponse.likeCount(); // when PostDislikeResponse response = postLikeService.dislikePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); @@ -112,7 +112,7 @@ class 게시글_좋아요_취소_테스트 { // when & then assertThatThrownBy(() -> postLikeService.dislikePost( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() )) diff --git a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java index 7ec36b0df..d9acf5845 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java @@ -53,12 +53,12 @@ class PostQueryServiceTest extends BaseIntegrationTest { Post testPost = createPost(자유게시판, 테스트유저_1, expectedImageUrl); List comments = createComments(testPost, 테스트유저_1, List.of("첫번째 댓글", "두번째 댓글")); - String validateKey = redisUtils.getValidatePostViewCountRedisKey(테스트유저_1.getEmail(), testPost.getId()); + String validateKey = redisUtils.getValidatePostViewCountRedisKey(테스트유저_1.getId(), testPost.getId()); String viewCountKey = redisUtils.getPostViewCountRedisKey(testPost.getId()); // when PostFindResponse response = postQueryService.findPostById( - 테스트유저_1.getEmail(), + 테스트유저_1, 자유게시판.getCode(), testPost.getId() ); diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 4a511d867..681b708a2 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -55,7 +55,7 @@ class ScoreServiceTest extends BaseIntegrationTest { ); // when - GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser); // then assertThat(response.gpaScoreStatusList()) @@ -73,7 +73,7 @@ class ScoreServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser.getEmail()); + GpaScoreStatusResponse response = scoreService.getGpaScoreStatus(testUser); // then assertThat(response.gpaScoreStatusList()).isEmpty(); @@ -87,9 +87,10 @@ class ScoreServiceTest extends BaseIntegrationTest { createLanguageTestScore(testUser, LanguageTestType.TOEIC, "100"), createLanguageTestScore(testUser, LanguageTestType.TOEFL_IBT, "7.5") ); + siteUserRepository.save(testUser); // when - LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser); // then assertThat(response.languageTestScoreStatusList()) @@ -107,7 +108,7 @@ class ScoreServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser.getEmail()); + LanguageTestScoreStatusResponse response = scoreService.getLanguageTestScoreStatus(testUser); // then assertThat(response.languageTestScoreStatusList()).isEmpty(); @@ -120,7 +121,7 @@ class ScoreServiceTest extends BaseIntegrationTest { GpaScoreRequest request = createGpaScoreRequest(); // when - long scoreId = scoreService.submitGpaScore(testUser.getEmail(), request); + long scoreId = scoreService.submitGpaScore(testUser, request); GpaScore savedScore = gpaScoreRepository.findById(scoreId).orElseThrow(); // then @@ -140,7 +141,7 @@ class ScoreServiceTest extends BaseIntegrationTest { LanguageTestScoreRequest request = createLanguageTestScoreRequest(); // when - long scoreId = scoreService.submitLanguageTestScore(testUser.getEmail(), request); + long scoreId = scoreService.submitLanguageTestScore(testUser, request); LanguageTestScore savedScore = languageTestScoreRepository.findById(scoreId).orElseThrow(); // then @@ -172,6 +173,7 @@ private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteri siteUser, LocalDate.now() ); + gpaScore.setSiteUser(siteUser); return gpaScoreRepository.save(gpaScore); } @@ -181,6 +183,7 @@ private LanguageTestScore createLanguageTestScore(SiteUser siteUser, LanguageTes LocalDate.now(), siteUser ); + languageTestScore.setSiteUser(siteUser); return languageTestScoreRepository.save(languageTestScore); } diff --git a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java index 8fdae031e..9fc6410d8 100644 --- a/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java +++ b/src/test/java/com/example/solidconnection/siteuser/service/SiteUserServiceTest.java @@ -64,7 +64,7 @@ class SiteUserServiceTest extends BaseIntegrationTest { int likedUniversityCount = createLikedUniversities(testUser); // when - MyPageResponse response = siteUserService.getMyPageInfo(testUser.getEmail()); + MyPageResponse response = siteUserService.getMyPageInfo(testUser); // then Assertions.assertAll( @@ -84,7 +84,7 @@ class SiteUserServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - MyPageUpdateResponse response = siteUserService.getMyPageInfoToUpdate(testUser.getEmail()); + MyPageUpdateResponse response = siteUserService.getMyPageInfoToUpdate(testUser); // then Assertions.assertAll( @@ -100,7 +100,7 @@ class SiteUserServiceTest extends BaseIntegrationTest { int likedUniversityCount = createLikedUniversities(testUser); // when - List response = siteUserService.getWishUniversity(testUser.getEmail()); + List response = siteUserService.getWishUniversity(testUser); // then assertThat(response) @@ -127,7 +127,7 @@ class 프로필_이미지_수정_테스트 { // when ProfileImageUpdateResponse response = siteUserService.updateProfileImage( - testUser.getEmail(), + testUser, imageFile ); @@ -144,7 +144,7 @@ class 프로필_이미지_수정_테스트 { .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when - siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + siteUserService.updateProfileImage(testUser, imageFile); // then then(s3Service).should(never()).deleteExProfile(any()); @@ -159,10 +159,10 @@ class 프로필_이미지_수정_테스트 { .willReturn(new UploadedFileUrlResponse("newProfileImageUrl")); // when - siteUserService.updateProfileImage(testUser.getEmail(), imageFile); + siteUserService.updateProfileImage(testUser, imageFile); // then - then(s3Service).should().deleteExProfile(testUser.getEmail()); + then(s3Service).should().deleteExProfile(testUser); } @Test @@ -172,7 +172,7 @@ class 프로필_이미지_수정_테스트 { MockMultipartFile emptyFile = createEmptyImageFile(); // when & then - assertThatCode(() -> siteUserService.updateProfileImage(testUser.getEmail(), emptyFile)) + assertThatCode(() -> siteUserService.updateProfileImage(testUser, emptyFile)) .isInstanceOf(CustomException.class) .hasMessage(PROFILE_IMAGE_NEEDED.getMessage()); } @@ -190,12 +190,12 @@ class 닉네임_수정_테스트 { // when NicknameUpdateResponse response = siteUserService.updateNickname( - testUser.getEmail(), + testUser, request ); // then - SiteUser updatedUser = siteUserRepository.getByEmail(testUser.getEmail()); + SiteUser updatedUser = siteUserRepository.findById(testUser.getId()).get(); assertThat(updatedUser.getNicknameModifiedAt()).isNotNull(); assertThat(response.nickname()).isEqualTo(newNickname); } @@ -208,7 +208,7 @@ class 닉네임_수정_테스트 { NicknameUpdateRequest request = new NicknameUpdateRequest("duplicatedNickname"); // when & then - assertThatCode(() -> siteUserService.updateNickname(testUser.getEmail(), request)) + assertThatCode(() -> siteUserService.updateNickname(testUser, request)) .isInstanceOf(CustomException.class) .hasMessage(NICKNAME_ALREADY_EXISTED.getMessage()); } @@ -225,7 +225,7 @@ class 닉네임_수정_테스트 { // when & then assertThatCode(() -> - siteUserService.updateNickname(testUser.getEmail(), request)) + siteUserService.updateNickname(testUser, request)) .isInstanceOf(CustomException.class) .hasMessage(createExpectedErrorMessage(modifiedAt)); } @@ -278,7 +278,7 @@ private int createLikedUniversities(SiteUser testUser) { likedUniversityRepository.save(likedUniversity1); likedUniversityRepository.save(likedUniversity2); likedUniversityRepository.save(likedUniversity3); - return likedUniversityRepository.countBySiteUser_Email(testUser.getEmail()); + return likedUniversityRepository.countBySiteUser_Id(testUser.getId()); } private MockMultipartFile createValidImageFile() { diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java index 14371486c..51958ed5d 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityLikeServiceTest.java @@ -40,8 +40,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - LikeResultResponse response = universityLikeService.likeUniversity( - testUser.getEmail(), 괌대학_A_지원_정보.getId()); + LikeResultResponse response = universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.result()).isEqualTo(LIKE_SUCCESS_MESSAGE); @@ -56,8 +55,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { saveLikedUniversity(testUser, 괌대학_A_지원_정보); // when - LikeResultResponse response = universityLikeService.likeUniversity( - testUser.getEmail(), 괌대학_A_지원_정보.getId()); + LikeResultResponse response = universityLikeService.likeUniversity(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.result()).isEqualTo(LIKE_CANCELED_MESSAGE); @@ -72,7 +70,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { Long invalidUniversityId = 9999L; // when & then - assertThatCode(() -> universityLikeService.likeUniversity(testUser.getEmail(), invalidUniversityId)) + assertThatCode(() -> universityLikeService.likeUniversity(testUser, invalidUniversityId)) .isInstanceOf(CustomException.class) .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); } @@ -84,7 +82,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { saveLikedUniversity(testUser, 괌대학_A_지원_정보); // when - IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + IsLikeResponse response = universityLikeService.getIsLiked(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.isLike()).isTrue(); @@ -96,7 +94,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { SiteUser testUser = createSiteUser(); // when - IsLikeResponse response = universityLikeService.getIsLiked(testUser.getEmail(), 괌대학_A_지원_정보.getId()); + IsLikeResponse response = universityLikeService.getIsLiked(testUser, 괌대학_A_지원_정보.getId()); // then assertThat(response.isLike()).isFalse(); @@ -109,7 +107,7 @@ class UniversityLikeServiceTest extends BaseIntegrationTest { Long invalidUniversityId = 9999L; // when & then - assertThatCode(() -> universityLikeService.getIsLiked(testUser.getEmail(), invalidUniversityId)) + assertThatCode(() -> universityLikeService.getIsLiked(testUser, invalidUniversityId)) .isInstanceOf(CustomException.class) .hasMessage(UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND.getMessage()); } diff --git a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java index 17d951614..102eb6dd6 100644 --- a/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java +++ b/src/test/java/com/example/solidconnection/university/service/UniversityRecommendServiceTest.java @@ -52,7 +52,7 @@ void setUp() { interestedRegionRepository.save(new InterestedRegion(testUser, 영미권)); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) @@ -72,7 +72,7 @@ void setUp() { interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) @@ -91,7 +91,7 @@ void setUp() { interestedCountyRepository.save(new InterestedCountry(testUser, 덴마크)); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) @@ -112,7 +112,7 @@ void setUp() { SiteUser testUser = createSiteUser(); // when - UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser.getEmail()); + UniversityRecommendsResponse response = universityRecommendService.getPersonalRecommends(testUser); // then assertThat(response.recommendedUniversities()) From ecc186a84d6d0279fdb5df00d5a4739dbe1475c7 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 6 Feb 2025 11:31:03 +0900 Subject: [PATCH 16/23] =?UTF-8?q?refactor:=20TokenProvider=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=81=20=ED=86=A0=ED=81=B0=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=BA=A1=EC=8A=90?= =?UTF-8?q?=ED=99=94=20(#183)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: TokenProvider 에서 각 토큰에 대한 로직을 캡슐화 * refactor: SignUpTokenProvider 생성, TokenProvider 추상화 * refactor: 함수 이름 변경 - 범용적으로 사용되는 지금 상황에 적합하도록 * chore: 달성한 todo 제거 * refactor: 변수명 변경, 불필요한 변수 할당 제가 --- .../auth/domain/TokenType.java | 8 +- .../auth/service/AuthService.java | 25 +-- .../auth/service/AuthTokenProvider.java | 53 +++++ .../auth/service/SignInService.java | 12 +- .../auth/service/SignUpService.java | 10 +- .../auth/service/SignUpTokenProvider.java | 26 +++ .../auth/service/TokenProvider.java | 28 +-- .../auth/service/TokenValidator.java | 10 +- .../security/filter/SignOutCheckFilter.java | 10 +- .../auth/service/AuthTokenProviderTest.java | 187 ++++++++++++++++++ .../auth/service/SignUpTokenProviderTest.java | 70 +++++++ .../auth/service/TokenProviderTest.java | 95 --------- .../filter/SignOutCheckFilterTest.java | 2 +- .../e2e/ApplicantsQueryTest.java | 20 +- .../solidconnection/e2e/MyPageTest.java | 10 +- .../solidconnection/e2e/MyPageUpdateTest.java | 10 +- .../solidconnection/e2e/SignInTest.java | 8 +- .../solidconnection/e2e/SignUpTest.java | 20 +- .../e2e/UniversityDetailTest.java | 12 +- .../e2e/UniversityLikeTest.java | 14 +- .../e2e/UniversityRecommendTest.java | 10 +- .../e2e/UniversitySearchTest.java | 10 +- 22 files changed, 422 insertions(+), 228 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java create mode 100644 src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java delete mode 100644 src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java diff --git a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java index ad5607a27..caf1c7a9d 100644 --- a/src/main/java/com/example/solidconnection/auth/domain/TokenType.java +++ b/src/main/java/com/example/solidconnection/auth/domain/TokenType.java @@ -7,8 +7,8 @@ public enum TokenType { ACCESS("ACCESS:", 1000 * 60 * 60), // 1hour REFRESH("REFRESH:", 1000 * 60 * 60 * 24 * 7), // 7days - KAKAO_OAUTH("KAKAO:", 1000 * 60 * 60), // 1hour - BLACKLIST("BLACKLIST:", ACCESS.expireTime) + BLACKLIST("BLACKLIST:", ACCESS.expireTime), + SIGN_UP("SIGN_UP:", 1000 * 60 * 10), // 10min ; private final String prefix; @@ -19,7 +19,7 @@ public enum TokenType { this.expireTime = expireTime; } - public String addPrefixToSubject(String subject) { - return prefix + subject; + public String addPrefix(String string) { + return prefix + string; } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index aed6f922f..04bcadde7 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -5,37 +5,26 @@ import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.ObjectUtils; import java.time.LocalDate; -import java.util.concurrent.TimeUnit; +import java.util.Optional; -import static com.example.solidconnection.auth.domain.TokenType.ACCESS; -import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; @RequiredArgsConstructor @Service public class AuthService { - private final RedisTemplate redisTemplate; - private final TokenProvider tokenProvider; + private final AuthTokenProvider authTokenProvider; /* * 로그아웃 한다. * - 엑세스 토큰을 블랙리스트에 추가한다. * */ public void signOut(String accessToken) { - redisTemplate.opsForValue().set( - BLACKLIST.addPrefixToSubject(accessToken), - accessToken, - BLACKLIST.getExpireTime(), - TimeUnit.MILLISECONDS - ); + authTokenProvider.generateAndSaveBlackListToken(accessToken); } /* @@ -56,14 +45,12 @@ public void quit(SiteUser siteUser) { * */ public ReissueResponse reissue(String subject) { // 리프레시 토큰 만료 확인 - String refreshTokenKey = REFRESH.addPrefixToSubject(subject); - String refreshToken = redisTemplate.opsForValue().get(refreshTokenKey); - if (ObjectUtils.isEmpty(refreshToken)) { + Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); + if (optionalRefreshToken.isEmpty()) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } // 액세스 토큰 재발급 - String newAccessToken = tokenProvider.generateToken(subject, ACCESS); - tokenProvider.saveToken(newAccessToken, ACCESS); + String newAccessToken = authTokenProvider.generateAccessToken(subject); return new ReissueResponse(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java new file mode 100644 index 000000000..da040a8d5 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -0,0 +1,53 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; + +@Component +public class AuthTokenProvider extends TokenProvider { + + public AuthTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + super(jwtProperties, redisTemplate); + } + + public String generateAccessToken(SiteUser siteUser) { + String subject = siteUser.getId().toString(); + return generateToken(subject, TokenType.ACCESS); + } + + public String generateAccessToken(String subject) { + return generateToken(subject, TokenType.ACCESS); + } + + public String generateAndSaveRefreshToken(SiteUser siteUser) { + String subject = siteUser.getId().toString(); + String refreshToken = generateToken(subject, TokenType.REFRESH); + return saveToken(refreshToken, TokenType.REFRESH); + } + + public String generateAndSaveBlackListToken(String accessToken) { + String blackListToken = generateToken(accessToken, TokenType.BLACKLIST); + return saveToken(blackListToken, TokenType.BLACKLIST); + } + + public Optional findRefreshToken(String subject) { + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); + return Optional.ofNullable(redisTemplate.opsForValue().get(refreshTokenKey)); + } + + public Optional findBlackListToken(String subject) { + String blackListTokenKey = TokenType.BLACKLIST.addPrefix(subject); + return Optional.ofNullable(redisTemplate.opsForValue().get(blackListTokenKey)); + } + + public String getEmail(String token) { + return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index ae4947596..8ca39eb62 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -6,7 +6,6 @@ import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -20,7 +19,8 @@ @Service public class SignInService { - private final TokenProvider tokenProvider; + private final AuthTokenProvider authTokenProvider; + private final SignUpTokenProvider signUpTokenProvider; private final SiteUserRepository siteUserRepository; private final KakaoOAuthClient kakaoOAuthClient; @@ -60,15 +60,13 @@ private void resetQuitedAt(SiteUser siteUser) { } private SignInResponse getSignInInfo(SiteUser siteUser) { - String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = authTokenProvider.generateAccessToken(siteUser); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); return new SignInResponse(true, accessToken, refreshToken); } private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { - String kakaoOauthToken = tokenProvider.generateToken(kakaoUserInfoDto.kakaoAccountDto().email(), TokenType.KAKAO_OAUTH); - tokenProvider.saveToken(kakaoOauthToken, TokenType.KAKAO_OAUTH); + String kakaoOauthToken = signUpTokenProvider.generateAndSaveSignUpToken(kakaoUserInfoDto.kakaoAccountDto().email()); return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java index 697cdbdc0..788b07e44 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -2,7 +2,6 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; @@ -28,7 +27,7 @@ public class SignUpService { private final TokenValidator tokenValidator; - private final TokenProvider tokenProvider; + private final AuthTokenProvider authTokenProvider; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -51,7 +50,7 @@ public class SignUpService { public SignUpResponse signUp(SignUpRequest signUpRequest) { // 검증 tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); - String email = tokenProvider.getEmail(signUpRequest.kakaoOauthToken()); + String email = authTokenProvider.getEmail(signUpRequest.kakaoOauthToken()); validateNicknameDuplicated(signUpRequest.nickname()); validateUserNotDuplicated(email); @@ -64,9 +63,8 @@ public SignUpResponse signUp(SignUpRequest signUpRequest) { saveInterestedCountry(signUpRequest, savedSiteUser); // 토큰 발급 - String accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + String accessToken = authTokenProvider.generateAccessToken(siteUser); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); return new SignUpResponse(accessToken, refreshToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java new file mode 100644 index 000000000..f04bf112b --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java @@ -0,0 +1,26 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component +public class SignUpTokenProvider extends TokenProvider { + + public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + super(jwtProperties, redisTemplate); + } + + public String generateAndSaveSignUpToken(String email) { + String signUpToken = generateToken(email, TokenType.SIGN_UP); + return saveToken(signUpToken, TokenType.SIGN_UP); + } + + public Optional findSignUpToken(String email) { + String signUpKey = TokenType.SIGN_UP.addPrefix(email); + return Optional.ofNullable(redisTemplate.opsForValue().get(signUpKey)); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java index 2dbf288ad..f5f638ab3 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenProvider.java @@ -2,33 +2,27 @@ import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.config.security.JwtProperties; -import com.example.solidconnection.siteuser.domain.SiteUser; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; -import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; import java.util.Date; import java.util.concurrent.TimeUnit; import static com.example.solidconnection.util.JwtUtils.parseSubject; -import static com.example.solidconnection.util.JwtUtils.parseSubjectIgnoringExpiration; -@RequiredArgsConstructor -@Component -public class TokenProvider { +public abstract class TokenProvider { - private final RedisTemplate redisTemplate; - private final JwtProperties jwtProperties; + protected final JwtProperties jwtProperties; + protected final RedisTemplate redisTemplate; - public String generateToken(SiteUser siteUser, TokenType tokenType) { - String subject = siteUser.getId().toString(); - return generateToken(subject, tokenType); + public TokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + this.jwtProperties = jwtProperties; + this.redisTemplate = redisTemplate; } - public String generateToken(String string, TokenType tokenType) { + protected final String generateToken(String string, TokenType tokenType) { Claims claims = Jwts.claims().setSubject(string); Date now = new Date(); Date expiredDate = new Date(now.getTime() + tokenType.getExpireTime()); @@ -40,18 +34,14 @@ public String generateToken(String string, TokenType tokenType) { .compact(); } - public String saveToken(String token, TokenType tokenType) { + protected final String saveToken(String token, TokenType tokenType) { String subject = parseSubject(token, jwtProperties.secret()); redisTemplate.opsForValue().set( - tokenType.addPrefixToSubject(subject), + tokenType.addPrefix(subject), token, tokenType.getExpireTime(), TimeUnit.MILLISECONDS ); return token; } - - public String getEmail(String token) { - return parseSubjectIgnoringExpiration(token, jwtProperties.secret()); - } } diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java index 8c17ad00c..a87a4aa2c 100644 --- a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java +++ b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java @@ -14,8 +14,8 @@ import java.util.Objects; import static com.example.solidconnection.auth.domain.TokenType.ACCESS; -import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.domain.TokenType.SIGN_UP; import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; import static com.example.solidconnection.custom.exception.ErrorCode.EMPTY_TOKEN; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; @@ -38,7 +38,7 @@ public void validateAccessToken(String token) { public void validateKakaoToken(String token) { validateTokenNotEmpty(token); - validateTokenNotExpired(token, KAKAO_OAUTH); + validateTokenNotExpired(token, SIGN_UP); validateKakaoTokenNotUsed(token); } @@ -55,7 +55,7 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { if (tokenType.equals(ACCESS)) { throw new CustomException(ACCESS_TOKEN_EXPIRED); } - if (token.equals(KAKAO_OAUTH)) { + if (token.equals(SIGN_UP)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } @@ -63,14 +63,14 @@ private void validateTokenNotExpired(String token, TokenType tokenType) { private void validateRefreshToken(String token) { String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(email)) == null) { + if (redisTemplate.opsForValue().get(REFRESH.addPrefix(email)) == null) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } } private void validateKakaoTokenNotUsed(String token) { String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email)), token)) { + if (!Objects.equals(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email)), token)) { throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); } } diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java index 90fb6866e..2cef8d1ac 100644 --- a/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilter.java @@ -1,6 +1,6 @@ package com.example.solidconnection.custom.security.filter; -import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.custom.exception.CustomException; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -8,13 +8,11 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -import static com.example.solidconnection.auth.domain.TokenType.BLACKLIST; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_SIGN_OUT; import static com.example.solidconnection.util.JwtUtils.parseTokenFromRequest; @@ -22,8 +20,7 @@ @RequiredArgsConstructor public class SignOutCheckFilter extends OncePerRequestFilter { - private final RedisTemplate redisTemplate; - private final JwtProperties jwtProperties; + private final AuthTokenProvider authTokenProvider; @Override protected void doFilterInternal(@NonNull HttpServletRequest request, @@ -37,7 +34,6 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, } private boolean hasSignedOut(String accessToken) { - String blacklistKey = BLACKLIST.addPrefixToSubject(accessToken); - return redisTemplate.opsForValue().get(blacklistKey) != null; + return authTokenProvider.findBlackListToken(accessToken).isPresent(); } } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java new file mode 100644 index 000000000..f5616973f --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -0,0 +1,187 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.util.JwtUtils; +import io.jsonwebtoken.Jwts; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("인증 토큰 제공자 테스트") +class AuthTokenProviderTest { + + @Autowired + private AuthTokenProvider authTokenProvider; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + private SiteUser siteUser; + private String subject; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + subject = siteUser.getId().toString(); + } + + @Nested + class 액세스_토큰을_제공한다 { + + @Test + void SiteUser_로_액세스_토큰을_생성한다() { + // when + String token = authTokenProvider.generateAccessToken(siteUser); + + // then + String actualSubject = JwtUtils.parseSubject(token, jwtProperties.secret()); + assertThat(actualSubject).isEqualTo(subject); + } + + @Test + void subject_로_액세스_토큰을_생성한다() { + // given + String subject = "subject123"; + + // when + String token = authTokenProvider.generateAccessToken(subject); + + // then + String actualSubject = JwtUtils.parseSubject(token, jwtProperties.secret()); + assertThat(actualSubject).isEqualTo(subject); + } + } + + @Nested + class 리프레시_토큰을_제공한다 { + + @Test + void SiteUser_로_리프레시_토큰을_생성하고_저장한다() { + // when + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + + // then + String actualSubject = JwtUtils.parseSubject(refreshToken, jwtProperties.secret()); + String refreshTokenKey = TokenType.REFRESH.addPrefix(subject); + assertAll( + () -> assertThat(actualSubject).isEqualTo(subject), + () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isEqualTo(refreshToken) + ); + } + + @Test + void 저장된_리프레시_토큰을_조회한다() { + // given + String refreshToken = "refreshToken"; + redisTemplate.opsForValue().set(TokenType.REFRESH.addPrefix(subject), refreshToken); + + // when + Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); + + // then + assertThat(optionalRefreshToken.get()).isEqualTo(refreshToken); + } + + @Test + void 저장되지_않은_리프레시_토큰을_조회한다() { + // when + Optional optionalRefreshToken = authTokenProvider.findRefreshToken(subject); + + // then + assertThat(optionalRefreshToken).isEmpty(); + } + } + + @Nested + class 블랙리스트_토큰을_제공한다 { + + @Test + void 엑세스_토큰으로_블랙리스트_토큰을_생성하고_저장한다() { + // when + String accessToken = "accessToken"; + String blackListToken = authTokenProvider.generateAndSaveBlackListToken(accessToken); + + // then + String actualSubject = JwtUtils.parseSubject(blackListToken, jwtProperties.secret()); + String blackListTokenKey = TokenType.BLACKLIST.addPrefix(accessToken); + assertAll( + () -> assertThat(actualSubject).isEqualTo(accessToken), + () -> assertThat(redisTemplate.opsForValue().get(blackListTokenKey)).isEqualTo(blackListToken) + ); + } + + @Test + void 저장된_블랙리스트_토큰을_조회한다() { + // given + String accessToken = "accessToken"; + String blackListToken = "token"; + redisTemplate.opsForValue().set(TokenType.BLACKLIST.addPrefix(accessToken), blackListToken); + + // when + Optional optionalBlackListToken = authTokenProvider.findBlackListToken(accessToken); + + // then + assertThat(optionalBlackListToken).hasValue(blackListToken); + } + + @Test + void 저장되지_않은_블랙리스트_토큰을_조회한다() { + // when + Optional optionalBlackListToken = authTokenProvider.findBlackListToken("accessToken"); + + // then + assertThat(optionalBlackListToken).isEmpty(); + } + } + + @Test + void 토큰을_생성한다() { + // when + String subject = "subject123"; + String token = authTokenProvider.generateToken(subject, TokenType.ACCESS); + + // then + String extractedSubject = Jwts.parser() + .setSigningKey(jwtProperties.secret()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + assertThat(subject).isEqualTo(extractedSubject); + } + + private SiteUser createSiteUser() { + SiteUser siteUser = new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + return siteUserRepository.save(siteUser); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java new file mode 100644 index 000000000..382008d8c --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java @@ -0,0 +1,70 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.util.JwtUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("회원가입 토큰 제공자 테스트") +class SignUpTokenProviderTest { + + @Autowired + private SignUpTokenProvider signUpTokenProvider; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 회원가입_토큰을_생성하고_저장한다() { + // when + String email = "email"; + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email); + + // then + String actualSubject = JwtUtils.parseSubject(signUpToken, jwtProperties.secret()); + String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); + assertAll( + () -> assertThat(actualSubject).isEqualTo(email), + () -> assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isEqualTo(signUpToken) + ); + } + + @Test + void 저장된_회원가입_토큰을_조회한다() { + // given + String email = "email"; + String signUpToken = "token"; + redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), signUpToken); + + // when + Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); + + // then + assertThat(actualSignUpToken).hasValue(signUpToken); + } + + @Test + void 저장되지_않은_회원가입_토큰을_조회한다() { + // given + String email = "email"; + + // when + Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); + + // then + assertThat(actualSignUpToken).isEmpty(); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java deleted file mode 100644 index 8cc91e2c0..000000000 --- a/src/test/java/com/example/solidconnection/auth/service/TokenProviderTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.config.security.JwtProperties; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.support.TestContainerSpringBootTest; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; - -import java.util.Date; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -@TestContainerSpringBootTest -@DisplayName("TokenProvider 테스트") -class TokenProviderTest { - - @Autowired - private TokenProvider tokenProvider; - - @Autowired - private RedisTemplate redisTemplate; - - @Autowired - private JwtProperties jwtProperties; - - @Test - void 토큰을_생성한다() { - // when - String subject = "subject123"; - String token = tokenProvider.generateToken(subject, TokenType.ACCESS); - - // then - String extractedSubject = Jwts.parser() - .setSigningKey(jwtProperties.secret()) - .parseClaimsJws(token) - .getBody() - .getSubject(); - assertThat(subject).isEqualTo(extractedSubject); - } - - @Nested - class 토큰을_저장한다 { - - @Test - void 토큰이_유효하면_저장한다() { - // given - String subject = "subject321"; - String token = createValidToken(subject); - - // when - tokenProvider.saveToken(token, TokenType.ACCESS); - - // then - String savedToken = redisTemplate.opsForValue().get(TokenType.ACCESS.addPrefixToSubject(subject)); - assertThat(savedToken).isEqualTo(token); - } - - @Test - void 토큰이_유효하지않으면_예외가_발생한다() { - // given - String token = createInvalidToken(); - - // when & then - assertThatCode(() -> tokenProvider.saveToken(token, TokenType.REFRESH)) - .isInstanceOf(CustomException.class) - .hasMessage(ErrorCode.INVALID_TOKEN.getMessage()); - } - } - - private String createValidToken(String subject) { - return Jwts.builder() - .setSubject(subject) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - } - - private String createInvalidToken() { - return Jwts.builder() - .setSubject("subject") - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() - 1000)) - .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) - .compact(); - } -} diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java index 7eac22c71..a11d8d28a 100644 --- a/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/SignOutCheckFilterTest.java @@ -59,7 +59,7 @@ void setUp() { // given String token = createToken(subject); request = createRequest(token); - String refreshTokenKey = BLACKLIST.addPrefixToSubject(token); + String refreshTokenKey = BLACKLIST.addPrefix(token); redisTemplate.opsForValue().set(refreshTokenKey, "signOut"); // when & then diff --git a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java index 40f39e646..fa2cf0b0b 100644 --- a/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java +++ b/src/test/java/com/example/solidconnection/e2e/ApplicantsQueryTest.java @@ -7,8 +7,7 @@ import com.example.solidconnection.application.dto.ApplicationsResponse; import com.example.solidconnection.application.dto.UniversityApplicantsResponse; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; @@ -36,7 +35,7 @@ class ApplicantsQueryTest extends UniversityDataSetUpEndToEndTest { private ApplicationRepository applicationRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; private String adminAccessToken; @@ -65,17 +64,14 @@ public void setUpUserAndToken() { SiteUser 사용자6 = siteUserRepository.save(createSiteUserByEmail("email6")); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(나, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(나, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(나); + authTokenProvider.generateAndSaveRefreshToken(나); - adminAccessToken = tokenProvider.generateToken(사용자5_관리자, TokenType.ACCESS); - String adminRefreshToken = tokenProvider.generateToken(사용자5_관리자, TokenType.REFRESH); - tokenProvider.saveToken(adminRefreshToken, TokenType.REFRESH); + adminAccessToken = authTokenProvider.generateAccessToken(사용자5_관리자); + authTokenProvider.generateAndSaveRefreshToken(사용자5_관리자); - user6AccessToken = tokenProvider.generateToken(사용자6, TokenType.ACCESS); - String user6RefreshToken = tokenProvider.generateToken(사용자6, TokenType.REFRESH); - tokenProvider.saveToken(user6RefreshToken, TokenType.REFRESH); + user6AccessToken = authTokenProvider.generateAccessToken(사용자6); + authTokenProvider.generateAndSaveRefreshToken(사용자6); // setUp - 지원 정보 저장 Gpa gpa = createDummyGpa(); diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java index 567b1016d..7a0ae07f4 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageResponse; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -25,7 +24,7 @@ class MyPageTest extends BaseEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; @@ -35,9 +34,8 @@ public void setUpUserAndToken() { siteUser = siteUserRepository.save(createSiteUserByEmail("email")); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java index 025ddb7d7..b16f3b822 100644 --- a/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java +++ b/src/test/java/com/example/solidconnection/e2e/MyPageUpdateTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.MyPageUpdateResponse; @@ -31,7 +30,7 @@ class MyPageUpdateTest extends BaseEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; @@ -44,9 +43,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index 26eba657a..8d3ddc75f 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -18,8 +18,8 @@ import java.time.LocalDate; -import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; +import static com.example.solidconnection.auth.domain.TokenType.SIGN_UP; import static com.example.solidconnection.e2e.DynamicFixture.createKakaoUserInfoDtoByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.scheduler.UserRemovalScheduler.ACCOUNT_RECOVER_DURATION; @@ -65,7 +65,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), () -> assertThat(response.kakaoOauthToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(KAKAO_OAUTH.addPrefixToSubject(email))) + assertThat(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email))) .as("카카오 인증 토큰을 저장한다.") .isEqualTo(response.kakaoOauthToken()); } @@ -95,7 +95,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.isRegistered()).isTrue(), () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefix(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -130,7 +130,7 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.accessToken()).isNotNull(), () -> assertThat(response.refreshToken()).isNotNull(), () -> assertThat(updatedSiteUser.getQuitedAt()).isNull()); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(siteUser.getId().toString()))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefix(siteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 1eb152387..1bbe150a8 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -2,7 +2,8 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.AuthTokenProvider; +import com.example.solidconnection.auth.service.SignUpTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -27,7 +28,6 @@ import java.util.List; -import static com.example.solidconnection.auth.domain.TokenType.KAKAO_OAUTH; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; @@ -56,7 +56,10 @@ class SignUpTest extends BaseEndToEndTest { InterestedCountyRepository interestedCountyRepository; @Autowired - TokenProvider tokenProvider; + AuthTokenProvider authTokenProvider; + + @Autowired + SignUpTokenProvider signUpTokenProvider; @Autowired RedisTemplate redisTemplate; @@ -71,8 +74,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); - tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); @@ -110,7 +112,7 @@ class SignUpTest extends BaseEndToEndTest { () -> assertThat(interestedCountries).containsExactlyInAnyOrderElementsOf(countries) ); - assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefixToSubject(savedSiteUser.getId().toString()))) + assertThat(redisTemplate.opsForValue().get(REFRESH.addPrefix(savedSiteUser.getId().toString()))) .as("리프레시 토큰을 저장한다.") .isEqualTo(response.refreshToken()); } @@ -124,8 +126,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = tokenProvider.generateToken(email, KAKAO_OAUTH); - tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -150,8 +151,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = tokenProvider.generateToken(alreadyExistEmail, KAKAO_OAUTH); - tokenProvider.saveToken(generatedKakaoToken, KAKAO_OAUTH); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java index b7e112d00..01b2b5730 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityDetailTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.LanguageRequirementResponse; @@ -24,7 +23,7 @@ class UniversityDetailTest extends UniversityDataSetUpEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; @@ -36,11 +35,10 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } - + @Test void 대학교_정보를_조회한다() { // request - 요청 diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java index 301b373c4..3b5733d82 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityLikeTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.LikedUniversityRepository; import com.example.solidconnection.siteuser.repository.SiteUserRepository; @@ -28,7 +27,7 @@ import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_CANCELED_MESSAGE; import static com.example.solidconnection.university.service.UniversityLikeService.LIKE_SUCCESS_MESSAGE; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("대학교 좋아요 테스트") class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { @@ -43,7 +42,7 @@ class UniversityLikeTest extends UniversityDataSetUpEndToEndTest { private LikedUniversityRepository likedUniversityRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; private SiteUser siteUser; @@ -55,9 +54,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test @@ -138,7 +136,7 @@ public void setUpUserAndToken() { // request - 요청 IsLikeResponse response = RestAssured.given().log().all() .header("Authorization", "Bearer " + accessToken) - .get("/university/"+ 괌대학_A_지원_정보.getId() +"/like") + .get("/university/" + 괌대학_A_지원_정보.getId() + "/like") .then().log().all() .statusCode(HttpStatus.OK.value()) .extract().as(IsLikeResponse.class); diff --git a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java index 358f779cd..8e1e8184f 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversityRecommendTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.service.TokenProvider; -import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; import com.example.solidconnection.repositories.InterestedCountyRepository; @@ -38,7 +37,7 @@ class UniversityRecommendTest extends UniversityDataSetUpEndToEndTest { private InterestedCountyRepository interestedCountyRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; @Autowired private GeneralUniversityRecommendService generalUniversityRecommendService; @@ -54,9 +53,8 @@ void setUp() { generalUniversityRecommendService.init(); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test diff --git a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java index 22abbfb53..3b508d014 100644 --- a/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java +++ b/src/test/java/com/example/solidconnection/e2e/UniversitySearchTest.java @@ -1,7 +1,6 @@ package com.example.solidconnection.e2e; -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.auth.service.AuthTokenProvider; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.university.dto.UniversityInfoForApplyPreviewResponse; @@ -23,7 +22,7 @@ class UniversitySearchTest extends UniversityDataSetUpEndToEndTest { private SiteUserRepository siteUserRepository; @Autowired - private TokenProvider tokenProvider; + private AuthTokenProvider authTokenProvider; private String accessToken; private SiteUser siteUser; @@ -35,9 +34,8 @@ public void setUpUserAndToken() { siteUserRepository.save(siteUser); // setUp - 엑세스 토큰 생성과 리프레시 토큰 생성 및 저장 - accessToken = tokenProvider.generateToken(siteUser, TokenType.ACCESS); - String refreshToken = tokenProvider.generateToken(siteUser, TokenType.REFRESH); - tokenProvider.saveToken(refreshToken, TokenType.REFRESH); + accessToken = authTokenProvider.generateAccessToken(siteUser); + authTokenProvider.generateAndSaveRefreshToken(siteUser); } @Test From b9a4af846fa7287b952363a12ad53531e80ea328 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Fri, 7 Feb 2025 20:29:29 +0900 Subject: [PATCH 17/23] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EA=B5=AC=ED=98=84=20(#184)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: kakao oauth 관련 값을 ConfigurationProperties 로 변경 * refactor: Oauth -> OAuth 용어 통일 * refactor: SignInService 가 로그인만 담당하도록 * refactor: 사용자 정보를 가져오는 의미가 드러나도록 함수명 변경 * refactor: 다양한 OAuth 종류를 포괄하도록 이름 변경 * refactor: OAuthService 추상화 * refactor: oauth 관련 서비스 패키지 이동 * feat: 애플 OAuth 설정 관리 클래스 생성 * feat: 애플 client secret 생성 클래스 생성 * feat: 애플 OAuthClient 구현 * feat: 애플 OAuthService 구현 * chore: 주석 내용 수정 * refactor: 회원가입 토큰에 가입 방법이 포함되도록 SignUpTokenProvider 수정 * refactor: 다양한 회원 가입이 가능하도록 회원 가입 로직 수정 * feat: 애플 인증 엔드포인트 추가 * feat: 애플 공개키를 가져와서 id_token 을 하도록 * refactor: 공개키를 캐싱하도록 * refactor: 잘못된 만료 시간 수정 * refactor: 응답 필드 명 변경 * refactor: code 요청 유효성 검사 추가 * chore: 주석 수정 --- .../auth/client/AppleOAuthClient.java | 83 ++++++++ .../AppleOAuthClientSecretProvider.java | 73 +++++++ .../auth/client/ApplePublicKeyProvider.java | 94 +++++++++ .../auth/client/KakaoOAuthClient.java | 48 ++--- .../auth/controller/AuthController.java | 38 ++-- .../auth/dto/SignInResponse.java | 6 +- .../auth/dto/SignUpRequest.java | 10 +- .../auth/dto/SignUpResponse.java | 6 - .../auth/dto/kakao/FirstAccessResponse.java | 19 -- .../auth/dto/kakao/KakaoCodeRequest.java | 5 - .../auth/dto/kakao/KakaoOauthResponse.java | 4 - .../auth/dto/oauth/AppleTokenDto.java | 10 + .../auth/dto/oauth/AppleUserInfoDto.java | 24 +++ .../dto/{kakao => oauth}/KakaoTokenDto.java | 2 +- .../{kakao => oauth}/KakaoUserInfoDto.java | 20 +- .../auth/dto/oauth/OAuthCodeRequest.java | 9 + .../auth/dto/oauth/OAuthResponse.java | 4 + .../auth/dto/oauth/OAuthSignInResponse.java | 7 + .../auth/dto/oauth/OAuthUserInfoDto.java | 10 + .../auth/dto/oauth/SignUpPrepareResponse.java | 19 ++ .../auth/service/SignInService.java | 55 +----- .../auth/service/SignUpTokenProvider.java | 26 --- .../auth/service/TokenValidator.java | 84 -------- .../auth/service/oauth/AppleOAuthService.java | 30 +++ .../auth/service/oauth/KakaoOAuthService.java | 30 +++ .../auth/service/oauth/OAuthService.java | 61 ++++++ .../OAuthSignUpService.java} | 54 +++--- .../service/oauth/SignUpTokenProvider.java | 81 ++++++++ .../client/AppleOAuthClientProperties.java | 15 ++ .../client/KakaoOAuthClientProperties.java | 12 ++ .../{rest => client}/RestTemplateConfig.java | 2 +- .../custom/exception/ErrorCode.java | 14 ++ .../solidconnection/util/JwtUtils.java | 10 +- .../auth/service/SignInServiceTest.java | 88 +++++++++ .../auth/service/SignUpTokenProviderTest.java | 70 ------- .../oauth/SignUpTokenProviderTest.java | 182 ++++++++++++++++++ .../solidconnection/e2e/DynamicFixture.java | 2 +- .../solidconnection/e2e/SignInTest.java | 42 ++-- .../solidconnection/e2e/SignUpTest.java | 18 +- 39 files changed, 979 insertions(+), 388 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java create mode 100644 src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java rename src/main/java/com/example/solidconnection/auth/dto/{kakao => oauth}/KakaoTokenDto.java (85%) rename src/main/java/com/example/solidconnection/auth/dto/{kakao => oauth}/KakaoUserInfoDto.java (61%) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java delete mode 100644 src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java delete mode 100644 src/main/java/com/example/solidconnection/auth/service/TokenValidator.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java rename src/main/java/com/example/solidconnection/auth/service/{SignUpService.java => oauth/OAuthSignUpService.java} (62%) create mode 100644 src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java create mode 100644 src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java rename src/main/java/com/example/solidconnection/config/{rest => client}/RestTemplateConfig.java (91%) create mode 100644 src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java delete mode 100644 src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java create mode 100644 src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java new file mode 100644 index 000000000..aef1309af --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClient.java @@ -0,0 +1,83 @@ +package com.example.solidconnection.auth.client; + +import com.example.solidconnection.auth.dto.oauth.AppleTokenDto; +import com.example.solidconnection.auth.dto.oauth.AppleUserInfoDto; +import com.example.solidconnection.config.client.AppleOAuthClientProperties; +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.security.PublicKey; +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_AUTHORIZATION_FAILED; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN; + +/* + * 애플 인증을 위한 OAuth2 클라이언트 + * https://developer.apple.com/documentation/signinwithapplerestapi/generate_and_validate_tokens + * */ +@Component +@RequiredArgsConstructor +public class AppleOAuthClient { + + private final RestTemplate restTemplate; + private final AppleOAuthClientProperties properties; + private final AppleOAuthClientSecretProvider clientSecretProvider; + private final ApplePublicKeyProvider publicKeyProvider; + + public AppleUserInfoDto processOAuth(String code) { + String idToken = requestIdToken(code); + PublicKey applePublicKey = publicKeyProvider.getApplePublicKey(idToken); + return new AppleUserInfoDto(parseEmailFromToken(applePublicKey, idToken)); + } + + public String requestIdToken(String code) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap formData = buildFormData(code); + + try { + ResponseEntity response = restTemplate.exchange( + properties.tokenUrl(), + HttpMethod.POST, + new HttpEntity<>(formData, headers), + AppleTokenDto.class + ); + return Objects.requireNonNull(response.getBody()).idToken(); + } catch (Exception e) { + throw new CustomException(APPLE_AUTHORIZATION_FAILED, e.getMessage()); + } + } + + private MultiValueMap buildFormData(String code) { + MultiValueMap formData = new LinkedMultiValueMap<>(); + formData.add("client_id", properties.clientId()); + formData.add("client_secret", clientSecretProvider.generateClientSecret()); + formData.add("code", code); + formData.add("grant_type", "authorization_code"); + formData.add("redirect_uri", properties.redirectUrl()); + return formData; + } + + private String parseEmailFromToken(PublicKey applePublicKey, String idToken) { + try { + return Jwts.parser() + .setSigningKey(applePublicKey) + .parseClaimsJws(idToken) + .getBody() + .get("email", String.class); + } catch (Exception e) { + throw new CustomException(INVALID_APPLE_ID_TOKEN); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java new file mode 100644 index 000000000..2de0b7291 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/client/AppleOAuthClientSecretProvider.java @@ -0,0 +1,73 @@ +package com.example.solidconnection.auth.client; + +import com.example.solidconnection.config.client.AppleOAuthClientProperties; +import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +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.PrivateKey; +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 에 필요하 클라이언트 시크릿은 매번 동적으로 생성해야 한다. + * 클라이언트 시크릿은 애플 개발자 계정에서 발급받은 개인키(*.p8)를 사용하여 JWT 를 생성한다. + * https://developer.apple.com/documentation/accountorganizationaldatasharing/creating-a-client-secret + * */ +@Component +@RequiredArgsConstructor +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(); + } + + public String generateClientSecret() { + Date now = new Date(); + Date expiration = new Date(now.getTime() + TOKEN_DURATION); + + return Jwts.builder() + .setHeaderParam("alg", "ES256") + .setHeaderParam(KEY_ID_HEADER, appleOAuthClientProperties.keyId()) + .setSubject(appleOAuthClientProperties.clientId()) + .setIssuer(appleOAuthClientProperties.teamId()) + .setAudience(appleOAuthClientProperties.clientSecretAudienceUrl()) + .setExpiration(expiration) + .signWith(SignatureAlgorithm.ES256, privateKey) + .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")); + byte[] encoded = Base64.decodeBase64(secretKey); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + KeyFactory keyFactory = KeyFactory.getInstance("EC"); + return keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + throw new CustomException(FAILED_TO_READ_APPLE_PRIVATE_KEY); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java b/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java new file mode 100644 index 000000000..1cc708cc7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/client/ApplePublicKeyProvider.java @@ -0,0 +1,94 @@ +package com.example.solidconnection.auth.client; + +import com.example.solidconnection.config.client.AppleOAuthClientProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.ExpiredJwtException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_ID_TOKEN_EXPIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.APPLE_PUBLIC_KEY_NOT_FOUND; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_APPLE_ID_TOKEN; +import static org.apache.tomcat.util.codec.binary.Base64.decodeBase64URLSafe; + +/* +* idToken 검증을 위해서 애플의 공개키를 가져온다. +* - 애플 공개키는 주기적으로 바뀐다. 이를 효율적으로 관리하기 위해 캐싱한다. +* - idToken 의 헤더에 있는 kid 값에 해당하는 키가 캐싱되어있으면 그것을 반환한다. +* - 그렇지 않다면 공개키가 바뀌었다는 뜻이므로, JSON 형식의 공개키 목록을 받아오고 캐시를 갱신한다. +* https://developer.apple.com/documentation/signinwithapplerestapi/fetch_apple_s_public_key_for_verifying_token_signature +* */ +@Component +@RequiredArgsConstructor +public class ApplePublicKeyProvider { + + private final AppleOAuthClientProperties properties; + private final RestTemplate restTemplate; + + private final Map applePublicKeyCache = new ConcurrentHashMap<>(); + + public PublicKey getApplePublicKey(String idToken) { + try { + String kid = getKeyIdFromTokenHeader(idToken); + if (applePublicKeyCache.containsKey(kid)) { + return applePublicKeyCache.get(kid); + } + + fetchApplePublicKeys(); + if (applePublicKeyCache.containsKey(kid)) { + return applePublicKeyCache.get(kid); + } else { + throw new CustomException(APPLE_PUBLIC_KEY_NOT_FOUND); + } + } catch (ExpiredJwtException e) { + throw new CustomException(APPLE_ID_TOKEN_EXPIRED); + } catch (Exception e) { + throw new CustomException(INVALID_APPLE_ID_TOKEN); + } + } + + /* + * idToken 은 JWS 이므로, 원칙적으로는 서명까지 검증되어야 parsing 이 가능하다 + * 하지만 이 시점에서는 서명(=공개키)을 알 수 없으므로, Jwt 를 직접 인코딩하여 헤더를 가져온다. + * */ + private String getKeyIdFromTokenHeader(String idToken) throws JsonProcessingException { + String[] jwtParts = idToken.split("\\."); + if (jwtParts.length < 2) { + throw new CustomException(INVALID_APPLE_ID_TOKEN); + } + String headerJson = new String(Base64.getUrlDecoder().decode(jwtParts[0]), StandardCharsets.UTF_8); + return new ObjectMapper().readTree(headerJson).get("kid").asText(); + } + + private void fetchApplePublicKeys() throws Exception { + ObjectMapper objectMapper = new ObjectMapper(); + ResponseEntity response = restTemplate.getForEntity(properties.publicKeyUrl(), String.class); + JsonNode jsonNode = objectMapper.readTree(response.getBody()).get("keys"); + + applePublicKeyCache.clear(); + for (JsonNode key : jsonNode) { + applePublicKeyCache.put(key.get("kid").asText(), generatePublicKey(key)); + } + } + + private PublicKey generatePublicKey(JsonNode key) throws Exception { + BigInteger modulus = new BigInteger(1, decodeBase64URLSafe(key.get("n").asText())); + BigInteger exponent = new BigInteger(1, decodeBase64URLSafe(key.get("e").asText())); + RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent); + return KeyFactory.getInstance("RSA").generatePublic(spec); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java index 9862d0074..5d625cb7c 100644 --- a/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java +++ b/src/main/java/com/example/solidconnection/auth/client/KakaoOAuthClient.java @@ -1,10 +1,10 @@ package com.example.solidconnection.auth.client; -import com.example.solidconnection.auth.dto.kakao.KakaoTokenDto; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.KakaoTokenDto; +import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto; +import com.example.solidconnection.config.client.KakaoOAuthClientProperties; import com.example.solidconnection.custom.exception.CustomException; import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -20,38 +20,24 @@ import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_REDIRECT_URI_MISMATCH; import static com.example.solidconnection.custom.exception.ErrorCode.KAKAO_USER_INFO_FAIL; +/* + * 카카오 인증을 위한 OAuth2 클라이언트 + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + * */ @Component @RequiredArgsConstructor public class KakaoOAuthClient { private final RestTemplate restTemplate; + private final KakaoOAuthClientProperties kakaoOAuthClientProperties; - @Value("${kakao.redirect_uri}") - public String redirectUri; - - @Value("${kakao.client_id}") - private String clientId; - - @Value("${kakao.token_url}") - private String tokenUrl; - - @Value("${kakao.user_info_url}") - private String userInfoUrl; - - /* - * 클라이언트에서 사용자가 카카오 로그인을 하면, 클라이언트는 '카카오 인가 코드'를 받아, 서버에 넘겨준다. - * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-code - * 서버는 카카오 인증 코드를 사용해 카카오 서버로부터 '카카오 토큰'을 받아온다. - * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token - * 그리고 카카오 엑세스 토큰으로 카카오 서버에 요청해 '카카오 사용자 정보'를 받아온다. - * - https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info - * */ - public KakaoUserInfoDto processOauth(String code) { + public KakaoUserInfoDto getUserInfo(String code) { String kakaoAccessToken = getKakaoAccessToken(code); return getKakaoUserInfo(kakaoAccessToken); } - // 카카오 토큰 요청 private String getKakaoAccessToken(String code) { try { ResponseEntity response = restTemplate.exchange( @@ -72,30 +58,26 @@ private String getKakaoAccessToken(String code) { } } - // 카카오 엑세스 토큰 요청하는 URI 생성 private String buildTokenUri(String code) { - return UriComponentsBuilder.fromHttpUrl(tokenUrl) + return UriComponentsBuilder.fromHttpUrl(kakaoOAuthClientProperties.tokenUrl()) .queryParam("grant_type", "authorization_code") - .queryParam("client_id", clientId) - .queryParam("redirect_uri", redirectUri) + .queryParam("client_id", kakaoOAuthClientProperties.clientId()) + .queryParam("redirect_uri", kakaoOAuthClientProperties.redirectUrl()) .queryParam("code", code) .toUriString(); } - // 카카오 사용자 정보 요청 private KakaoUserInfoDto getKakaoUserInfo(String accessToken) { HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); - // 사용자의 정보 요청 ResponseEntity response = restTemplate.exchange( - userInfoUrl, + kakaoOAuthClientProperties.userInfoUrl(), HttpMethod.GET, new HttpEntity<>(headers), KakaoUserInfoDto.class ); - // 응답 예외처리 if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { return response.getBody(); } else { diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index 1f6415157..aa3ce4f20 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,13 +1,14 @@ package com.example.solidconnection.auth.controller; import com.example.solidconnection.auth.dto.ReissueResponse; +import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.dto.SignUpResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; +import com.example.solidconnection.auth.dto.oauth.OAuthResponse; import com.example.solidconnection.auth.service.AuthService; -import com.example.solidconnection.auth.service.SignInService; -import com.example.solidconnection.auth.service.SignUpService; +import com.example.solidconnection.auth.service.oauth.AppleOAuthService; +import com.example.solidconnection.auth.service.oauth.KakaoOAuthService; +import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.custom.resolver.ExpiredToken; import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; @@ -27,23 +28,32 @@ public class AuthController { private final AuthService authService; - private final SignUpService signUpService; - private final SignInService signInService; + private final OAuthSignUpService oAuthSignUpService; + private final AppleOAuthService appleOAuthService; + private final KakaoOAuthService kakaoOAuthService; + + @PostMapping("/apple") + public ResponseEntity processAppleOAuth( + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest + ) { + OAuthResponse oAuthResponse = appleOAuthService.processOAuth(oAuthCodeRequest); + return ResponseEntity.ok(oAuthResponse); + } @PostMapping("/kakao") - public ResponseEntity processKakaoOauth( - @RequestBody KakaoCodeRequest kakaoCodeRequest + public ResponseEntity processKakaoOAuth( + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest ) { - KakaoOauthResponse kakaoOauthResponse = signInService.signIn(kakaoCodeRequest); - return ResponseEntity.ok(kakaoOauthResponse); + OAuthResponse oAuthResponse = kakaoOAuthService.processOAuth(oAuthCodeRequest); + return ResponseEntity.ok(oAuthResponse); } @PostMapping("/sign-up") - public ResponseEntity signUp( + public ResponseEntity signUp( @Valid @RequestBody SignUpRequest signUpRequest ) { - SignUpResponse signUpResponseDto = signUpService.signUp(signUpRequest); - return ResponseEntity.ok(signUpResponseDto); + SignInResponse signInResponse = oAuthSignUpService.signUp(signUpRequest); + return ResponseEntity.ok(signInResponse); } @PostMapping("/sign-out") diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java index 400491b42..a4ae442e2 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignInResponse.java @@ -1,9 +1,7 @@ package com.example.solidconnection.auth.dto; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; - public record SignInResponse( - boolean isRegistered, String accessToken, - String refreshToken) implements KakaoOauthResponse { + String refreshToken +) { } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java index fcb68cad1..b28b467bd 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java @@ -1,5 +1,6 @@ package com.example.solidconnection.auth.dto; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.PreparationStatus; @@ -10,7 +11,7 @@ import java.util.List; public record SignUpRequest( - String kakaoOauthToken, + String signUpToken, List interestedRegions, List interestedCountries, PreparationStatus preparationStatus, @@ -23,15 +24,16 @@ public record SignUpRequest( @JsonFormat(pattern = "yyyy-MM-dd") String birth) { - public SiteUser toSiteUser(String email, Role role) { + public SiteUser toSiteUser(String email, AuthType authType) { return new SiteUser( email, this.nickname, this.profileImageUrl, this.birth, this.preparationStatus, - role, - this.gender + Role.MENTEE, + this.gender, + authType ); } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java deleted file mode 100644 index 2d74610cc..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpResponse.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.solidconnection.auth.dto; - -public record SignUpResponse( - String accessToken, - String refreshToken) { -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java deleted file mode 100644 index 6d7130bf0..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/FirstAccessResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -public record FirstAccessResponse( - boolean isRegistered, - String nickname, - String email, - String profileImageUrl, - String kakaoOauthToken) implements KakaoOauthResponse { - - public static FirstAccessResponse of(KakaoUserInfoDto kakaoUserInfoDto, String kakaoOauthToken) { - return new FirstAccessResponse( - false, - kakaoUserInfoDto.kakaoAccountDto().profile().nickname(), - kakaoUserInfoDto.kakaoAccountDto().email(), - kakaoUserInfoDto.kakaoAccountDto().profile().profileImageUrl(), - kakaoOauthToken - ); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java deleted file mode 100644 index 4fcfc5576..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoCodeRequest.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -public record KakaoCodeRequest( - String code) { -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java b/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java deleted file mode 100644 index 1e2320e35..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoOauthResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.example.solidconnection.auth.dto.kakao; - -public interface KakaoOauthResponse { -} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java new file mode 100644 index 000000000..6772cb2c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleTokenDto.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.auth.dto.oauth; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record AppleTokenDto( + @JsonProperty("id_token") String idToken +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java new file mode 100644 index 000000000..5c4363e51 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/AppleUserInfoDto.java @@ -0,0 +1,24 @@ +package com.example.solidconnection.auth.dto.oauth; + +/* +* 애플로부터 사용자의 정보를 받아올 때 사용한다. +* 카카오와 달리 애플은 더 엄격하게 사용자 정보를 관리하여, 이름이나 프로필 이미지 url 을 제공하지 않는다. +* 따라서 닉네임, 프로필 정보는 null 을 반환한다. +* */ +public record AppleUserInfoDto(String email) implements OAuthUserInfoDto { + + @Override + public String getEmail() { + return email; + } + + @Override + public String getProfileImageUrl() { + return null; + } + + @Override + public String getNickname() { + return null; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java similarity index 85% rename from src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java rename to src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java index 767645e3b..6d4ccd10c 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoTokenDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoTokenDto.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.auth.dto.kakao; +package com.example.solidconnection.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoUserInfoDto.java similarity index 61% rename from src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java rename to src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoUserInfoDto.java index 85aea091d..fbd975b50 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/kakao/KakaoUserInfoDto.java +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/KakaoUserInfoDto.java @@ -1,11 +1,11 @@ -package com.example.solidconnection.auth.dto.kakao; +package com.example.solidconnection.auth.dto.oauth; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) public record KakaoUserInfoDto( - @JsonProperty("kakao_account") KakaoAccountDto kakaoAccountDto) { + @JsonProperty("kakao_account") KakaoAccountDto kakaoAccountDto) implements OAuthUserInfoDto { @JsonIgnoreProperties(ignoreUnknown = true) public record KakaoAccountDto( @@ -16,6 +16,22 @@ public record KakaoAccountDto( public record KakaoProfileDto( @JsonProperty("profile_image_url") String profileImageUrl, String nickname) { + } } + + @Override + public String getEmail() { + return kakaoAccountDto.email; + } + + @Override + public String getProfileImageUrl() { + return kakaoAccountDto.profile.profileImageUrl; + } + + @Override + public String getNickname() { + return kakaoAccountDto.profile.nickname; + } } diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java new file mode 100644 index 000000000..abbdb7802 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthCodeRequest.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.auth.dto.oauth; + +import jakarta.validation.constraints.NotBlank; + +public record OAuthCodeRequest( + + @NotBlank(message = "인증 코드를 입력해주세요.") + String code) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java new file mode 100644 index 000000000..ddbe121f7 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthResponse.java @@ -0,0 +1,4 @@ +package com.example.solidconnection.auth.dto.oauth; + +public interface OAuthResponse { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java new file mode 100644 index 000000000..8ad429876 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthSignInResponse.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.auth.dto.oauth; + +public record OAuthSignInResponse( + boolean isRegistered, + String accessToken, + String refreshToken) implements OAuthResponse { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java new file mode 100644 index 000000000..ed794851b --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/OAuthUserInfoDto.java @@ -0,0 +1,10 @@ +package com.example.solidconnection.auth.dto.oauth; + +public interface OAuthUserInfoDto { + + String getEmail(); + + String getProfileImageUrl(); + + String getNickname(); +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java b/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java new file mode 100644 index 000000000..5a6c60c57 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/oauth/SignUpPrepareResponse.java @@ -0,0 +1,19 @@ +package com.example.solidconnection.auth.dto.oauth; + +public record SignUpPrepareResponse( + boolean isRegistered, + String nickname, + String email, + String profileImageUrl, + String signUpToken) implements OAuthResponse { + + public static SignUpPrepareResponse of(OAuthUserInfoDto oAuthUserInfoDto, String signUpToken) { + return new SignUpPrepareResponse( + false, + oAuthUserInfoDto.getNickname(), + oAuthUserInfoDto.getEmail(), + oAuthUserInfoDto.getProfileImageUrl(), + signUpToken + ); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index 8ca39eb62..820d2e573 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -1,72 +1,29 @@ package com.example.solidconnection.auth.service; -import com.example.solidconnection.auth.client.KakaoOAuthClient; import com.example.solidconnection.auth.dto.SignInResponse; -import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; -import com.example.solidconnection.auth.dto.kakao.KakaoOauthResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; -import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Optional; - -@RequiredArgsConstructor @Service +@RequiredArgsConstructor public class SignInService { private final AuthTokenProvider authTokenProvider; - private final SignUpTokenProvider signUpTokenProvider; - private final SiteUserRepository siteUserRepository; - private final KakaoOAuthClient kakaoOAuthClient; - /* - * 카카오에서 받아온 사용자 정보에 있는 이메일을 통해 기존 회원인지, 신규 회원인지 판별하고, 이에 따라 다르게 응답한다. - * 기존 회원 : 로그인 - * - 우리 서비스의 탈퇴 회원 방침을 적용한다. (계정 복구 기간 안에 접속하면 탈퇴를 무효화) - * - 액세스 토큰과 리프레시 토큰을 발급한다. - * 신규 회원 : 회원가입 페이지로 리다이렉트할 때 필요한 정보 제공 - * - 회원가입 시 입력하는 '닉네임'과 '프로필 사진' 부분을 미리 채우기 위해 사용자 정보를 리턴한다. - * - 또한, 우리 서비스에서 카카오 인증을 받았는지 나타내기 위한 'kakaoOauthToken' 을 발급해서 응답한다. - * - 회원가입할 때 클라이언트는 이때 발급받은 kakaoOauthToken 를 요청에 포함해 요청한다. (SignUpService 참고) - * */ @Transactional - public KakaoOauthResponse signIn(KakaoCodeRequest kakaoCodeRequest) { - KakaoUserInfoDto kakaoUserInfoDto = kakaoOAuthClient.processOauth(kakaoCodeRequest.code()); - String email = kakaoUserInfoDto.kakaoAccountDto().email(); - Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO); - - if (optionalSiteUser.isPresent()) { - SiteUser siteUser = optionalSiteUser.get(); - resetQuitedAt(siteUser); - return getSignInInfo(siteUser); - } - - return getFirstAccessInfo(kakaoUserInfoDto); + public SignInResponse signIn(SiteUser siteUser) { + resetQuitedAt(siteUser); + String accessToken = authTokenProvider.generateAccessToken(siteUser); + String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + return new SignInResponse(accessToken, refreshToken); } - // 계적 복구 기한이 지난 회원은 자정마다 삭제된다. (UserRemovalScheduler 참고) - // 따라서 DB 에서 조회되었다면 아직 기한이 지나지 않았다는 뜻이므로, 탈퇴 날짜를 초기화한다. private void resetQuitedAt(SiteUser siteUser) { if (siteUser.getQuitedAt() == null) { return; } - siteUser.setQuitedAt(null); } - - private SignInResponse getSignInInfo(SiteUser siteUser) { - String accessToken = authTokenProvider.generateAccessToken(siteUser); - String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); - return new SignInResponse(true, accessToken, refreshToken); - } - - private FirstAccessResponse getFirstAccessInfo(KakaoUserInfoDto kakaoUserInfoDto) { - String kakaoOauthToken = signUpTokenProvider.generateAndSaveSignUpToken(kakaoUserInfoDto.kakaoAccountDto().email()); - return FirstAccessResponse.of(kakaoUserInfoDto, kakaoOauthToken); - } } diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java deleted file mode 100644 index f04bf112b..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpTokenProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.config.security.JwtProperties; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; - -import java.util.Optional; - -@Component -public class SignUpTokenProvider extends TokenProvider { - - public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { - super(jwtProperties, redisTemplate); - } - - public String generateAndSaveSignUpToken(String email) { - String signUpToken = generateToken(email, TokenType.SIGN_UP); - return saveToken(signUpToken, TokenType.SIGN_UP); - } - - public Optional findSignUpToken(String email) { - String signUpKey = TokenType.SIGN_UP.addPrefix(email); - return Optional.ofNullable(redisTemplate.opsForValue().get(signUpKey)); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java b/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java deleted file mode 100644 index a87a4aa2c..000000000 --- a/src/main/java/com/example/solidconnection/auth/service/TokenValidator.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.custom.exception.CustomException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import java.util.Date; -import java.util.Objects; - -import static com.example.solidconnection.auth.domain.TokenType.ACCESS; -import static com.example.solidconnection.auth.domain.TokenType.REFRESH; -import static com.example.solidconnection.auth.domain.TokenType.SIGN_UP; -import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_TOKEN_EXPIRED; -import static com.example.solidconnection.custom.exception.ErrorCode.EMPTY_TOKEN; -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN; -import static com.example.solidconnection.custom.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; - -@Component -@RequiredArgsConstructor -public class TokenValidator { - - private final RedisTemplate redisTemplate; - - @Value("${jwt.secret}") - private String secretKey; - - public void validateAccessToken(String token) { - validateTokenNotEmpty(token); - validateTokenNotExpired(token, ACCESS); - validateRefreshToken(token); - } - - public void validateKakaoToken(String token) { - validateTokenNotEmpty(token); - validateTokenNotExpired(token, SIGN_UP); - validateKakaoTokenNotUsed(token); - } - - private void validateTokenNotEmpty(String token) { - if (!StringUtils.hasText(token)) { - throw new CustomException(EMPTY_TOKEN); - } - } - - private void validateTokenNotExpired(String token, TokenType tokenType) { - Date expiration = getClaim(token).getExpiration(); - long now = new Date().getTime(); - if ((expiration.getTime() - now) < 0) { - if (tokenType.equals(ACCESS)) { - throw new CustomException(ACCESS_TOKEN_EXPIRED); - } - if (token.equals(SIGN_UP)) { - throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); - } - } - } - - private void validateRefreshToken(String token) { - String email = getClaim(token).getSubject(); - if (redisTemplate.opsForValue().get(REFRESH.addPrefix(email)) == null) { - throw new CustomException(REFRESH_TOKEN_EXPIRED); - } - } - - private void validateKakaoTokenNotUsed(String token) { - String email = getClaim(token).getSubject(); - if (!Objects.equals(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email)), token)) { - throw new CustomException(INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN); - } - } - - private Claims getClaim(String token) { - return Jwts.parser() - .setSigningKey(this.secretKey) - .parseClaimsJws(token) - .getBody(); - } -} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java new file mode 100644 index 000000000..2af82e07d --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.client.AppleOAuthClient; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.stereotype.Service; + +@Service +public class AppleOAuthService extends OAuthService { + + private final AppleOAuthClient appleOAuthClient; + + public AppleOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + AppleOAuthClient appleOAuthClient, SignInService signInService) { + super(signUpTokenProvider, siteUserRepository, signInService); + this.appleOAuthClient = appleOAuthClient; + } + + @Override + protected OAuthUserInfoDto getOAuthUserInfo(String code) { + return appleOAuthClient.processOAuth(code); + } + + @Override + protected AuthType getAuthType() { + return AuthType.APPLE; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java new file mode 100644 index 000000000..5dc6faea1 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java @@ -0,0 +1,30 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.client.KakaoOAuthClient; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.stereotype.Service; + +@Service +public class KakaoOAuthService extends OAuthService { + + private final KakaoOAuthClient kakaoOAuthClient; + + public KakaoOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + KakaoOAuthClient kakaoOAuthClient, SignInService signInService) { + super(signUpTokenProvider, siteUserRepository, signInService); + this.kakaoOAuthClient = kakaoOAuthClient; + } + + @Override + protected OAuthUserInfoDto getOAuthUserInfo(String code) { + return kakaoOAuthClient.getUserInfo(code); + } + + @Override + protected AuthType getAuthType() { + return AuthType.KAKAO; + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java new file mode 100644 index 000000000..4f37db060 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -0,0 +1,61 @@ +package com.example.solidconnection.auth.service.oauth; + + +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; +import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; +import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +/* + * OAuth 제공자로부터 이메일을 받아 기존 회원인지, 신규 회원인지 판별하고, 이에 따라 다르게 응답한다. + * 기존 회원 : 로그인한다. + * 신규 회원 : 회원가입할 때 필요한 정보를 제공한다. + * */ +public abstract class OAuthService { + + private final SignUpTokenProvider signUpTokenProvider; + private final SignInService signInService; + private final SiteUserRepository siteUserRepository; + + protected OAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) { + this.signUpTokenProvider = signUpTokenProvider; + this.siteUserRepository = siteUserRepository; + this.signInService = signInService; + } + + @Transactional + public OAuthResponse processOAuth(OAuthCodeRequest oauthCodeRequest) { + OAuthUserInfoDto userInfoDto = getOAuthUserInfo(oauthCodeRequest.code()); + String email = userInfoDto.getEmail(); + Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(email, getAuthType()); + + if (optionalSiteUser.isPresent()) { + SiteUser siteUser = optionalSiteUser.get(); + return getSignInResponse(siteUser); + } + + return getSignUpPrepareResponse(userInfoDto); + } + + protected final OAuthSignInResponse getSignInResponse(SiteUser siteUser) { + SignInResponse signInResponse = signInService.signIn(siteUser); + return new OAuthSignInResponse(true, signInResponse.accessToken(), signInResponse.refreshToken()); + } + + protected final SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto) { + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType()); + return SignUpPrepareResponse.of(userInfoDto, signUpToken); + } + + protected abstract OAuthUserInfoDto getOAuthUserInfo(String code); + protected abstract AuthType getAuthType(); +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java similarity index 62% rename from src/main/java/com/example/solidconnection/auth/service/SignUpService.java rename to src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java index 788b07e44..7b6d44d26 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java @@ -1,7 +1,8 @@ -package com.example.solidconnection.auth.service; +package com.example.solidconnection.auth.service.oauth; +import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.dto.SignUpResponse; +import com.example.solidconnection.auth.service.SignInService; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.entity.InterestedCountry; import com.example.solidconnection.entity.InterestedRegion; @@ -12,7 +13,6 @@ import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import com.example.solidconnection.type.Role; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,10 +24,10 @@ @RequiredArgsConstructor @Service -public class SignUpService { +public class OAuthSignUpService { - private final TokenValidator tokenValidator; - private final AuthTokenProvider authTokenProvider; + private final SignUpTokenProvider signUpTokenProvider; + private final SignInService signInService; private final SiteUserRepository siteUserRepository; private final RegionRepository regionRepository; private final InterestedRegionRepository interestedRegionRepository; @@ -35,43 +35,31 @@ public class SignUpService { private final InterestedCountyRepository interestedCountyRepository; /* - * 회원가입을 한다. - * - 카카오로 최초 로그인 시 우리 서비스에서 발급한 카카오 토큰 kakaoOauthToken 을 검증한다. - * - 이는 '카카오 인증을 하지 않고 회원가입 api 만으로 회원가입 하는 상황'을 방지하기 위함이다. - * - 만약 api 만으로 회원가입을 한다면, 카카오 인증과 이메일에 대한 검증 없이 회원가입이 가능해진다. - * - 이메일은 우리 서비스에서 사용자를 식별하는 중요한 정보이기 때문에 '우리 서비스에서 발급한 카카오 토큰인지 검증하는' 단계가 필요하다. + * OAuth 인증 후 회원가입을 한다. + * - 우리 서버에서 OAuth 인증했음을 확인하기 위한 signUpToken 을 검증한다. * - 사용자 정보를 DB에 저장한다. * - 관심 국가와 지역을 DB에 저장한다. * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. * */ - // todo: 여러가지 가입 방법 적용해야 함 @Transactional - public SignUpResponse signUp(SignUpRequest signUpRequest) { + public SignInResponse signUp(SignUpRequest signUpRequest) { // 검증 - tokenValidator.validateKakaoToken(signUpRequest.kakaoOauthToken()); - String email = authTokenProvider.getEmail(signUpRequest.kakaoOauthToken()); + signUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); validateNicknameDuplicated(signUpRequest.nickname()); - validateUserNotDuplicated(email); + String email = signUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + AuthType authType = signUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); + validateUserNotDuplicated(email, authType); // 사용자 저장 - SiteUser siteUser = signUpRequest.toSiteUser(email, Role.MENTEE); - SiteUser savedSiteUser = siteUserRepository.save(siteUser); + SiteUser siteUser = siteUserRepository.save(signUpRequest.toSiteUser(email, authType)); // 관심 지역, 국가 저장 - saveInterestedRegion(signUpRequest, savedSiteUser); - saveInterestedCountry(signUpRequest, savedSiteUser); + saveInterestedRegion(signUpRequest, siteUser); + saveInterestedCountry(signUpRequest, siteUser); - // 토큰 발급 - String accessToken = authTokenProvider.generateAccessToken(siteUser); - String refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); - return new SignUpResponse(accessToken, refreshToken); - } - - private void validateUserNotDuplicated(String email) { - if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.KAKAO)) { - throw new CustomException(USER_ALREADY_EXISTED); - } + // 로그인 + return signInService.signIn(siteUser); } private void validateNicknameDuplicated(String nickname) { @@ -80,6 +68,12 @@ private void validateNicknameDuplicated(String nickname) { } } + private void validateUserNotDuplicated(String email, AuthType authType) { + if (siteUserRepository.existsByEmailAndAuthType(email, authType)) { + throw new CustomException(USER_ALREADY_EXISTED); + } + } + private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { List interestedRegionNames = signUpRequest.interestedRegions(); List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java new file mode 100644 index 000000000..5399dc1eb --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java @@ -0,0 +1,81 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.service.TokenProvider; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.util.JwtUtils.parseClaims; +import static com.example.solidconnection.util.JwtUtils.parseSubject; + +@Component +public class SignUpTokenProvider extends TokenProvider { + + static final String AUTH_TYPE_CLAIM_KEY = "authType"; + + public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + super(jwtProperties, redisTemplate); + } + + public String generateAndSaveSignUpToken(String email, AuthType authType) { + Map authTypeClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, authType)); + Claims claims = Jwts.claims(authTypeClaim).setSubject(email); + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + TokenType.SIGN_UP.getExpireTime()); + + String signUpToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) + .compact(); + return saveToken(signUpToken, TokenType.SIGN_UP); + } + + public void validateSignUpToken(String token) { + validateFormatAndExpiration(token); + String email = parseEmail(token); + validateIssuedByServer(email); + } + + private void validateFormatAndExpiration(String token) { + try { + Claims claims = parseClaims(token, jwtProperties.secret()); + Objects.requireNonNull(claims.getSubject()); + String serializedAuthType = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); + AuthType.valueOf(serializedAuthType); + } catch (Exception e) { + throw new CustomException(OAUTH_SIGN_UP_TOKEN_INVALID); + } + } + + private void validateIssuedByServer(String email) { + String key = TokenType.SIGN_UP.addPrefix(email); + if (redisTemplate.opsForValue().get(key) == null) { + throw new CustomException(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); + } + } + + public String parseEmail(String token) { + return parseSubject(token, jwtProperties.secret()); + } + + public AuthType parseAuthType(String token) { + Claims claims = parseClaims(token, jwtProperties.secret()); + String authTypeStr = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); + return AuthType.valueOf(authTypeStr); + } +} diff --git a/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java new file mode 100644 index 000000000..609e9ee89 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/client/AppleOAuthClientProperties.java @@ -0,0 +1,15 @@ +package com.example.solidconnection.config.client; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth.apple") +public record AppleOAuthClientProperties( + String tokenUrl, + String clientSecretAudienceUrl, + String redirectUrl, + String publicKeyUrl, + String clientId, + String teamId, + String keyId +) { +} diff --git a/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java b/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java new file mode 100644 index 000000000..73b196d76 --- /dev/null +++ b/src/main/java/com/example/solidconnection/config/client/KakaoOAuthClientProperties.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.config.client; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "oauth.kakao") +public record KakaoOAuthClientProperties( + String tokenUrl, + String userInfoUrl, + String redirectUrl, + String clientId +) { +} diff --git a/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java b/src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java similarity index 91% rename from src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java rename to src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java index 51f7205be..36ce3f67b 100644 --- a/src/main/java/com/example/solidconnection/config/rest/RestTemplateConfig.java +++ b/src/main/java/com/example/solidconnection/config/client/RestTemplateConfig.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.config.rest; +package com.example.solidconnection.config.client; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 8c3032284..d3fdf136d 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -11,6 +11,16 @@ @AllArgsConstructor public enum ErrorCode { + // apple + APPLE_AUTHORIZATION_FAILED(HttpStatus.BAD_REQUEST.value(), "애플 인증에 실패했습니다."), + APPLE_ID_TOKEN_MISSING_EMAIL(HttpStatus.BAD_REQUEST.value(), "애플 idToken 에 이메일이 없습니다."), + APPLE_ID_TOKEN_EXPIRED(HttpStatus.BAD_REQUEST.value(), "애플 idToken 이 만료되었습니다."), + INVALID_APPLE_ID_TOKEN(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 애플 idToken 입니다."), + APPLE_ID_TOKEN_MALFORMED(HttpStatus.BAD_REQUEST.value(), "애플 idToken 의 형식이 잘못되었습니다."), + APPLE_PUBLIC_KEY_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR.value(), "idToken 를 서명한 애플 공개키를 찾을 수 없습니다"), + FAILED_TO_READ_APPLE_PRIVATE_KEY(HttpStatus.INTERNAL_SERVER_ERROR.value(), "애플 private key 파일을 읽을 수 없습니다."), + APPLE_CLIENT_SECRET_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR.value(), "애플 client secret JWT 생성에 실패했습니다."), + // kakao KAKAO_REDIRECT_URI_MISMATCH(HttpStatus.BAD_REQUEST.value(), "리다이렉트 uri가 잘못되었습니다."), INVALID_OR_EXPIRED_KAKAO_AUTH_CODE(HttpStatus.BAD_REQUEST.value(), "사용할 수 없는 카카오 인증 코드입니다. 카카오 인증 코드는 일회용이며, 인증 만료 시간은 10분입니다."), @@ -18,6 +28,10 @@ public enum ErrorCode { KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(), "카카오 사용자 정보 조회에 실패했습니다."), INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN(HttpStatus.BAD_REQUEST.value(), "우리 서비스에서 발급한 카카오 토큰이 아닙니다"), + // oauth + OAUTH_SIGN_UP_TOKEN_INVALID(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 회원가입 토큰입니다."), + OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER(HttpStatus.BAD_REQUEST.value(), "회원가입 토큰이 우리 서버에서 발급되지 않았습니다."), + // data not found UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 대학교 지원 정보입니다."), UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND_FOR_TERM(HttpStatus.NOT_FOUND.value(), "해당하는 대학교가 이번 모집 기간에 열리지 않았습니다."), diff --git a/src/main/java/com/example/solidconnection/util/JwtUtils.java b/src/main/java/com/example/solidconnection/util/JwtUtils.java index 3a1b58520..d3ea8fed9 100644 --- a/src/main/java/com/example/solidconnection/util/JwtUtils.java +++ b/src/main/java/com/example/solidconnection/util/JwtUtils.java @@ -1,6 +1,7 @@ package com.example.solidconnection.util; import com.example.solidconnection.custom.exception.CustomException; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import jakarta.servlet.http.HttpServletRequest; @@ -29,7 +30,7 @@ public static String parseTokenFromRequest(HttpServletRequest request) { public static String parseSubjectIgnoringExpiration(String token, String secretKey) { try { - return extractSubject(token, secretKey); + return parseClaims(token, secretKey).getSubject(); } catch (ExpiredJwtException e) { return e.getClaims().getSubject(); } catch (Exception e) { @@ -39,7 +40,7 @@ public static String parseSubjectIgnoringExpiration(String token, String secretK public static String parseSubject(String token, String secretKey) { try { - return extractSubject(token, secretKey); + return parseClaims(token, secretKey).getSubject(); } catch (Exception e) { throw new CustomException(INVALID_TOKEN); } @@ -58,11 +59,10 @@ public static boolean isExpired(String token, String secretKey) { } } - private static String extractSubject(String token, String secretKey) throws ExpiredJwtException { + public static Claims parseClaims(String token, String secretKey) throws ExpiredJwtException { return Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) - .getBody() - .getSubject(); + .getBody(); } } diff --git a/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java new file mode 100644 index 000000000..b80c4ca5d --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/SignInServiceTest.java @@ -0,0 +1,88 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import com.example.solidconnection.util.JwtUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("로그인 서비스 테스트") +@TestContainerSpringBootTest +class SignInServiceTest { + + @Autowired + private SignInService signInService; + + @Autowired + private JwtProperties jwtProperties; + + @Autowired + private AuthTokenProvider authTokenProvider; + + @Autowired + private SiteUserRepository siteUserRepository; + + private SiteUser siteUser; + private String subject; + + @BeforeEach + void setUp() { + siteUser = createSiteUser(); + siteUserRepository.save(siteUser); + subject = siteUser.getId().toString(); + } + + @Test + void 성공적으로_로그인한다() { + // when + SignInResponse signInResponse = signInService.signIn(siteUser); + + // then + String accessTokenSubject = JwtUtils.parseSubject(signInResponse.accessToken(), jwtProperties.secret()); + String refreshTokenSubject = JwtUtils.parseSubject(signInResponse.refreshToken(), jwtProperties.secret()); + Optional savedRefreshToken = authTokenProvider.findRefreshToken(subject); + assertAll( + () -> assertThat(accessTokenSubject).isEqualTo(subject), + () -> assertThat(refreshTokenSubject).isEqualTo(subject), + () -> assertThat(savedRefreshToken).hasValue(signInResponse.refreshToken())); + } + + @Test + void 탈퇴한_이력이_있으면_초기화한다() { + // given + siteUser.setQuitedAt(LocalDate.now().minusDays(1)); + siteUserRepository.save(siteUser); + + // when + signInService.signIn(siteUser); + + // then + assertThat(siteUser.getQuitedAt()).isNull(); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java deleted file mode 100644 index 382008d8c..000000000 --- a/src/test/java/com/example/solidconnection/auth/service/SignUpTokenProviderTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.example.solidconnection.auth.service; - -import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.config.security.JwtProperties; -import com.example.solidconnection.support.TestContainerSpringBootTest; -import com.example.solidconnection.util.JwtUtils; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertAll; - -@TestContainerSpringBootTest -@DisplayName("회원가입 토큰 제공자 테스트") -class SignUpTokenProviderTest { - - @Autowired - private SignUpTokenProvider signUpTokenProvider; - - @Autowired - private RedisTemplate redisTemplate; - - @Autowired - private JwtProperties jwtProperties; - - @Test - void 회원가입_토큰을_생성하고_저장한다() { - // when - String email = "email"; - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email); - - // then - String actualSubject = JwtUtils.parseSubject(signUpToken, jwtProperties.secret()); - String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); - assertAll( - () -> assertThat(actualSubject).isEqualTo(email), - () -> assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isEqualTo(signUpToken) - ); - } - - @Test - void 저장된_회원가입_토큰을_조회한다() { - // given - String email = "email"; - String signUpToken = "token"; - redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), signUpToken); - - // when - Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); - - // then - assertThat(actualSignUpToken).hasValue(signUpToken); - } - - @Test - void 저장되지_않은_회원가입_토큰을_조회한다() { - // given - String email = "email"; - - // when - Optional actualSignUpToken = signUpTokenProvider.findSignUpToken(email); - - // then - assertThat(actualSignUpToken).isEmpty(); - } -} diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java new file mode 100644 index 000000000..d3a1efac1 --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java @@ -0,0 +1,182 @@ +package com.example.solidconnection.auth.service.oauth; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.util.JwtUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static com.example.solidconnection.auth.service.oauth.SignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@TestContainerSpringBootTest +@DisplayName("회원가입 토큰 제공자 테스트") +class SignUpTokenProviderTest { + + @Autowired + private SignUpTokenProvider signUpTokenProvider; + + @Autowired + private RedisTemplate redisTemplate; + + @Autowired + private JwtProperties jwtProperties; + + @Test + void 회원가입_토큰을_생성하고_저장한다() { + // given + String email = "email"; + AuthType authType = AuthType.KAKAO; + + // when + String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType); + + // then + Claims claims = JwtUtils.parseClaims(signUpToken, jwtProperties.secret()); + String actualSubject = claims.getSubject(); + AuthType actualAuthType = AuthType.valueOf(claims.get(AUTH_TYPE_CLAIM_KEY, String.class)); + String signUpTokenKey = TokenType.SIGN_UP.addPrefix(email); + assertAll( + () -> assertThat(actualSubject).isEqualTo(email), + () -> assertThat(actualAuthType).isEqualTo(authType), + () -> assertThat(redisTemplate.opsForValue().get(signUpTokenKey)).isEqualTo(signUpToken) + ); + } + + @Nested + class 주어진_회원가입_토큰을_검증한다 { + + @Test + void 검증_성공한다() { + // given + String email = "email@test.com"; + Map claim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); + redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); + } + + @Test + void 만료되었으면_예외_응답을_반환한다() { + // given + String expiredToken = createExpiredToken(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(expiredToken)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 정해진_형식에_맞지_않으면_예외_응답을_반환한다_jwt_가_아닌_토큰() { + // given + String notJwt = "not jwt"; + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(notJwt)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 정해진_형식에_맞지_않으면_예외_응답을_반환한다_authType_클래스_불일치() { + // given + Map wrongClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, "카카오")); + String wrongAuthType = createBaseJwtBuilder().addClaims(wrongClaim).compact(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(wrongAuthType)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 정해진_형식에_맞지_않으면_예외_응답을_반환한다_subject_누락() { + // given + Map claim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + String noSubject = createBaseJwtBuilder().addClaims(claim).compact(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(noSubject)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + } + + @Test + void 우리_서버에_발급된_토큰이_아니면_예외_응답을_반환한다() { + // given + Map validClaim = new HashMap<>(Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE)); + String signUpToken = createBaseJwtBuilder().addClaims(validClaim).setSubject("email").compact(); + + // when & then + assertThatCode(() -> signUpTokenProvider.validateSignUpToken(signUpToken)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER.getMessage()); + } + } + + @Test + void 회원가입_토큰에서_이메일을_추출한다() { + // given + String email = "email@test.com"; + Map claim = Map.of(AUTH_TYPE_CLAIM_KEY, AuthType.APPLE); + String validToken = createBaseJwtBuilder().setSubject(email).addClaims(claim).compact(); + redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); + + // when + String extractedEmail = signUpTokenProvider.parseEmail(validToken); + + // then + assertThat(extractedEmail).isEqualTo(email); + } + + @Test + void 회원가입_토큰에서_인증_타입을_추출한다() { + // given + AuthType authType = AuthType.APPLE; + Map claim = Map.of(AUTH_TYPE_CLAIM_KEY, authType); + String validToken = createBaseJwtBuilder().setSubject("email").addClaims(claim).compact(); + + // when + AuthType extractedAuthType = signUpTokenProvider.parseAuthType(validToken); + + // then + assertThat(extractedAuthType).isEqualTo(authType); + } + + private String createExpiredToken() { + return Jwts.builder() + .setSubject("subject") + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() - 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()) + .compact(); + } + + private JwtBuilder createBaseJwtBuilder() { + return Jwts.builder() + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + 1000)) + .signWith(SignatureAlgorithm.HS256, jwtProperties.secret()); + } +} diff --git a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java index a549b62a2..09c97d46e 100644 --- a/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java +++ b/src/test/java/com/example/solidconnection/e2e/DynamicFixture.java @@ -2,7 +2,7 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.Gender; import com.example.solidconnection.type.LanguageTestType; diff --git a/src/test/java/com/example/solidconnection/e2e/SignInTest.java b/src/test/java/com/example/solidconnection/e2e/SignInTest.java index 8d3ddc75f..cc16f71c1 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignInTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignInTest.java @@ -1,10 +1,10 @@ package com.example.solidconnection.e2e; import com.example.solidconnection.auth.client.KakaoOAuthClient; -import com.example.solidconnection.auth.dto.SignInResponse; -import com.example.solidconnection.auth.dto.kakao.FirstAccessResponse; -import com.example.solidconnection.auth.dto.kakao.KakaoCodeRequest; -import com.example.solidconnection.auth.dto.kakao.KakaoUserInfoDto; +import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; +import com.example.solidconnection.auth.dto.oauth.SignUpPrepareResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; +import com.example.solidconnection.auth.dto.oauth.KakaoUserInfoDto; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import io.restassured.RestAssured; @@ -45,18 +45,18 @@ class SignInTest extends BaseEndToEndTest { String kakaoCode = "kakaoCode"; String email = "email@email.com"; KakaoUserInfoDto kakaoUserInfoDto = createKakaoUserInfoDtoByEmail(email); - given(kakaoOAuthClient.processOauth(kakaoCode)) + given(kakaoOAuthClient.getUserInfo(kakaoCode)) .willReturn(kakaoUserInfoDto); // request - body 생성 및 요청 - KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); - FirstAccessResponse response = RestAssured.given().log().all() + OAuthCodeRequest OAuthCodeRequest = new OAuthCodeRequest(kakaoCode); + SignUpPrepareResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) - .body(kakaoCodeRequest) + .body(OAuthCodeRequest) .when().post("/auth/kakao") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(FirstAccessResponse.class); + .extract().as(SignUpPrepareResponse.class); KakaoUserInfoDto.KakaoAccountDto.KakaoProfileDto kakaoProfileDto = kakaoUserInfoDto.kakaoAccountDto().profile(); assertAll("카카오톡 사용자 정보를 응답한다.", @@ -64,10 +64,10 @@ class SignInTest extends BaseEndToEndTest { () -> assertThat(response.email()).isEqualTo(email), () -> assertThat(response.nickname()).isEqualTo(kakaoProfileDto.nickname()), () -> assertThat(response.profileImageUrl()).isEqualTo(kakaoProfileDto.profileImageUrl()), - () -> assertThat(response.kakaoOauthToken()).isNotNull()); + () -> assertThat(response.signUpToken()).isNotNull()); assertThat(redisTemplate.opsForValue().get(SIGN_UP.addPrefix(email))) .as("카카오 인증 토큰을 저장한다.") - .isEqualTo(response.kakaoOauthToken()); + .isEqualTo(response.signUpToken()); } @Test @@ -75,21 +75,21 @@ class SignInTest extends BaseEndToEndTest { // stub - kakaoOAuthClient 가 정해진 사용자 프로필 정보를 반환하도록 String kakaoCode = "kakaoCode"; String email = "email@email.com"; - given(kakaoOAuthClient.processOauth(kakaoCode)) + given(kakaoOAuthClient.getUserInfo(kakaoCode)) .willReturn(createKakaoUserInfoDtoByEmail(email)); // setUp - 사용자 정보 저장 SiteUser siteUser = siteUserRepository.save(createSiteUserByEmail(email)); // request - body 생성 및 요청 - KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); - SignInResponse response = RestAssured.given().log().all() + OAuthCodeRequest oAuthCodeRequest = new OAuthCodeRequest(kakaoCode); + OAuthSignInResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) - .body(kakaoCodeRequest) + .body(oAuthCodeRequest) .when().post("/auth/kakao") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(SignInResponse.class); + .extract().as(OAuthSignInResponse.class); assertAll("리프레스 토큰과 엑세스 토큰을 응답한다.", () -> assertThat(response.isRegistered()).isTrue(), @@ -105,7 +105,7 @@ class SignInTest extends BaseEndToEndTest { // stub - kakaoOAuthClient 가 정해진 사용자 프로필 정보를 반환하도록 String kakaoCode = "kakaoCode"; String email = "email@email.com"; - given(kakaoOAuthClient.processOauth(kakaoCode)) + given(kakaoOAuthClient.getUserInfo(kakaoCode)) .willReturn(createKakaoUserInfoDtoByEmail(email)); // setUp - 계정 복구 기간이 되지 않은 사용자 저장 @@ -115,14 +115,14 @@ class SignInTest extends BaseEndToEndTest { SiteUser siteUser = siteUserRepository.save(siteUserFixture); // request - body 생성 및 요청 - KakaoCodeRequest kakaoCodeRequest = new KakaoCodeRequest(kakaoCode); - SignInResponse response = RestAssured.given().log().all() + OAuthCodeRequest OAuthCodeRequest = new OAuthCodeRequest(kakaoCode); + OAuthSignInResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) - .body(kakaoCodeRequest) + .body(OAuthCodeRequest) .when().post("/auth/kakao") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(SignInResponse.class); + .extract().as(OAuthSignInResponse.class); SiteUser updatedSiteUser = siteUserRepository.findById(siteUser.getId()).get(); assertAll("리프레스 토큰과 엑세스 토큰을 응답하고, 탈퇴 날짜를 초기화한다.", diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 1bbe150a8..7d01f365c 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -1,9 +1,9 @@ package com.example.solidconnection.e2e; +import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; -import com.example.solidconnection.auth.dto.SignUpResponse; import com.example.solidconnection.auth.service.AuthTokenProvider; -import com.example.solidconnection.auth.service.SignUpTokenProvider; +import com.example.solidconnection.auth.service.oauth.SignUpTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -29,8 +29,8 @@ import java.util.List; import static com.example.solidconnection.auth.domain.TokenType.REFRESH; -import static com.example.solidconnection.custom.exception.ErrorCode.JWT_EXCEPTION; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; +import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByNickName; @@ -74,20 +74,20 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); List interestedCountryNames = List.of("프랑스", "독일"); SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, interestedRegionNames, interestedCountryNames, PreparationStatus.CONSIDERING, "profile", Gender.FEMALE, "nickname", "2000-01-01"); - SignUpResponse response = RestAssured.given().log().all() + SignInResponse response = RestAssured.given().log().all() .contentType(ContentType.JSON) .body(signUpRequest) .when().post("/auth/sign-up") .then().log().all() .statusCode(HttpStatus.OK.value()) - .extract().as(SignUpResponse.class); + .extract().as(SignInResponse.class); SiteUser savedSiteUser = siteUserRepository.findByEmailAndAuthType(email, AuthType.KAKAO).get(); assertAll( @@ -126,7 +126,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -151,7 +151,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail); + String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -181,6 +181,6 @@ class SignUpTest extends BaseEndToEndTest { .extract().as(ErrorResponse.class); assertThat(errorResponse.message()) - .contains(JWT_EXCEPTION.getMessage()); + .contains(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); } } From 25e9f407b159d187538532a85f9c7fea6c038e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 8 Feb 2025 09:39:33 +0900 Subject: [PATCH 18/23] =?UTF-8?q?refactor:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EC=9D=84=20=EC=BB=AC=EB=9F=BC=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: issueDate 컬럼 제거 * chore: issueDate 컬럼 제거 마이그레이션 파일 추가 * test: 모든 테스트 코드 정상 작동하도록 수정 * style: 사용하지 않는 import문 제거 --- .../service/ApplicationSubmissionService.java | 1 - .../example/solidconnection/score/domain/GpaScore.java | 7 +------ .../score/domain/LanguageTestScore.java | 7 +------ .../solidconnection/score/dto/GpaScoreRequest.java | 5 ----- .../solidconnection/score/dto/GpaScoreStatus.java | 4 ---- .../score/dto/LanguageTestScoreRequest.java | 6 ------ .../score/dto/LanguageTestScoreStatus.java | 4 ---- .../score/dto/LanguageTestScoreStatusResponse.java | 1 - .../solidconnection/score/service/ScoreService.java | 4 ++-- .../db/migration/V4__remove_issue_date_columns.sql | 5 +++++ .../service/ApplicationSubmissionServiceTest.java | 10 ++-------- .../score/service/ScoreServiceTest.java | 9 +-------- .../support/integration/BaseIntegrationTest.java | 5 +---- 13 files changed, 13 insertions(+), 55 deletions(-) create mode 100644 src/main/resources/db/migration/V4__remove_issue_date_columns.sql diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index beb2f0cb0..c7652dce0 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -10,7 +10,6 @@ import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; -import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.type.VerifyStatus; import com.example.solidconnection.university.domain.UniversityInfoForApply; import com.example.solidconnection.university.repository.UniversityInfoForApplyRepository; diff --git a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java index 17b5cca48..54df13759 100644 --- a/src/main/java/com/example/solidconnection/score/domain/GpaScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/GpaScore.java @@ -18,8 +18,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDate; - @Getter @Entity @NoArgsConstructor @@ -33,8 +31,6 @@ public class GpaScore extends BaseEntity { @Embedded private Gpa gpa; - private LocalDate issueDate; - @Setter @Column(columnDefinition = "varchar(50) not null default 'PENDING'") @Enumerated(EnumType.STRING) @@ -45,10 +41,9 @@ public class GpaScore extends BaseEntity { @ManyToOne private SiteUser siteUser; - public GpaScore(Gpa gpa, SiteUser siteUser, LocalDate issueDate) { + public GpaScore(Gpa gpa, SiteUser siteUser) { this.gpa = gpa; this.siteUser = siteUser; - this.issueDate = issueDate; this.verifyStatus = VerifyStatus.PENDING; this.rejectedReason = null; } diff --git a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java index 88501f686..7939e1db8 100644 --- a/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java +++ b/src/main/java/com/example/solidconnection/score/domain/LanguageTestScore.java @@ -18,8 +18,6 @@ import lombok.NoArgsConstructor; import lombok.Setter; -import java.time.LocalDate; - @Getter @Entity @NoArgsConstructor @@ -33,8 +31,6 @@ public class LanguageTestScore extends BaseEntity { @Embedded private LanguageTest languageTest; - private LocalDate issueDate; - @Setter @Column(columnDefinition = "varchar(50) not null default 'PENDING'") @Enumerated(EnumType.STRING) @@ -45,9 +41,8 @@ public class LanguageTestScore extends BaseEntity { @ManyToOne private SiteUser siteUser; - public LanguageTestScore(LanguageTest languageTest, LocalDate issueDate, SiteUser siteUser) { + public LanguageTestScore(LanguageTest languageTest, SiteUser siteUser) { this.languageTest = languageTest; - this.issueDate = issueDate; this.verifyStatus = VerifyStatus.PENDING; this.siteUser = siteUser; } diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java index 5227ba9ed..613ac5b54 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreRequest.java @@ -4,8 +4,6 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - public record GpaScoreRequest( @NotNull(message = "학점을 입력해주세요.") Double gpa, @@ -13,9 +11,6 @@ public record GpaScoreRequest( @NotNull(message = "학점 기준을 입력해주세요.") Double gpaCriteria, - @NotNull(message = "발급일자를 입력해주세요.") - LocalDate issueDate, - @NotBlank(message = "대학 성적 증명서를 첨부해주세요.") String gpaReportUrl) { diff --git a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java index 0361cf0e7..5798e3cf0 100644 --- a/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java +++ b/src/main/java/com/example/solidconnection/score/dto/GpaScoreStatus.java @@ -4,12 +4,9 @@ import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.type.VerifyStatus; -import java.time.LocalDate; - public record GpaScoreStatus( Long id, Gpa gpa, - LocalDate issueDate, VerifyStatus verifyStatus, String rejectedReason ) { @@ -17,7 +14,6 @@ public static GpaScoreStatus from(GpaScore gpaScore) { return new GpaScoreStatus( gpaScore.getId(), gpaScore.getGpa(), - gpaScore.getIssueDate(), gpaScore.getVerifyStatus(), gpaScore.getRejectedReason() ); diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java index c39e5fcb9..92522949e 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreRequest.java @@ -1,13 +1,10 @@ package com.example.solidconnection.score.dto; - import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.type.LanguageTestType; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; -import java.time.LocalDate; - public record LanguageTestScoreRequest( @NotNull(message = "어학 종류를 입력해주세요.") LanguageTestType languageTestType, @@ -15,9 +12,6 @@ public record LanguageTestScoreRequest( @NotBlank(message = "어학 점수를 입력해주세요.") String languageTestScore, - @NotNull(message = "발급일자를 입력해주세요.") - LocalDate issueDate, - @NotBlank(message = "어학 증명서를 첨부해주세요.") String languageTestReportUrl) { diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java index 2d1d8fcb1..9e5fcae4f 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatus.java @@ -4,12 +4,9 @@ import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.type.VerifyStatus; -import java.time.LocalDate; - public record LanguageTestScoreStatus( Long id, LanguageTest languageTest, - LocalDate issueDate, VerifyStatus verifyStatus, String rejectedReason ) { @@ -17,7 +14,6 @@ public static LanguageTestScoreStatus from(LanguageTestScore languageTestScore) return new LanguageTestScoreStatus( languageTestScore.getId(), languageTestScore.getLanguageTest(), - languageTestScore.getIssueDate(), languageTestScore.getVerifyStatus(), languageTestScore.getRejectedReason() ); diff --git a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java index 3d4f74894..e19c0e855 100644 --- a/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java +++ b/src/main/java/com/example/solidconnection/score/dto/LanguageTestScoreStatusResponse.java @@ -1,6 +1,5 @@ package com.example.solidconnection.score.dto; - import java.util.List; public record LanguageTestScoreStatusResponse( diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index e6c9d5c6e..45efb2aa1 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -30,7 +30,7 @@ public class ScoreService { @Transactional public Long submitGpaScore(SiteUser siteUser, GpaScoreRequest gpaScoreRequest) { - GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser, gpaScoreRequest.issueDate()); + GpaScore newGpaScore = new GpaScore(gpaScoreRequest.toGpa(), siteUser); newGpaScore.setSiteUser(siteUser); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); // 저장 후 반환된 객체 return savedNewGpaScore.getId(); // 저장된 GPA Score의 ID 반환 @@ -41,7 +41,7 @@ public Long submitLanguageTestScore(SiteUser siteUser, LanguageTestScoreRequest LanguageTest languageTest = languageTestScoreRequest.toLanguageTest(); LanguageTestScore newScore = new LanguageTestScore( - languageTest, languageTestScoreRequest.issueDate(), siteUser); + languageTest, siteUser); newScore.setSiteUser(siteUser); LanguageTestScore savedNewScore = languageTestScoreRepository.save(newScore); // 새로 저장한 객체 return savedNewScore.getId(); // 저장된 객체의 ID 반환 diff --git a/src/main/resources/db/migration/V4__remove_issue_date_columns.sql b/src/main/resources/db/migration/V4__remove_issue_date_columns.sql new file mode 100644 index 000000000..9a8e0700b --- /dev/null +++ b/src/main/resources/db/migration/V4__remove_issue_date_columns.sql @@ -0,0 +1,5 @@ +ALTER TABLE gpa_score + DROP COLUMN issue_date; + +ALTER TABLE language_test_score + DROP COLUMN issue_date; \ No newline at end of file diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index 911172bfd..1d40d094b 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -19,8 +19,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.time.LocalDate; - import static com.example.solidconnection.application.service.ApplicationSubmissionService.APPLICATION_UPDATE_COUNT_LIMIT; import static com.example.solidconnection.custom.exception.ErrorCode.APPLY_UPDATE_LIMIT_EXCEED; import static com.example.solidconnection.custom.exception.ErrorCode.CANT_APPLY_FOR_SAME_UNIVERSITY; @@ -165,8 +163,7 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { private GpaScore createUnapprovedGpaScore(SiteUser siteUser) { GpaScore gpaScore = new GpaScore( new Gpa(4.0, 4.5, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); return gpaScoreRepository.save(gpaScore); } @@ -174,8 +171,7 @@ private GpaScore createUnapprovedGpaScore(SiteUser siteUser) { private GpaScore createApprovedGpaScore(SiteUser siteUser) { GpaScore gpaScore = new GpaScore( new Gpa(4.0, 4.5, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); gpaScore.setVerifyStatus(VerifyStatus.APPROVED); return gpaScoreRepository.save(gpaScore); @@ -184,7 +180,6 @@ private GpaScore createApprovedGpaScore(SiteUser siteUser) { private LanguageTestScore createUnapprovedLanguageTestScore(SiteUser siteUser) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), - LocalDate.now(), siteUser ); return languageTestScoreRepository.save(languageTestScore); @@ -193,7 +188,6 @@ private LanguageTestScore createUnapprovedLanguageTestScore(SiteUser siteUser) { private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), - LocalDate.now(), siteUser ); languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 681b708a2..038aa91b6 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -24,7 +24,6 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import java.time.LocalDate; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -129,7 +128,6 @@ class ScoreServiceTest extends BaseIntegrationTest { () -> assertThat(savedScore.getId()).isEqualTo(scoreId), () -> assertThat(savedScore.getGpa().getGpa()).isEqualTo(request.gpa()), () -> assertThat(savedScore.getGpa().getGpaCriteria()).isEqualTo(request.gpaCriteria()), - () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) ); } @@ -149,7 +147,6 @@ class ScoreServiceTest extends BaseIntegrationTest { () -> assertThat(savedScore.getId()).isEqualTo(scoreId), () -> assertThat(savedScore.getLanguageTest().getLanguageTestType()).isEqualTo(request.languageTestType()), () -> assertThat(savedScore.getLanguageTest().getLanguageTestScore()).isEqualTo(request.languageTestScore()), - () -> assertThat(savedScore.getIssueDate()).isEqualTo(request.issueDate()), () -> assertThat(savedScore.getVerifyStatus()).isEqualTo(VerifyStatus.PENDING) ); } @@ -170,8 +167,7 @@ private SiteUser createSiteUser() { private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteria) { GpaScore gpaScore = new GpaScore( new Gpa(gpa, gpaCriteria, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); gpaScore.setSiteUser(siteUser); return gpaScoreRepository.save(gpaScore); @@ -180,7 +176,6 @@ private GpaScore createGpaScore(SiteUser siteUser, double gpa, double gpaCriteri private LanguageTestScore createLanguageTestScore(SiteUser siteUser, LanguageTestType languageTestType, String score) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(languageTestType, score, "/gpa-report.pdf"), - LocalDate.now(), siteUser ); languageTestScore.setSiteUser(siteUser); @@ -191,7 +186,6 @@ private GpaScoreRequest createGpaScoreRequest() { return new GpaScoreRequest( 3.5, 4.5, - LocalDate.now(), "/gpa-report.pdf" ); } @@ -200,7 +194,6 @@ private LanguageTestScoreRequest createLanguageTestScoreRequest() { return new LanguageTestScoreRequest( LanguageTestType.TOEFL_IBT, "100", - LocalDate.now(), "/gpa-report.pdf" ); } diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index ec29b8499..f7378468c 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -39,7 +39,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; -import java.time.LocalDate; import java.util.HashSet; import java.util.List; @@ -511,8 +510,7 @@ private void saveLanguageTestRequirement( private GpaScore createApprovedGpaScore(SiteUser siteUser) { GpaScore gpaScore = new GpaScore( new Gpa(4.0, 4.5, "/gpa-report.pdf"), - siteUser, - LocalDate.now() + siteUser ); gpaScore.setVerifyStatus(VerifyStatus.APPROVED); return gpaScoreRepository.save(gpaScore); @@ -521,7 +519,6 @@ private GpaScore createApprovedGpaScore(SiteUser siteUser) { private LanguageTestScore createApprovedLanguageTestScore(SiteUser siteUser) { LanguageTestScore languageTestScore = new LanguageTestScore( new LanguageTest(LanguageTestType.TOEIC, "100", "/gpa-report.pdf"), - LocalDate.now(), siteUser ); languageTestScore.setVerifyStatus(VerifyStatus.APPROVED); From 2c8b20078fa9465be1909bef021b439f42460299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:43:32 +0900 Subject: [PATCH 19/23] =?UTF-8?q?refactor:=20=EC=BB=A4=EB=AE=A4=EB=8B=88?= =?UTF-8?q?=ED=8B=B0=20=EA=B4=80=EB=A0=A8=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EA=B0=9C=EC=84=A0=20(#185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: board, comment, post 패키지를 community 패키지로 이동 및 통합 * refactor: 게시글 목록 조회 API PostController로 이동 * test: 변경된 패키지 구조에 맞게 테스트 구조 변경 * refactor: 레이어별 패키지 구조를 도메인 중심으로 변경 --- .../board/service/BoardService.java | 61 --------------- .../board/controller/BoardController.java | 18 +---- .../{ => community}/board/domain/Board.java | 4 +- .../board/dto/PostFindBoardResponse.java | 4 +- .../board/repository/BoardRepository.java | 4 +- .../community/board/service/BoardService.java | 9 +++ .../comment/controller/CommentController.java | 14 ++-- .../comment/domain/Comment.java | 4 +- .../comment/dto/CommentCreateRequest.java | 6 +- .../comment/dto/CommentCreateResponse.java | 4 +- .../comment/dto/CommentDeleteResponse.java | 2 +- .../comment/dto/CommentUpdateRequest.java | 2 +- .../comment/dto/CommentUpdateResponse.java | 4 +- .../comment/dto/PostFindCommentResponse.java | 4 +- .../comment/repository/CommentRepository.java | 4 +- .../comment/service/CommentService.java | 24 +++--- .../post/controller/PostController.java | 35 ++++++--- .../{ => community}/post/domain/Post.java | 9 +-- .../post/domain}/PostImage.java | 3 +- .../{ => community}/post/domain/PostLike.java | 2 +- .../post/dto/PostCreateRequest.java | 6 +- .../post/dto/PostCreateResponse.java | 4 +- .../post/dto/PostDeleteResponse.java | 2 +- .../post/dto/PostDislikeResponse.java | 4 +- .../post/dto/PostFindPostImageResponse.java | 4 +- .../post/dto/PostFindResponse.java | 8 +- .../post/dto/PostLikeResponse.java | 4 +- .../post/dto/PostListResponse.java} | 16 ++-- .../post/dto/PostUpdateRequest.java | 2 +- .../post/dto/PostUpdateResponse.java | 4 +- .../post/repository}/PostImageRepository.java | 4 +- .../post/repository/PostLikeRepository.java | 6 +- .../post/repository/PostRepository.java | 4 +- .../post/service/PostCommandService.java | 22 +++--- .../post/service/PostLikeService.java | 14 ++-- .../post/service/PostQueryService.java | 56 +++++++++++--- .../service/UpdateViewCountService.java | 4 +- .../siteuser/domain/SiteUser.java | 6 +- .../board/service/BoardServiceTest.java | 72 ----------------- .../comment/service/CommentServiceTest.java | 26 +++---- .../post/service/PostCommandServiceTest.java | 22 +++--- .../post/service/PostLikeServiceTest.java | 14 ++-- .../post/service/PostQueryServiceTest.java | 77 ++++++++++++++++--- .../PostLikeCountConcurrencyTest.java | 10 +-- .../PostViewCountConcurrencyTest.java | 8 +- .../integration/BaseIntegrationTest.java | 12 +-- 46 files changed, 293 insertions(+), 335 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/board/service/BoardService.java rename src/main/java/com/example/solidconnection/{ => community}/board/controller/BoardController.java (52%) rename src/main/java/com/example/solidconnection/{ => community}/board/domain/Board.java (85%) rename src/main/java/com/example/solidconnection/{ => community}/board/dto/PostFindBoardResponse.java (69%) rename src/main/java/com/example/solidconnection/{ => community}/board/repository/BoardRepository.java (88%) create mode 100644 src/main/java/com/example/solidconnection/community/board/service/BoardService.java rename src/main/java/com/example/solidconnection/{ => community}/comment/controller/CommentController.java (80%) rename src/main/java/com/example/solidconnection/{ => community}/comment/domain/Comment.java (96%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentCreateRequest.java (81%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentCreateResponse.java (62%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentDeleteResponse.java (50%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentUpdateRequest.java (85%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/CommentUpdateResponse.java (62%) rename src/main/java/com/example/solidconnection/{ => community}/comment/dto/PostFindCommentResponse.java (88%) rename src/main/java/com/example/solidconnection/{ => community}/comment/repository/CommentRepository.java (91%) rename src/main/java/com/example/solidconnection/{ => community}/comment/service/CommentService.java (84%) rename src/main/java/com/example/solidconnection/{ => community}/post/controller/PostController.java (75%) rename src/main/java/com/example/solidconnection/{ => community}/post/domain/Post.java (92%) rename src/main/java/com/example/solidconnection/{entity => community/post/domain}/PostImage.java (90%) rename src/main/java/com/example/solidconnection/{ => community}/post/domain/PostLike.java (96%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostCreateRequest.java (87%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostCreateResponse.java (62%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostDeleteResponse.java (50%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostDislikeResponse.java (68%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostFindPostImageResponse.java (82%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostFindResponse.java (86%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostLikeResponse.java (68%) rename src/main/java/com/example/solidconnection/{post/dto/BoardFindPostResponse.java => community/post/dto/PostListResponse.java} (72%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostUpdateRequest.java (92%) rename src/main/java/com/example/solidconnection/{ => community}/post/dto/PostUpdateResponse.java (62%) rename src/main/java/com/example/solidconnection/{repositories => community/post/repository}/PostImageRepository.java (61%) rename src/main/java/com/example/solidconnection/{ => community}/post/repository/PostLikeRepository.java (79%) rename src/main/java/com/example/solidconnection/{ => community}/post/repository/PostRepository.java (93%) rename src/main/java/com/example/solidconnection/{ => community}/post/service/PostCommandService.java (87%) rename src/main/java/com/example/solidconnection/{ => community}/post/service/PostLikeService.java (83%) rename src/main/java/com/example/solidconnection/{ => community}/post/service/PostQueryService.java (55%) delete mode 100644 src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java rename src/test/java/com/example/solidconnection/{ => community}/comment/service/CommentServiceTest.java (95%) rename src/test/java/com/example/solidconnection/{ => community}/post/service/PostCommandServiceTest.java (94%) rename src/test/java/com/example/solidconnection/{ => community}/post/service/PostLikeServiceTest.java (91%) rename src/test/java/com/example/solidconnection/{ => community}/post/service/PostQueryServiceTest.java (60%) diff --git a/src/main/java/com/example/solidconnection/board/service/BoardService.java b/src/main/java/com/example/solidconnection/board/service/BoardService.java deleted file mode 100644 index 2513e0903..000000000 --- a/src/main/java/com/example/solidconnection/board/service/BoardService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.example.solidconnection.board.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.custom.exception.ErrorCode; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.BoardFindPostResponse; -import com.example.solidconnection.type.BoardCode; -import com.example.solidconnection.type.PostCategory; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.EnumUtils; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.stream.Collectors; - -import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; - -@Service -@RequiredArgsConstructor -public class BoardService { - private final BoardRepository boardRepository; - - @Transactional(readOnly = true) - public List findPostsByCodeAndPostCategory(String code, String category) { - - String boardCode = validateCode(code); - PostCategory postCategory = validatePostCategory(category); - - Board board = boardRepository.getByCodeUsingEntityGraph(boardCode); - List postList = getPostListByPostCategory(board.getPostList(), postCategory); - - return BoardFindPostResponse.from(postList); - } - - private String validateCode(String code) { - try { - return String.valueOf(BoardCode.valueOf(code)); - } catch (IllegalArgumentException ex) { - throw new CustomException(ErrorCode.INVALID_BOARD_CODE); - } - } - - private PostCategory validatePostCategory(String category) { - if (!EnumUtils.isValidEnum(PostCategory.class, category)) { - throw new CustomException(INVALID_POST_CATEGORY); - } - return PostCategory.valueOf(category); - } - - private List getPostListByPostCategory(List postList, PostCategory postCategory) { - if (postCategory.equals(PostCategory.전체)) { - return postList; - } - return postList.stream() - .filter(post -> post.getCategory().equals(postCategory)) - .collect(Collectors.toList()); - } -} diff --git a/src/main/java/com/example/solidconnection/board/controller/BoardController.java b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java similarity index 52% rename from src/main/java/com/example/solidconnection/board/controller/BoardController.java rename to src/main/java/com/example/solidconnection/community/board/controller/BoardController.java index f6ebb27d0..9329535a1 100644 --- a/src/main/java/com/example/solidconnection/board/controller/BoardController.java +++ b/src/main/java/com/example/solidconnection/community/board/controller/BoardController.java @@ -1,14 +1,10 @@ -package com.example.solidconnection.board.controller; +package com.example.solidconnection.community.board.controller; -import com.example.solidconnection.board.service.BoardService; -import com.example.solidconnection.post.dto.BoardFindPostResponse; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; @@ -19,8 +15,6 @@ @RequestMapping("/communities") public class BoardController { - private final BoardService boardService; - // todo: 회원별로 접근 가능한 게시판 목록 조회 기능 개발 @GetMapping() public ResponseEntity findAccessibleCodes() { @@ -30,14 +24,4 @@ public ResponseEntity findAccessibleCodes() { } return ResponseEntity.ok().body(accessibleCodeList); } - - @GetMapping("/{code}") - public ResponseEntity findPostsByCodeAndCategory( - @PathVariable(value = "code") String code, - @RequestParam(value = "category", defaultValue = "전체") String category) { - - List postsByCodeAndPostCategory = boardService - .findPostsByCodeAndPostCategory(code, category); - return ResponseEntity.ok().body(postsByCodeAndPostCategory); - } } diff --git a/src/main/java/com/example/solidconnection/board/domain/Board.java b/src/main/java/com/example/solidconnection/community/board/domain/Board.java similarity index 85% rename from src/main/java/com/example/solidconnection/board/domain/Board.java rename to src/main/java/com/example/solidconnection/community/board/domain/Board.java index 77d0aada8..fbf13b44d 100644 --- a/src/main/java/com/example/solidconnection/board/domain/Board.java +++ b/src/main/java/com/example/solidconnection/community/board/domain/Board.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.board.domain; +package com.example.solidconnection.community.board.domain; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; diff --git a/src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java b/src/main/java/com/example/solidconnection/community/board/dto/PostFindBoardResponse.java similarity index 69% rename from src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java rename to src/main/java/com/example/solidconnection/community/board/dto/PostFindBoardResponse.java index b06baa305..e4f66afdd 100644 --- a/src/main/java/com/example/solidconnection/board/dto/PostFindBoardResponse.java +++ b/src/main/java/com/example/solidconnection/community/board/dto/PostFindBoardResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.board.dto; +package com.example.solidconnection.community.board.dto; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; public record PostFindBoardResponse( String code, diff --git a/src/main/java/com/example/solidconnection/board/repository/BoardRepository.java b/src/main/java/com/example/solidconnection/community/board/repository/BoardRepository.java similarity index 88% rename from src/main/java/com/example/solidconnection/board/repository/BoardRepository.java rename to src/main/java/com/example/solidconnection/community/board/repository/BoardRepository.java index 5c4538279..06dd01161 100644 --- a/src/main/java/com/example/solidconnection/board/repository/BoardRepository.java +++ b/src/main/java/com/example/solidconnection/community/board/repository/BoardRepository.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.board.repository; +package com.example.solidconnection.community.board.repository; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.custom.exception.CustomException; import com.example.solidconnection.custom.exception.ErrorCode; import org.springframework.data.jpa.repository.EntityGraph; diff --git a/src/main/java/com/example/solidconnection/community/board/service/BoardService.java b/src/main/java/com/example/solidconnection/community/board/service/BoardService.java new file mode 100644 index 000000000..c918f8126 --- /dev/null +++ b/src/main/java/com/example/solidconnection/community/board/service/BoardService.java @@ -0,0 +1,9 @@ +package com.example.solidconnection.community.board.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class BoardService { +} diff --git a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java b/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java similarity index 80% rename from src/main/java/com/example/solidconnection/comment/controller/CommentController.java rename to src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java index fda360b4a..e215fea72 100644 --- a/src/main/java/com/example/solidconnection/comment/controller/CommentController.java +++ b/src/main/java/com/example/solidconnection/community/comment/controller/CommentController.java @@ -1,11 +1,11 @@ -package com.example.solidconnection.comment.controller; +package com.example.solidconnection.community.comment.controller; -import com.example.solidconnection.comment.dto.CommentCreateRequest; -import com.example.solidconnection.comment.dto.CommentCreateResponse; -import com.example.solidconnection.comment.dto.CommentDeleteResponse; -import com.example.solidconnection.comment.dto.CommentUpdateRequest; -import com.example.solidconnection.comment.dto.CommentUpdateResponse; -import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.community.comment.dto.CommentCreateRequest; +import com.example.solidconnection.community.comment.dto.CommentCreateResponse; +import com.example.solidconnection.community.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.community.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.community.comment.service.CommentService; import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; diff --git a/src/main/java/com/example/solidconnection/comment/domain/Comment.java b/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java similarity index 96% rename from src/main/java/com/example/solidconnection/comment/domain/Comment.java rename to src/main/java/com/example/solidconnection/community/comment/domain/Comment.java index a4d147a61..abed4b8f0 100644 --- a/src/main/java/com/example/solidconnection/comment/domain/Comment.java +++ b/src/main/java/com/example/solidconnection/community/comment/domain/Comment.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.comment.domain; +package com.example.solidconnection.community.comment.domain; import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.persistence.CascadeType; import jakarta.persistence.Column; diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java similarity index 81% rename from src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java index c2065685b..610f602c8 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateRequest.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateRequest.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateResponse.java index 60d7529c2..58964f326 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentCreateResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentCreateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; public record CommentCreateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentDeleteResponse.java similarity index 50% rename from src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentDeleteResponse.java index 393e4fe8b..5283bb87f 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentDeleteResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentDeleteResponse.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; public record CommentDeleteResponse( Long id diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateRequest.java similarity index 85% rename from src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateRequest.java index d99429931..6e14dab45 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateRequest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; diff --git a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateResponse.java index b621ab111..5446753e4 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/CommentUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/CommentUpdateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; public record CommentUpdateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java b/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java similarity index 88% rename from src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java rename to src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java index a0d68066a..f1fd78ad0 100644 --- a/src/main/java/com/example/solidconnection/comment/dto/PostFindCommentResponse.java +++ b/src/main/java/com/example/solidconnection/community/comment/dto/PostFindCommentResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.dto; +package com.example.solidconnection.community.comment.dto; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import java.time.ZonedDateTime; diff --git a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java similarity index 91% rename from src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java rename to src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java index ce37c42a1..e5feb3f04 100644 --- a/src/main/java/com/example/solidconnection/comment/repository/CommentRepository.java +++ b/src/main/java/com/example/solidconnection/community/comment/repository/CommentRepository.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.comment.repository; +package com.example.solidconnection.community.comment.repository; -import com.example.solidconnection.comment.domain.Comment; +import com.example.solidconnection.community.comment.domain.Comment; import com.example.solidconnection.custom.exception.CustomException; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; diff --git a/src/main/java/com/example/solidconnection/comment/service/CommentService.java b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java similarity index 84% rename from src/main/java/com/example/solidconnection/comment/service/CommentService.java rename to src/main/java/com/example/solidconnection/community/comment/service/CommentService.java index b7c1c6068..209dd6987 100644 --- a/src/main/java/com/example/solidconnection/comment/service/CommentService.java +++ b/src/main/java/com/example/solidconnection/community/comment/service/CommentService.java @@ -1,16 +1,16 @@ -package com.example.solidconnection.comment.service; - -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.CommentCreateRequest; -import com.example.solidconnection.comment.dto.CommentCreateResponse; -import com.example.solidconnection.comment.dto.CommentDeleteResponse; -import com.example.solidconnection.comment.dto.CommentUpdateRequest; -import com.example.solidconnection.comment.dto.CommentUpdateResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.repository.CommentRepository; +package com.example.solidconnection.community.comment.service; + +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.comment.dto.CommentCreateRequest; +import com.example.solidconnection.community.comment.dto.CommentCreateResponse; +import com.example.solidconnection.community.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.community.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.comment.repository.CommentRepository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; diff --git a/src/main/java/com/example/solidconnection/post/controller/PostController.java b/src/main/java/com/example/solidconnection/community/post/controller/PostController.java similarity index 75% rename from src/main/java/com/example/solidconnection/post/controller/PostController.java rename to src/main/java/com/example/solidconnection/community/post/controller/PostController.java index bc3f9d123..a2479f08b 100644 --- a/src/main/java/com/example/solidconnection/post/controller/PostController.java +++ b/src/main/java/com/example/solidconnection/community/post/controller/PostController.java @@ -1,17 +1,18 @@ -package com.example.solidconnection.post.controller; +package com.example.solidconnection.community.post.controller; +import com.example.solidconnection.community.post.dto.PostListResponse; import com.example.solidconnection.custom.resolver.AuthorizedUser; -import com.example.solidconnection.post.dto.PostCreateRequest; -import com.example.solidconnection.post.dto.PostCreateResponse; -import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; -import com.example.solidconnection.post.dto.PostUpdateRequest; -import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.service.PostCommandService; -import com.example.solidconnection.post.service.PostLikeService; -import com.example.solidconnection.post.service.PostQueryService; +import com.example.solidconnection.community.post.dto.PostCreateRequest; +import com.example.solidconnection.community.post.dto.PostCreateResponse; +import com.example.solidconnection.community.post.dto.PostDeleteResponse; +import com.example.solidconnection.community.post.dto.PostDislikeResponse; +import com.example.solidconnection.community.post.dto.PostFindResponse; +import com.example.solidconnection.community.post.dto.PostLikeResponse; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateResponse; +import com.example.solidconnection.community.post.service.PostCommandService; +import com.example.solidconnection.community.post.service.PostLikeService; +import com.example.solidconnection.community.post.service.PostQueryService; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -39,6 +40,16 @@ public class PostController { private final PostCommandService postCommandService; private final PostLikeService postLikeService; + @GetMapping("/{code}") + public ResponseEntity findPostsByCodeAndCategory( + @PathVariable(value = "code") String code, + @RequestParam(value = "category", defaultValue = "전체") String category) { + + List postsByCodeAndPostCategory = postQueryService + .findPostsByCodeAndPostCategory(code, category); + return ResponseEntity.ok().body(postsByCodeAndPostCategory); + } + @PostMapping(value = "/{code}/posts") public ResponseEntity createPost( @AuthorizedUser SiteUser siteUser, diff --git a/src/main/java/com/example/solidconnection/post/domain/Post.java b/src/main/java/com/example/solidconnection/community/post/domain/Post.java similarity index 92% rename from src/main/java/com/example/solidconnection/post/domain/Post.java rename to src/main/java/com/example/solidconnection/community/post/domain/Post.java index 31125f8bd..4d96b9b22 100644 --- a/src/main/java/com/example/solidconnection/post/domain/Post.java +++ b/src/main/java/com/example/solidconnection/community/post/domain/Post.java @@ -1,10 +1,9 @@ -package com.example.solidconnection.post.domain; +package com.example.solidconnection.community.post.domain; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.comment.domain.Comment; import com.example.solidconnection.entity.common.BaseEntity; -import com.example.solidconnection.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; import jakarta.persistence.CascadeType; diff --git a/src/main/java/com/example/solidconnection/entity/PostImage.java b/src/main/java/com/example/solidconnection/community/post/domain/PostImage.java similarity index 90% rename from src/main/java/com/example/solidconnection/entity/PostImage.java rename to src/main/java/com/example/solidconnection/community/post/domain/PostImage.java index 653beecc4..5bf885741 100644 --- a/src/main/java/com/example/solidconnection/entity/PostImage.java +++ b/src/main/java/com/example/solidconnection/community/post/domain/PostImage.java @@ -1,6 +1,5 @@ -package com.example.solidconnection.entity; +package com.example.solidconnection.community.post.domain; -import com.example.solidconnection.post.domain.Post; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; diff --git a/src/main/java/com/example/solidconnection/post/domain/PostLike.java b/src/main/java/com/example/solidconnection/community/post/domain/PostLike.java similarity index 96% rename from src/main/java/com/example/solidconnection/post/domain/PostLike.java rename to src/main/java/com/example/solidconnection/community/post/domain/PostLike.java index 9edf4052e..bbe1ff361 100644 --- a/src/main/java/com/example/solidconnection/post/domain/PostLike.java +++ b/src/main/java/com/example/solidconnection/community/post/domain/PostLike.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.post.domain; +package com.example.solidconnection.community.post.domain; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.persistence.Entity; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java similarity index 87% rename from src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java index a1ba1c696..db271a80f 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateRequest.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateRequest.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.PostCategory; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostCreateResponse.java index a514ffca6..51cc0c72e 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostCreateResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostCreateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostCreateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostDeleteResponse.java similarity index 50% rename from src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostDeleteResponse.java index 23c67670d..f98f5264f 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostDeleteResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostDeleteResponse.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; public record PostDeleteResponse( Long id diff --git a/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostDislikeResponse.java similarity index 68% rename from src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostDislikeResponse.java index 14de9987d..83ffc8305 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostDislikeResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostDislikeResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostDislikeResponse( Long likeCount, diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostFindPostImageResponse.java similarity index 82% rename from src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostFindPostImageResponse.java index 63adf0020..648bdb72c 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindPostImageResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostFindPostImageResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.post.domain.PostImage; import java.util.List; import java.util.stream.Collectors; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostFindResponse.java similarity index 86% rename from src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostFindResponse.java index 1562dd5bc..735defac1 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostFindResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostFindResponse.java @@ -1,8 +1,8 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.board.dto.PostFindBoardResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.board.dto.PostFindBoardResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.post.domain.Post; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import java.time.ZonedDateTime; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostLikeResponse.java similarity index 68% rename from src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostLikeResponse.java index 35d7d58c9..35b2840c0 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostLikeResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostLikeResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostLikeResponse( Long likeCount, diff --git a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostListResponse.java similarity index 72% rename from src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostListResponse.java index 4f475824c..f02af017e 100644 --- a/src/main/java/com/example/solidconnection/post/dto/BoardFindPostResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostListResponse.java @@ -1,13 +1,13 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; import java.time.ZonedDateTime; import java.util.List; import java.util.stream.Collectors; -public record BoardFindPostResponse( +public record PostListResponse( Long id, String title, String content, @@ -19,8 +19,8 @@ public record BoardFindPostResponse( String url ) { - public static BoardFindPostResponse from(Post post) { - return new BoardFindPostResponse( + public static PostListResponse from(Post post) { + return new PostListResponse( post.getId(), post.getTitle(), post.getContent(), @@ -33,9 +33,9 @@ public static BoardFindPostResponse from(Post post) { ); } - public static List from(List postList) { + public static List from(List postList) { return postList.stream() - .map(BoardFindPostResponse::from) + .map(PostListResponse::from) .collect(Collectors.toList()); } diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateRequest.java similarity index 92% rename from src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostUpdateRequest.java index b9bdc6f54..339be3519 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateRequest.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateRequest.java @@ -1,4 +1,4 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; diff --git a/src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateResponse.java similarity index 62% rename from src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java rename to src/main/java/com/example/solidconnection/community/post/dto/PostUpdateResponse.java index 70d656766..5c35f031d 100644 --- a/src/main/java/com/example/solidconnection/post/dto/PostUpdateResponse.java +++ b/src/main/java/com/example/solidconnection/community/post/dto/PostUpdateResponse.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.post.dto; +package com.example.solidconnection.community.post.dto; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; public record PostUpdateResponse( Long id diff --git a/src/main/java/com/example/solidconnection/repositories/PostImageRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostImageRepository.java similarity index 61% rename from src/main/java/com/example/solidconnection/repositories/PostImageRepository.java rename to src/main/java/com/example/solidconnection/community/post/repository/PostImageRepository.java index 0ae776877..54c43f375 100644 --- a/src/main/java/com/example/solidconnection/repositories/PostImageRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostImageRepository.java @@ -1,6 +1,6 @@ -package com.example.solidconnection.repositories; +package com.example.solidconnection.community.post.repository; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.post.domain.PostImage; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java similarity index 79% rename from src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java rename to src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java index bebde7a92..417e97310 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostLikeRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostLikeRepository.java @@ -1,8 +1,8 @@ -package com.example.solidconnection.post.repository; +package com.example.solidconnection.community.post.repository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostLike; import com.example.solidconnection.siteuser.domain.SiteUser; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; diff --git a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java similarity index 93% rename from src/main/java/com/example/solidconnection/post/repository/PostRepository.java rename to src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java index e96881147..336189b05 100644 --- a/src/main/java/com/example/solidconnection/post/repository/PostRepository.java +++ b/src/main/java/com/example/solidconnection/community/post/repository/PostRepository.java @@ -1,7 +1,7 @@ -package com.example.solidconnection.post.repository; +package com.example.solidconnection.community.post.repository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; +import com.example.solidconnection.community.post.domain.Post; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; diff --git a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java similarity index 87% rename from src/main/java/com/example/solidconnection/post/service/PostCommandService.java rename to src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java index 74eb86310..1e66b52a4 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostCommandService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostCommandService.java @@ -1,16 +1,16 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostCreateRequest; -import com.example.solidconnection.post.dto.PostCreateResponse; -import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostUpdateRequest; -import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostCreateRequest; +import com.example.solidconnection.community.post.dto.PostCreateResponse; +import com.example.solidconnection.community.post.dto.PostDeleteResponse; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateResponse; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; diff --git a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java b/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java similarity index 83% rename from src/main/java/com/example/solidconnection/post/service/PostLikeService.java rename to src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java index 5aaf994c7..045c069cd 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostLikeService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostLikeService.java @@ -1,12 +1,12 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostLike; +import com.example.solidconnection.community.post.dto.PostDislikeResponse; +import com.example.solidconnection.community.post.dto.PostLikeResponse; +import com.example.solidconnection.community.post.repository.PostLikeRepository; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.type.BoardCode; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java similarity index 55% rename from src/main/java/com/example/solidconnection/post/service/PostQueryService.java rename to src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java index a45ca3968..1d7f292ea 100644 --- a/src/main/java/com/example/solidconnection/post/service/PostQueryService.java +++ b/src/main/java/com/example/solidconnection/community/post/service/PostQueryService.java @@ -1,36 +1,56 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.dto.PostFindBoardResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.service.CommentService; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.dto.PostFindBoardResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.post.dto.PostListResponse; +import com.example.solidconnection.community.board.repository.BoardRepository; +import com.example.solidconnection.community.comment.service.CommentService; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.community.post.dto.PostFindResponse; +import com.example.solidconnection.community.post.repository.PostLikeRepository; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.dto.PostFindSiteUserResponse; import com.example.solidconnection.type.BoardCode; +import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.EnumUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; +import java.util.stream.Collectors; import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_BOARD_CODE; +import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_POST_CATEGORY; @Service @RequiredArgsConstructor public class PostQueryService { + private final BoardRepository boardRepository; private final PostRepository postRepository; + private final PostLikeRepository postLikeRepository; private final CommentService commentService; private final RedisService redisService; private final RedisUtils redisUtils; - private final PostLikeRepository postLikeRepository; + + @Transactional(readOnly = true) + public List findPostsByCodeAndPostCategory(String code, String category) { + + String boardCode = validateCode(code); + PostCategory postCategory = validatePostCategory(category); + + Board board = boardRepository.getByCodeUsingEntityGraph(boardCode); + List postList = getPostListByPostCategory(board.getPostList(), postCategory); + + return PostListResponse.from(postList); + } @Transactional(readOnly = true) public PostFindResponse findPostById(SiteUser siteUser, String code, Long postId) { @@ -70,4 +90,20 @@ private Boolean getIsLiked(Post post, SiteUser siteUser) { return postLikeRepository.findPostLikeByPostAndSiteUser(post, siteUser) .isPresent(); } + + private PostCategory validatePostCategory(String category) { + if (!EnumUtils.isValidEnum(PostCategory.class, category)) { + throw new CustomException(INVALID_POST_CATEGORY); + } + return PostCategory.valueOf(category); + } + + private List getPostListByPostCategory(List postList, PostCategory postCategory) { + if (postCategory.equals(PostCategory.전체)) { + return postList; + } + return postList.stream() + .filter(post -> post.getCategory().equals(postCategory)) + .collect(Collectors.toList()); + } } diff --git a/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java index 55d4d9eba..2b67e25ec 100644 --- a/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java +++ b/src/main/java/com/example/solidconnection/service/UpdateViewCountService.java @@ -1,7 +1,7 @@ package com.example.solidconnection.service; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.util.RedisUtils; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 2c2a5d8be..83a439b19 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -1,8 +1,8 @@ package com.example.solidconnection.siteuser.domain; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.domain.PostLike; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.domain.PostLike; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.type.Gender; diff --git a/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java b/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java deleted file mode 100644 index 98c2b28fa..000000000 --- a/src/test/java/com/example/solidconnection/board/service/BoardServiceTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.example.solidconnection.board.service; - -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.BoardFindPostResponse; -import com.example.solidconnection.support.integration.BaseIntegrationTest; -import com.example.solidconnection.type.BoardCode; -import com.example.solidconnection.type.PostCategory; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.time.ZonedDateTime; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -@DisplayName("게시판 서비스 테스트") -class BoardServiceTest extends BaseIntegrationTest { - - @Autowired - private BoardService boardService; - - @Test - void 게시판_코드와_카테고리로_게시글_목록을_조회한다() { - // given - List posts = List.of( - 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, - 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 - ); - List expectedPosts = posts.stream() - .filter(post -> post.getCategory().equals(PostCategory.자유) && post.getBoard().getCode().equals(BoardCode.FREE.name())) - .toList(); - List expectedResponses = BoardFindPostResponse.from(expectedPosts); - - // when - List actualResponses = boardService.findPostsByCodeAndPostCategory( - BoardCode.FREE.name(), - PostCategory.자유.name() - ); - - // then - assertThat(actualResponses) - .usingRecursiveComparison() - .ignoringFieldsOfTypes(ZonedDateTime.class) - .isEqualTo(expectedResponses); - } - - @Test - void 전체_카테고리로_조회시_해당_게시판의_모든_게시글을_조회한다() { - // given - List posts = List.of( - 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, - 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 - ); - List expectedPosts = posts.stream() - .filter(post -> post.getBoard().getCode().equals(BoardCode.FREE.name())) - .toList(); - List expectedResponses = BoardFindPostResponse.from(expectedPosts); - - // when - List actualResponses = boardService.findPostsByCodeAndPostCategory( - BoardCode.FREE.name(), - PostCategory.전체.name() - ); - - // then - assertThat(actualResponses) - .usingRecursiveComparison() - .ignoringFieldsOfTypes(ZonedDateTime.class) - .isEqualTo(expectedResponses); - } -} diff --git a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java similarity index 95% rename from src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java rename to src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java index d38463dcb..fca6cd41e 100644 --- a/src/test/java/com/example/solidconnection/comment/service/CommentServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/comment/service/CommentServiceTest.java @@ -1,17 +1,17 @@ -package com.example.solidconnection.comment.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.CommentCreateRequest; -import com.example.solidconnection.comment.dto.CommentCreateResponse; -import com.example.solidconnection.comment.dto.CommentDeleteResponse; -import com.example.solidconnection.comment.dto.CommentUpdateRequest; -import com.example.solidconnection.comment.dto.CommentUpdateResponse; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.repository.CommentRepository; +package com.example.solidconnection.community.comment.service; + +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.comment.dto.CommentCreateRequest; +import com.example.solidconnection.community.comment.dto.CommentCreateResponse; +import com.example.solidconnection.community.comment.dto.CommentDeleteResponse; +import com.example.solidconnection.community.comment.dto.CommentUpdateRequest; +import com.example.solidconnection.community.comment.dto.CommentUpdateResponse; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.comment.repository.CommentRepository; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.PostCategory; diff --git a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java similarity index 94% rename from src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java rename to src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java index 3cdc5a40c..a8052a89c 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostCommandServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostCommandServiceTest.java @@ -1,16 +1,16 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostCreateRequest; -import com.example.solidconnection.post.dto.PostCreateResponse; -import com.example.solidconnection.post.dto.PostDeleteResponse; -import com.example.solidconnection.post.dto.PostUpdateRequest; -import com.example.solidconnection.post.dto.PostUpdateResponse; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostCreateRequest; +import com.example.solidconnection.community.post.dto.PostCreateResponse; +import com.example.solidconnection.community.post.dto.PostDeleteResponse; +import com.example.solidconnection.community.post.dto.PostUpdateRequest; +import com.example.solidconnection.community.post.dto.PostUpdateResponse; +import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.community.post.repository.PostImageRepository; import com.example.solidconnection.s3.S3Service; import com.example.solidconnection.s3.UploadedFileUrlResponse; import com.example.solidconnection.service.RedisService; diff --git a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java similarity index 91% rename from src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java rename to src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java index 460b9a15b..1b1e1d2fd 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostLikeServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostLikeServiceTest.java @@ -1,12 +1,12 @@ -package com.example.solidconnection.post.service; +package com.example.solidconnection.community.post.service; -import com.example.solidconnection.board.domain.Board; +import com.example.solidconnection.community.board.domain.Board; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostDislikeResponse; -import com.example.solidconnection.post.dto.PostLikeResponse; -import com.example.solidconnection.post.repository.PostLikeRepository; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostDislikeResponse; +import com.example.solidconnection.community.post.dto.PostLikeResponse; +import com.example.solidconnection.community.post.repository.PostLikeRepository; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; import com.example.solidconnection.type.PostCategory; diff --git a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java similarity index 60% rename from src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java rename to src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java index d9acf5845..33246e981 100644 --- a/src/test/java/com/example/solidconnection/post/service/PostQueryServiceTest.java +++ b/src/test/java/com/example/solidconnection/community/post/service/PostQueryServiceTest.java @@ -1,24 +1,27 @@ -package com.example.solidconnection.post.service; - -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.comment.domain.Comment; -import com.example.solidconnection.comment.dto.PostFindCommentResponse; -import com.example.solidconnection.comment.repository.CommentRepository; -import com.example.solidconnection.entity.PostImage; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.dto.PostFindPostImageResponse; -import com.example.solidconnection.post.dto.PostFindResponse; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.repositories.PostImageRepository; +package com.example.solidconnection.community.post.service; + +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.comment.domain.Comment; +import com.example.solidconnection.community.comment.dto.PostFindCommentResponse; +import com.example.solidconnection.community.post.dto.PostListResponse; +import com.example.solidconnection.community.comment.repository.CommentRepository; +import com.example.solidconnection.community.post.domain.PostImage; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.dto.PostFindPostImageResponse; +import com.example.solidconnection.community.post.dto.PostFindResponse; +import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.community.post.repository.PostImageRepository; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.support.integration.BaseIntegrationTest; +import com.example.solidconnection.type.BoardCode; import com.example.solidconnection.type.PostCategory; import com.example.solidconnection.util.RedisUtils; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.time.ZonedDateTime; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -45,6 +48,56 @@ class PostQueryServiceTest extends BaseIntegrationTest { @Autowired private PostImageRepository postImageRepository; + @Test + void 게시판_코드와_카테고리로_게시글_목록을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getCategory().equals(PostCategory.자유) && post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = PostListResponse.from(expectedPosts); + + // when + List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.자유.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } + + @Test + void 전체_카테고리로_조회시_해당_게시판의_모든_게시글을_조회한다() { + // given + List posts = List.of( + 미주권_자유게시글, 아시아권_자유게시글, 유럽권_자유게시글, 자유게시판_자유게시글, + 미주권_질문게시글, 아시아권_질문게시글, 유럽권_질문게시글, 자유게시판_질문게시글 + ); + List expectedPosts = posts.stream() + .filter(post -> post.getBoard().getCode().equals(BoardCode.FREE.name())) + .toList(); + List expectedResponses = PostListResponse.from(expectedPosts); + + // when + List actualResponses = postQueryService.findPostsByCodeAndPostCategory( + BoardCode.FREE.name(), + PostCategory.전체.name() + ); + + // then + assertThat(actualResponses) + .usingRecursiveComparison() + .ignoringFieldsOfTypes(ZonedDateTime.class) + .isEqualTo(expectedResponses); + } + @Test void 게시글을_성공적으로_조회한다() { // given diff --git a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java index 544b31b4c..3903f31ff 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostLikeCountConcurrencyTest.java @@ -1,10 +1,10 @@ package com.example.solidconnection.concurrency; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; -import com.example.solidconnection.post.service.PostLikeService; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; +import com.example.solidconnection.community.post.service.PostLikeService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import com.example.solidconnection.support.TestContainerSpringBootTest; diff --git a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java index 678e2b084..2cb6eaa27 100644 --- a/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java +++ b/src/test/java/com/example/solidconnection/concurrency/PostViewCountConcurrencyTest.java @@ -1,9 +1,9 @@ package com.example.solidconnection.concurrency; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.service.RedisService; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; diff --git a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java index f7378468c..989e0bc31 100644 --- a/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/support/integration/BaseIntegrationTest.java @@ -4,15 +4,15 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.application.repository.ApplicationRepository; -import com.example.solidconnection.board.domain.Board; -import com.example.solidconnection.board.repository.BoardRepository; +import com.example.solidconnection.community.board.domain.Board; +import com.example.solidconnection.community.board.repository.BoardRepository; import com.example.solidconnection.entity.Country; -import com.example.solidconnection.entity.PostImage; +import com.example.solidconnection.community.post.domain.PostImage; import com.example.solidconnection.entity.Region; -import com.example.solidconnection.post.domain.Post; -import com.example.solidconnection.post.repository.PostRepository; +import com.example.solidconnection.community.post.domain.Post; +import com.example.solidconnection.community.post.repository.PostRepository; import com.example.solidconnection.repositories.CountryRepository; -import com.example.solidconnection.repositories.PostImageRepository; +import com.example.solidconnection.community.post.repository.PostImageRepository; import com.example.solidconnection.repositories.RegionRepository; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.domain.LanguageTestScore; From 0adbb4cf4389049077b3594347821677f5c63696 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sun, 9 Feb 2025 02:07:30 +0900 Subject: [PATCH 20/23] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84=20(#188)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 사용자에 password 컬럼 추가 * feat: 비밀번호 암호화 빈 추가 * refactor: Oauth 가입용 토큰 제공자 이름 변경 * feat: 이메일 가입용 토큰 제공자 구현 * feat: 이메일 회원가입 구현, 회원가입 서비스 추상화 * feat: 이메일 로그인 구현 * feat: 이메일 로그인, 회원가입 엔트포인트 추가 * refactor: 잘못 이해해서 다르게 구현한 api 수정 * test: 이메일 로그인 테스트 작성 * refactor: 이메일 형식으로 검증하노록 dto 어노테이션 수정 * refactor: 이메일 중복 검증이 앞에 위치하도록 --- .../auth/controller/AuthController.java | 35 +++++++ .../auth/dto/EmailSignInRequest.java | 13 +++ .../auth/dto/EmailSignUpTokenRequest.java | 14 +++ .../auth/dto/EmailSignUpTokenResponse.java | 6 ++ .../auth/dto/SignUpRequest.java | 16 ++- .../service/CommonSignUpTokenProvider.java | 27 +++++ .../auth/service/EmailSignInService.java | 43 ++++++++ .../auth/service/EmailSignUpService.java | 54 ++++++++++ .../service/EmailSignUpTokenProvider.java | 92 +++++++++++++++++ .../auth/service/SignUpService.java | 90 +++++++++++++++++ .../auth/service/oauth/AppleOAuthService.java | 4 +- .../auth/service/oauth/KakaoOAuthService.java | 4 +- .../auth/service/oauth/OAuthService.java | 8 +- .../service/oauth/OAuthSignUpService.java | 83 ++++------------ ...der.java => OAuthSignUpTokenProvider.java} | 12 +-- .../security/SecurityConfiguration.java | 7 ++ .../custom/exception/ErrorCode.java | 6 +- .../siteuser/domain/AuthType.java | 4 + .../siteuser/domain/SiteUser.java | 25 +++++ .../db/migration/V5__add_password_column.sql | 2 + .../auth/service/EmailSignInServiceTest.java | 99 +++++++++++++++++++ ...java => OAuthSignUpTokenProviderTest.java} | 40 ++++---- .../solidconnection/e2e/SignUpTest.java | 14 +-- 23 files changed, 591 insertions(+), 107 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java create mode 100644 src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java create mode 100644 src/main/java/com/example/solidconnection/auth/service/SignUpService.java rename src/main/java/com/example/solidconnection/auth/service/oauth/{SignUpTokenProvider.java => OAuthSignUpTokenProvider.java} (88%) create mode 100644 src/main/resources/db/migration/V5__add_password_column.sql create mode 100644 src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java rename src/test/java/com/example/solidconnection/auth/service/oauth/{SignUpTokenProviderTest.java => OAuthSignUpTokenProviderTest.java} (79%) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index aa3ce4f20..80520942f 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -1,17 +1,25 @@ package com.example.solidconnection.auth.controller; +import com.example.solidconnection.auth.dto.EmailSignInRequest; +import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; +import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; import com.example.solidconnection.auth.service.AuthService; +import com.example.solidconnection.auth.service.CommonSignUpTokenProvider; +import com.example.solidconnection.auth.service.EmailSignInService; +import com.example.solidconnection.auth.service.EmailSignUpService; +import com.example.solidconnection.auth.service.EmailSignUpTokenProvider; import com.example.solidconnection.auth.service.oauth.AppleOAuthService; import com.example.solidconnection.auth.service.oauth.KakaoOAuthService; import com.example.solidconnection.auth.service.oauth.OAuthSignUpService; import com.example.solidconnection.custom.resolver.AuthorizedUser; import com.example.solidconnection.custom.resolver.ExpiredToken; import com.example.solidconnection.custom.security.authentication.ExpiredTokenAuthentication; +import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -31,6 +39,10 @@ public class AuthController { private final OAuthSignUpService oAuthSignUpService; private final AppleOAuthService appleOAuthService; private final KakaoOAuthService kakaoOAuthService; + private final EmailSignInService emailSignInService; + private final EmailSignUpService emailSignUpService; + private final EmailSignUpTokenProvider emailSignUpTokenProvider; + private final CommonSignUpTokenProvider commonSignUpTokenProvider; @PostMapping("/apple") public ResponseEntity processAppleOAuth( @@ -48,10 +60,33 @@ public ResponseEntity processKakaoOAuth( return ResponseEntity.ok(oAuthResponse); } + @PostMapping("/email/sign-in") + public ResponseEntity signInWithEmail( + @Valid @RequestBody EmailSignInRequest signInRequest + ) { + SignInResponse signInResponse = emailSignInService.signIn(signInRequest); + return ResponseEntity.ok(signInResponse); + } + + /* 이메일 회원가입 시 signUpToken 을 발급받기 위한 api */ + @PostMapping("/email/sign-up") + public ResponseEntity signUpWithEmail( + @Valid @RequestBody EmailSignUpTokenRequest signUpRequest + ) { + emailSignUpService.validateUniqueEmail(signUpRequest.email()); + String signUpToken = emailSignUpTokenProvider.generateAndSaveSignUpToken(signUpRequest); + return ResponseEntity.ok(new EmailSignUpTokenResponse(signUpToken)); + } + @PostMapping("/sign-up") public ResponseEntity signUp( @Valid @RequestBody SignUpRequest signUpRequest ) { + AuthType authType = commonSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); + if (AuthType.isEmail(authType)) { + SignInResponse signInResponse = emailSignUpService.signUp(signUpRequest); + return ResponseEntity.ok(signInResponse); + } SignInResponse signInResponse = oAuthSignUpService.signUp(signUpRequest); return ResponseEntity.ok(signInResponse); } diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java new file mode 100644 index 000000000..306a8185a --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignInRequest.java @@ -0,0 +1,13 @@ +package com.example.solidconnection.auth.dto; + +import jakarta.validation.constraints.NotBlank; + +public record EmailSignInRequest( + + @NotBlank(message = "이메일을 입력해주세요.") + String email, + + @NotBlank(message = "비밀번호를 입력해주세요.") + String password +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java new file mode 100644 index 000000000..92073b434 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenRequest.java @@ -0,0 +1,14 @@ +package com.example.solidconnection.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record EmailSignUpTokenRequest( + + @Email(message = "이메일을 입력해주세요.") + String email, + + @NotBlank(message = "비밀번호를 입력해주세요.") + String password +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java new file mode 100644 index 000000000..c8e983d0c --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/dto/EmailSignUpTokenResponse.java @@ -0,0 +1,6 @@ +package com.example.solidconnection.auth.dto; + +public record EmailSignUpTokenResponse( + String signUpToken +) { +} diff --git a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java index b28b467bd..43f8e6caf 100644 --- a/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java +++ b/src/main/java/com/example/solidconnection/auth/dto/SignUpRequest.java @@ -24,7 +24,7 @@ public record SignUpRequest( @JsonFormat(pattern = "yyyy-MM-dd") String birth) { - public SiteUser toSiteUser(String email, AuthType authType) { + public SiteUser toOAuthSiteUser(String email, AuthType authType) { return new SiteUser( email, this.nickname, @@ -36,4 +36,18 @@ public SiteUser toSiteUser(String email, AuthType authType) { authType ); } + + public SiteUser toEmailSiteUser(String email, String encodedPassword) { + return new SiteUser( + email, + this.nickname, + this.profileImageUrl, + this.birth, + this.preparationStatus, + Role.MENTEE, + this.gender, + AuthType.EMAIL, + encodedPassword + ); + } } diff --git a/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java new file mode 100644 index 000000000..3d0eda53b --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/CommonSignUpTokenProvider.java @@ -0,0 +1,27 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.util.JwtUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import static com.example.solidconnection.auth.service.EmailSignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; + +@Component +@RequiredArgsConstructor +public class CommonSignUpTokenProvider { + + private final JwtProperties jwtProperties; + + public AuthType parseAuthType(String signUpToken) { + try { + String authTypeStr = JwtUtils.parseClaims(signUpToken, jwtProperties.secret()).get(AUTH_TYPE_CLAIM_KEY, String.class); + return AuthType.valueOf(authTypeStr); + } catch (Exception e) { + throw new CustomException(SIGN_UP_TOKEN_INVALID); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java new file mode 100644 index 000000000..bbbb4f85c --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignInService.java @@ -0,0 +1,43 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.EmailSignInRequest; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_NOT_FOUND; + +/* + * 보안을 위해 이메일과 비밀번호 중 무엇이 틀렸는지 구체적으로 응답하지 않는다. + * */ +@Service +@RequiredArgsConstructor +public class EmailSignInService { + + private final SignInService signInService; + private final SiteUserRepository siteUserRepository; + private final PasswordEncoder passwordEncoder; + + public SignInResponse signIn(EmailSignInRequest signInRequest) { + Optional optionalSiteUser = siteUserRepository.findByEmailAndAuthType(signInRequest.email(), AuthType.EMAIL); + if (optionalSiteUser.isPresent()) { + SiteUser siteUser = optionalSiteUser.get(); + validatePassword(signInRequest.password(), siteUser.getPassword()); + return signInService.signIn(siteUser); + } + throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); + } + + private void validatePassword(String rawPassword, String encodedPassword) throws CustomException { + if (!passwordEncoder.matches(rawPassword, encodedPassword)) { + throw new CustomException(USER_NOT_FOUND, "이메일과 비밀번호를 확인해주세요."); + } + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java new file mode 100644 index 000000000..37f6681ea --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpService.java @@ -0,0 +1,54 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.stereotype.Service; + +import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; + +@Service +public class EmailSignUpService extends SignUpService { + + private final EmailSignUpTokenProvider emailSignUpTokenProvider; + + public EmailSignUpService(SignInService signInService, SiteUserRepository siteUserRepository, + RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, + CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository, + EmailSignUpTokenProvider emailSignUpTokenProvider) { + super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountyRepository); + this.emailSignUpTokenProvider = emailSignUpTokenProvider; + } + + public void validateUniqueEmail(String email) { + if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) { + throw new CustomException(USER_ALREADY_EXISTED); + } + } + + @Override + protected void validateSignUpToken(SignUpRequest signUpRequest) { + emailSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); + } + + @Override + protected void validateUserNotDuplicated(SignUpRequest signUpRequest) { + String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + if (siteUserRepository.existsByEmailAndAuthType(email, AuthType.EMAIL)) { + throw new CustomException(USER_ALREADY_EXISTED); + } + } + + @Override + protected SiteUser createSiteUser(SignUpRequest signUpRequest) { + String email = emailSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + String encodedPassword = emailSignUpTokenProvider.parseEncodedPassword(signUpRequest.signUpToken()); + return signUpRequest.toEmailSiteUser(email, encodedPassword); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java new file mode 100644 index 000000000..1c27a87bd --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/EmailSignUpTokenProvider.java @@ -0,0 +1,92 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; +import com.example.solidconnection.config.security.JwtProperties; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.siteuser.domain.AuthType; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.util.JwtUtils.parseClaims; +import static com.example.solidconnection.util.JwtUtils.parseSubject; + +@Component +public class EmailSignUpTokenProvider extends TokenProvider { + + static final String PASSWORD_CLAIM_KEY = "password"; + static final String AUTH_TYPE_CLAIM_KEY = "authType"; + + private final PasswordEncoder passwordEncoder; + + public EmailSignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate, + PasswordEncoder passwordEncoder) { + super(jwtProperties, redisTemplate); + this.passwordEncoder = passwordEncoder; + } + + public String generateAndSaveSignUpToken(EmailSignUpTokenRequest request) { + String email = request.email(); + String password = request.password(); + String encodedPassword = passwordEncoder.encode(password); + Map emailSignUpClaims = new HashMap<>(Map.of( + PASSWORD_CLAIM_KEY, encodedPassword, + AUTH_TYPE_CLAIM_KEY, AuthType.EMAIL + )); + Claims claims = Jwts.claims(emailSignUpClaims).setSubject(email); + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + TokenType.SIGN_UP.getExpireTime()); + + String signUpToken = Jwts.builder() + .setClaims(claims) + .setIssuedAt(now) + .setExpiration(expiredDate) + .signWith(SignatureAlgorithm.HS512, jwtProperties.secret()) + .compact(); + return saveToken(signUpToken, TokenType.SIGN_UP); + } + + public void validateSignUpToken(String token) { + validateFormatAndExpiration(token); + String email = parseEmail(token); + validateIssuedByServer(email); + } + + private void validateFormatAndExpiration(String token) { + try { + Claims claims = parseClaims(token, jwtProperties.secret()); + Objects.requireNonNull(claims.getSubject()); + String encodedPassword = claims.get(PASSWORD_CLAIM_KEY, String.class); + Objects.requireNonNull(encodedPassword); + } catch (Exception e) { + throw new CustomException(SIGN_UP_TOKEN_INVALID); + } + } + + private void validateIssuedByServer(String email) { + String key = TokenType.SIGN_UP.addPrefix(email); + if (redisTemplate.opsForValue().get(key) == null) { + throw new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); + } + } + + public String parseEmail(String token) { + return parseSubject(token, jwtProperties.secret()); + } + + public String parseEncodedPassword(String token) { + Claims claims = parseClaims(token, jwtProperties.secret()); + return claims.get(PASSWORD_CLAIM_KEY, String.class); + } +} diff --git a/src/main/java/com/example/solidconnection/auth/service/SignUpService.java b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java new file mode 100644 index 000000000..319083658 --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/service/SignUpService.java @@ -0,0 +1,90 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.auth.dto.SignUpRequest; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.entity.InterestedCountry; +import com.example.solidconnection.entity.InterestedRegion; +import com.example.solidconnection.repositories.CountryRepository; +import com.example.solidconnection.repositories.InterestedCountyRepository; +import com.example.solidconnection.repositories.InterestedRegionRepository; +import com.example.solidconnection.repositories.RegionRepository; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; + +/* + * 우리 서버에서 인증되었음을 확인하기 위한 signUpToken 을 검증한다. + * - 사용자 정보를 DB에 저장한다. + * - 관심 국가와 지역을 DB에 저장한다. + * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. + * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. + * */ +public abstract class SignUpService { + + protected final SignInService signInService; + protected final SiteUserRepository siteUserRepository; + protected final RegionRepository regionRepository; + protected final InterestedRegionRepository interestedRegionRepository; + protected final CountryRepository countryRepository; + protected final InterestedCountyRepository interestedCountyRepository; + + protected SignUpService(SignInService signInService, SiteUserRepository siteUserRepository, + RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, + CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository) { + this.signInService = signInService; + this.siteUserRepository = siteUserRepository; + this.regionRepository = regionRepository; + this.interestedRegionRepository = interestedRegionRepository; + this.countryRepository = countryRepository; + this.interestedCountyRepository = interestedCountyRepository; + } + + @Transactional + public SignInResponse signUp(SignUpRequest signUpRequest) { + // 검증 + validateSignUpToken(signUpRequest); + validateUserNotDuplicated(signUpRequest); + validateNicknameDuplicated(signUpRequest.nickname()); + + // 사용자 저장 + SiteUser siteUser = siteUserRepository.save(createSiteUser(signUpRequest)); + + // 관심 지역, 국가 저장 + saveInterestedRegion(signUpRequest, siteUser); + saveInterestedCountry(signUpRequest, siteUser); + + // 로그인 + return signInService.signIn(siteUser); + } + + private void validateNicknameDuplicated(String nickname) { + if (siteUserRepository.existsByNickname(nickname)) { + throw new CustomException(NICKNAME_ALREADY_EXISTED); + } + } + + private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { + List interestedRegionNames = signUpRequest.interestedRegions(); + List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() + .map(region -> new InterestedRegion(savedSiteUser, region)) + .toList(); + interestedRegionRepository.saveAll(interestedRegions); + } + + private void saveInterestedCountry(SignUpRequest signUpRequest, SiteUser savedSiteUser) { + List interestedCountryNames = signUpRequest.interestedCountries(); + List interestedCountries = countryRepository.findByKoreanNames(interestedCountryNames).stream() + .map(country -> new InterestedCountry(savedSiteUser, country)) + .toList(); + interestedCountyRepository.saveAll(interestedCountries); + } + + protected abstract void validateSignUpToken(SignUpRequest signUpRequest); + protected abstract void validateUserNotDuplicated(SignUpRequest signUpRequest); + protected abstract SiteUser createSiteUser(SignUpRequest signUpRequest); +} diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java index 2af82e07d..2605ad89f 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/AppleOAuthService.java @@ -12,9 +12,9 @@ public class AppleOAuthService extends OAuthService { private final AppleOAuthClient appleOAuthClient; - public AppleOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + public AppleOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, AppleOAuthClient appleOAuthClient, SignInService signInService) { - super(signUpTokenProvider, siteUserRepository, signInService); + super(OAuthSignUpTokenProvider, siteUserRepository, signInService); this.appleOAuthClient = appleOAuthClient; } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java index 5dc6faea1..c2202ab2a 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/KakaoOAuthService.java @@ -12,9 +12,9 @@ public class KakaoOAuthService extends OAuthService { private final KakaoOAuthClient kakaoOAuthClient; - public KakaoOAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, + public KakaoOAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, KakaoOAuthClient kakaoOAuthClient, SignInService signInService) { - super(signUpTokenProvider, siteUserRepository, signInService); + super(OAuthSignUpTokenProvider, siteUserRepository, signInService); this.kakaoOAuthClient = kakaoOAuthClient; } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java index 4f37db060..6e9bf7030 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthService.java @@ -22,12 +22,12 @@ * */ public abstract class OAuthService { - private final SignUpTokenProvider signUpTokenProvider; + private final OAuthSignUpTokenProvider OAuthSignUpTokenProvider; private final SignInService signInService; private final SiteUserRepository siteUserRepository; - protected OAuthService(SignUpTokenProvider signUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) { - this.signUpTokenProvider = signUpTokenProvider; + protected OAuthService(OAuthSignUpTokenProvider OAuthSignUpTokenProvider, SiteUserRepository siteUserRepository, SignInService signInService) { + this.OAuthSignUpTokenProvider = OAuthSignUpTokenProvider; this.siteUserRepository = siteUserRepository; this.signInService = signInService; } @@ -52,7 +52,7 @@ protected final OAuthSignInResponse getSignInResponse(SiteUser siteUser) { } protected final SignUpPrepareResponse getSignUpPrepareResponse(OAuthUserInfoDto userInfoDto) { - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType()); + String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(userInfoDto.getEmail(), getAuthType()); return SignUpPrepareResponse.of(userInfoDto, signUpToken); } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java index 7b6d44d26..a46728bb2 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpService.java @@ -1,11 +1,9 @@ package com.example.solidconnection.auth.service.oauth; -import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.service.SignInService; +import com.example.solidconnection.auth.service.SignUpService; import com.example.solidconnection.custom.exception.CustomException; -import com.example.solidconnection.entity.InterestedCountry; -import com.example.solidconnection.entity.InterestedRegion; import com.example.solidconnection.repositories.CountryRepository; import com.example.solidconnection.repositories.InterestedCountyRepository; import com.example.solidconnection.repositories.InterestedRegionRepository; @@ -13,80 +11,41 @@ import com.example.solidconnection.siteuser.domain.AuthType; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; -import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import java.util.List; - -import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; -@RequiredArgsConstructor @Service -public class OAuthSignUpService { - - private final SignUpTokenProvider signUpTokenProvider; - private final SignInService signInService; - private final SiteUserRepository siteUserRepository; - private final RegionRepository regionRepository; - private final InterestedRegionRepository interestedRegionRepository; - private final CountryRepository countryRepository; - private final InterestedCountyRepository interestedCountyRepository; - - /* - * OAuth 인증 후 회원가입을 한다. - * - 우리 서버에서 OAuth 인증했음을 확인하기 위한 signUpToken 을 검증한다. - * - 사용자 정보를 DB에 저장한다. - * - 관심 국가와 지역을 DB에 저장한다. - * - 관심 국가와 지역은 site_user_id를 참조하므로, 사용자 저장 후 저장한다. - * - 바로 로그인하도록 액세스 토큰과 리프레시 토큰을 발급한다. - * */ - @Transactional - public SignInResponse signUp(SignUpRequest signUpRequest) { - // 검증 - signUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); - validateNicknameDuplicated(signUpRequest.nickname()); - String email = signUpTokenProvider.parseEmail(signUpRequest.signUpToken()); - AuthType authType = signUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); - validateUserNotDuplicated(email, authType); - - // 사용자 저장 - SiteUser siteUser = siteUserRepository.save(signUpRequest.toSiteUser(email, authType)); +public class OAuthSignUpService extends SignUpService { - // 관심 지역, 국가 저장 - saveInterestedRegion(signUpRequest, siteUser); - saveInterestedCountry(signUpRequest, siteUser); + private final OAuthSignUpTokenProvider oAuthSignUpTokenProvider; - // 로그인 - return signInService.signIn(siteUser); + OAuthSignUpService(SignInService signInService, SiteUserRepository siteUserRepository, + RegionRepository regionRepository, InterestedRegionRepository interestedRegionRepository, + CountryRepository countryRepository, InterestedCountyRepository interestedCountyRepository, + OAuthSignUpTokenProvider oAuthSignUpTokenProvider) { + super(signInService, siteUserRepository, regionRepository, interestedRegionRepository, countryRepository, interestedCountyRepository); + this.oAuthSignUpTokenProvider = oAuthSignUpTokenProvider; } - private void validateNicknameDuplicated(String nickname) { - if (siteUserRepository.existsByNickname(nickname)) { - throw new CustomException(NICKNAME_ALREADY_EXISTED); - } + @Override + protected void validateSignUpToken(SignUpRequest signUpRequest) { + oAuthSignUpTokenProvider.validateSignUpToken(signUpRequest.signUpToken()); } - private void validateUserNotDuplicated(String email, AuthType authType) { + @Override + protected void validateUserNotDuplicated(SignUpRequest signUpRequest) { + String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); if (siteUserRepository.existsByEmailAndAuthType(email, authType)) { throw new CustomException(USER_ALREADY_EXISTED); } } - private void saveInterestedRegion(SignUpRequest signUpRequest, SiteUser savedSiteUser) { - List interestedRegionNames = signUpRequest.interestedRegions(); - List interestedRegions = regionRepository.findByKoreanNames(interestedRegionNames).stream() - .map(region -> new InterestedRegion(savedSiteUser, region)) - .toList(); - interestedRegionRepository.saveAll(interestedRegions); - } - - private void saveInterestedCountry(SignUpRequest signUpRequest, SiteUser savedSiteUser) { - List interestedCountryNames = signUpRequest.interestedCountries(); - List interestedCountries = countryRepository.findByKoreanNames(interestedCountryNames).stream() - .map(country -> new InterestedCountry(savedSiteUser, country)) - .toList(); - interestedCountyRepository.saveAll(interestedCountries); + @Override + protected SiteUser createSiteUser(SignUpRequest signUpRequest) { + String email = oAuthSignUpTokenProvider.parseEmail(signUpRequest.signUpToken()); + AuthType authType = oAuthSignUpTokenProvider.parseAuthType(signUpRequest.signUpToken()); + return signUpRequest.toOAuthSiteUser(email, authType); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java similarity index 88% rename from src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java rename to src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java index 5399dc1eb..c3a95dbe9 100644 --- a/src/main/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProvider.java @@ -16,17 +16,17 @@ import java.util.Map; import java.util.Objects; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; import static com.example.solidconnection.util.JwtUtils.parseClaims; import static com.example.solidconnection.util.JwtUtils.parseSubject; @Component -public class SignUpTokenProvider extends TokenProvider { +public class OAuthSignUpTokenProvider extends TokenProvider { static final String AUTH_TYPE_CLAIM_KEY = "authType"; - public SignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { + public OAuthSignUpTokenProvider(JwtProperties jwtProperties, RedisTemplate redisTemplate) { super(jwtProperties, redisTemplate); } @@ -58,14 +58,14 @@ private void validateFormatAndExpiration(String token) { String serializedAuthType = claims.get(AUTH_TYPE_CLAIM_KEY, String.class); AuthType.valueOf(serializedAuthType); } catch (Exception e) { - throw new CustomException(OAUTH_SIGN_UP_TOKEN_INVALID); + throw new CustomException(SIGN_UP_TOKEN_INVALID); } } private void validateIssuedByServer(String email) { String key = TokenType.SIGN_UP.addPrefix(email); if (redisTemplate.opsForValue().get(key) == null) { - throw new CustomException(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); + throw new CustomException(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER); } } diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 6851b3e8c..98492568b 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -10,6 +10,8 @@ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; @@ -40,6 +42,11 @@ public CorsConfigurationSource corsConfigurationSource() { return source; } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index d3fdf136d..cd0bb6695 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -28,9 +28,9 @@ public enum ErrorCode { KAKAO_USER_INFO_FAIL(HttpStatus.BAD_REQUEST.value(), "카카오 사용자 정보 조회에 실패했습니다."), INVALID_SERVICE_PUBLISHED_KAKAO_TOKEN(HttpStatus.BAD_REQUEST.value(), "우리 서비스에서 발급한 카카오 토큰이 아닙니다"), - // oauth - OAUTH_SIGN_UP_TOKEN_INVALID(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 회원가입 토큰입니다."), - OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER(HttpStatus.BAD_REQUEST.value(), "회원가입 토큰이 우리 서버에서 발급되지 않았습니다."), + // sign up token + SIGN_UP_TOKEN_INVALID(HttpStatus.BAD_REQUEST.value(), "유효하지 않은 회원가입 토큰입니다."), + SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER(HttpStatus.BAD_REQUEST.value(), "회원가입 토큰이 우리 서버에서 발급되지 않았습니다."), // data not found UNIVERSITY_INFO_FOR_APPLY_NOT_FOUND(HttpStatus.NOT_FOUND.value(), "존재하지 않는 대학교 지원 정보입니다."), diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java index d9d0b582c..d13462298 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/AuthType.java @@ -6,4 +6,8 @@ public enum AuthType { APPLE, EMAIL, ; + + public static boolean isEmail(AuthType authType) { + return EMAIL.equals(authType); + } } diff --git a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java index 83a439b19..b1cf6c1cc 100644 --- a/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java +++ b/src/main/java/com/example/solidconnection/siteuser/domain/SiteUser.java @@ -82,6 +82,9 @@ public class SiteUser { @Setter private LocalDate quitedAt; + @Column(nullable = true) + private String password; + @OneToMany(mappedBy = "siteUser", cascade = CascadeType.ALL, orphanRemoval = true) private List postList = new ArrayList<>(); @@ -133,4 +136,26 @@ public SiteUser( this.gender = gender; this.authType = authType; } + + // todo: 가입 방법에 따라서 정해진 인자만 받고, 그렇지 않을 경우 예외 발생하도록 수정 필요 + public SiteUser( + String email, + String nickname, + String profileImageUrl, + String birth, + PreparationStatus preparationStage, + Role role, + Gender gender, + AuthType authType, + String password) { + this.email = email; + this.nickname = nickname; + this.profileImageUrl = profileImageUrl; + this.birth = birth; + this.preparationStage = preparationStage; + this.role = role; + this.gender = gender; + this.authType = authType; + this.password = password; + } } diff --git a/src/main/resources/db/migration/V5__add_password_column.sql b/src/main/resources/db/migration/V5__add_password_column.sql new file mode 100644 index 000000000..948e2a97d --- /dev/null +++ b/src/main/resources/db/migration/V5__add_password_column.sql @@ -0,0 +1,2 @@ +ALTER TABLE site_user +ADD COLUMN password VARCHAR(255) NULL; diff --git a/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java new file mode 100644 index 000000000..e9663f5df --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/service/EmailSignInServiceTest.java @@ -0,0 +1,99 @@ +package com.example.solidconnection.auth.service; + +import com.example.solidconnection.auth.dto.EmailSignInRequest; +import com.example.solidconnection.auth.dto.SignInResponse; +import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; +import com.example.solidconnection.siteuser.domain.AuthType; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.password.PasswordEncoder; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DisplayName("이메일 로그인 서비스 테스트") +@TestContainerSpringBootTest +class EmailSignInServiceTest { + + @Autowired + private EmailSignInService emailSignInService; + + @Autowired + private SiteUserRepository siteUserRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Test + void 로그인에_성공한다() { + // given + String email = "testEmail"; + String rawPassword = "testPassword"; + SiteUser siteUser = createSiteUser(email, rawPassword); + siteUserRepository.save(siteUser); + EmailSignInRequest signInRequest = new EmailSignInRequest(siteUser.getEmail(), rawPassword); + + // when + SignInResponse signInResponse = emailSignInService.signIn(signInRequest); + + // then + assertAll( + () -> Assertions.assertThat(signInResponse.accessToken()).isNotNull(), + () -> Assertions.assertThat(signInResponse.refreshToken()).isNotNull() + ); + } + + @Nested + class 로그인에_실패한다 { + + @Test + void 이메일과_일치하는_사용자가_없으면_예외_응답을_반환한다() { + // given + EmailSignInRequest signInRequest = new EmailSignInRequest("이메일", "비밀번호"); + + // when & then + assertThatCode(() -> emailSignInService.signIn(signInRequest)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + + @Test + void 비밀번호가_일치하지_않으면_예외_응답을_반환한다() { + // given + String email = "testEmail"; + SiteUser siteUser = createSiteUser(email, "testPassword"); + siteUserRepository.save(siteUser); + EmailSignInRequest signInRequest = new EmailSignInRequest(email, "틀린비밀번호"); + + // when & then + assertThatCode(() -> emailSignInService.signIn(signInRequest)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.USER_NOT_FOUND.getMessage()); + } + } + + private SiteUser createSiteUser(String email, String rawPassword) { + String encodedPassword = passwordEncoder.encode(rawPassword); + return new SiteUser( + email, + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE, + AuthType.EMAIL, + encodedPassword + ); + } +} diff --git a/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java similarity index 79% rename from src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java rename to src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java index d3a1efac1..12ab6f666 100644 --- a/src/test/java/com/example/solidconnection/auth/service/oauth/SignUpTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/oauth/OAuthSignUpTokenProviderTest.java @@ -20,19 +20,19 @@ import java.util.HashMap; import java.util.Map; -import static com.example.solidconnection.auth.service.oauth.SignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; +import static com.example.solidconnection.auth.service.oauth.OAuthSignUpTokenProvider.AUTH_TYPE_CLAIM_KEY; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; @TestContainerSpringBootTest -@DisplayName("회원가입 토큰 제공자 테스트") -class SignUpTokenProviderTest { +@DisplayName("OAuth 회원가입 토큰 제공자 테스트") +class OAuthSignUpTokenProviderTest { @Autowired - private SignUpTokenProvider signUpTokenProvider; + private OAuthSignUpTokenProvider OAuthSignUpTokenProvider; @Autowired private RedisTemplate redisTemplate; @@ -47,7 +47,7 @@ class SignUpTokenProviderTest { AuthType authType = AuthType.KAKAO; // when - String signUpToken = signUpTokenProvider.generateAndSaveSignUpToken(email, authType); + String signUpToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, authType); // then Claims claims = JwtUtils.parseClaims(signUpToken, jwtProperties.secret()); @@ -73,7 +73,7 @@ class 주어진_회원가입_토큰을_검증한다 { redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(validToken)).doesNotThrowAnyException(); } @Test @@ -82,9 +82,9 @@ class 주어진_회원가입_토큰을_검증한다 { String expiredToken = createExpiredToken(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(expiredToken)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(expiredToken)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -93,9 +93,9 @@ class 주어진_회원가입_토큰을_검증한다 { String notJwt = "not jwt"; // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(notJwt)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(notJwt)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -105,9 +105,9 @@ class 주어진_회원가입_토큰을_검증한다 { String wrongAuthType = createBaseJwtBuilder().addClaims(wrongClaim).compact(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(wrongAuthType)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(wrongAuthType)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -117,9 +117,9 @@ class 주어진_회원가입_토큰을_검증한다 { String noSubject = createBaseJwtBuilder().addClaims(claim).compact(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(noSubject)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(noSubject)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_INVALID.getMessage()); } @Test @@ -129,9 +129,9 @@ class 주어진_회원가입_토큰을_검증한다 { String signUpToken = createBaseJwtBuilder().addClaims(validClaim).setSubject("email").compact(); // when & then - assertThatCode(() -> signUpTokenProvider.validateSignUpToken(signUpToken)) + assertThatCode(() -> OAuthSignUpTokenProvider.validateSignUpToken(signUpToken)) .isInstanceOf(CustomException.class) - .hasMessageContaining(OAUTH_SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER.getMessage()); + .hasMessageContaining(SIGN_UP_TOKEN_NOT_ISSUED_BY_SERVER.getMessage()); } } @@ -144,7 +144,7 @@ class 주어진_회원가입_토큰을_검증한다 { redisTemplate.opsForValue().set(TokenType.SIGN_UP.addPrefix(email), validToken); // when - String extractedEmail = signUpTokenProvider.parseEmail(validToken); + String extractedEmail = OAuthSignUpTokenProvider.parseEmail(validToken); // then assertThat(extractedEmail).isEqualTo(email); @@ -158,7 +158,7 @@ class 주어진_회원가입_토큰을_검증한다 { String validToken = createBaseJwtBuilder().setSubject("email").addClaims(claim).compact(); // when - AuthType extractedAuthType = signUpTokenProvider.parseAuthType(validToken); + AuthType extractedAuthType = OAuthSignUpTokenProvider.parseAuthType(validToken); // then assertThat(extractedAuthType).isEqualTo(authType); diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 7d01f365c..6599c20cc 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -3,7 +3,7 @@ import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.service.AuthTokenProvider; -import com.example.solidconnection.auth.service.oauth.SignUpTokenProvider; +import com.example.solidconnection.auth.service.oauth.OAuthSignUpTokenProvider; import com.example.solidconnection.custom.response.ErrorResponse; import com.example.solidconnection.entity.Country; import com.example.solidconnection.entity.InterestedCountry; @@ -30,7 +30,7 @@ import static com.example.solidconnection.auth.domain.TokenType.REFRESH; import static com.example.solidconnection.custom.exception.ErrorCode.NICKNAME_ALREADY_EXISTED; -import static com.example.solidconnection.custom.exception.ErrorCode.OAUTH_SIGN_UP_TOKEN_INVALID; +import static com.example.solidconnection.custom.exception.ErrorCode.SIGN_UP_TOKEN_INVALID; import static com.example.solidconnection.custom.exception.ErrorCode.USER_ALREADY_EXISTED; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByEmail; import static com.example.solidconnection.e2e.DynamicFixture.createSiteUserByNickName; @@ -59,7 +59,7 @@ class SignUpTest extends BaseEndToEndTest { AuthTokenProvider authTokenProvider; @Autowired - SignUpTokenProvider signUpTokenProvider; + OAuthSignUpTokenProvider OAuthSignUpTokenProvider; @Autowired RedisTemplate redisTemplate; @@ -74,7 +74,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); + String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 List interestedRegionNames = List.of("유럽"); @@ -126,7 +126,7 @@ class SignUpTest extends BaseEndToEndTest { // setup - 카카오 토큰 발급 String email = "email@email.com"; - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); + String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -151,7 +151,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String generatedKakaoToken = signUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail, AuthType.KAKAO); + String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(alreadyExistEmail, AuthType.KAKAO); // request - body 생성 및 요청 SignUpRequest signUpRequest = new SignUpRequest(generatedKakaoToken, null, null, @@ -181,6 +181,6 @@ class SignUpTest extends BaseEndToEndTest { .extract().as(ErrorResponse.class); assertThat(errorResponse.message()) - .contains(OAUTH_SIGN_UP_TOKEN_INVALID.getMessage()); + .contains(SIGN_UP_TOKEN_INVALID.getMessage()); } } From 306d8b6094a5e0cf1bbb0ebb21fe9b7449e960ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Sun, 9 Feb 2025 09:59:18 +0900 Subject: [PATCH 21/23] =?UTF-8?q?refactor:=20=EC=9E=85=EB=A0=A5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EA=B3=BC=20=EA=B4=80=EB=A0=A8=EB=90=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=80=20Dto=20=EB=82=B4=EB=B6=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=95=98=EA=B2=8C=20=ED=95=9C=EB=8B=A4.=20(#187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 서비스의 대학 선택 검증을 DTO에서 수행하도록 변경 * refactor: 대학 지망 검증 에러 메시지를 상수로 분리 * test: 기존 서비스 통합테스트에서 대학 선택 유효성 검사 테스트로 변경 * feat: 기본 응답메시지 수정 * feat: 대학 선택 검증 메시지를 ErrorCode enum으로 통합 * style: 불필요한 개행 제거 * feat: 대학 선택 필수값 검증 로직 통합 * refactor: 검증 로직 리팩토링 및 가독성 개선 --- .../application/dto/ApplyRequest.java | 3 + .../dto/UniversityChoiceRequest.java | 5 +- .../service/ApplicationSubmissionService.java | 22 +---- .../custom/exception/ErrorCode.java | 3 + .../annotation/ValidUniversityChoice.java | 20 ++++ .../ValidUniversityChoiceValidator.java | 62 ++++++++++++ .../ApplicationSubmissionServiceTest.java | 20 ---- .../ValidUniversityChoiceValidatorTest.java | 99 +++++++++++++++++++ 8 files changed, 191 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java create mode 100644 src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java create mode 100644 src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java diff --git a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java index 49c4b01ce..7c4da1c99 100644 --- a/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/ApplyRequest.java @@ -1,14 +1,17 @@ package com.example.solidconnection.application.dto; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; public record ApplyRequest( + @NotNull(message = "gpa score id를 입력해주세요.") Long gpaScoreId, @NotNull(message = "language test score id를 입력해주세요.") Long languageTestScoreId, + @Valid UniversityChoiceRequest universityChoiceRequest ) { } diff --git a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java index 2d05cfe5b..d219dbc2e 100644 --- a/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java +++ b/src/main/java/com/example/solidconnection/application/dto/UniversityChoiceRequest.java @@ -1,11 +1,10 @@ package com.example.solidconnection.application.dto; -import jakarta.validation.constraints.NotNull; +import com.example.solidconnection.custom.validation.annotation.ValidUniversityChoice; +@ValidUniversityChoice public record UniversityChoiceRequest( - @NotNull(message = "1지망 대학교를 입력해주세요.") Long firstChoiceUniversityId, - Long secondChoiceUniversityId, Long thirdChoiceUniversityId) { } diff --git a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java index c7652dce0..dec092f5e 100644 --- a/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java +++ b/src/main/java/com/example/solidconnection/application/service/ApplicationSubmissionService.java @@ -18,7 +18,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Optional; import java.util.Set; @@ -48,7 +50,6 @@ public class ApplicationSubmissionService { @Transactional public boolean apply(SiteUser siteUser, ApplyRequest applyRequest) { UniversityChoiceRequest universityChoiceRequest = applyRequest.universityChoiceRequest(); - validateUniversityChoices(universityChoiceRequest); Long gpaScoreId = applyRequest.gpaScoreId(); Long languageTestScoreId = applyRequest.languageTestScoreId(); @@ -116,23 +117,4 @@ private void validateUpdateLimitNotExceed(Application application) { throw new CustomException(APPLY_UPDATE_LIMIT_EXCEED); } } - - // 입력값 유효성 검증 - private void validateUniversityChoices(UniversityChoiceRequest universityChoiceRequest) { - Set uniqueUniversityIds = new HashSet<>(); - uniqueUniversityIds.add(universityChoiceRequest.firstChoiceUniversityId()); - if (universityChoiceRequest.secondChoiceUniversityId() != null) { - addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.secondChoiceUniversityId()); - } - if (universityChoiceRequest.thirdChoiceUniversityId() != null) { - addUniversityChoice(uniqueUniversityIds, universityChoiceRequest.thirdChoiceUniversityId()); - } - } - - private void addUniversityChoice(Set uniqueUniversityIds, Long universityId) { - boolean notAdded = !uniqueUniversityIds.add(universityId); - if (notAdded) { - throw new CustomException(CANT_APPLY_FOR_SAME_UNIVERSITY); - } - } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index cd0bb6695..529430749 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -66,6 +66,9 @@ public enum ErrorCode { CANT_APPLY_FOR_SAME_UNIVERSITY(HttpStatus.BAD_REQUEST.value(), "1, 2, 3지망에 동일한 대학교를 입력할 수 없습니다."), CAN_NOT_CHANGE_NICKNAME_YET(HttpStatus.BAD_REQUEST.value(), "마지막 닉네임 변경으로부터 " + MIN_DAYS_BETWEEN_NICKNAME_CHANGES + "일이 지나지 않았습니다."), PROFILE_IMAGE_NEEDED(HttpStatus.BAD_REQUEST.value(), "프로필 이미지가 필요합니다."), + FIRST_CHOICE_REQUIRED(HttpStatus.BAD_REQUEST.value(), "1지망 대학교를 입력해주세요."), + THIRD_CHOICE_REQUIRES_SECOND(HttpStatus.BAD_REQUEST.value(), "2지망 없이 3지망을 선택할 수 없습니다."), + DUPLICATE_UNIVERSITY_CHOICE(HttpStatus.BAD_REQUEST.value(), "지망 선택이 중복되었습니다."), // community INVALID_POST_CATEGORY(HttpStatus.BAD_REQUEST.value(), "잘못된 카테고리명입니다."), diff --git a/src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java b/src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java new file mode 100644 index 000000000..7e5827113 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/validation/annotation/ValidUniversityChoice.java @@ -0,0 +1,20 @@ +package com.example.solidconnection.custom.validation.annotation; + +import com.example.solidconnection.custom.validation.validator.ValidUniversityChoiceValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = ValidUniversityChoiceValidator.class) +public @interface ValidUniversityChoice { + + String message() default "유효하지 않은 지망 대학 선택입니다."; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java b/src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java new file mode 100644 index 000000000..6ac9fe1c2 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidator.java @@ -0,0 +1,62 @@ +package com.example.solidconnection.custom.validation.validator; + +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import com.example.solidconnection.custom.validation.annotation.ValidUniversityChoice; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Stream; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_UNIVERSITY_CHOICE; +import static com.example.solidconnection.custom.exception.ErrorCode.FIRST_CHOICE_REQUIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.THIRD_CHOICE_REQUIRES_SECOND; + +public class ValidUniversityChoiceValidator implements ConstraintValidator { + + @Override + public boolean isValid(UniversityChoiceRequest request, ConstraintValidatorContext context) { + context.disableDefaultConstraintViolation(); + + if (isFirstChoiceNotSelected(request)) { + context.buildConstraintViolationWithTemplate(FIRST_CHOICE_REQUIRED.getMessage()) + .addConstraintViolation(); + return false; + } + + if (isThirdChoiceWithoutSecond(request)) { + context.buildConstraintViolationWithTemplate(THIRD_CHOICE_REQUIRES_SECOND.getMessage()) + .addConstraintViolation(); + return false; + } + + if (isDuplicate(request)) { + context.buildConstraintViolationWithTemplate(DUPLICATE_UNIVERSITY_CHOICE.getMessage()) + .addConstraintViolation(); + return false; + } + + return true; + } + + private boolean isFirstChoiceNotSelected(UniversityChoiceRequest request) { + return request.firstChoiceUniversityId() == null; + } + + private boolean isThirdChoiceWithoutSecond(UniversityChoiceRequest request) { + return request.thirdChoiceUniversityId() != null && request.secondChoiceUniversityId() == null; + } + + private boolean isDuplicate(UniversityChoiceRequest request) { + Set uniqueIds = new HashSet<>(); + return Stream.of( + request.firstChoiceUniversityId(), + request.secondChoiceUniversityId(), + request.thirdChoiceUniversityId() + ) + .filter(Objects::nonNull) + .anyMatch(id -> !uniqueIds.add(id)); + } +} diff --git a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java index 1d40d094b..ffd3818ce 100644 --- a/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java +++ b/src/test/java/com/example/solidconnection/application/service/ApplicationSubmissionServiceTest.java @@ -116,26 +116,6 @@ class ApplicationSubmissionServiceTest extends BaseIntegrationTest { .hasMessage(INVALID_LANGUAGE_TEST_SCORE_STATUS.getMessage()); } - @Test - void 동일한_대학을_중복_선택하면_예외_응답을_반환한다() { - // given - GpaScore gpaScore = createApprovedGpaScore(테스트유저_1); - LanguageTestScore languageTestScore = createUnapprovedLanguageTestScore(테스트유저_1); - UniversityChoiceRequest universityChoiceRequest = new UniversityChoiceRequest( - 괌대학_A_지원_정보.getId(), - 괌대학_A_지원_정보.getId(), - 메모리얼대학_세인트존스_A_지원_정보.getId() - ); - ApplyRequest request = new ApplyRequest(gpaScore.getId(), languageTestScore.getId(), universityChoiceRequest); - - // when & then - assertThatCode(() -> - applicationSubmissionService.apply(테스트유저_1, request) - ) - .isInstanceOf(CustomException.class) - .hasMessage(CANT_APPLY_FOR_SAME_UNIVERSITY.getMessage()); - } - @Test void 지원서_수정_횟수를_초과하면_예외_응답을_반환한다() { // given diff --git a/src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java b/src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java new file mode 100644 index 000000000..b0267a08b --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/validation/validator/ValidUniversityChoiceValidatorTest.java @@ -0,0 +1,99 @@ +package com.example.solidconnection.custom.validation.validator; + +import com.example.solidconnection.application.dto.UniversityChoiceRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static com.example.solidconnection.custom.exception.ErrorCode.DUPLICATE_UNIVERSITY_CHOICE; +import static com.example.solidconnection.custom.exception.ErrorCode.FIRST_CHOICE_REQUIRED; +import static com.example.solidconnection.custom.exception.ErrorCode.THIRD_CHOICE_REQUIRES_SECOND; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("대학 선택 유효성 검사 테스트") +class ValidUniversityChoiceValidatorTest { + + private static final String MESSAGE = "message"; + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + void 정상적인_지망_선택은_유효하다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, 2L, 3L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations).isEmpty(); + } + + @Test + void 첫_번째_지망만_선택하는_것은_유효하다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, null, null); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations).isEmpty(); + } + + @Test + void 두_번째_지망_없이_세_번째_지망을_선택하면_예외_응답을_반환한다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, null, 3L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .extracting(MESSAGE) + .contains(THIRD_CHOICE_REQUIRES_SECOND.getMessage()); + } + + @Test + void 첫_번째_지망을_선택하지_않으면_예외_응답을_반환한다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(null, 2L, 3L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .isNotEmpty() + .extracting(MESSAGE) + .contains(FIRST_CHOICE_REQUIRED.getMessage()); + } + + @Test + void 대학을_중복_선택하면_예외_응답을_반환한다() { + // given + UniversityChoiceRequest request = new UniversityChoiceRequest(1L, 1L, 2L); + + // when + Set> violations = validator.validate(request); + + // then + assertThat(violations) + .isNotEmpty() + .extracting(MESSAGE) + .contains(DUPLICATE_UNIVERSITY_CHOICE.getMessage()); + } +} From 75927c81a08855f53a5eb20c0c8e2054eec585a6 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Tue, 11 Feb 2025 12:51:39 +0900 Subject: [PATCH 22/23] =?UTF-8?q?fix:=20=EC=8B=A4=ED=8C=A8=ED=95=9C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=ED=95=B4=EA=B2=B0,=20=EC=84=9C?= =?UTF-8?q?=EB=B8=8C=EB=AA=A8=EB=93=88=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20(#193)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 깨지는 테스트 해결 - 같은 email, 닉네임으로 회원가입하는 유저에 대한 테스트케이스가 '같은 닉네임이면 예외를 반환한다' 였는데, 검증 순서 상 email 중복 검증이 더 중요한 검증이므로 앞에 있어서 '같은 이메일로 로그인한 회원이 있다'는 메세지가 발생했다. 테스트 데이터에스 email 을 각각 다르게 주어 이를 해결한다. * chore: submodule 업데이트 --- src/main/resources/secret | 2 +- src/test/java/com/example/solidconnection/e2e/SignUpTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/secret b/src/main/resources/secret index 80a569b4c..44128519c 160000 --- a/src/main/resources/secret +++ b/src/main/resources/secret @@ -1 +1 @@ -Subproject commit 80a569b4c023225c77874e140521c703010414eb +Subproject commit 44128519c61cf80b02e113b1cd4e6387c8f54add diff --git a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java index 6599c20cc..f6b356178 100644 --- a/src/test/java/com/example/solidconnection/e2e/SignUpTest.java +++ b/src/test/java/com/example/solidconnection/e2e/SignUpTest.java @@ -125,7 +125,7 @@ class SignUpTest extends BaseEndToEndTest { siteUserRepository.save(alreadyExistUser); // setup - 카카오 토큰 발급 - String email = "email@email.com"; + String email = "test@email.com"; String generatedKakaoToken = OAuthSignUpTokenProvider.generateAndSaveSignUpToken(email, AuthType.KAKAO); // request - body 생성 및 요청 From 14bd0b9bcdb17f6ac67d92c126a666b0a5ba617e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=99=A9=EA=B7=9C=ED=98=81?= <126947828+Gyuhyeok99@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:50:59 +0900 Subject: [PATCH 23/23] =?UTF-8?q?feat:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20rol?= =?UTF-8?q?e=20=EC=B6=94=EA=B0=80=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Role에 ADMIN 추가 및 Spring Security 권한 처리 추가 * feat: 사용자 권한 처리 및 ADMIN 경로 보호 추가 * refactor: Security 필터 체인 실행 순서 조정 * feat: 인증 안된 사용자 및 권한 없는 사용자 예외 처리 추가 * test: ExceptionHandlerFilter 관련 테스트 코드 추가 * chore: ADMIN Role 추가 마이그레이션 파일 추가 * fix: JWT 인증 시 권한 정보 누락 문제 해결 - JwtAuthentication 생성자에서 권한 정보를 상위 클래스에 전달하도록 수정 * fix: 만료된 토큰에 대한 인증 처리 시 예외 발생 문제 해결 - JwtAuthentication 생성자에서 권한 정보 처리 로직 수정 * refactor: customException를 활용하여 코드 간결화 * refactor: 불필요한 중복 코드 제거 * refactor: 삼항 연산자로 예외 처리 코드 간결화 * style: 형 변환 시 공백 추가 * refactor: Role enum을 순수 도메인 객체로 변경 * test: 테스트 코드 추가 * refactor: 매퍼 security.userdetails 패키지로 이동 * test: SiteUserDetailsTest 추가 및 권한 테스트 이동 --- .../security/SecurityConfiguration.java | 10 +++- .../custom/exception/ErrorCode.java | 1 + .../authentication/JwtAuthentication.java | 7 ++- .../filter/ExceptionHandlerFilter.java | 26 ++++++---- .../userdetails/SecurityRoleMapper.java | 18 +++++++ .../security/userdetails/SiteUserDetails.java | 2 +- .../example/solidconnection/type/Role.java | 4 +- .../migration/V6__add_admin_to_role_enum.sql | 2 + .../filter/ExceptionHandlerFilterTest.java | 39 ++++++++++++++ .../SiteUserDetailsServiceTest.java | 2 +- .../userdetails/SiteUserDetailsTest.java | 51 +++++++++++++++++++ 11 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java create mode 100644 src/main/resources/db/migration/V6__add_admin_to_role_enum.sql create mode 100644 src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java diff --git a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java index 98492568b..6afc199de 100644 --- a/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java +++ b/src/main/java/com/example/solidconnection/config/security/SecurityConfiguration.java @@ -13,11 +13,14 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.ExceptionTranslationFilter; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import static com.example.solidconnection.type.Role.ADMIN; + @Configuration @EnableWebSecurity @RequiredArgsConstructor @@ -55,10 +58,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .formLogin(AbstractHttpConfigurer::disable) .cors(corsConfigurer -> corsConfigurer.configurationSource(corsConfigurationSource())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").hasRole(ADMIN.name()) + .anyRequest().permitAll() + ) .addFilterBefore(jwtAuthenticationFilter, BasicAuthenticationFilter.class) .addFilterBefore(signOutCheckFilter, JwtAuthenticationFilter.class) - .addFilterBefore(exceptionHandlerFilter, SignOutCheckFilter.class) + .addFilterAfter(exceptionHandlerFilter, ExceptionTranslationFilter.class) .build(); } } diff --git a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java index 529430749..8c74ea55b 100644 --- a/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/custom/exception/ErrorCode.java @@ -48,6 +48,7 @@ public enum ErrorCode { AUTHENTICATION_FAILED(HttpStatus.UNAUTHORIZED.value(), "인증이 필요한 접근입니다."), ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), + ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), // s3 S3_SERVICE_EXCEPTION(HttpStatus.BAD_REQUEST.value(), "S3 서비스 에러 발생"), diff --git a/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java index ba195caff..6c9f2fa21 100644 --- a/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java +++ b/src/main/java/com/example/solidconnection/custom/security/authentication/JwtAuthentication.java @@ -1,6 +1,9 @@ package com.example.solidconnection.custom.security.authentication; import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collections; public abstract class JwtAuthentication extends AbstractAuthenticationToken { @@ -9,7 +12,9 @@ public abstract class JwtAuthentication extends AbstractAuthenticationToken { private final Object principal; public JwtAuthentication(String token, Object principal) { - super(null); + super(principal instanceof UserDetails ? + ((UserDetails) principal).getAuthorities() : + Collections.emptyList()); this.credentials = token; this.principal = principal; } diff --git a/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java index 8d09bfada..1b8fac2bb 100644 --- a/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java +++ b/src/main/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilter.java @@ -1,6 +1,7 @@ package com.example.solidconnection.custom.security.filter; import com.example.solidconnection.custom.exception.CustomException; +import com.example.solidconnection.custom.exception.ErrorCode; import com.example.solidconnection.custom.response.ErrorResponse; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; @@ -9,12 +10,16 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; +import static com.example.solidconnection.custom.exception.ErrorCode.ACCESS_DENIED; import static com.example.solidconnection.custom.exception.ErrorCode.AUTHENTICATION_FAILED; @Component @@ -31,25 +36,28 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, filterChain.doFilter(request, response); } catch (CustomException e) { customCommence(response, e); + } catch (AccessDeniedException e) { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + ErrorCode errorCode = auth instanceof AnonymousAuthenticationToken ? AUTHENTICATION_FAILED : ACCESS_DENIED; + generalCommence(response, e, errorCode); } catch (Exception e) { - generalCommence(response, e); + generalCommence(response, e, AUTHENTICATION_FAILED); } } public void customCommence(HttpServletResponse response, CustomException customException) throws IOException { - SecurityContextHolder.clearContext(); ErrorResponse errorResponse = new ErrorResponse(customException); - writeResponse(response, errorResponse); + writeResponse(response, errorResponse, customException.getCode()); } - public void generalCommence(HttpServletResponse response, Exception exception) throws IOException { - SecurityContextHolder.clearContext(); - ErrorResponse errorResponse = new ErrorResponse(AUTHENTICATION_FAILED, exception.getMessage()); - writeResponse(response, errorResponse); + public void generalCommence(HttpServletResponse response, Exception exception, ErrorCode errorCode) throws IOException { + ErrorResponse errorResponse = new ErrorResponse(errorCode, exception.getMessage()); + writeResponse(response, errorResponse, errorCode.getCode()); } - private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse) throws IOException { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + private void writeResponse(HttpServletResponse response, ErrorResponse errorResponse, int statusCode) throws IOException { + SecurityContextHolder.clearContext(); + response.setStatus(statusCode); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); diff --git a/src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java new file mode 100644 index 000000000..3af238f13 --- /dev/null +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SecurityRoleMapper.java @@ -0,0 +1,18 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.type.Role; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.List; + +public class SecurityRoleMapper { + + private static final String ROLE_PREFIX = "ROLE_"; + + private SecurityRoleMapper() { + } + + public static List mapRoleToAuthorities(Role role) { + return List.of(new SimpleGrantedAuthority(ROLE_PREFIX + role.name())); + } +} diff --git a/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java index 36a0b815a..008f77ef5 100644 --- a/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java +++ b/src/main/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetails.java @@ -27,7 +27,7 @@ public String getUsername() { @Override public Collection getAuthorities() { - return null; + return SecurityRoleMapper.mapRoleToAuthorities(siteUser.getRole()); } @Override diff --git a/src/main/java/com/example/solidconnection/type/Role.java b/src/main/java/com/example/solidconnection/type/Role.java index aaf464bf8..8223e8de0 100644 --- a/src/main/java/com/example/solidconnection/type/Role.java +++ b/src/main/java/com/example/solidconnection/type/Role.java @@ -1,6 +1,8 @@ package com.example.solidconnection.type; public enum Role { + + ADMIN, MENTOR, - MENTEE + MENTEE; } diff --git a/src/main/resources/db/migration/V6__add_admin_to_role_enum.sql b/src/main/resources/db/migration/V6__add_admin_to_role_enum.sql new file mode 100644 index 000000000..a661a2a61 --- /dev/null +++ b/src/main/resources/db/migration/V6__add_admin_to_role_enum.sql @@ -0,0 +1,2 @@ +ALTER TABLE site_user + modify ROLE enum ('MENTEE', 'MENTOR', 'ADMIN') NOT NULL; diff --git a/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java index 091a75eb8..fd4bd62a8 100644 --- a/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/filter/ExceptionHandlerFilterTest.java @@ -13,8 +13,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import java.util.stream.Stream; @@ -82,10 +85,46 @@ void setUp() { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); } + @Test + void 익명_사용자의_접근_거부시_401_예외_응답을_반환한다() throws Exception { + // given + Authentication anonymousAuth = getAnonymousAuth(); + SecurityContextHolder.getContext().setAuthentication(anonymousAuth); + willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @Test + void 인증된_사용자의_접근_거부하면_403_예외_응답을_반환한다() throws Exception { + // given + Authentication auth = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + SecurityContextHolder.getContext().setAuthentication(auth); + willThrow(new AccessDeniedException("Access Denied")).given(filterChain).doFilter(request, response); + + // when + exceptionHandlerFilter.doFilterInternal(request, response, filterChain); + + // then + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_FORBIDDEN); + } + private static Stream provideException() { return Stream.of( new RuntimeException(), new CustomException(ErrorCode.INVALID_TOKEN) ); } + + private Authentication getAnonymousAuth() { + return new AnonymousAuthenticationToken( + "key", + "anonymousUser", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS") + ); + } } diff --git a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java index 731f840f3..99e463955 100644 --- a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java +++ b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsServiceTest.java @@ -18,7 +18,7 @@ import static com.example.solidconnection.custom.exception.ErrorCode.INVALID_TOKEN; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertAll; @DisplayName("사용자 인증 정보 서비스 테스트") @TestContainerSpringBootTest diff --git a/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java new file mode 100644 index 000000000..912072d2b --- /dev/null +++ b/src/test/java/com/example/solidconnection/custom/security/userdetails/SiteUserDetailsTest.java @@ -0,0 +1,51 @@ +package com.example.solidconnection.custom.security.userdetails; + +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.type.Gender; +import com.example.solidconnection.type.PreparationStatus; +import com.example.solidconnection.type.Role; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("사용자 인증 정보 테스트") +@TestContainerSpringBootTest +class SiteUserDetailsTest { + + @Autowired + private SiteUserRepository siteUserRepository; + + @Test + void 사용자_권한을_정상적으로_반환한다() { + // given + SiteUser siteUser = siteUserRepository.save(createSiteUser()); + SiteUserDetails siteUserDetails = new SiteUserDetails(siteUser); + + // when + Collection authorities = siteUserDetails.getAuthorities(); + + // then + assertThat(authorities) + .extracting("authority") + .containsExactly("ROLE_" + siteUser.getRole().name()); + } + + private SiteUser createSiteUser() { + return new SiteUser( + "test@example.com", + "nickname", + "profileImageUrl", + "1999-01-01", + PreparationStatus.CONSIDERING, + Role.MENTEE, + Gender.MALE + ); + } +}