diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt index 8ec2dcef4..7dd1511f8 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/article/ArticleMainCardDao.kt @@ -23,7 +23,7 @@ class ArticleMainCardDao( .toSet() } - private fun selectArticleMainCardsRecordQuery(articleIds: Set) = dslContext.select( + fun selectArticleMainCardsRecordQuery(articleIds: Set) = dslContext.select( ARTICLE_MAIN_CARD.ID.`as`(ArticleMainCardRecord::articleId.name), ARTICLE_MAIN_CARD.TITLE.`as`(ArticleMainCardRecord::articleTitle.name), ARTICLE_MAIN_CARD.MAIN_IMAGE_URL.`as`(ArticleMainCardRecord::mainImageUrl.name), @@ -46,9 +46,9 @@ class ArticleMainCardDao( * NOTE - The query performed in this function do not save the workbook. */ fun insertArticleMainCard(command: ArticleMainCardExcludeWorkbookCommand) = - insertArticleMainCardQuery(command).execute() + insertArticleMainCardCommand(command).execute() - fun insertArticleMainCardQuery(command: ArticleMainCardExcludeWorkbookCommand) = dslContext + fun insertArticleMainCardCommand(command: ArticleMainCardExcludeWorkbookCommand) = dslContext .insertInto( ARTICLE_MAIN_CARD, ARTICLE_MAIN_CARD.ID, @@ -79,9 +79,9 @@ class ArticleMainCardDao( ) fun updateArticleMainCardSetWorkbook(command: UpdateArticleMainCardWorkbookCommand) = - updateArticleMainCardSetWorkbookQuery(command).execute() + updateArticleMainCardSetWorkbookCommand(command).execute() - fun updateArticleMainCardSetWorkbookQuery(command: UpdateArticleMainCardWorkbookCommand) = dslContext + fun updateArticleMainCardSetWorkbookCommand(command: UpdateArticleMainCardWorkbookCommand) = dslContext .update(ARTICLE_MAIN_CARD) .set(ARTICLE_MAIN_CARD.WORKBOOKS, JSON.valueOf(articleMainCardMapper.toJsonStr(command.workbooks))) .where(ARTICLE_MAIN_CARD.ID.eq(command.articleId)) diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt index 4ef9984b0..e4ddd84d1 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/subscription/SubscriptionDao.kt @@ -32,22 +32,28 @@ class SubscriptionDao( .set(SUBSCRIPTION.TARGET_WORKBOOK_ID, command.workbookId) fun reSubscribeWorkbookSubscription(command: InsertWorkbookSubscriptionCommand) { + reSubscribeWorkBookSubscriptionCommand(command) + .execute() + } + + fun reSubscribeWorkBookSubscriptionCommand(command: InsertWorkbookSubscriptionCommand) = dslContext.update(SUBSCRIPTION) .set(SUBSCRIPTION.DELETED_AT, null as LocalDateTime?) .set(SUBSCRIPTION.UNSUBS_OPINION, null as String?) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(command.workbookId)) + + fun updateDeletedAtInWorkbookSubscription(command: UpdateDeletedAtInWorkbookSubscriptionCommand) { + updateDeletedAtInWorkbookSubscriptionCommand(command) .execute() } - fun updateDeletedAtInWorkbookSubscription(command: UpdateDeletedAtInWorkbookSubscriptionCommand) { + fun updateDeletedAtInWorkbookSubscriptionCommand(command: UpdateDeletedAtInWorkbookSubscriptionCommand) = dslContext.update(SUBSCRIPTION) .set(SUBSCRIPTION.DELETED_AT, LocalDateTime.now()) .set(SUBSCRIPTION.UNSUBS_OPINION, command.opinion) .where(SUBSCRIPTION.MEMBER_ID.eq(command.memberId)) .and(SUBSCRIPTION.TARGET_WORKBOOK_ID.eq(command.workbookId)) - .execute() - } fun selectTopWorkbookSubscriptionStatus(query: SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery): WorkbookSubscriptionStatus? { return selectTopWorkbookSubscriptionStatusQuery(query) diff --git a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt index ace5b2e89..16462a44f 100644 --- a/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt +++ b/api-repo/src/main/kotlin/com/few/api/repo/dao/workbook/WorkbookDao.kt @@ -56,12 +56,15 @@ class WorkbookDao( .set(Workbook.WORKBOOK.DESCRIPTION, command.description) fun mapWorkBookToArticle(command: MapWorkBookToArticleCommand) { + mapWorkBookToArticleCommand(command) + .execute() + } + + fun mapWorkBookToArticleCommand(command: MapWorkBookToArticleCommand) = dslContext.insertInto(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE) .set(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.WORKBOOK_ID, command.workbookId) .set(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.ARTICLE_ID, command.articleId) .set(MappingWorkbookArticle.MAPPING_WORKBOOK_ARTICLE.DAY_COL, command.dayCol) - .execute() - } /** * category에 따라서 조회된 구독자 수가 포함된 Workbook 목록을 반환한다. diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleDaoExplainGenerateTest.kt index 6b52e402e..7c7d2b88a 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleDaoExplainGenerateTest.kt @@ -134,4 +134,15 @@ class ArticleDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(query, explain, "selectArticleIdByWorkbookIdAndDayQueryExplain") } + + @Test + fun selectArticleContentsQueryExplain() { + val query = setOf(1L).let { + articleDao.selectArticleContentsQuery(it) + } + + val explain = dslContext.explain(query).toString() + + ResultGenerator.execute(query, explain, "selectArticleContentsQueryExplain") + } } \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleMainCardDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleMainCardDaoExplainGenerateTest.kt new file mode 100644 index 000000000..de27d1285 --- /dev/null +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleMainCardDaoExplainGenerateTest.kt @@ -0,0 +1,110 @@ +package com.few.api.repo.explain.article + +import com.few.api.repo.dao.article.ArticleMainCardDao +import com.few.api.repo.dao.article.command.ArticleMainCardExcludeWorkbookCommand +import com.few.api.repo.dao.article.command.UpdateArticleMainCardWorkbookCommand +import com.few.api.repo.dao.article.command.WorkbookCommand +import com.few.api.repo.explain.ResultGenerator +import com.few.api.repo.jooq.JooqTestSpec +import com.few.data.common.code.CategoryType +import io.github.oshai.kotlinlogging.KotlinLogging +import jooq.jooq_dsl.tables.ArticleMainCard +import org.jooq.DSLContext +import org.jooq.JSON +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import java.net.URL +import java.time.LocalDateTime + +@Tag("explain") +class ArticleMainCardDaoExplainGenerateTest : JooqTestSpec() { + private val log = KotlinLogging.logger {} + + @Autowired + private lateinit var dslContext: DSLContext + + @Autowired + private lateinit var articleMainCardDao: ArticleMainCardDao + + @BeforeEach + fun setUp() { + log.debug { "===== start setUp =====" } + dslContext.deleteFrom(ArticleMainCard.ARTICLE_MAIN_CARD).execute() + dslContext.insertInto(ArticleMainCard.ARTICLE_MAIN_CARD) + .set(ArticleMainCard.ARTICLE_MAIN_CARD.ID, 1L) + .set(ArticleMainCard.ARTICLE_MAIN_CARD.TITLE, "this is title1") + .set( + ArticleMainCard.ARTICLE_MAIN_CARD.MAIN_IMAGE_URL, + "http://localhost:8080/image1.jpg" + ) + .set(ArticleMainCard.ARTICLE_MAIN_CARD.CATEGORY_CD, CategoryType.fromCode(0)!!.code) + .set(ArticleMainCard.ARTICLE_MAIN_CARD.CREATED_AT, LocalDateTime.now()) + .set(ArticleMainCard.ARTICLE_MAIN_CARD.WRITER_ID, 1L) + .set(ArticleMainCard.ARTICLE_MAIN_CARD.WRITER_EMAIL, "writer@gmail.com") + .set( + ArticleMainCard.ARTICLE_MAIN_CARD.WRITER_DESCRIPTION, + JSON.valueOf("{ \"name\": \"writer\", \"url\": \"http://localhost:8080/writer\", \"imgUrl\": \"http://localhost:8080/writer.jpg\" }") + ) + .set( + ArticleMainCard.ARTICLE_MAIN_CARD.WORKBOOKS, + JSON.valueOf("[{\"id\": 1, \"title\": \"title\"}]") + ) + .execute() + log.debug { "===== finish setUp =====" } + } + + @Test + fun selectArticleMainCardsRecordQueryExplain() { + val query = articleMainCardDao.selectArticleMainCardsRecordQuery(setOf(1L)) + + val explain = dslContext.explain(query).toString() + ResultGenerator.execute(query, explain, "selectArticleMainCardsRecordQueryExplain") + } + + @Test + fun insertArticleMainCardCommandExplain() { + val command = ArticleMainCardExcludeWorkbookCommand( + articleId = 2L, + articleTitle = "this is title2", + mainImageUrl = URL("http://localhost:8080/image2.jpg"), + categoryCd = CategoryType.fromCode(0)!!.code, + createdAt = LocalDateTime.now(), + writerId = 1L, + writerEmail = "writer@gmail.com", + writerName = "writer", + writerUrl = URL("http://localhost:8080/writer"), + writerImgUrl = URL("http://localhost:8080/writer.jpg") + ).let { + articleMainCardDao.insertArticleMainCardCommand(it) + } + + val explain = command.toString() + + ResultGenerator.execute(command, explain, "insertArticleMainCardCommandExplain") + } + + @Test + fun updateArticleMainCardSetWorkbookCommandExplain() { + val command = UpdateArticleMainCardWorkbookCommand( + articleId = 1L, + workbooks = listOf( + WorkbookCommand( + id = 1L, + title = "workbook1" + ), + WorkbookCommand( + id = 2L, + title = "workbook2" + ) + ) + ).let { + articleMainCardDao.updateArticleMainCardSetWorkbookCommand(it) + } + + val explain = command.toString() + + ResultGenerator.execute(command, explain, "updateArticleMainCardSetWorkbookCommandExplain") + } +} \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleViewCountDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleViewCountDaoExplainGenerateTest.kt index 81ee73653..2902167e1 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleViewCountDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/article/ArticleViewCountDaoExplainGenerateTest.kt @@ -3,6 +3,8 @@ package com.few.api.repo.explain.article import com.few.api.repo.dao.article.ArticleViewCountDao import com.few.api.repo.dao.article.command.ArticleViewCountCommand import com.few.api.repo.dao.article.query.ArticleViewCountQuery +import com.few.api.repo.dao.article.query.SelectArticlesOrderByViewsQuery +import com.few.api.repo.dao.article.query.SelectRankByViewsQuery import com.few.api.repo.explain.InsertUpdateExplainGenerator import com.few.api.repo.explain.ResultGenerator import com.few.api.repo.jooq.JooqTestSpec @@ -61,4 +63,41 @@ class ArticleViewCountDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(command, explain, "upsertArticleViewCountQueryExplain") } + + @Test + fun insertArticleViewCountToZeroQueryExplain() { + val command = ArticleViewCountQuery( + articleId = 1L, + categoryType = CategoryType.fromCode(0)!! + ).let { + articleViewCountDao.insertArticleViewCountToZeroQuery(it) + } + + val explain = InsertUpdateExplainGenerator.execute(dslContext, command.sql, command.bindValues) + + ResultGenerator.execute(command, explain, "insertArticleViewCountToZeroQueryExplain") + } + + @Test + fun selectRankByViewsQueryExplain() { + val query = SelectRankByViewsQuery(1L).let { + articleViewCountDao.selectRankByViewsQuery(it) + } + + val explain = dslContext.explain(query).toString() + ResultGenerator.execute(query, explain, "selectRankByViewsQueryExplain") + } + + @Test + fun selectArticlesOrderByViewsQueryExplain() { + val query = SelectArticlesOrderByViewsQuery( + offset = 0, + category = CategoryType.fromCode(0)!! + ).let { + articleViewCountDao.selectArticlesOrderByViewsQuery(it) + } + + val explain = dslContext.explain(query).toString() + ResultGenerator.execute(query, explain, "selectArticlesOrderByViewsQueryExplain") + } } \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt index 1bed29ec0..3f85b76b5 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/member/MemberDaoExplainGenerateTest.kt @@ -1,10 +1,12 @@ package com.few.api.repo.explain.member import com.few.api.repo.dao.member.MemberDao +import com.few.api.repo.dao.member.command.DeleteMemberCommand import com.few.api.repo.dao.member.command.InsertMemberCommand import com.few.api.repo.dao.member.command.UpdateDeletedMemberTypeCommand import com.few.api.repo.dao.member.command.UpdateMemberTypeCommand import com.few.api.repo.dao.member.query.BrowseWorkbookWritersQuery +import com.few.api.repo.dao.member.query.SelectMemberByEmailNotConsiderDeletedAtQuery import com.few.api.repo.dao.member.query.SelectMemberByEmailQuery import com.few.api.repo.dao.member.query.SelectWriterQuery import com.few.api.repo.dao.member.support.WriterDescription @@ -117,7 +119,7 @@ class MemberDaoExplainGenerateTest : JooqTestSpec() { @Test fun selectMemberByEmailNotConsiderDeletedAtQueryExplain() { - val query = SelectMemberByEmailQuery("test@gmail.com").let { + val query = SelectMemberByEmailNotConsiderDeletedAtQuery("test@gmail.com").let { memberDao.selectMemberByEmailQuery(it) } @@ -181,11 +183,10 @@ class MemberDaoExplainGenerateTest : JooqTestSpec() { @Test fun deleteMemberCommandExplain() { - val command = UpdateDeletedMemberTypeCommand( - id = 1, - memberType = MemberType.WRITER + val command = DeleteMemberCommand( + memberId = 1 ).let { - memberDao.updateMemberTypeCommand(it) + memberDao.deleteMemberCommand(it) } val explain = InsertUpdateExplainGenerator.execute(dslContext, command.sql, command.bindValues) diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt index 08166d265..6bfb9a271 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/subscription/SubscriptionDaoExplainGenerateTest.kt @@ -3,6 +3,7 @@ package com.few.api.repo.explain.subscription import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.InsertWorkbookSubscriptionCommand import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInAllSubscriptionCommand +import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInWorkbookSubscriptionCommand import com.few.api.repo.dao.subscription.query.CountWorkbookMappedArticlesQuery import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookActiveSubscription import com.few.api.repo.dao.subscription.query.SelectAllWorkbookSubscriptionStatusNotConsiderDeletedAtQuery @@ -137,4 +138,33 @@ class SubscriptionDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(query, explain, "countAllWorkbookSubscriptionQueryExplain") } + + @Test + fun reSubscribeWorkBookSubscriptionCommandExplain() { + val command = InsertWorkbookSubscriptionCommand( + memberId = 1L, + workbookId = 1L + ).let { + subscriptionDao.reSubscribeWorkBookSubscriptionCommand(it) + } + + val explain = InsertUpdateExplainGenerator.execute(dslContext, command.sql, command.bindValues) + + ResultGenerator.execute(command, explain, "reSubscribeWorkBookSubscriptionCommandExplain") + } + + @Test + fun updateDeletedAtInWorkbookSubscriptionCommandExplain() { + val command = UpdateDeletedAtInWorkbookSubscriptionCommand( + memberId = 1L, + workbookId = 1L, + opinion = "test" + ).let { + subscriptionDao.updateDeletedAtInWorkbookSubscriptionCommand(it) + } + + val explain = InsertUpdateExplainGenerator.execute(dslContext, command.sql, command.bindValues) + + ResultGenerator.execute(command, explain, "updateDeletedAtInWorkbookSubscriptionCommandExplain") + } } \ No newline at end of file diff --git a/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt b/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt index 536839f45..f736d1c69 100644 --- a/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt +++ b/api-repo/src/test/kotlin/com/few/api/repo/explain/workbook/WorkbookDaoExplainGenerateTest.kt @@ -2,6 +2,7 @@ package com.few.api.repo.explain.workbook import com.few.api.repo.dao.workbook.WorkbookDao import com.few.api.repo.dao.workbook.command.InsertWorkBookCommand +import com.few.api.repo.dao.workbook.command.MapWorkBookToArticleCommand import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCount import com.few.api.repo.dao.workbook.query.SelectWorkBookRecordQuery import com.few.api.repo.explain.InsertUpdateExplainGenerator @@ -89,4 +90,19 @@ class WorkbookDaoExplainGenerateTest : JooqTestSpec() { ResultGenerator.execute(query, explain, "browseWorkBookQueryCategoryCondition") } + + @Test + fun mapWorkBookToArticleCommandExplain() { + val command = MapWorkBookToArticleCommand( + workbookId = 1L, + articleId = 1L, + dayCol = 1 + ).let { + workbookDao.mapWorkBookToArticleCommand(it) + } + + val explain = InsertUpdateExplainGenerator.execute(dslContext, command.sql, command.bindValues) + + ResultGenerator.execute(command, explain, "mapWorkBookToArticleCommandExplain") + } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt b/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt index 73e60b24e..991e310a6 100644 --- a/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/member/usecase/TokenUseCase.kt @@ -24,6 +24,8 @@ class TokenUseCase( @Transactional fun execute(useCaseIn: TokenUseCaseIn): TokenUseCaseOut { + var isLogin = true + /** refreshToken이 요청에 포함되어 있으면 refreshToken을 통해 memberId를 추출하여 새로운 토큰을 발급 */ var _memberId: Long? = null var _memberEmail: String? = null @@ -59,6 +61,7 @@ class TokenUseCase( ?: throw NotFoundException("member.notfound.id") if (memberEmailAndTypeRecord.memberType == MemberType.PREAUTH) { + isLogin = false UpdateMemberTypeCommand( id = memberId, memberType = MemberType.NORMAL @@ -79,7 +82,7 @@ class TokenUseCase( return TokenUseCaseOut( accessToken = token.accessToken, refreshToken = token.refreshToken, - isLogin = false + isLogin = isLogin ) } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt index 3c93b376b..6a039f5f2 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCase.kt @@ -13,6 +13,7 @@ import com.few.api.repo.dao.subscription.query.SelectAllMemberWorkbookInActiveSu import com.few.api.web.support.WorkBookStatus import org.springframework.stereotype.Component import org.springframework.transaction.annotation.Transactional +import org.webjars.NotFoundException @Component class BrowseSubscribeWorkbooksUseCase( @@ -41,7 +42,7 @@ class BrowseSubscribeWorkbooksUseCase( val workbookSubscriptionCurrentArticleIdRecords = subscriptionRecords.associate { it -> val articleId = ReadArticleIdByWorkbookIdAndDayDto(it.workbookId, it.currentDay).let { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(it) - } ?: throw IllegalArgumentException("articleId is null") + } ?: throw NotFoundException("article.notfound.workbookIdAndCurrentDay") it.workbookId to articleId } diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt index 76bdb0425..69449cc32 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCase.kt @@ -49,7 +49,7 @@ class SubscribeWorkbookUseCase( subscriptionDao.reSubscribeWorkbookSubscription(command) } - /** 이미 구독한 히스토리가 있고 구독이 취소되지 않은 경우 */ + /** 구독 중인 경우 */ else -> { throw SubscribeIllegalArgumentException("subscribe.state.subscribed") } diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt index 1887d9453..48f57e687 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCase.kt @@ -1,6 +1,5 @@ package com.few.api.domain.subscription.usecase -import com.few.api.domain.subscription.service.MemberService import com.few.api.domain.subscription.usecase.dto.UnsubscribeAllUseCaseIn import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInAllSubscriptionCommand @@ -10,7 +9,6 @@ import org.springframework.transaction.annotation.Transactional @Component class UnsubscribeAllUseCase( private val subscriptionDao: SubscriptionDao, - private val memberService: MemberService, ) { @Transactional diff --git a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt index ba5afb038..057193e47 100644 --- a/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCase.kt @@ -1,6 +1,5 @@ package com.few.api.domain.subscription.usecase -import com.few.api.domain.subscription.service.MemberService import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.command.UpdateDeletedAtInWorkbookSubscriptionCommand import com.few.api.domain.subscription.usecase.dto.UnsubscribeWorkbookUseCaseIn @@ -10,9 +9,9 @@ import org.springframework.transaction.annotation.Transactional @Component class UnsubscribeWorkbookUseCase( private val subscriptionDao: SubscriptionDao, - private val memberService: MemberService, ) { + // todo add test @Transactional fun execute(useCaseIn: UnsubscribeWorkbookUseCaseIn) { // TODO: request sending email diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt index 2bf5deb4e..61f10409f 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCase.kt @@ -8,9 +8,10 @@ import com.few.api.domain.workbook.usecase.dto.BrowseWorkBookDetail import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseIn import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseOut import com.few.api.domain.workbook.usecase.dto.WriterDetail -import com.few.api.domain.workbook.usecase.model.BasicWorkbookOrderDelegator -import com.few.api.domain.workbook.usecase.model.AuthMainViewWorkbookOrderDelegator -import com.few.api.domain.workbook.usecase.service.WorkbookOrderDelegatorExecutor +import com.few.api.domain.workbook.usecase.model.* +import com.few.api.domain.workbook.usecase.service.order.AuthMainViewWorkbookOrderDelegator +import com.few.api.domain.workbook.usecase.service.order.BasicWorkbookOrderDelegator +import com.few.api.domain.workbook.usecase.service.order.WorkbookOrderDelegatorExecutor import com.few.api.repo.dao.workbook.WorkbookDao import com.few.api.repo.dao.workbook.query.BrowseWorkBookQueryWithSubscriptionCount import com.few.api.web.support.ViewCategory @@ -56,7 +57,7 @@ class BrowseWorkbooksUseCase( } val workbookDetails = workbookRecords.map { record -> - BrowseWorkBookDetail( + WorkBook( id = record.id, mainImageUrl = record.mainImageUrl, title = record.title, @@ -64,7 +65,7 @@ class BrowseWorkbooksUseCase( category = CategoryType.convertToDisplayName(record.category), createdAt = record.createdAt, writerDetails = writerRecords[record.id]?.map { - WriterDetail( + WorkBookWriter( id = it.writerId, name = it.name, url = it.url @@ -84,8 +85,14 @@ class BrowseWorkbooksUseCase( WorkBookOrderStrategy.MAIN_VIEW_AUTH -> { BrowseMemberSubscribeWorkbooksInDto(useCaseIn.memberId!!).let { dto -> workbookSubscribeService.browseMemberSubscribeWorkbooks(dto) - }.let { memberSubscribeWorkbooks -> - AuthMainViewWorkbookOrderDelegator(workbookDetails, memberSubscribeWorkbooks) + }.map { + MemberSubscribedWorkbook( + workbookId = it.workbookId, + isActiveSub = it.isActiveSub, + currentDay = it.currentDay + ) + }.let { subscribedWorkbooks -> + AuthMainViewWorkbookOrderDelegator(workbookDetails, subscribedWorkbooks) } } WorkBookOrderStrategy.MAIN_VIEW_UNAUTH -> { @@ -96,8 +103,27 @@ class BrowseWorkbooksUseCase( workbookOrderDelegatorExecutor.execute(delegator) } - return BrowseWorkbooksUseCaseOut( - workbooks = orderedWorkbooks - ) + orderedWorkbooks.map { workBook -> + BrowseWorkBookDetail( + id = workBook.id, + mainImageUrl = workBook.mainImageUrl, + title = workBook.title, + description = workBook.description, + category = workBook.category, + createdAt = workBook.createdAt, + writerDetails = workBook.writerDetails.map { + WriterDetail( + id = it.id, + name = it.name, + url = it.url + ) + }, + subscriptionCount = workBook.subscriptionCount + ) + }.let { + return BrowseWorkbooksUseCaseOut( + workbooks = it + ) + } } } \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/BasicWorkbookOrderDelegator.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/BasicWorkbookOrderDelegator.kt deleted file mode 100644 index 505e4aa1a..000000000 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/BasicWorkbookOrderDelegator.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.few.api.domain.workbook.usecase.model - -import com.few.api.domain.workbook.usecase.dto.BrowseWorkBookDetail - -class BasicWorkbookOrderDelegator( - private val workbooks: List, -) : WorkbookOrderDelegator { - override fun order(): List { - return workbooks - } -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/MemberSubscribedWorkbook.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/MemberSubscribedWorkbook.kt new file mode 100644 index 000000000..23ec8ceb2 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/MemberSubscribedWorkbook.kt @@ -0,0 +1,7 @@ +package com.few.api.domain.workbook.usecase.model + +data class MemberSubscribedWorkbook( + val workbookId: Long, + val isActiveSub: Boolean, + val currentDay: Int, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/WorkBook.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/WorkBook.kt new file mode 100644 index 000000000..a769073a0 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/WorkBook.kt @@ -0,0 +1,21 @@ +package com.few.api.domain.workbook.usecase.model + +import java.net.URL +import java.time.LocalDateTime + +data class WorkBook( + val id: Long, + val mainImageUrl: URL, + val title: String, + val description: String, + val category: String, + val createdAt: LocalDateTime, + val writerDetails: List, + val subscriptionCount: Long, +) + +data class WorkBookWriter( + val id: Long, + val name: String, + val url: URL, +) \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/WorkbookOrderDelegator.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/WorkbookOrderDelegator.kt deleted file mode 100644 index ffd458bc8..000000000 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/WorkbookOrderDelegator.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.few.api.domain.workbook.usecase.model - -import com.few.api.domain.workbook.usecase.dto.BrowseWorkBookDetail - -interface WorkbookOrderDelegator { - - /** - * 워크북을 정렬합니다. - * */ - fun order(): List -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/WorkbookOrderDelegatorExecutor.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/WorkbookOrderDelegatorExecutor.kt deleted file mode 100644 index 050d5bbfb..000000000 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/WorkbookOrderDelegatorExecutor.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.few.api.domain.workbook.usecase.service - -import com.few.api.domain.workbook.usecase.dto.BrowseWorkBookDetail -import com.few.api.domain.workbook.usecase.model.WorkbookOrderDelegator -import org.springframework.stereotype.Service - -@Service -class WorkbookOrderDelegatorExecutor { - - fun execute(delegator: WorkbookOrderDelegator): List { - return delegator.order() - } -} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/AuthMainViewWorkbookOrderDelegator.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/AuthMainViewWorkbookOrderDelegator.kt similarity index 65% rename from api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/AuthMainViewWorkbookOrderDelegator.kt rename to api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/AuthMainViewWorkbookOrderDelegator.kt index 5608522e7..fe44f1f59 100644 --- a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/model/AuthMainViewWorkbookOrderDelegator.kt +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/AuthMainViewWorkbookOrderDelegator.kt @@ -1,30 +1,33 @@ -package com.few.api.domain.workbook.usecase.model +package com.few.api.domain.workbook.usecase.service.order -import com.few.api.domain.workbook.service.dto.BrowseMemberSubscribeWorkbooksOutDto -import com.few.api.domain.workbook.usecase.dto.BrowseWorkBookDetail +import com.few.api.domain.workbook.usecase.model.MemberSubscribedWorkbook +import com.few.api.domain.workbook.usecase.model.WorkBook class AuthMainViewWorkbookOrderDelegator( /** * @see com.few.api.repo.dao.workbook.WorkbookDao.browseWorkBookWithSubscriptionCount */ - private val workbooks: List, - private val memberSubscribeWorkbooks: List, + private val workbooks: List, + private val memberSubscribedWorkbooks: List, ) : WorkbookOrderDelegator { /** * 메인 화면에 보여질 워크북을 정렬합니다. * 1. 활성화된 구독 워크북을 먼저 보여줍니다. + * - 구독 워크북 정렬 기준은 currentDay를 기준으로 내림차순입니다. * 2. 구독 기록이 없는 워크북을 보여줍니다. * 3. 비활성화된 구독 워크북을 보여줍니다. */ - override fun order(): List { + override fun order(): List { val allWorkbookIds = workbooks.associate { it.id to false }.toMutableMap() - val activeSubWorkbookIds = memberSubscribeWorkbooks.filter { it.isActiveSub }.sortedByDescending { - it.currentDay - }.map { it.workbookId } - val inActiveSubWorkbookIds = memberSubscribeWorkbooks.filter { !it.isActiveSub }.map { it.workbookId } + val activeSubWorkbookIds = + memberSubscribedWorkbooks.filter { it.isActiveSub }.sortedByDescending { + it.currentDay + }.map { it.workbookId } + val inActiveSubWorkbookIds = + memberSubscribedWorkbooks.filter { !it.isActiveSub }.map { it.workbookId } - val orderedWorkbooks = mutableListOf() + val orderedWorkbooks = mutableListOf() /** * 활성화된 구독 워크북을 먼저 보여줍니다. @@ -39,7 +42,7 @@ class AuthMainViewWorkbookOrderDelegator( /** * 비활성화된 구독 워크북을 모아둡니다. */ - val lastAddWorkbooks = mutableListOf() + val lastAddWorkbooks = mutableListOf() inActiveSubWorkbookIds.forEach { inActiveSubWorkbookId -> workbooks.find { it.id == inActiveSubWorkbookId }?.let { lastAddWorkbooks.add(it) diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/BasicWorkbookOrderDelegator.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/BasicWorkbookOrderDelegator.kt new file mode 100644 index 000000000..daae07090 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/BasicWorkbookOrderDelegator.kt @@ -0,0 +1,11 @@ +package com.few.api.domain.workbook.usecase.service.order + +import com.few.api.domain.workbook.usecase.model.WorkBook + +class BasicWorkbookOrderDelegator( + private val workbooks: List, +) : WorkbookOrderDelegator { + override fun order(): List { + return workbooks + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/WorkbookOrderDelegator.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/WorkbookOrderDelegator.kt new file mode 100644 index 000000000..1da8614c7 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/WorkbookOrderDelegator.kt @@ -0,0 +1,11 @@ +package com.few.api.domain.workbook.usecase.service.order + +import com.few.api.domain.workbook.usecase.model.WorkBook + +interface WorkbookOrderDelegator { + + /** + * 워크북을 정렬합니다. + * */ + fun order(): List +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/WorkbookOrderDelegatorExecutor.kt b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/WorkbookOrderDelegatorExecutor.kt new file mode 100644 index 000000000..cf37cd2e2 --- /dev/null +++ b/api/src/main/kotlin/com/few/api/domain/workbook/usecase/service/order/WorkbookOrderDelegatorExecutor.kt @@ -0,0 +1,12 @@ +package com.few.api.domain.workbook.usecase.service.order + +import com.few.api.domain.workbook.usecase.model.WorkBook +import org.springframework.stereotype.Service + +@Service +class WorkbookOrderDelegatorExecutor { + + fun execute(delegator: WorkbookOrderDelegator): List { + return delegator.order() + } +} \ No newline at end of file diff --git a/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt b/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt index 090ef090f..26e4ff94c 100644 --- a/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt +++ b/api/src/main/kotlin/com/few/api/web/controller/workbook/response/ReadWorkBookResponse.kt @@ -1,5 +1,6 @@ package com.few.api.web.controller.workbook.response +import com.fasterxml.jackson.annotation.JsonFormat import com.few.api.domain.workbook.usecase.dto.ReadWorkbookUseCaseOut import java.net.URL import java.time.LocalDateTime @@ -10,6 +11,7 @@ data class ReadWorkBookResponse( val title: String, val description: String, val category: String, + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") val createdAt: LocalDateTime, val writers: List, val articles: List, diff --git a/api/src/main/resources/messages/article.properties b/api/src/main/resources/messages/article.properties index e1f4acb6e..b24e04b0c 100644 --- a/api/src/main/resources/messages/article.properties +++ b/api/src/main/resources/messages/article.properties @@ -1,4 +1,5 @@ article.notfound.id=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0069\u0064 article.notfound.articleidworkbookid=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u0069\u0064\u0077\u006f\u0072\u006b\u0062\u006f\u006f\u006b\u0069 +article.notfound.workbookIdAndCurrentDay=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u002e\u006e\u006f\u0074\u0066\u006f\u0075\u006e\u0064\u002e\u0077\u006f\u0072\u006b\u0062\u006f\u006f\u006b\u0049\u0064\u0041\u006e\u0064\u0043\u0075\u0072\u0072\u0065\u006e\u0074\u0044\u0061\u0079 article.invalid.category=\uc874\uc7ac\ud558\uc9c0\u0020\uc54a\ub294\u0020\uc544\ud2f0\ud074\u0020\uce74\ud14c\uace0\ub9ac\uc785\ub2c8\ub2e4\u000d articlemaincard.notfound.id=\u0061\u0072\u0074\u0069\u0063\u006c\u0065\u0020\u006d\u0061\u0069\u006e\u0020\u0063\u0061\u0072\u0064\uac00\u0020\uc874\uc7ac\ud558\uc9c0\u0020\uc54a\uc2b5\ub2c8\ub2e4\u000d diff --git a/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt index d8300cec2..439bade14 100644 --- a/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/article/usecase/ReadArticleUseCaseTest.kt @@ -9,9 +9,11 @@ import com.few.api.domain.article.service.dto.ReadWriterOutDto import com.few.api.domain.article.usecase.dto.ReadArticleUseCaseIn import com.few.api.repo.dao.article.ArticleDao import com.few.api.repo.dao.article.record.SelectArticleRecord +import com.few.data.common.code.CategoryType import io.github.oshai.kotlinlogging.KotlinLogging import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.* import java.net.URL @@ -26,7 +28,6 @@ class ReadArticleUseCaseTest : BehaviorSpec({ lateinit var useCase: ReadArticleUseCase lateinit var articleViewHisAsyncHandler: ArticleViewHisAsyncHandler lateinit var articleViewCountHandler: ArticleViewCountHandler - val useCaseIn = ReadArticleUseCaseIn(articleId = 1L, memberId = 1L) beforeContainer { articleDao = mockk() @@ -43,35 +44,60 @@ class ReadArticleUseCaseTest : BehaviorSpec({ ) } - given("아티클 조회 요청이 온 상황에서") { - `when`("아티클과 작가가 존재할 경우") { - val record = SelectArticleRecord( - articleId = 1L, - writerId = 1L, - mainImageURL = URL("https://jh-labs.tistory.com/"), - title = "title", - category = (10).toByte(), - content = "content", + given("로그인 여부와 상관없이 아티클 조회 요청이 온 상황에서") { + val articleId = 1L + val memberId = 1L + val useCaseIn = ReadArticleUseCaseIn(articleId, memberId) + + `when`("요청한 아티클과 작가가 존재할 경우") { + val writerId = 1L + val mainImageURL = URL("http://localhost:8080/image/main/1") + val title = "title" + val category = CategoryType.ECONOMY.code + val content = "content" + every { articleDao.selectArticleRecord(any()) } returns SelectArticleRecord( + articleId = articleId, + writerId = writerId, + mainImageURL = mainImageURL, + title = title, + category = category, + content = content, createdAt = LocalDateTime.now() ) - val writerSvcOutDto = ReadWriterOutDto( - writerId = 1L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/"), - imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") + + val writerName = "writer" + val writerProfileImageURL = URL("http://localhost:8080/image/writer/1") + every { readArticleWriterRecordService.execute(any()) } returns ReadWriterOutDto( + writerId = writerId, + name = writerName, + url = mainImageURL, + imageUrl = writerProfileImageURL ) - val probSvcOutDto = BrowseArticleProblemsOutDto(problemIds = listOf(1, 2, 3)) - every { articleDao.selectArticleRecord(any()) } returns record - every { readArticleWriterRecordService.execute(any()) } returns writerSvcOutDto - every { browseArticleProblemsService.execute(any()) } returns probSvcOutDto - every { articleViewCountHandler.browseArticleViewCount(any()) } returns 1L + val problemIds = listOf(1L, 2L, 3L) + every { browseArticleProblemsService.execute(any()) } returns BrowseArticleProblemsOutDto(problemIds = problemIds) + + val views = 1L + every { articleViewCountHandler.browseArticleViewCount(any()) } returns views + every { articleViewHisAsyncHandler.addArticleViewHis(any(), any(), any()) } answers { log.debug { "Inserting article view history asynchronously" } } - then("아티클이 정상 조회된다") { - useCase.execute(useCaseIn) + then("아티클과 연관된 정보를 조회한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.id shouldBe articleId + useCaseOut.writer.id shouldBe writerId + useCaseOut.writer.name shouldBe writerName + useCaseOut.writer.url shouldBe mainImageURL + useCaseOut.writer.imageUrl shouldBe writerProfileImageURL + useCaseOut.mainImageUrl shouldBe mainImageURL + useCaseOut.title shouldBe title + useCaseOut.content shouldBe content + useCaseOut.problemIds shouldBe problemIds + useCaseOut.category shouldBe CategoryType.ECONOMY.displayName + useCaseOut.views shouldBe views + useCaseOut.workbooks shouldBe emptyList() verify(exactly = 1) { articleDao.selectArticleRecord(any()) } verify(exactly = 1) { readArticleWriterRecordService.execute(any()) } @@ -81,13 +107,46 @@ class ReadArticleUseCaseTest : BehaviorSpec({ } } - `when`("존재하지 않는 아티클일 경우") { + `when`("요청한 아티클이 존재하지 않을 경우") { every { articleDao.selectArticleRecord(any()) } returns null then("예외가 발생한다") { shouldThrow { useCase.execute(useCaseIn) } verify(exactly = 1) { articleDao.selectArticleRecord(any()) } + verify(exactly = 0) { readArticleWriterRecordService.execute(any()) } + verify(exactly = 0) { browseArticleProblemsService.execute(any()) } + verify(exactly = 0) { articleViewCountHandler.browseArticleViewCount(any()) } + verify(exactly = 0) { articleViewHisAsyncHandler.addArticleViewHis(any(), any(), any()) } + } + } + + `when`("요청한 아티클의 작가가 존재하지 않을 경우") { + val writerId = 1L + val mainImageURL = URL("http://localhost:8080/image/main/1") + val title = "title" + val category = CategoryType.ECONOMY.code + val content = "content" + every { articleDao.selectArticleRecord(any()) } returns SelectArticleRecord( + articleId = articleId, + writerId = writerId, + mainImageURL = mainImageURL, + title = title, + category = category, + content = content, + createdAt = LocalDateTime.now() + ) + + every { readArticleWriterRecordService.execute(any()) } returns null + + then("예외가 발생한다") { + shouldThrow { useCase.execute(useCaseIn) } + + verify(exactly = 1) { articleDao.selectArticleRecord(any()) } + verify(exactly = 1) { readArticleWriterRecordService.execute(any()) } + verify(exactly = 0) { browseArticleProblemsService.execute(any()) } + verify(exactly = 0) { articleViewCountHandler.browseArticleViewCount(any()) } + verify(exactly = 0) { articleViewHisAsyncHandler.addArticleViewHis(any(), any(), any()) } } } } diff --git a/api/src/test/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCaseTest.kt index b70c2947a..50aad9d5e 100644 --- a/api/src/test/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/member/usecase/SaveMemberUseCaseTest.kt @@ -7,17 +7,19 @@ import com.few.api.repo.dao.member.command.UpdateDeletedMemberTypeCommand import com.few.api.repo.dao.member.query.SelectMemberByEmailNotConsiderDeletedAtQuery import com.few.api.repo.dao.member.record.MemberIdAndIsDeletedRecord import com.few.email.service.member.SendAuthEmailService +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify +import org.mockito.ArgumentMatchers.any class SaveMemberUseCaseTest : BehaviorSpec({ lateinit var memberDao: MemberDao lateinit var sendAuthEmailService: SendAuthEmailService lateinit var idEncryption: IdEncryption lateinit var useCase: SaveMemberUseCase - val useCaseIn = SaveMemberUseCaseIn(email = "test@gmail.com") beforeContainer { memberDao = mockk() @@ -27,69 +29,99 @@ class SaveMemberUseCaseTest : BehaviorSpec({ } given("회원가입/로그인 요청이 온 상황에서") { + val email = "test@gmail.com" + val useCaseIn = SaveMemberUseCaseIn(email = email) + `when`("요청의 이메일이 가입 이력이 없는 경우") { every { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } returns null + every { memberDao.insertMember(any()) } returns 1L - every { idEncryption.encrypt(any()) } returns "encryptedToken" + + val token = "encryptedToken" + every { idEncryption.encrypt(any()) } returns token + every { sendAuthEmailService.send(any()) } returns Unit then("인증 이메일 발송 성공 응답을 반환한다") { - useCase.execute(useCaseIn) + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.isSendAuthEmail shouldBe true verify(exactly = 1) { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } verify(exactly = 1) { memberDao.insertMember(any()) } + verify(exactly = 0) { memberDao.updateMemberType(any(UpdateDeletedMemberTypeCommand::class)) } verify(exactly = 1) { idEncryption.encrypt(any()) } + verify(exactly = 1) { sendAuthEmailService.send(any()) } } } `when`("요청의 이메일이 가입되어 있는 경우") { + val memberId = 1L every { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } returns MemberIdAndIsDeletedRecord( - memberId = 1L, + memberId = memberId, isDeleted = false ) - every { idEncryption.encrypt(any()) } returns "encryptedToken" + + val token = "encryptedToken" + every { idEncryption.encrypt(any()) } returns token + every { sendAuthEmailService.send(any()) } returns Unit then("인증 이메일 발송 성공 응답을 반환한다") { - useCase.execute(useCaseIn) + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.isSendAuthEmail shouldBe true verify(exactly = 1) { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } verify(exactly = 0) { memberDao.insertMember(any()) } + verify(exactly = 0) { memberDao.updateMemberType(any(UpdateDeletedMemberTypeCommand::class)) } verify(exactly = 1) { idEncryption.encrypt(any()) } + verify(exactly = 1) { sendAuthEmailService.send(any()) } } } `when`("요청의 이메일이 삭제된 회원인 경우") { + val memberId = 1L every { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } returns MemberIdAndIsDeletedRecord( - memberId = 1L, + memberId = memberId, isDeleted = true ) - every { memberDao.updateMemberType(any(UpdateDeletedMemberTypeCommand::class)) } returns 1L - every { idEncryption.encrypt(any()) } returns "encryptedToken" + + every { memberDao.updateMemberType(any(UpdateDeletedMemberTypeCommand::class)) } returns memberId + + val token = "encryptedToken" + every { idEncryption.encrypt(any()) } returns token + + every { sendAuthEmailService.send(any()) } returns Unit then("인증 이메일 발송 성공 응답을 반환한다") { - useCase.execute(useCaseIn) + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.isSendAuthEmail shouldBe true verify(exactly = 1) { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } verify(exactly = 0) { memberDao.insertMember(any()) } verify(exactly = 1) { memberDao.updateMemberType(any(UpdateDeletedMemberTypeCommand::class)) } verify(exactly = 1) { idEncryption.encrypt(any()) } + verify(exactly = 1) { sendAuthEmailService.send(any()) } } } `when`("인증 이메일 발송에 실패한 경우") { every { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } returns null - every { memberDao.insertMember(any()) } returns 1L - every { idEncryption.encrypt(any()) } returns "encryptedToken" + + val memberId = 1L + every { memberDao.insertMember(any()) } returns memberId + + val token = "encryptedToken" + every { idEncryption.encrypt(any()) } returns token every { sendAuthEmailService.send(any()) } throws Exception() then("인증 이메일 발송 실패 응답을 반환한다") { - useCase.execute(useCaseIn) + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.isSendAuthEmail shouldBe false - verify(exactly = 1) { memberDao.selectMemberByEmail(any(SelectMemberByEmailNotConsiderDeletedAtQuery::class)) } - verify(exactly = 1) { memberDao.insertMember(any()) } - verify(exactly = 1) { idEncryption.encrypt(any()) } + shouldThrow { + sendAuthEmailService.send(any()) + } } } } diff --git a/api/src/test/kotlin/com/few/api/domain/member/usecase/TokenUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/member/usecase/TokenUseCaseTest.kt index f4b8d4e7a..9f0133b0d 100644 --- a/api/src/test/kotlin/com/few/api/domain/member/usecase/TokenUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/member/usecase/TokenUseCaseTest.kt @@ -9,7 +9,9 @@ import com.few.api.security.token.AuthToken import com.few.api.security.token.TokenGenerator import com.few.api.security.token.TokenResolver import com.few.data.common.code.MemberType +import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -29,97 +31,135 @@ class TokenUseCaseTest : BehaviorSpec({ useCase = TokenUseCase(tokenGenerator, tokenResolver, memberDao, idEncryption) } - given("토큰 요청이 온 상황에서") { - `when`("요청에 refreshToken이 포함되어 있는 경우") { - every { tokenResolver.resolveId(any()) } returns 1L - every { tokenResolver.resolveEmail(any()) } returns "test@gmail.com" + given("리프레시 토큰이 포함된 토큰 갱신 요청이 온 상황에서") { + val oldRefreshToken = "refreshToken" + val useCaseIn = TokenUseCaseIn( + token = null, + refreshToken = oldRefreshToken, + at = null, + rt = null + ) + + `when`("유효한 리프레시 토큰인 경우") { + val memberId = 1L + every { tokenResolver.resolveId(any()) } returns memberId + + val email = "test@gmail.com" + every { tokenResolver.resolveEmail(any()) } returns email + + val accessToken = "newAccessToken" + val refreshToken = "newRefreshToken" every { tokenGenerator.generateAuthToken(any(), any(), any()) } returns AuthToken( - accessToken = "accessToken", - refreshToken = "refreshToken" + accessToken = accessToken, + refreshToken = refreshToken ) + then("새로운 토큰을 반환한다") { - useCase.execute( - TokenUseCaseIn( - token = null, - refreshToken = "refreshToken", - at = null, - rt = null - ) - ) + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.accessToken shouldBe accessToken + useCaseOut.refreshToken shouldBe refreshToken + useCaseOut.isLogin shouldBe true + verify(exactly = 1) { tokenResolver.resolveId(any()) } verify(exactly = 1) { tokenResolver.resolveEmail(any()) } verify(exactly = 1) { tokenGenerator.generateAuthToken(any(), any(), any()) } } } - `when`("요청에 로그인을 하려는 멤버의 token이 포함되어 있는 경우") { - every { idEncryption.decrypt(any()) } returns "1" + `when`("유효하지 않은 리프레시 토큰인 경우") { + every { tokenResolver.resolveId(any()) } throws IllegalStateException() + + then("예외를 반환한다") { + shouldThrow { useCase.execute(useCaseIn) } + + verify(exactly = 1) { tokenResolver.resolveId(any()) } + verify(exactly = 0) { tokenResolver.resolveEmail(any()) } + verify(exactly = 0) { tokenGenerator.generateAuthToken(any(), any(), any()) } + } + } + } + + given("멤버 아이디 정보를 암호화한 토큰이 포함된 토큰 갱신 요청이 온 상황에서") { + val encryptedIdToken = "token" + val useCaseIn = TokenUseCaseIn( + token = encryptedIdToken, + refreshToken = null, + at = null, + rt = null + ) + + `when`("멤버 인증 토큰이 유효하고 인증을 위한 요청인 경우") { + val decryptedId = "1" + every { idEncryption.decrypt(any()) } returns decryptedId + + val accessToken = "newAccessToken" + val refreshToken = "newRefreshToken" every { tokenGenerator.generateAuthToken(any(), any(), any(), any(), any()) } returns AuthToken( - accessToken = "accessToken", - refreshToken = "refreshToken" + accessToken = accessToken, + refreshToken = refreshToken ) + + val email = "test@gmail.com" every { memberDao.selectMemberEmailAndType(any()) } returns MemberEmailAndTypeRecord( - email = "test@gmail.com", + email = email, memberType = MemberType.NORMAL ) then("새로운 토큰을 반환한다") { - useCase.execute( - TokenUseCaseIn( - token = "token", - refreshToken = null, - at = null, - rt = null - ) - ) + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.accessToken shouldBe accessToken + useCaseOut.refreshToken shouldBe refreshToken + useCaseOut.isLogin shouldBe true + verify(exactly = 1) { idEncryption.decrypt(any()) } - verify(exactly = 1) { - tokenGenerator.generateAuthToken( - any(), - any(), - any(), - any(), - any() - ) - } verify(exactly = 1) { memberDao.selectMemberEmailAndType(any()) } + verify(exactly = 0) { memberDao.updateMemberType(any(UpdateMemberTypeCommand::class)) } + verify(exactly = 1) { tokenGenerator.generateAuthToken(any(), any(), any(), any(), any()) } } } - `when`("요청에 회원가입을 완료 하려는 멤버의 token이 포함되어 있는 경우") { - every { idEncryption.decrypt(any()) } returns "1" + `when`("멤버 인증 토큰이 유효하고 가입을 위한 요청인 경우") { + val decryptedId = "1" + every { idEncryption.decrypt(any()) } returns decryptedId + + val accessToken = "newAccessToken" + val refreshToken = "newRefreshToken" every { tokenGenerator.generateAuthToken(any(), any(), any(), any(), any()) } returns AuthToken( - accessToken = "accessToken", - refreshToken = "refreshToken" + accessToken = accessToken, + refreshToken = refreshToken ) + + val email = "test@gmail.com" every { memberDao.selectMemberEmailAndType(any()) } returns MemberEmailAndTypeRecord( - email = "test@gmail.com", + email = email, memberType = MemberType.PREAUTH ) every { memberDao.updateMemberType(any(UpdateMemberTypeCommand::class)) } returns Unit then("새로운 토큰을 반환한다") { - useCase.execute( - TokenUseCaseIn( - token = "token", - refreshToken = null, - at = null, - rt = null - ) - ) + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.accessToken shouldBe accessToken + useCaseOut.refreshToken shouldBe refreshToken + useCaseOut.isLogin shouldBe false + verify(exactly = 1) { idEncryption.decrypt(any()) } - verify(exactly = 1) { - tokenGenerator.generateAuthToken( - any(), - any(), - any(), - any(), - any() - ) - } verify(exactly = 1) { memberDao.selectMemberEmailAndType(any()) } verify(exactly = 1) { memberDao.updateMemberType(any(UpdateMemberTypeCommand::class)) } + verify(exactly = 1) { tokenGenerator.generateAuthToken(any(), any(), any(), any(), any()) } + } + } + + `when`("유효하지 않은 멤버 인증 토큰인 경우") { + every { idEncryption.decrypt(any()) } throws IllegalStateException() + + then("예외를 반환한다") { + shouldThrow { useCase.execute(useCaseIn) } + + verify(exactly = 1) { idEncryption.decrypt(any()) } + verify(exactly = 0) { memberDao.selectMemberEmailAndType(any()) } + verify(exactly = 0) { memberDao.updateMemberType(any(UpdateMemberTypeCommand::class)) } + verify(exactly = 0) { tokenGenerator.generateAuthToken(any(), any(), any(), any(), any()) } } } } diff --git a/api/src/test/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCaseTest.kt index ca54ea69e..6a75891cb 100644 --- a/api/src/test/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/problem/usecase/BrowseProblemsUseCaseTest.kt @@ -5,6 +5,7 @@ import com.few.api.repo.dao.problem.ProblemDao import com.few.api.repo.dao.problem.record.ProblemIdsRecord import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -13,26 +14,29 @@ class BrowseProblemsUseCaseTest : BehaviorSpec({ lateinit var problemDao: ProblemDao lateinit var useCase: BrowseProblemsUseCase - val useCaseIn = BrowseProblemsUseCaseIn(articleId = 1L) beforeContainer { problemDao = mockk() useCase = BrowseProblemsUseCase(problemDao) } - given("특정 아티클에 대한") { - `when`("문제가 존재할 경우") { - val problemIdsRecord = ProblemIdsRecord(listOf(1, 2, 3)) - every { problemDao.selectProblemsByArticleId(any()) } returns problemIdsRecord + given("특정 아티클에 대한 문제 조회 요청이 온 상황에서") { + val articleId = 1L + val useCaseIn = BrowseProblemsUseCaseIn(articleId = articleId) - then("문제번호가 정상적으로 조회된다") { - useCase.execute(useCaseIn) + `when`("아티클의 문제가 존재할 경우") { + val problemIds = listOf(1L, 2L, 3L) + every { problemDao.selectProblemsByArticleId(any()) } returns ProblemIdsRecord(problemIds) + + then("문제 목록을 반환한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.problemIds shouldBe problemIds verify(exactly = 1) { problemDao.selectProblemsByArticleId(any()) } } } - `when`("문제가 존재하지 않을 경우") { + `when`("아티클의 문제가 존재하지 않을 경우") { every { problemDao.selectProblemsByArticleId(any()) } returns null then("예외가 발생한다") { diff --git a/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt index 5bdd418b4..035bada61 100644 --- a/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/problem/usecase/CheckProblemUseCaseTest.kt @@ -23,51 +23,61 @@ class CheckProblemUseCaseTest : BehaviorSpec({ useCase = CheckProblemUseCase(problemDao, submitHistoryDao) } - given("문제 정답 확인 요청이 온 상황에서") { + given("특정 문제에 대한 정답 확인 요청이 온 상황에서") { + val problemId = 1L + val submissionVal = "1" + val useCaseIn = CheckProblemUseCaseIn(problemId = problemId, sub = submissionVal) + `when`("제출 값과 문제 정답이 같을 경우") { - val submissionVal = "1" val answer = submissionVal - val useCaseIn = CheckProblemUseCaseIn(problemId = 1L, sub = submissionVal) - val answerRecord = SelectProblemAnswerRecord(id = 1L, answer = answer, explanation = "해설입니다.") + val explanation = "해설입니다." + every { problemDao.selectProblemAnswer(any()) } returns SelectProblemAnswerRecord(id = problemId, answer = answer, explanation = explanation) - every { problemDao.selectProblemAnswer(any()) } returns answerRecord - every { submitHistoryDao.insertSubmitHistory(any()) } returns 1L + val problemSubmitHistoryId = 1L + every { submitHistoryDao.insertSubmitHistory(any()) } returns problemSubmitHistoryId then("문제가 정답처리 된다") { val useCaseOut = useCase.execute(useCaseIn) - useCaseOut.isSolved shouldBe true + useCaseOut.answer shouldBe answer + useCaseOut.explanation shouldBe explanation + verify(exactly = 1) { problemDao.selectProblemAnswer(any()) } verify(exactly = 1) { submitHistoryDao.insertSubmitHistory(any()) } } } `when`("제출 값과 문제 정답이 다를 경우") { - val submissionVal = "1" val answer = "2" - val useCaseIn = CheckProblemUseCaseIn(problemId = 1L, sub = submissionVal) - val answerRecord = SelectProblemAnswerRecord(id = 1L, answer = answer, explanation = "해설입니다.") + val explanation = "해설입니다." + every { problemDao.selectProblemAnswer(any()) } returns SelectProblemAnswerRecord( + id = problemId, + answer = answer, + explanation = explanation + ) - every { problemDao.selectProblemAnswer(any()) } returns answerRecord - every { submitHistoryDao.insertSubmitHistory(any()) } returns 1L + val problemSubmitHistoryId = 1L + every { submitHistoryDao.insertSubmitHistory(any()) } returns problemSubmitHistoryId then("문제가 오답처리 된다") { val useCaseOut = useCase.execute(useCaseIn) - useCaseOut.isSolved shouldBe false + useCaseOut.answer shouldBe answer + useCaseOut.explanation shouldBe explanation + verify(exactly = 1) { problemDao.selectProblemAnswer(any()) } verify(exactly = 1) { submitHistoryDao.insertSubmitHistory(any()) } } } `when`("존재하지 않는 문제일 경우") { - val useCaseIn = CheckProblemUseCaseIn(problemId = 1L, sub = "1") - every { problemDao.selectProblemAnswer(any()) } returns null then("예외가 발생한다") { shouldThrow { useCase.execute(useCaseIn) } + verify(exactly = 1) { problemDao.selectProblemAnswer(any()) } + verify(exactly = 0) { submitHistoryDao.insertSubmitHistory(any()) } } } } diff --git a/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt index d97f6490c..ff4e7d8e9 100644 --- a/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/problem/usecase/ReadProblemUseCaseTest.kt @@ -8,16 +8,17 @@ import com.few.api.repo.dao.problem.support.Contents import com.few.api.repo.dao.problem.support.ContentsJsonMapper import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify +import java.util.stream.IntStream class ReadProblemUseCaseTest : BehaviorSpec({ lateinit var problemDao: ProblemDao lateinit var contentsJsonMapper: ContentsJsonMapper lateinit var useCase: ReadProblemUseCase - val useCaseIn = ReadProblemUseCaseIn(problemId = 1L) beforeContainer { problemDao = mockk() @@ -25,21 +26,30 @@ class ReadProblemUseCaseTest : BehaviorSpec({ useCase = ReadProblemUseCase(problemDao, contentsJsonMapper) } - given("문제를 조회할 상황에서") { + given("특정 문제를 조회하는 요청이 온 상황에서") { + val problemId = 1L + val useCaseIn = ReadProblemUseCaseIn(problemId = problemId) + `when`("문제가 존재할 경우") { - val problemRecord = SelectProblemRecord(id = 1L, title = "title", contents = "{}") - val contents = Contents( - listOf( - Content(number = 1, content = "{}"), - Content(number = 2, content = "{}") - ) - ) + val title = "title" + val problemContents = "{}" + every { problemDao.selectProblemContents(any()) } returns SelectProblemRecord(id = problemId, title = title, contents = problemContents) - every { problemDao.selectProblemContents(any()) } returns problemRecord - every { contentsJsonMapper.toObject(any()) } returns contents + val contentCount = 2 + every { contentsJsonMapper.toObject(any()) } returns Contents( + IntStream.range(1, 1 + contentCount) + .mapToObj { Content(number = it.toLong(), content = "{}") }.toList() + ) - then("정상적으로 실행되어야 한다") { - useCase.execute(useCaseIn) + then("문제 정보를 반환한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.id shouldBe problemId + useCaseOut.title shouldBe title + useCaseOut.contents.size shouldBe contentCount + useCaseOut.contents.forEachIndexed { index, content -> + content.number shouldBe (index + 1).toLong() + content.content shouldBe "{}" + } verify(exactly = 1) { problemDao.selectProblemContents(any()) } verify(exactly = 1) { contentsJsonMapper.toObject(any()) } @@ -49,10 +59,11 @@ class ReadProblemUseCaseTest : BehaviorSpec({ `when`("문제가 존재하지 않을 경우") { every { problemDao.selectProblemContents(any()) } returns null - then("예외가 발생해야 한다") { + then("예외가 발생한다") { shouldThrow { useCase.execute(useCaseIn) } verify(exactly = 1) { problemDao.selectProblemContents(any()) } + verify(exactly = 0) { contentsJsonMapper.toObject(any()) } } } } diff --git a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt index d5018a7f4..b7bb374df 100644 --- a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/BrowseSubscribeWorkbooksUseCaseTest.kt @@ -5,8 +5,10 @@ import com.few.api.domain.subscription.service.SubscriptionArticleService import com.few.api.domain.subscription.usecase.dto.BrowseSubscribeWorkbooksUseCaseIn import com.few.api.repo.dao.subscription.SubscriptionDao import com.few.api.repo.dao.subscription.record.MemberWorkbookSubscriptionStatusRecord +import com.few.api.web.support.WorkBookStatus import io.github.oshai.kotlinlogging.KotlinLogging import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -26,46 +28,167 @@ class BrowseSubscribeWorkbooksUseCaseTest : BehaviorSpec({ useCase = BrowseSubscribeWorkbooksUseCase(subscriptionDao, subscriptionArticleService, objectMapper) } - given("사용자 구독 정보 조회 요청이 온 상황에서") { - `when`("사용자의 구독 정보가 있는 경우") { + given("멤버의 구독 워크북 정보 조회 요청이 온 상황에서") { + val memberId = 1L + val useCaseIn = BrowseSubscribeWorkbooksUseCaseIn(memberId = memberId) + + `when`("멤버의 구독 워크북 정보가 존재할 경우") { + val inactiveWorkbookId = 1L + val inactiveWorkbookCurrentDay = 2 + val inactiveWorkbookTotalDay = 3 every { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } returns listOf( MemberWorkbookSubscriptionStatusRecord( - workbookId = 1L, + workbookId = inactiveWorkbookId, isActiveSub = false, - currentDay = 1, - totalDay = 3 + currentDay = inactiveWorkbookCurrentDay, + totalDay = inactiveWorkbookTotalDay ) ) + val activeWorkbookId = 2L + val activeWorkbookCurrentDay = 1 + val activeWorkbookTotalDay = 3 every { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } returns listOf( MemberWorkbookSubscriptionStatusRecord( - workbookId = 2L, + workbookId = activeWorkbookId, isActiveSub = true, - currentDay = 2, - totalDay = 3 + currentDay = activeWorkbookCurrentDay, + totalDay = activeWorkbookTotalDay ) + ) every { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) - } returns 1L andThen 2L + } returns inactiveWorkbookId andThen activeWorkbookId + val activeWorkbookSubscriptionCount = 1 + val inactiveWorkbookSubscriptionCount = 2 every { subscriptionDao.countAllWorkbookSubscription(any()) } returns mapOf( - 1L to 1, - 2L to 2 + inactiveWorkbookId to inactiveWorkbookSubscriptionCount, + activeWorkbookId to activeWorkbookSubscriptionCount ) - every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":1}" andThen "{\"articleId\":2}" + every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":$inactiveWorkbookId}" andThen "{\"articleId\":$activeWorkbookId}" + + then("멤버의 구독 워크북 정보를 반환한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.workbooks.size shouldBe 2 - then("사용자의 구독 정보를 조회한다") { - val useCaseIn = BrowseSubscribeWorkbooksUseCaseIn(memberId = 1L) - useCase.execute(useCaseIn) + val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] + inActiveSubscriptionWorkbook.workbookId shouldBe inactiveWorkbookId + inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE + inActiveSubscriptionWorkbook.currentDay shouldBe inactiveWorkbookCurrentDay + inActiveSubscriptionWorkbook.totalDay shouldBe inactiveWorkbookTotalDay + inActiveSubscriptionWorkbook.totalSubscriber shouldBe inactiveWorkbookSubscriptionCount + inActiveSubscriptionWorkbook.articleInfo shouldBe "{\"articleId\":$inactiveWorkbookId}" + + val activeSubscriptionWorkbook = useCaseOut.workbooks[1] + activeSubscriptionWorkbook.workbookId shouldBe activeWorkbookId + activeSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.ACTIVE + activeSubscriptionWorkbook.currentDay shouldBe activeWorkbookCurrentDay + activeSubscriptionWorkbook.totalDay shouldBe activeWorkbookTotalDay + activeSubscriptionWorkbook.totalSubscriber shouldBe activeWorkbookSubscriptionCount + activeSubscriptionWorkbook.articleInfo shouldBe "{\"articleId\":$activeWorkbookId}" verify(exactly = 1) { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } verify(exactly = 1) { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } + verify(exactly = 2) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } verify(exactly = 1) { subscriptionDao.countAllWorkbookSubscription(any()) } verify(exactly = 2) { objectMapper.writeValueAsString(any()) } } } + + `when`("멤버의 구독 비활성 워크북 정보만 존재할 경우") { + val inactiveWorkbookId = 1L + val inactiveWorkbookCurrentDay = 2 + val inactiveWorkbookTotalDay = 3 + every { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } returns listOf( + MemberWorkbookSubscriptionStatusRecord( + workbookId = inactiveWorkbookId, + isActiveSub = false, + currentDay = inactiveWorkbookCurrentDay, + totalDay = inactiveWorkbookTotalDay + ) + ) + + every { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } returns emptyList() + + every { + subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) + } returns inactiveWorkbookId + + val inactiveWorkbookSubscriptionCount = 2 + every { subscriptionDao.countAllWorkbookSubscription(any()) } returns mapOf( + inactiveWorkbookId to inactiveWorkbookSubscriptionCount + ) + + every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":$inactiveWorkbookId}" + + then("멤버의 구독 비활성 워크북 정보를 반환한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.workbooks.size shouldBe 1 + + val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] + inActiveSubscriptionWorkbook.workbookId shouldBe inactiveWorkbookId + inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE + inActiveSubscriptionWorkbook.currentDay shouldBe inactiveWorkbookCurrentDay + inActiveSubscriptionWorkbook.totalDay shouldBe inactiveWorkbookTotalDay + inActiveSubscriptionWorkbook.totalSubscriber shouldBe inactiveWorkbookSubscriptionCount + inActiveSubscriptionWorkbook.articleInfo shouldBe "{\"articleId\":$inactiveWorkbookId}" + + verify(exactly = 1) { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } + verify(exactly = 1) { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } + verify(exactly = 1) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } + verify(exactly = 1) { subscriptionDao.countAllWorkbookSubscription(any()) } + verify(exactly = 1) { objectMapper.writeValueAsString(any()) } + } + } + + `when`("멤버의 구독 활성 워크북 정보만 존재할 경우") { + every { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } returns emptyList() + + val activeWorkbookId = 1L + val activeWorkbookCurrentDay = 2 + val activeWorkbookTotalDay = 3 + every { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } returns listOf( + MemberWorkbookSubscriptionStatusRecord( + workbookId = activeWorkbookId, + isActiveSub = false, + currentDay = activeWorkbookCurrentDay, + totalDay = activeWorkbookTotalDay + ) + ) + + every { + subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) + } returns activeWorkbookId + + val activeWorkbookSubscriptionCount = 1 + every { subscriptionDao.countAllWorkbookSubscription(any()) } returns mapOf( + activeWorkbookId to activeWorkbookSubscriptionCount + ) + + every { objectMapper.writeValueAsString(any()) } returns "{\"articleId\":$activeWorkbookId}" + + then("멤버의 구독 활성 워크북 정보를 반환한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.workbooks.size shouldBe 1 + + val inActiveSubscriptionWorkbook = useCaseOut.workbooks[0] + inActiveSubscriptionWorkbook.workbookId shouldBe activeWorkbookId + inActiveSubscriptionWorkbook.isActiveSub shouldBe WorkBookStatus.DONE + inActiveSubscriptionWorkbook.currentDay shouldBe activeWorkbookCurrentDay + inActiveSubscriptionWorkbook.totalDay shouldBe activeWorkbookTotalDay + inActiveSubscriptionWorkbook.totalSubscriber shouldBe activeWorkbookSubscriptionCount + inActiveSubscriptionWorkbook.articleInfo shouldBe "{\"articleId\":$activeWorkbookId}" + + verify(exactly = 1) { subscriptionDao.selectAllInActiveWorkbookSubscriptionStatus(any()) } + verify(exactly = 1) { subscriptionDao.selectAllActiveWorkbookSubscriptionStatus(any()) } + verify(exactly = 1) { subscriptionArticleService.readArticleIdByWorkbookIdAndDay(any()) } + verify(exactly = 1) { subscriptionDao.countAllWorkbookSubscription(any()) } + verify(exactly = 1) { objectMapper.writeValueAsString(any()) } + } + } } }) \ No newline at end of file diff --git a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt index 116344038..6b86b8eb5 100644 --- a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/SubscribeWorkbookUseCaseTest.kt @@ -20,8 +20,6 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ lateinit var subscriptionDao: SubscriptionDao lateinit var applicationEventPublisher: ApplicationEventPublisher lateinit var useCase: SubscribeWorkbookUseCase - val workbookId = 1L - val useCaseIn = SubscribeWorkbookUseCaseIn(workbookId = workbookId, memberId = 1L) beforeContainer { subscriptionDao = mockk() @@ -29,19 +27,25 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ useCase = SubscribeWorkbookUseCase(subscriptionDao, applicationEventPublisher) } - given("구독 요청이 온 상황에서") { - `when`("subscriptionStatus가 null일 경우") { - val event = WorkbookSubscriptionEvent(workbookId) + given("멤버의 워크북 구독 요청이 온 상황에서") { + val workbookId = 1L + val memberId = 1L + val useCaseIn = SubscribeWorkbookUseCaseIn(workbookId = workbookId, memberId = memberId) + `when`("멤버의 구독 히스토리가 없는 경우") { every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns null + every { subscriptionDao.insertWorkbookSubscription(any()) } just Runs + + val event = WorkbookSubscriptionEvent(workbookId) every { applicationEventPublisher.publishEvent(event) } answers { log.debug { "Mocking applicationEventPublisher.publishEvent(any()) was called" } } - then("신규 구독을 추가한다") { + then("구독한다") { useCase.execute(useCaseIn) + verify(exactly = 1) { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } verify(exactly = 1) { subscriptionDao.insertWorkbookSubscription(any()) } verify(exactly = 0) { subscriptionDao.countWorkbookMappedArticles(any()) } verify(exactly = 0) { subscriptionDao.reSubscribeWorkbookSubscription(any()) } @@ -49,15 +53,20 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ } } - `when`("구독을 취소한 경우") { + `when`("이미 구독한 히스토리가 있고 구독이 취소된 경우") { val day = 2 - val lastDay = 3 - val subscriptionStatusRecord = WorkbookSubscriptionStatus(workbookId = workbookId, isActiveSub = false, day) - val event = WorkbookSubscriptionEvent(workbookId) + every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns WorkbookSubscriptionStatus( + workbookId = workbookId, + isActiveSub = false, + day + ) - every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns subscriptionStatusRecord + val lastDay = 3 every { subscriptionDao.countWorkbookMappedArticles(any()) } returns lastDay + every { subscriptionDao.reSubscribeWorkbookSubscription(any()) } just Runs + + val event = WorkbookSubscriptionEvent(workbookId) every { applicationEventPublisher.publishEvent(event) } answers { log.debug { "Mocking applicationEventPublisher.publishEvent(any()) was called" } } @@ -65,6 +74,7 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ then("재구독한다") { useCase.execute(useCaseIn) + verify(exactly = 1) { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } verify(exactly = 0) { subscriptionDao.insertWorkbookSubscription(any()) } verify(exactly = 1) { subscriptionDao.countWorkbookMappedArticles(any()) } verify(exactly = 1) { subscriptionDao.reSubscribeWorkbookSubscription(any()) } @@ -72,26 +82,18 @@ class SubscribeWorkbookUseCaseTest : BehaviorSpec({ } } - `when`("이미 구독하고 있을 경우") { + `when`("구독 중인 경우") { val day = 2 - val lastDay = 3 - val subscriptionStatusRecord = WorkbookSubscriptionStatus(workbookId = workbookId, isActiveSub = true, day) - val event = WorkbookSubscriptionEvent(workbookId) - - every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns subscriptionStatusRecord - every { subscriptionDao.countWorkbookMappedArticles(any()) } returns lastDay - every { subscriptionDao.reSubscribeWorkbookSubscription(any()) } just Runs - every { applicationEventPublisher.publishEvent(event) } answers { - log.debug { "Mocking applicationEventPublisher.publishEvent(any()) was called" } - } + every { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } returns WorkbookSubscriptionStatus(workbookId = workbookId, isActiveSub = true, day) then("예외가 발생한다") { shouldThrow { useCase.execute(useCaseIn) } + verify(exactly = 1) { subscriptionDao.selectTopWorkbookSubscriptionStatus(any()) } verify(exactly = 0) { subscriptionDao.insertWorkbookSubscription(any()) } verify(exactly = 0) { subscriptionDao.countWorkbookMappedArticles(any()) } verify(exactly = 0) { subscriptionDao.reSubscribeWorkbookSubscription(any()) } - verify(exactly = 0) { applicationEventPublisher.publishEvent(event) } + verify(exactly = 0) { applicationEventPublisher.publishEvent(WorkbookSubscriptionEvent(workbookId)) } } } } diff --git a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCaseTest.kt new file mode 100644 index 000000000..37b7ea8dd --- /dev/null +++ b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeAllUseCaseTest.kt @@ -0,0 +1,54 @@ +package com.few.api.domain.subscription.usecase + +import com.few.api.domain.subscription.usecase.dto.UnsubscribeAllUseCaseIn +import com.few.api.repo.dao.subscription.SubscriptionDao +import io.github.oshai.kotlinlogging.KotlinLogging +import io.kotest.core.spec.style.BehaviorSpec +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.mockk.just +import io.mockk.Runs +class UnsubscribeAllUseCaseTest : BehaviorSpec({ + val log = KotlinLogging.logger {} + + lateinit var subscriptionDao: SubscriptionDao + lateinit var useCase: UnsubscribeAllUseCase + + beforeContainer { + subscriptionDao = mockk() + useCase = UnsubscribeAllUseCase(subscriptionDao) + } + + given("구독 취소 의견이 포함된 전체 구독 취소 요청이 온 상황에서") { + val memberId = 1L + val opinion = "취소합니다." + val useCaseIn = UnsubscribeAllUseCaseIn(memberId = memberId, opinion = opinion) + + `when`("멤버의 구독 히스토리가 있는 경우") { + every { subscriptionDao.updateDeletedAtInAllSubscription(any()) } just Runs + + then("의견을 저장하고 구독 전체를 취소한다") { + useCase.execute(useCaseIn) + + verify(exactly = 1) { subscriptionDao.updateDeletedAtInAllSubscription(any()) } + } + } + } + + given("구독 취소 의견이 포함되지 않은 전체 구독 취소 요청이 온 상황에서") { + val memberId = 1L + val opinion = "" + val useCaseIn = UnsubscribeAllUseCaseIn(memberId = memberId, opinion = opinion) + + `when`("멤버의 구독 히스토리가 있는 경우") { + every { subscriptionDao.updateDeletedAtInAllSubscription(any()) } just Runs + + then("의견을 cancel로 저장하고 구독 전체를 취소한다") { + useCase.execute(useCaseIn) + + verify(exactly = 1) { subscriptionDao.updateDeletedAtInAllSubscription(any()) } + } + } + } +}) \ No newline at end of file diff --git a/api/src/test/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCaseTest.kt new file mode 100644 index 000000000..94224f0bb --- /dev/null +++ b/api/src/test/kotlin/com/few/api/domain/subscription/usecase/UnsubscribeWorkbookUseCaseTest.kt @@ -0,0 +1,54 @@ +package com.few.api.domain.subscription.usecase + +import com.few.api.domain.subscription.usecase.dto.UnsubscribeWorkbookUseCaseIn +import com.few.api.repo.dao.subscription.SubscriptionDao +import io.github.oshai.kotlinlogging.KotlinLogging +import io.kotest.core.spec.style.BehaviorSpec +import io.mockk.* +import org.junit.jupiter.api.Assertions.* + +class UnsubscribeWorkbookUseCaseTest : BehaviorSpec({ + val log = KotlinLogging.logger {} + + lateinit var subscriptionDao: SubscriptionDao + lateinit var useCase: UnsubscribeWorkbookUseCase + + beforeContainer { + subscriptionDao = mockk() + useCase = UnsubscribeWorkbookUseCase(subscriptionDao) + } + + given("구독 취소 의견이 포함된 특정 워크북 구독 취소 요청이 온 상황에서") { + val memberId = 1L + val workbookId = 1L + val opinion = "취소합니다." + val useCaseIn = UnsubscribeWorkbookUseCaseIn(memberId = memberId, workbookId = workbookId, opinion = opinion) + + `when`("멤버의 특정 워크북 구독 히스토리가 있는 경우") { + every { subscriptionDao.updateDeletedAtInWorkbookSubscription(any()) } just Runs + + then("의견을 저장하고 구독 전체를 취소한다") { + useCase.execute(useCaseIn) + + verify(exactly = 1) { subscriptionDao.updateDeletedAtInWorkbookSubscription(any()) } + } + } + } + + given("구독 취소 의견이 포함되지 않은 특정 워크북 구독 취소 요청이 온 상황에서") { + val memberId = 1L + val workbookId = 1L + val opinion = "" + val useCaseIn = UnsubscribeWorkbookUseCaseIn(memberId = memberId, workbookId = workbookId, opinion = opinion) + + `when`("멤버의 특정 워크북 구독 히스토리가 있는 경우") { + every { subscriptionDao.updateDeletedAtInWorkbookSubscription(any()) } just Runs + + then("의견을 cancel로 저장하고 특정 위크북에 대한 구독을 취소한다") { + useCase.execute(useCaseIn) + + verify(exactly = 1) { subscriptionDao.updateDeletedAtInWorkbookSubscription(any()) } + } + } + } +}) \ No newline at end of file diff --git a/api/src/test/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCaseTest.kt index 2d93b4ace..bef68a2e2 100644 --- a/api/src/test/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/workbook/usecase/BrowseWorkbooksUseCaseTest.kt @@ -4,23 +4,26 @@ import com.few.api.domain.workbook.service.WorkbookMemberService import com.few.api.domain.workbook.service.WorkbookSubscribeService import com.few.api.domain.workbook.service.dto.BrowseMemberSubscribeWorkbooksOutDto import com.few.api.domain.workbook.service.dto.WriterMappedWorkbookOutDto -import com.few.api.domain.workbook.usecase.dto.BrowseWorkBookDetail import com.few.api.domain.workbook.usecase.dto.BrowseWorkbooksUseCaseIn -import com.few.api.domain.workbook.usecase.dto.WriterDetail -import com.few.api.domain.workbook.usecase.model.AuthMainViewWorkbookOrderDelegator -import com.few.api.domain.workbook.usecase.model.BasicWorkbookOrderDelegator -import com.few.api.domain.workbook.usecase.service.WorkbookOrderDelegatorExecutor +import com.few.api.domain.workbook.usecase.model.WorkBook +import com.few.api.domain.workbook.usecase.model.WorkBookWriter +import com.few.api.domain.workbook.usecase.service.order.AuthMainViewWorkbookOrderDelegator +import com.few.api.domain.workbook.usecase.service.order.BasicWorkbookOrderDelegator +import com.few.api.domain.workbook.usecase.service.order.WorkbookOrderDelegatorExecutor import com.few.api.repo.dao.workbook.WorkbookDao import com.few.api.repo.dao.workbook.record.SelectWorkBookRecordWithSubscriptionCount import com.few.api.web.support.ViewCategory import com.few.api.web.support.WorkBookCategory +import com.few.data.common.code.CategoryType import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify import java.net.URL import java.time.LocalDateTime +import java.util.stream.IntStream class BrowseWorkbooksUseCaseTest : BehaviorSpec({ lateinit var workbookDao: WorkbookDao @@ -38,103 +41,23 @@ class BrowseWorkbooksUseCaseTest : BehaviorSpec({ BrowseWorkbooksUseCase(workbookDao, workbookMemberService, workbookSubscribeService, workbookOrderDelegatorExecutor) } - given("다수 워크북 조회 요청이 온 상황에서") { - `when`("카테고리가 지정되어 있을 경우") { - every { workbookDao.browseWorkBookWithSubscriptionCount(any()) } returns listOf( - SelectWorkBookRecordWithSubscriptionCount( - id = 1L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = (10).toByte(), - description = "workbook description", - createdAt = LocalDateTime.now(), - subscriptionCount = 10 - ), - SelectWorkBookRecordWithSubscriptionCount( - id = 2L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = (10).toByte(), - description = "workbook description", - createdAt = LocalDateTime.now(), - subscriptionCount = 10 - ) - ) - - every { workbookMemberService.browseWorkbookWriterRecords(any()) } returns mapOf( - 1L to listOf( - WriterMappedWorkbookOutDto( - writerId = 1L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/"), - workbookId = 1L - ) - ), - 2L to listOf( - WriterMappedWorkbookOutDto( - writerId = 2L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/"), - workbookId = 2L - ) - ) - ) + given("메인 뷰에서 로그인 된 상태로 카테고리를 지정하여 다수 워크북 조회 요청이 온 상황에서") { + val memberId = 1L + val useCaseIn = BrowseWorkbooksUseCaseIn(category = WorkBookCategory.ECONOMY, viewCategory = ViewCategory.MAIN_CARD, memberId = memberId) - every { - workbookOrderDelegatorExecutor.execute(any()) - } returns listOf( - BrowseWorkBookDetail( - id = 1L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = WorkBookCategory.ECONOMY.displayName, - description = "workbook description", - createdAt = LocalDateTime.now(), - writerDetails = listOf( - WriterDetail( - id = 1L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/") - ) - ), - subscriptionCount = 10 - ) - ) - - then("지정한 카테고리의 워크북이 조회된다") { - val useCaseIn = BrowseWorkbooksUseCaseIn(category = WorkBookCategory.ECONOMY, viewCategory = null, memberId = null) - useCase.execute(useCaseIn) - - verify(exactly = 1) { workbookDao.browseWorkBookWithSubscriptionCount(any()) } - verify(exactly = 1) { workbookMemberService.browseWorkbookWriterRecords(any()) } - verify(exactly = 0) { workbookSubscribeService.browseMemberSubscribeWorkbooks(any()) } - verify(exactly = 1) { workbookOrderDelegatorExecutor.execute(any()) } - } - } - } - - given("메인에서 다수 워크북 조회 요청이 온 상황에서") { - `when`("로그인이 되어 있을 경우") { - every { workbookDao.browseWorkBookWithSubscriptionCount(any()) } returns listOf( - SelectWorkBookRecordWithSubscriptionCount( - id = 1L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = (10).toByte(), - description = "workbook description", - createdAt = LocalDateTime.now(), - subscriptionCount = 10 - ), + `when`("특정 카테고리로 지정되어 있을 경우") { + val workbookCount = 2 + every { workbookDao.browseWorkBookWithSubscriptionCount(any()) } returns IntStream.range(1, 1 + workbookCount).mapToObj { SelectWorkBookRecordWithSubscriptionCount( - id = 2L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = (10).toByte(), - description = "workbook description", + id = it.toLong(), + title = "workbook title$it", + mainImageUrl = URL("http://localhost:8080/image/main/$it"), + category = CategoryType.ECONOMY.code, + description = "workbook$it description", createdAt = LocalDateTime.now(), - subscriptionCount = 10 + subscriptionCount = it.toLong() ) - ) + }.toList() every { workbookMemberService.browseWorkbookWriterRecords(any()) } returns mapOf( 1L to listOf( @@ -172,72 +95,174 @@ class BrowseWorkbooksUseCaseTest : BehaviorSpec({ every { workbookOrderDelegatorExecutor.execute(any(AuthMainViewWorkbookOrderDelegator::class)) - } returns listOf( - BrowseWorkBookDetail( - id = 1L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), + } returns IntStream.range(1, 1 + workbookCount).mapToObj { + WorkBook( + id = it.toLong(), + title = "workbook title$it", + mainImageUrl = URL("http://localhost:8080/image/main/$it"), category = WorkBookCategory.ECONOMY.displayName, - description = "workbook description", + description = "workbook$it description", createdAt = LocalDateTime.now(), writerDetails = listOf( - WriterDetail( - id = 1L, - name = "hunca", + WorkBookWriter( + id = it.toLong(), + name = "writer$it", url = URL("https://jh-labs.tistory.com/") ) ), - subscriptionCount = 10 + subscriptionCount = it.toLong() ) - ) + }.toList() + + then("경제 카테고리의 워크북이 조회된다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.workbooks.size shouldBe workbookCount - then("인증 메인뷰 워크북 정렬이 실행된 결과가 반환된다") { - val useCaseIn = BrowseWorkbooksUseCaseIn(category = WorkBookCategory.ECONOMY, viewCategory = ViewCategory.MAIN_CARD, memberId = 1L) - useCase.execute(useCaseIn) + useCaseOut.workbooks.forEachIndexed { index, browseWorkBookDetail -> + browseWorkBookDetail.id shouldBe (index + 1).toLong() + browseWorkBookDetail.title shouldBe "workbook title${index + 1}" + browseWorkBookDetail.mainImageUrl shouldBe URL("http://localhost:8080/image/main/${index + 1}") + browseWorkBookDetail.category shouldBe WorkBookCategory.ECONOMY.displayName + browseWorkBookDetail.description shouldBe "workbook${index + 1} description" + browseWorkBookDetail.writerDetails.size shouldBe 1 + browseWorkBookDetail.writerDetails[0].id shouldBe (index + 1).toLong() + browseWorkBookDetail.writerDetails[0].name shouldBe "writer${index + 1}" + browseWorkBookDetail.writerDetails[0].url shouldBe URL("https://jh-labs.tistory.com/") + browseWorkBookDetail.subscriptionCount shouldBe (index + 1).toLong() + } verify(exactly = 1) { workbookDao.browseWorkBookWithSubscriptionCount(any()) } verify(exactly = 1) { workbookMemberService.browseWorkbookWriterRecords(any()) } verify(exactly = 1) { workbookSubscribeService.browseMemberSubscribeWorkbooks(any()) } - verify(exactly = 1) { workbookOrderDelegatorExecutor.execute(any(AuthMainViewWorkbookOrderDelegator::class)) } + verify(exactly = 1) { + workbookOrderDelegatorExecutor.execute( + any( + AuthMainViewWorkbookOrderDelegator::class + ) + ) + } } } + } - `when`("로그인이 되어 있지 않은 경우") { - every { workbookDao.browseWorkBookWithSubscriptionCount(any()) } returns listOf( - SelectWorkBookRecordWithSubscriptionCount( - id = 1L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = (10).toByte(), - description = "workbook description", - createdAt = LocalDateTime.now(), - subscriptionCount = 10 - ), + given("메인 뷰에서 로그인 안된 상태로 카테고리를 지정하여 다수 워크북 조회 요청이 온 상황에서") { + val useCaseIn = BrowseWorkbooksUseCaseIn(category = WorkBookCategory.ECONOMY, viewCategory = ViewCategory.MAIN_CARD, memberId = null) + + `when`("특정 카테고리로 지정되어 있을 경우") { + val workbookCount = 2 + every { workbookDao.browseWorkBookWithSubscriptionCount(any()) } returns + IntStream.range(1, 1 + workbookCount).mapToObj { + SelectWorkBookRecordWithSubscriptionCount( + id = it.toLong(), + title = "workbook title$it", + mainImageUrl = URL("http://localhost:8080/image/main/$it"), + category = CategoryType.ECONOMY.code, + description = "workbook$it description", + createdAt = LocalDateTime.now(), + subscriptionCount = it.toLong() + ) + }.toList() + + val workbookWriterRecords = HashMap>() + for (i in 1..workbookCount) { + workbookWriterRecords[i.toLong()] = listOf( + WriterMappedWorkbookOutDto( + writerId = i.toLong(), + name = "writer$i", + url = URL("http://localhost:8080/image/writer/$i"), + workbookId = i.toLong() + ) + ) + } + every { workbookMemberService.browseWorkbookWriterRecords(any()) } returns workbookWriterRecords + + every { + workbookOrderDelegatorExecutor.execute(any(BasicWorkbookOrderDelegator::class)) + } returns + IntStream.range(1, 1 + workbookCount).mapToObj { + WorkBook( + id = it.toLong(), + title = "workbook title$it", + mainImageUrl = URL("http://localhost:8080/image/main/$it"), + category = WorkBookCategory.ECONOMY.displayName, + description = "workbook$it description", + createdAt = LocalDateTime.now(), + writerDetails = listOf( + WorkBookWriter( + id = it.toLong(), + name = "writer$it", + url = URL("http://localhost:8080/image/writer/$it") + ) + ), + subscriptionCount = it.toLong() + ) + }.toList() + + then("경제 카테고리의 워크북이 조회된다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.workbooks.size shouldBe workbookCount + + useCaseOut.workbooks.forEachIndexed { index, browseWorkBookDetail -> + browseWorkBookDetail.id shouldBe (index + 1).toLong() + browseWorkBookDetail.title shouldBe "workbook title${index + 1}" + browseWorkBookDetail.mainImageUrl shouldBe URL("http://localhost:8080/image/main/${index + 1}") + browseWorkBookDetail.category shouldBe WorkBookCategory.ECONOMY.displayName + browseWorkBookDetail.description shouldBe "workbook${index + 1} description" + browseWorkBookDetail.writerDetails.size shouldBe 1 + browseWorkBookDetail.writerDetails[0].id shouldBe (index + 1).toLong() + browseWorkBookDetail.writerDetails[0].name shouldBe "writer${index + 1}" + browseWorkBookDetail.writerDetails[0].url shouldBe URL("http://localhost:8080/image/writer/${index + 1}") + browseWorkBookDetail.subscriptionCount shouldBe (index + 1).toLong() + } + + verify(exactly = 1) { workbookDao.browseWorkBookWithSubscriptionCount(any()) } + verify(exactly = 1) { workbookMemberService.browseWorkbookWriterRecords(any()) } + verify(exactly = 0) { workbookSubscribeService.browseMemberSubscribeWorkbooks(any()) } + verify(exactly = 1) { + workbookOrderDelegatorExecutor.execute( + any( + BasicWorkbookOrderDelegator::class + ) + ) + } + } + } + } + + given("메인 뷰가 아닌 상태로 카테고리를 지정하여 다수 워크북 조회 요청이 온 상황에서") { + val useCaseIn = BrowseWorkbooksUseCaseIn(category = WorkBookCategory.ECONOMY, viewCategory = null, memberId = null) + + val workbookCount = 2 + `when`("특정 카테고리로 지정되어 있을 경우") { + every { workbookDao.browseWorkBookWithSubscriptionCount(any()) } returns IntStream.range( + 1, + 1 + workbookCount + ).mapToObj { SelectWorkBookRecordWithSubscriptionCount( - id = 2L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = (10).toByte(), - description = "workbook description", + id = it.toLong(), + title = "workbook title$it", + mainImageUrl = URL("http://localhost:8080/image/main/$it"), + category = CategoryType.ECONOMY.code, + description = "workbook$it description", createdAt = LocalDateTime.now(), - subscriptionCount = 10 + subscriptionCount = it.toLong() ) - ) + }.toList() every { workbookMemberService.browseWorkbookWriterRecords(any()) } returns mapOf( 1L to listOf( WriterMappedWorkbookOutDto( writerId = 1L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/"), + name = "writer1", + url = URL("http://localhost:8080/image/writer/1"), workbookId = 1L ) ), 2L to listOf( WriterMappedWorkbookOutDto( writerId = 2L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/"), + name = "writer2", + url = URL("http://localhost:8080/image/writer/2"), workbookId = 2L ) ) @@ -245,33 +270,51 @@ class BrowseWorkbooksUseCaseTest : BehaviorSpec({ every { workbookOrderDelegatorExecutor.execute(any(BasicWorkbookOrderDelegator::class)) - } returns listOf( - BrowseWorkBookDetail( - id = 1L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), + } returns IntStream.range(1, 1 + workbookCount).mapToObj { + WorkBook( + id = it.toLong(), + title = "workbook title$it", + mainImageUrl = URL("http://localhost:8080/image/main/$it"), category = WorkBookCategory.ECONOMY.displayName, - description = "workbook description", + description = "workbook$it description", createdAt = LocalDateTime.now(), writerDetails = listOf( - WriterDetail( - id = 1L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/") + WorkBookWriter( + id = it.toLong(), + name = "writer$it", + url = URL("http://localhost:8080/image/writer/$it") ) ), - subscriptionCount = 10 + subscriptionCount = it.toLong() ) - ) + }.toList() - then("지정한 카테고리의 워크북이 조회된다") { - val useCaseIn = BrowseWorkbooksUseCaseIn(category = WorkBookCategory.ECONOMY, viewCategory = ViewCategory.MAIN_CARD, memberId = null) - useCase.execute(useCaseIn) + then("경제 카테고리의 워크북이 조회된다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.workbooks.size shouldBe workbookCount + useCaseOut.workbooks.forEachIndexed { index, browseWorkBookDetail -> + browseWorkBookDetail.id shouldBe (index + 1).toLong() + browseWorkBookDetail.title shouldBe "workbook title${index + 1}" + browseWorkBookDetail.mainImageUrl shouldBe URL("http://localhost:8080/image/main/${index + 1}") + browseWorkBookDetail.category shouldBe WorkBookCategory.ECONOMY.displayName + browseWorkBookDetail.description shouldBe "workbook${index + 1} description" + browseWorkBookDetail.writerDetails.size shouldBe 1 + browseWorkBookDetail.writerDetails[0].id shouldBe (index + 1).toLong() + browseWorkBookDetail.writerDetails[0].name shouldBe "writer${index + 1}" + browseWorkBookDetail.writerDetails[0].url shouldBe URL("http://localhost:8080/image/writer/${index + 1}") + browseWorkBookDetail.subscriptionCount shouldBe (index + 1).toLong() + } verify(exactly = 1) { workbookDao.browseWorkBookWithSubscriptionCount(any()) } verify(exactly = 1) { workbookMemberService.browseWorkbookWriterRecords(any()) } verify(exactly = 0) { workbookSubscribeService.browseMemberSubscribeWorkbooks(any()) } - verify(exactly = 1) { workbookOrderDelegatorExecutor.execute(any(BasicWorkbookOrderDelegator::class)) } + verify(exactly = 1) { + workbookOrderDelegatorExecutor.execute( + any( + BasicWorkbookOrderDelegator::class + ) + ) + } } } } diff --git a/api/src/test/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCaseTest.kt b/api/src/test/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCaseTest.kt index 2099903d2..add8efdbc 100644 --- a/api/src/test/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCaseTest.kt +++ b/api/src/test/kotlin/com/few/api/domain/workbook/usecase/ReadWorkbookUseCaseTest.kt @@ -7,8 +7,10 @@ import com.few.api.domain.workbook.service.dto.WriterOutDto import com.few.api.domain.workbook.usecase.dto.ReadWorkbookUseCaseIn import com.few.api.repo.dao.workbook.WorkbookDao import com.few.api.repo.dao.workbook.record.SelectWorkBookRecord +import com.few.data.common.code.CategoryType import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -21,7 +23,6 @@ class ReadWorkbookUseCaseTest : BehaviorSpec({ lateinit var workbookArticleService: WorkbookArticleService lateinit var workbookMemberService: WorkbookMemberService lateinit var useCase: ReadWorkbookUseCase - val useCaseIn = ReadWorkbookUseCaseIn(workbookId = 1L) beforeContainer { workbookDao = mockk() @@ -30,37 +31,61 @@ class ReadWorkbookUseCaseTest : BehaviorSpec({ useCase = ReadWorkbookUseCase(workbookDao, workbookArticleService, workbookMemberService) } - given("워크북 조회 요청이 온 상황에서") { - `when`("워크북과 작가가 존재할 경우") { + given("특정 워크북 조회 요청이 온 상황에서") { + val workbookId = 1L + val useCaseIn = ReadWorkbookUseCaseIn(workbookId = workbookId) + + `when`("워크북이 존재할 경우") { + val workbookId = 1L + val title = "workbook title" + val workBookMainImageUrl = URL("http://localhost:8080/image/main/1") + val category = CategoryType.ECONOMY.code + val workbookDescription = "workbook description" every { workbookDao.selectWorkBook(any()) } returns SelectWorkBookRecord( - id = 1L, - title = "workbook title", - mainImageUrl = URL("https://jh-labs.tistory.com/"), - category = (10).toByte(), - description = "workbook description", + id = workbookId, + title = title, + mainImageUrl = workBookMainImageUrl, + category = category, + description = workbookDescription, createdAt = LocalDateTime.now() ) + + val articleId = 1L + val articleWriterId = 1L + val articleMainImageUrl = URL("http://localhost:8080/image/main/1") + val articleTitle = "article title" + val articleContent = "article content" every { workbookArticleService.browseWorkbookArticles(any()) } returns listOf( WorkBookArticleOutDto( - articleId = 1L, - writerId = 1L, - mainImageURL = URL("https://jh-labs.tistory.com/"), - title = "article title", - category = (10).toByte(), - content = "article description", + articleId = articleId, + writerId = articleWriterId, + mainImageURL = articleMainImageUrl, + title = articleTitle, + category = category, + content = articleContent, createdAt = LocalDateTime.now() ) ) + + val writerName = "writer" + val writerUrl = URL("http://localhost:8080/writer/1") every { workbookMemberService.browseWriterRecords(any()) } returns listOf( WriterOutDto( - writerId = 1L, - name = "hunca", - url = URL("https://jh-labs.tistory.com/") + writerId = articleWriterId, + name = writerName, + url = writerUrl ) ) - then("워크북 정상 조회된다") { - useCase.execute(useCaseIn) + then("워크북과 관련된 정보를 반환한다") { + val useCaseOut = useCase.execute(useCaseIn) + useCaseOut.id shouldBe workbookId + useCaseOut.title shouldBe title + useCaseOut.mainImageUrl shouldBe workBookMainImageUrl + useCaseOut.category shouldBe CategoryType.convertToDisplayName(category) + useCaseOut.description shouldBe workbookDescription + useCaseOut.writers.size shouldBe 1 + useCaseOut.articles.size shouldBe 1 verify(exactly = 1) { workbookDao.selectWorkBook(any()) } verify(exactly = 1) { workbookArticleService.browseWorkbookArticles(any()) } diff --git a/api/src/test/kotlin/com/few/api/domain/workbook/usecase/service/order/AuthMainViewWorkbookOrderDelegatorTest.kt b/api/src/test/kotlin/com/few/api/domain/workbook/usecase/service/order/AuthMainViewWorkbookOrderDelegatorTest.kt new file mode 100644 index 000000000..e1402a943 --- /dev/null +++ b/api/src/test/kotlin/com/few/api/domain/workbook/usecase/service/order/AuthMainViewWorkbookOrderDelegatorTest.kt @@ -0,0 +1,191 @@ +package com.few.api.domain.workbook.usecase.service.order + +import com.few.api.domain.workbook.usecase.model.MemberSubscribedWorkbook +import com.few.api.domain.workbook.usecase.model.WorkBook +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import java.net.URL +import java.time.LocalDateTime +import java.util.stream.IntStream +import kotlin.streams.toList + +class AuthMainViewWorkbookOrderDelegatorTest { + + @Test + fun `워크북과 멤버 구독 워크북이 모두 주어지는 경우`() { + // given + val totalWorkbookCount = 10 + val workbooksOrderBySubscriptionCount = IntStream.range(1, 1 + totalWorkbookCount) + .mapToObj { + WorkBook( + it.toLong(), + URL("http://localhost:8080/$it"), + "title$it", + "description$it", + "category$it", + LocalDateTime.now(), + emptyList(), + (1 + totalWorkbookCount) - it.toLong() + ) + } + .toList() + + /** + * 1 : inactive, current day 5 + * 2 : active, current day 4 + * 3 : inactive, current day 3 + * 4 : active, current day 2 + * 5 : inactive, current day 1 + */ + val activeWorkbookIds = listOf(2, 4) + val inActiveList = listOf(1, 3, 5) + val totalMemberSubscribedWorkbookCount = 5 + val memberSubscribedWorkbooksReverseOrderByCurrentDay = IntStream.range(1, 1 + totalMemberSubscribedWorkbookCount) + .mapToObj { + MemberSubscribedWorkbook( + it.toLong(), + it % 2 == 0, + (1 + totalMemberSubscribedWorkbookCount) - it + ) + } + .toList() + + val notSubscribeWorkbookIds = workbooksOrderBySubscriptionCount.filter { !activeWorkbookIds.contains(it.id.toInt()) && !inActiveList.contains(it.id.toInt()) } + .map { it.id.toInt() } + + // when + val delegator = AuthMainViewWorkbookOrderDelegator( + workbooksOrderBySubscriptionCount, + memberSubscribedWorkbooksReverseOrderByCurrentDay + ) + + // then + val orderedWorkbooks = delegator.order() + assertEquals(totalWorkbookCount, orderedWorkbooks.size) + + val expectedOrderedWorkbookIds = activeWorkbookIds + notSubscribeWorkbookIds + inActiveList + for (i in expectedOrderedWorkbookIds.indices) { + assertEquals(expectedOrderedWorkbookIds[i].toLong(), orderedWorkbooks[i].id) + } + } + + @Test + fun `워크북과 멤버 구독 워크북만 주어지는 경우`() { + // given + val totalWorkbookCount = 10 + val workbooksOrderBySubscriptionCount = IntStream.range(1, 1 + totalWorkbookCount) + .mapToObj { + WorkBook( + it.toLong(), + URL("http://localhost:8080/$it"), + "title$it", + "description$it", + "category$it", + LocalDateTime.now(), + emptyList(), + (1 + totalWorkbookCount) - it.toLong() + ) + } + .toList() + + /** + * 1 : active, current day 5 + * 2 : active, current day 4 + * 3 : active, current day 3 + * 4 : active, current day 2 + * 5 : active, current day 1 + */ + val totalMemberSubscribedWorkbookCount = 5 + val activeWorkbookIds = IntStream.range(1, 1 + totalMemberSubscribedWorkbookCount).toList() + val memberSubscribedWorkbooksReverseOrderByCurrentDay = IntStream.range(1, 1 + totalMemberSubscribedWorkbookCount) + .mapToObj { + MemberSubscribedWorkbook( + it.toLong(), + true, + (1 + totalMemberSubscribedWorkbookCount) - it + ) + } + .toList() + + val notSubscribeWorkbookIds = workbooksOrderBySubscriptionCount.filter { + !activeWorkbookIds.contains( + it.id.toInt() + ) + }.map { it.id.toInt() } + + // when + val delegator = AuthMainViewWorkbookOrderDelegator( + workbooksOrderBySubscriptionCount, + memberSubscribedWorkbooksReverseOrderByCurrentDay + ) + + // then + val orderedWorkbooks = delegator.order() + assertEquals(totalWorkbookCount, orderedWorkbooks.size) + + val expectedOrderedWorkbookIds = activeWorkbookIds + notSubscribeWorkbookIds + for (i in expectedOrderedWorkbookIds.indices) { + assertEquals(expectedOrderedWorkbookIds[i].toLong(), orderedWorkbooks[i].id) + } + } + + @Test + fun `워크북과 멤버 구독 완료 워크북만 주어지는 경우`() { + // given + val totalWorkbookCount = 10 + val workbooksOrderBySubscriptionCount = IntStream.range(1, 1 + totalWorkbookCount) + .mapToObj { + WorkBook( + it.toLong(), + URL("http://localhost:8080/$it"), + "title$it", + "description$it", + "category$it", + LocalDateTime.now(), + emptyList(), + (1 + totalWorkbookCount) - it.toLong() + ) + } + .toList() + + /** + * 1 : inactive, current day 5 + * 2 : inactive, current day 4 + * 3 : inactive, current day 3 + * 4 : inactive, current day 2 + * 5 : inactive, current day 1 + */ + val totalMemberSubscribedWorkbookCount = 5 + val inActiveWorkbookIds = IntStream.range(1, 1 + totalMemberSubscribedWorkbookCount).toList() + val memberSubscribedWorkbooksReverseOrderByCurrentDay = IntStream.range(1, 1 + totalMemberSubscribedWorkbookCount) + .mapToObj { + MemberSubscribedWorkbook( + it.toLong(), + false, + (1 + totalMemberSubscribedWorkbookCount) - it + ) + } + .toList() + + val notSubscribeWorkbookIds = workbooksOrderBySubscriptionCount.filter { + !inActiveWorkbookIds.contains( + it.id.toInt() + ) + }.map { it.id.toInt() } + + // when + val delegator = AuthMainViewWorkbookOrderDelegator( + workbooksOrderBySubscriptionCount, + memberSubscribedWorkbooksReverseOrderByCurrentDay + ) + + // then + val orderedWorkbooks = delegator.order() + assertEquals(totalWorkbookCount, orderedWorkbooks.size) + + val expectedOrderedWorkbookIds = notSubscribeWorkbookIds + inActiveWorkbookIds + for (i in expectedOrderedWorkbookIds.indices) { + assertEquals(expectedOrderedWorkbookIds[i].toLong(), orderedWorkbooks[i].id) + } + } +} \ No newline at end of file diff --git a/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt b/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt index 573784ea3..ac5d49cb7 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/ControllerTestSpec.kt @@ -19,7 +19,6 @@ import com.few.api.domain.workbook.article.usecase.ReadWorkBookArticleUseCase import com.few.api.domain.workbook.usecase.BrowseWorkbooksUseCase import com.few.api.domain.workbook.usecase.ReadWorkbookUseCase import com.few.api.security.token.TokenResolver -import com.few.api.web.handler.ApiControllerExceptionHandler import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs @@ -29,7 +28,6 @@ import org.springframework.boot.test.mock.mockito.MockBean import org.springframework.restdocs.RestDocumentationExtension import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.ContextConfiguration -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.MockMvc @ActiveProfiles(value = ["test", "new"]) @@ -41,14 +39,9 @@ import org.springframework.test.web.servlet.MockMvc abstract class ControllerTestSpec { /** Common */ - @Autowired - lateinit var apiControllerExceptionHandler: ApiControllerExceptionHandler - @Autowired lateinit var objectMapper: ObjectMapper - lateinit var webTestClient: WebTestClient - @Autowired lateinit var mockMvc: MockMvc diff --git a/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt index b4d65e5f8..d4567fd68 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/admin/AdminControllerTest.kt @@ -1,58 +1,34 @@ package com.few.api.web.controller.admin -import com.epages.restdocs.apispec.ResourceDocumentation import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema -import com.few.api.domain.admin.document.usecase.* import com.few.api.domain.admin.document.usecase.dto.* import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.admin.request.* -import com.few.api.web.controller.admin.response.ImageSourceResponse import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* import com.few.data.common.code.CategoryType -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito.doNothing import org.mockito.Mockito.`when` -import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType -import org.springframework.http.codec.json.Jackson2JsonDecoder -import org.springframework.http.codec.json.Jackson2JsonEncoder import org.springframework.mock.web.MockMultipartFile -import org.springframework.restdocs.RestDocumentationContextProvider import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.multipart +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder import java.net.URL +import java.util.stream.IntStream class AdminControllerTest : ControllerTestSpec() { - @Autowired - lateinit var adminController: AdminController - companion object { - private val BASE_URL = "/api/v1/admin" - private val TAG = "AdminController" - } - - @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - webTestClient = WebTestClient.bindToController(adminController) - .controllerAdvice(super.apiControllerExceptionHandler) - .httpMessageCodecs { - it.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper)) - it.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) - } - .configureClient() - .filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)) - .build() + private const val BASE_URL = "/api/v1/admin" + private const val TAG = "AdminController" } @Test @@ -67,21 +43,22 @@ class AdminControllerTest : ControllerTestSpec() { val description = "description" val request = AddWorkbookRequest(title, mainImageUrl, category, description) val body = objectMapper.writeValueAsString(request) - `when`(addWorkbookUseCase.execute(AddWorkbookUseCaseIn(title, mainImageUrl, category, description))).thenReturn( - AddWorkbookUseCaseOut(1L) - ) + + val useCaseIn = AddWorkbookUseCaseIn(title, mainImageUrl, category, description) + val useCaseOut = AddWorkbookUseCaseOut(1L) + `when`(addWorkbookUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when - this.webTestClient.post() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri) + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder().description("학습지 추가") .summary(api.toIdentifier()).privateResource(false).deprecated(false) .tag(TAG).requestSchema(Schema.schema(api.toRequestSchema())) @@ -106,88 +83,66 @@ class AdminControllerTest : ControllerTestSpec() { // given val api = "AddArticle" val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/articles").build().toUriString() - val request = AddArticleRequest( - "writer@gmail.com", - URL("http://localhost:8080"), - "title", - CategoryType.fromCode(0)!!.name, - "md", - "content source", - listOf( - ProblemDto( - "title1", - listOf( - ProblemContentDto(1L, "content1"), - ProblemContentDto(2L, "content2"), - ProblemContentDto(3L, "content3"), - ProblemContentDto(4L, "content4") - ), - "1", - "explanation" - ), - ProblemDto( - "title2", - listOf( - ProblemContentDto(1L, "content1"), - ProblemContentDto(2L, "content2"), - ProblemContentDto(3L, "content3"), - ProblemContentDto(4L, "content4") - ), - "2", - "explanation" - ) + val writerEmail = "writer@gmail.com" + val articleImageURL = URL("http://localhost:8080") + val title = "title" + val category = CategoryType.fromCode(0)!!.name + val contentType = "md" + val contentSource = "content source" + val problemDTOs = IntStream.range(0, 2).mapToObj { + ProblemDto( + "title$it", + IntStream.range(0, 4).mapToObj { ProblemContentDto(it.toLong(), "content$it") }.toList(), + "$it", + "explanation$it" ) + }.toList() + val problemDetails = problemDTOs.map { + ProblemDetail( + it.title, + it.contents.map { content -> ProblemContentDetail(content.number, content.content) }, + it.answer, + it.explanation + ) + } + val request = AddArticleRequest( + writerEmail, + articleImageURL, + title, + category, + contentType, + contentSource, + problemDTOs ) val body = objectMapper.writeValueAsString(request) + val useCaseIn = AddArticleUseCaseIn( + writerEmail, + articleImageURL, + title, + category, + contentType, + contentSource, + problemDetails + ) + val useCaseOut = AddArticleUseCaseOut(1L) `when`( addArticleUseCase.execute( - AddArticleUseCaseIn( - "writer@gmail.com", - URL("http://localhost:8080"), - "title", - CategoryType.fromCode(0)!!.name, - "md", - "content source", - listOf( - ProblemDetail( - "title1", - listOf( - ProblemContentDetail(1L, "content1"), - ProblemContentDetail(2L, "content2"), - ProblemContentDetail(3L, "content3"), - ProblemContentDetail(4L, "content4") - ), - "1", - "explanation" - ), - ProblemDetail( - "title2", - listOf( - ProblemContentDetail(1L, "content1"), - ProblemContentDetail(2L, "content2"), - ProblemContentDetail(3L, "content3"), - ProblemContentDetail(4L, "content4") - ), - "2", - "explanation" - ) - ) - ) + useCaseIn ) - ).thenReturn(AddArticleUseCaseOut(1L)) + ).thenReturn(useCaseOut) // when - this.webTestClient.post() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri) + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder().description("아티클 추가") .summary(api.toIdentifier()).privateResource(false).deprecated(false) .tag(TAG).requestSchema(Schema.schema(api.toRequestSchema())) @@ -212,19 +167,24 @@ class AdminControllerTest : ControllerTestSpec() { // given val api = "MapArticle" val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/relations/articles").build().toUriString() - val request = MapArticleRequest(1L, 1L, 1) + val workbookId = 1L + val articleId = 1L + val dayCol = 1 + val request = MapArticleRequest(workbookId, articleId, dayCol) val body = objectMapper.writeValueAsString(request) - doNothing().`when`(mapArticleUseCase).execute(MapArticleUseCaseIn(1L, 1L, 1)) + + val useCaseIn = MapArticleUseCaseIn(workbookId, articleId, dayCol) + doNothing().`when`(mapArticleUseCase).execute(useCaseIn) // when - this.webTestClient.post() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri) + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk) + .andDo( + document( api.toIdentifier(), resource( ResourceSnippetParameters.builder().description("아티클 매핑") @@ -244,7 +204,12 @@ class AdminControllerTest : ControllerTestSpec() { // given val api = "ConvertContent" val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/utilities/conversion/content").build().toUriString() - val request = ConvertContentRequest(MockMultipartFile("content", "test.md", "text/markdown", "#test".toByteArray())) + val name = "content" + val originalFilename = "test.md" + val contentType = "text/markdown" + val content = "#test".toByteArray() + val request = ConvertContentRequest(MockMultipartFile(name, originalFilename, contentType, content)) + val useCaseOut = ConvertContentUseCaseOut("converted content", URL("http://localhost:8080/test.md")) val useCaseIn = ConvertContentUseCaseIn(request.content) `when`(convertContentUseCase.execute(useCaseIn)).thenReturn(useCaseOut) @@ -288,9 +253,13 @@ class AdminControllerTest : ControllerTestSpec() { // given val api = "PutImage" val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/utilities/conversion/image").build().toUriString() - val request = ImageSourceRequest(source = MockMultipartFile("source", "test.jpg", "image/jpeg", "test".toByteArray())) - val response = ImageSourceResponse(URL("http://localhost:8080/test.jpg"), listOf("jpg", "webp")) - val useCaseOut = PutImageUseCaseOut(response.url, response.supportSuffix) + val name = "source" + val originalFilename = "test.jpg" + val contentType = "image/jpeg" + val content = "test".toByteArray() + val request = ImageSourceRequest(source = MockMultipartFile(name, originalFilename, contentType, content)) + + val useCaseOut = PutImageUseCaseOut(URL("http://localhost:8080/test.jpg"), listOf("jpg", "webp")) val useCaseIn = PutImageUseCaseIn(request.source) `when`(putImageUseCase.execute(useCaseIn)).thenReturn(useCaseOut) diff --git a/api/src/test/kotlin/com/few/api/web/controller/admin/ApiLogControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/admin/ApiLogControllerTest.kt index 09131b083..03da8a246 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/admin/ApiLogControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/admin/ApiLogControllerTest.kt @@ -20,8 +20,8 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status class ApiLogControllerTest : ControllerTestSpec() { companion object { - private val BASE_URL = "/api/v1/logs" - private val TAG = "ApiLogControllerTest" + private const val BASE_URL = "/api/v1/logs" + private const val TAG = "ApiLogControllerTest" } @Test @@ -32,14 +32,15 @@ class ApiLogControllerTest : ControllerTestSpec() { val history = objectMapper.writeValueAsString(mapOf("from" to "email", "to" to "readArticle")) val request = ApiLogRequest(history) - val content = objectMapper.writeValueAsString(request) + val body = objectMapper.writeValueAsString(request) + val useCaseIn = AddApiLogUseCaseIn(history) Mockito.doNothing().`when`(addApiLogUseCase).execute(useCaseIn) // When mockMvc.perform( post(BASE_URL) - .content(content) + .content(body) .contentType(MediaType.APPLICATION_JSON) ).andExpect( status().is2xxSuccessful diff --git a/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt index 44a0ea559..aa7772367 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/article/ArticleControllerTest.kt @@ -2,6 +2,7 @@ package com.few.api.web.controller.article import com.epages.restdocs.apispec.ResourceDocumentation import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName +import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema import com.few.api.domain.article.usecase.dto.* @@ -9,46 +10,24 @@ import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* import com.few.data.common.code.CategoryType -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito.`when` -import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType -import org.springframework.http.codec.json.Jackson2JsonDecoder -import org.springframework.http.codec.json.Jackson2JsonEncoder -import org.springframework.restdocs.RestDocumentationContextProvider import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder import java.net.URL import java.time.LocalDateTime +import java.util.stream.IntStream class ArticleControllerTest : ControllerTestSpec() { - @Autowired - lateinit var articleController: ArticleController - companion object { - private val BASE_URL = "/api/v1/articles" - private val TAG = "ArticleController" - } - - @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - webTestClient = WebTestClient.bindToController(articleController) - .controllerAdvice(super.apiControllerExceptionHandler) - .httpMessageCodecs { - it.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper)) - it.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) - } - .configureClient() - .filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)) - .build() + private const val BASE_URL = "/api/v1/articles" + private const val TAG = "ArticleController" } /** @@ -59,40 +38,44 @@ class ArticleControllerTest : ControllerTestSpec() { fun readArticle() { // given val api = "ReadArticle" + val token = "thisisaccesstoken" val uri = UriComponentsBuilder.newInstance().path("$BASE_URL/{articleId}").build().toUriString() - // set usecase mock val articleId = 1L + val memberId = 1L + `when`(tokenResolver.resolveId(token)).thenReturn(memberId) - `when`(tokenResolver.resolveId("thisisaccesstoken")).thenReturn(memberId) - `when`(readArticleUseCase.execute(ReadArticleUseCaseIn(articleId, memberId))).thenReturn( - ReadArticleUseCaseOut( + val useCaseIn = ReadArticleUseCaseIn(articleId, memberId) + val useCaseOut = ReadArticleUseCaseOut( + id = 1L, + writer = WriterDetail( id = 1L, - writer = WriterDetail( - id = 1L, - name = "안나포", - url = URL("http://localhost:8080/api/v1/writers/1"), - imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") - ), - mainImageUrl = URL("https://github.com/YAPP-Github/24th-Web-Team-1-BE/assets/102807742/0643d805-5f3a-4563-8c48-2a7d51795326"), - title = "ETF(상장 지수 펀드)란? 모르면 손해라고?", - content = CategoryType.fromCode(0)!!.name, - problemIds = listOf(1L, 2L, 3L), - category = "경제", - createdAt = LocalDateTime.now(), - views = 1L - ) + name = "안나포", + url = URL("http://localhost:8080/api/v1/writers/1"), + imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") + ), + mainImageUrl = URL("https://github.com/YAPP-Github/24th-Web-Team-1-BE/assets/102807742/0643d805-5f3a-4563-8c48-2a7d51795326"), + title = "ETF(상장 지수 펀드)란? 모르면 손해라고?", + content = CategoryType.fromCode(0)!!.name, + problemIds = listOf(1L, 2L, 3L), + category = "경제", + createdAt = LocalDateTime.now(), + views = 1L + ) + `when`(readArticleUseCase.execute(useCaseIn)).thenReturn( + useCaseOut ) // when mockMvc.perform( get(uri, articleId) - .header("Authorization", "Bearer thisisaccesstoken") + .header("Authorization", "Bearer $token") + .contentType(MediaType.APPLICATION_JSON) ).andExpect(status().is2xxSuccessful) .andDo( document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder().description("아티클 Id로 아티클 조회") .summary(api.toIdentifier()).privateResource(false).deprecated(false) .tag(TAG).requestSchema(Schema.schema(api.toRequestSchema())) @@ -152,52 +135,50 @@ class ArticleControllerTest : ControllerTestSpec() { val prevArticleId = 1L val categoryCd: Byte = CategoryType.IT.code val uri = UriComponentsBuilder.newInstance() - .path("$BASE_URL") + .path(BASE_URL) .queryParam("prevArticleId", prevArticleId) .queryParam("categoryCd", categoryCd) .build() .toUriString() - // set usecase mock - `when`(readArticlesUseCase.execute(ReadArticlesUseCaseIn(prevArticleId, categoryCd))).thenReturn( - ReadArticlesUseCaseOut( - listOf( - ReadArticleUseCaseOut( + + val useCaseIn = ReadArticlesUseCaseIn(prevArticleId, categoryCd) + val useCaseOut = ReadArticlesUseCaseOut( + IntStream.range(0, 10).mapToObj { + ReadArticleUseCaseOut( + id = it.toLong(), + writer = WriterDetail( id = 1L, - writer = WriterDetail( - id = 1L, - name = "안나포", - url = URL("http://localhost:8080/api/v1/writers/1"), - imageUrl = URL("https://github.com/user-attachments/assets/28df9078-488c-49d6-9375-54ce5a250742") - ), - mainImageUrl = URL("https://github.com/YAPP-Github/24th-Web-Team-1-BE/assets/102807742/0643d805-5f3a-4563-8c48-2a7d51795326"), - title = "ETF(상장 지수 펀드)란? 모르면 손해라고?", - content = CategoryType.fromCode(0)!!.name, - problemIds = emptyList(), - category = CategoryType.IT.displayName, - createdAt = LocalDateTime.now(), - views = 9876543210L, - workbooks = listOf( - WorkbookDetail( - id = 1, - title = "워크북1 타이틀" - ), - WorkbookDetail( - id = 2, - title = "워크북2 타이틀" - ) + name = "writer$it", + url = URL("http://localhost:8080/api/v1/writers/$it"), + imageUrl = URL("http://localhost:8080/api/v1/writers/images/$it") + ), + mainImageUrl = URL("http://localhost:8080/api/v1/articles/main/images/$it"), + title = "title$it", + content = "content$it", + problemIds = emptyList(), + category = CategoryType.ECONOMY.displayName, + createdAt = LocalDateTime.now(), + views = it.toLong(), + workbooks = IntStream.range(0, 2).mapToObj { j -> + WorkbookDetail( + id = "$it$j".toLong(), + title = "workbook$it$j" ) - ) - ), - true - ) + }.toList() + ) + }.toList(), + true ) + `when`(readArticlesUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when - this.webTestClient.get().uri(uri).accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk().expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri, prevArticleId, categoryCd).contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder().description("아티 목록 10개씩 조회(조회수 기반 정렬)") .summary(api.toIdentifier()).privateResource(false).deprecated(false) .tag(TAG).requestSchema(Schema.schema(api.toRequestSchema())) @@ -265,11 +246,13 @@ class ArticleControllerTest : ControllerTestSpec() { .toUriString() // when, then - this.webTestClient.get().uri(uri).accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk().expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri).contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder().description("아티클 카테고리 code, name 조회") .summary(api.toIdentifier()).privateResource(false).deprecated(false) .tag(TAG).requestSchema(Schema.schema(api.toRequestSchema())) @@ -281,9 +264,9 @@ class ArticleControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data.categories") .fieldWithArray("카테고리 목록"), PayloadDocumentation.fieldWithPath("data.categories[].code") - .fieldWithNumber("카테고리 code"), + .fieldWithNumber("카테고리 코드"), PayloadDocumentation.fieldWithPath("data.categories[].name") - .fieldWithString("카테고리 name") + .fieldWithString("카테고리 이름") ) ) ).build() diff --git a/api/src/test/kotlin/com/few/api/web/controller/member/MemberControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/member/MemberControllerTest.kt index 40ee90af6..1eeaff20d 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/member/MemberControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/member/MemberControllerTest.kt @@ -1,6 +1,7 @@ package com.few.api.web.controller.member import com.epages.restdocs.apispec.ResourceDocumentation +import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema import com.few.api.domain.member.usecase.dto.SaveMemberUseCaseIn @@ -12,39 +13,21 @@ import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* import com.few.api.web.controller.member.request.SaveMemberRequest import com.few.api.web.controller.member.request.TokenRequest -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito.`when` -import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType -import org.springframework.restdocs.RestDocumentationContextProvider import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder class MemberControllerTest : ControllerTestSpec() { - @Autowired - lateinit var memberController: MemberController - companion object { - private val BASE_URL = "/api/v1/members" - private val TAG = "MemberController" - } - - @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - webTestClient = WebTestClient - .bindToController(memberController) - .controllerAdvice(super.apiControllerExceptionHandler) - .configureClient() - .filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)) - .build() + private const val BASE_URL = "/api/v1/members" + private const val TAG = "MemberController" } @Test @@ -59,21 +42,21 @@ class MemberControllerTest : ControllerTestSpec() { val email = "test@gmail.com" val body = objectMapper.writeValueAsString(SaveMemberRequest(email = email)) - // set mock val useCaseIn = SaveMemberUseCaseIn(email = email) - `when`(saveMemberUseCase.execute(useCaseIn)).thenReturn(SaveMemberUseCaseOut(isSendAuthEmail = true)) + val useCaseOut = SaveMemberUseCaseOut(isSendAuthEmail = true) + `when`(saveMemberUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when - this.webTestClient.post() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri) + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder() .description("회원가입") .summary(api.toIdentifier()) @@ -121,27 +104,27 @@ class MemberControllerTest : ControllerTestSpec() { rt = rt, refreshToken = tokenRequest.refreshToken ) - `when`(tokenUseCase.execute(useCaseIn)).thenReturn( - TokenUseCaseOut( - accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NTcsIm1lbWJlclJvbGUiOiJbUk9MRV9VU0VSXSIsImlhdCI6MTcyMjI1NTE4MywiZXhwIjoxNzUzODEyNzgzfQ.1KXRim0MVvz1vxOQB_700XPCD9zPQtHNItF_A9upvA8", - refreshToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NTcsIm1lbWJlclJvbGUiOiJbUk9MRV9VU0VSXSIsImlhdCI6MTcyMjI1NTE4MywiZXhwIjoxNzUzODEyNzgzfQ.1KXRim0MVvz1vxOQB_700XPCD9zPQtHNItF_A9upvA8", - isLogin = false - ) + val useCaseOut = TokenUseCaseOut( + accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NTcsIm1lbWJlclJvbGUiOiJbUk9MRV9VU0VSXSIsImlhdCI6MTcyMjI1NTE4MywiZXhwIjoxNzUzODEyNzgzfQ.1KXRim0MVvz1vxOQB_700XPCD9zPQtHNItF_A9upvA8", + refreshToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtZW1iZXJJZCI6NTcsIm1lbWJlclJvbGUiOiJbUk9MRV9VU0VSXSIsImlhdCI6MTcyMjI1NTE4MywiZXhwIjoxNzUzODEyNzgzfQ.1KXRim0MVvz1vxOQB_700XPCD9zPQtHNItF_A9upvA8", + isLogin = false ) + `when`(tokenUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when mockMvc.perform( post(uri) - .contentType(MediaType.APPLICATION_JSON) .queryParam("auth_token", auth_token) .queryParam("at", at.toString()) .queryParam("rt", rt.toString()) .content(body) - ).andExpect(status().is2xxSuccessful) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk) .andDo( document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder() .summary(api.toIdentifier()) .description("토큰 발급") @@ -166,7 +149,7 @@ class MemberControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data.refreshToken") .fieldWithString("refreshToken"), PayloadDocumentation.fieldWithPath("data.isLogin") - .fieldWithBoolean("로그인 여부") + .fieldWithBoolean("로그인/회원가입 여부") ) ) ) diff --git a/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt index fa5cf414a..afaed0955 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/problem/ProblemControllerTest.kt @@ -1,7 +1,7 @@ package com.few.api.web.controller.problem -import com.epages.restdocs.apispec.ResourceDocumentation import com.epages.restdocs.apispec.ResourceDocumentation.parameterWithName +import com.epages.restdocs.apispec.ResourceDocumentation.resource import com.epages.restdocs.apispec.ResourceSnippetParameters import com.epages.restdocs.apispec.Schema import com.few.api.web.controller.ControllerTestSpec @@ -15,37 +15,22 @@ import com.few.api.domain.problem.usecase.dto.BrowseProblemsUseCaseOut import com.few.api.domain.problem.usecase.dto.CheckProblemUseCaseOut import com.few.api.domain.problem.usecase.dto.ReadProblemContentsUseCaseOutDetail import com.few.api.domain.problem.usecase.dto.ReadProblemUseCaseOut -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.mockito.Mockito import org.mockito.Mockito.`when` -import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType -import org.springframework.restdocs.RestDocumentationContextProvider +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation -import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder class ProblemControllerTest : ControllerTestSpec() { - @Autowired - lateinit var problemController: ProblemController - companion object { - private val BASE_URL = "/api/v1/problems" - private val TAG = "ProblemController" - } - - @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - webTestClient = WebTestClient - .bindToController(problemController) - .controllerAdvice(super.apiControllerExceptionHandler) - .configureClient() - .filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)) - .build() + private const val BASE_URL = "/api/v1/problems" + private const val TAG = "ProblemController" } @Test @@ -62,18 +47,17 @@ class ProblemControllerTest : ControllerTestSpec() { val useCaseIn = BrowseProblemsUseCaseIn(articleId) val useCaseOut = BrowseProblemsUseCaseOut(listOf(1L, 2L, 3L)) - `when`(browseProblemsUseCase.execute(useCaseIn)) - .thenReturn(useCaseOut) + `when`(browseProblemsUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when - this.webTestClient.get() - .uri(uri) - .accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder() .description("아티클 Id로 문제 목록 조회") .summary(api.toIdentifier()) @@ -109,7 +93,6 @@ class ProblemControllerTest : ControllerTestSpec() { .build() .toUriString() - // set usecase mock val problemId = 1L val useCaseIn = ReadProblemUseCaseIn(problemId) val useCaseOut = ReadProblemUseCaseOut( @@ -122,18 +105,17 @@ class ProblemControllerTest : ControllerTestSpec() { ReadProblemContentsUseCaseOutDetail(4L, "투명성") ) ) - Mockito.`when`(readProblemUseCase.execute(useCaseIn)) - .thenReturn(useCaseOut) + `when`(readProblemUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when - this.webTestClient.get() - .uri(uri, 1) - .accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri, problemId) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder() .description("문제 Id로 문제 조회") .summary(api.toIdentifier()) @@ -174,11 +156,10 @@ class ProblemControllerTest : ControllerTestSpec() { val api = "CheckProblem" val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/{problemId}").build().toUriString() - - // set usecase mock val problemId = 1L val sub = "제출답" val body = objectMapper.writeValueAsString(CheckProblemRequest(sub = sub)) + val useCaseIn = CheckProblemUseCaseIn(problemId, sub = sub) val useCaseOut = CheckProblemUseCaseOut( explanation = "ETF는 일반적으로 낮은 운용 비용을 특징으로 합니다.이는 ETF가 보통 지수 추종(passive management) 방식으로 운용되기 때문입니다. 지수를 추종하는 전략은 액티브 매니지먼트(active management)에 비해 관리가 덜 복잡하고, 따라서 비용이 낮습니다.", @@ -188,16 +169,16 @@ class ProblemControllerTest : ControllerTestSpec() { `when`(checkProblemUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when - this.webTestClient.post() - .uri(uri, problemId) - .accept(MediaType.APPLICATION_JSON) - .contentType(MediaType.APPLICATION_JSON) - .bodyValue(body) - .exchange().expectStatus().is2xxSuccessful() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + post(uri, problemId) + .content(body) + .contentType(MediaType.APPLICATION_JSON) + ) + .andExpect(status().isOk) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder() .description("문제 Id로 문제 정답 확인") .summary(api.toIdentifier()) diff --git a/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt index d20e5d4b5..bffabac20 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/subscription/SubscriptionControllerTest.kt @@ -11,47 +11,24 @@ import com.few.api.domain.subscription.usecase.dto.* import com.few.api.web.controller.helper.* import com.few.api.web.support.ViewCategory import com.few.api.web.support.WorkBookStatus -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito.doNothing import org.mockito.Mockito.doReturn -import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType -import org.springframework.http.codec.json.Jackson2JsonDecoder -import org.springframework.http.codec.json.Jackson2JsonEncoder -import org.springframework.restdocs.RestDocumentationContextProvider import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation import org.springframework.security.test.context.support.WithUserDetails -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.web.util.UriComponentsBuilder class SubscriptionControllerTest : ControllerTestSpec() { - @Autowired - lateinit var subscriptionController: SubscriptionController - companion object { - private val BASE_URL = "/api/v1/" - private val TAG = "WorkbookSubscriptionController" - } - - @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - webTestClient = WebTestClient - .bindToController(subscriptionController) - .controllerAdvice(super.apiControllerExceptionHandler).httpMessageCodecs { - it.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper)) - it.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) - } - .configureClient() - .filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)) - .build() + private const val BASE_URL = "/api/v1/" + private const val TAG = "WorkbookSubscriptionController" } @Test @@ -60,6 +37,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { fun browseSubscribeWorkbooks() { // given val api = "BrowseSubscribeWorkBooks" + val token = "thisisaccesstoken" val view = ViewCategory.MAIN_CARD val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/subscriptions/workbooks") @@ -67,7 +45,6 @@ class SubscriptionControllerTest : ControllerTestSpec() { .build() .toUriString() - // set usecase mock val memberId = 1L val useCaseIn = BrowseSubscribeWorkbooksUseCaseIn(memberId) val useCaseOut = BrowseSubscribeWorkbooksUseCaseOut( @@ -101,13 +78,12 @@ class SubscriptionControllerTest : ControllerTestSpec() { ) ) ) - doReturn(useCaseOut).`when`(browseSubscribeWorkbooksUseCase).execute(useCaseIn) // when mockMvc.perform( get(uri) - .header("Authorization", "Bearer thisisaccesstoken") + .header("Authorization", "Bearer $token") .contentType(MediaType.APPLICATION_JSON) ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) .andDo( @@ -145,7 +121,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data.workbooks[].rank") .fieldWithNumber("순위"), PayloadDocumentation.fieldWithPath("data.workbooks[].totalSubscriber") - .fieldWithNumber("전체 구독자 수"), + .fieldWithNumber("누적 구독자 수"), PayloadDocumentation.fieldWithPath("data.workbooks[].articleInfo") .fieldWithString("학습지 정보(Json 타입)") ) @@ -163,13 +139,11 @@ class SubscriptionControllerTest : ControllerTestSpec() { fun subscribeWorkbook() { // given val api = "SubscribeWorkBook" + val token = "thisisaccesstoken" val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/workbooks/{workbookId}/subs") .build().toUriString() - val email = "test@gmail.com" - - // set usecase mock val memberId = 1L val workbookId = 1L val useCaseIn = SubscribeWorkbookUseCaseIn(workbookId = workbookId, memberId = memberId) @@ -178,7 +152,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { // when mockMvc.perform( post(uri, workbookId) - .header("Authorization", "Bearer thisisaccesstoken") + .header("Authorization", "Bearer $token") .contentType(MediaType.APPLICATION_JSON) ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) .andDo( @@ -214,15 +188,15 @@ class SubscriptionControllerTest : ControllerTestSpec() { fun unsubscribeWorkbook() { // given val api = "UnsubscribeWorkBook" + val token = "thisisaccesstoken" val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/workbooks/{workbookId}/unsubs") .build() .toUriString() - // set usecase mock val workbookId = 1L val memberId = 1L - val opinion = "취소합니다." + val opinion = "cancel." val body = objectMapper.writeValueAsString( UnsubscribeWorkbookRequest(opinion = opinion) ) @@ -236,7 +210,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { // when mockMvc.perform( post(uri, workbookId) - .header("Authorization", "Bearer thisisaccesstoken") + .header("Authorization", "Bearer $token") .contentType(MediaType.APPLICATION_JSON) .content(body) ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) @@ -273,17 +247,18 @@ class SubscriptionControllerTest : ControllerTestSpec() { fun deactivateAllSubscriptions() { // given val api = "UnsubscribeAll" + val token = "thisisaccesstoken" val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/subscriptions/unsubs") .build() .toUriString() - // set usecase mock - val memberId = 1L val opinion = "전체 수신거부합니다." val body = objectMapper.writeValueAsString( UnsubscribeWorkbookRequest(opinion = opinion) ) + + val memberId = 1L val useCaseIn = UnsubscribeAllUseCaseIn( memberId = memberId, opinion = opinion @@ -293,7 +268,7 @@ class SubscriptionControllerTest : ControllerTestSpec() { // when mockMvc.perform( post(uri) - .header("Authorization", "Bearer thisisaccesstoken") + .header("Authorization", "Bearer $token") .content(body) .contentType(MediaType.APPLICATION_JSON) ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) diff --git a/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt index 999e262ba..1126263cf 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/workbook/WorkBookControllerTest.kt @@ -13,19 +13,12 @@ import com.few.api.web.support.ViewCategory import com.few.api.web.support.WorkBookCategory import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get import com.few.data.common.code.CategoryType -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito.`when` -import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.MediaType -import org.springframework.http.codec.json.Jackson2JsonDecoder -import org.springframework.http.codec.json.Jackson2JsonEncoder -import org.springframework.restdocs.RestDocumentationContextProvider import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.web.util.UriComponentsBuilder import java.net.URL @@ -33,25 +26,9 @@ import java.time.LocalDateTime class WorkBookControllerTest : ControllerTestSpec() { - @Autowired - lateinit var workBookController: WorkBookController - companion object { - private val BASE_URL = "/api/v1/workbooks" - private val TAG = "WorkBookController" - } - - @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - webTestClient = WebTestClient - .bindToController(workBookController) - .controllerAdvice(super.apiControllerExceptionHandler).httpMessageCodecs { - it.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper)) - it.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) - } - .configureClient() - .filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)) - .build() + private const val BASE_URL = "/api/v1/workbooks" + private const val TAG = "WorkBookController" } @Test @@ -106,8 +83,8 @@ class WorkBookControllerTest : ControllerTestSpec() { fun browseWorkBooks() { // given val api = "BrowseWorkBooks" + val token = "thisisaccesstoken" val viewCategory = ViewCategory.MAIN_CARD - val memberId = 1L val uri = UriComponentsBuilder.newInstance() .path(BASE_URL) .queryParam("category", WorkBookCategory.ECONOMY.code) @@ -115,33 +92,35 @@ class WorkBookControllerTest : ControllerTestSpec() { .build() .toUriString() - // set usecase mock - `when`(tokenResolver.resolveId("thisisaccesstoken")).thenReturn(memberId) - `when`(browseWorkBooksUseCase.execute(BrowseWorkbooksUseCaseIn(WorkBookCategory.ECONOMY, viewCategory, memberId))).thenReturn( - BrowseWorkbooksUseCaseOut( - workbooks = listOf( - BrowseWorkBookDetail( - id = 1L, - mainImageUrl = URL("http://localhost:8080/api/v1/workbooks/1/image"), - title = "재태크, 투자 필수 용어 모음집", - description = "사회 초년생부터, 직장인, 은퇴자까지 모두가 알아야 할 기본적인 재태크, 투자 필수 용어 모음집 입니다.", - category = CategoryType.fromCode(0)!!.name, - createdAt = LocalDateTime.now(), - writerDetails = listOf( - WriterDetail(1L, "안나포", URL("http://localhost:8080/api/v1/users/1")), - WriterDetail(2L, "퓨퓨", URL("http://localhost:8080/api/v1/users/2")), - WriterDetail(3L, "프레소", URL("http://localhost:8080/api/v1/users/3")) - ), - subscriptionCount = 100 - ) + val memberId = 1L + `when`(tokenResolver.resolveId(token)).thenReturn(memberId) + + val useCaseIn = + BrowseWorkbooksUseCaseIn(WorkBookCategory.ECONOMY, viewCategory, memberId) + val useCaseOut = BrowseWorkbooksUseCaseOut( + workbooks = listOf( + BrowseWorkBookDetail( + id = 1L, + mainImageUrl = URL("http://localhost:8080/api/v1/workbooks/1/image"), + title = "재태크, 투자 필수 용어 모음집", + description = "사회 초년생부터, 직장인, 은퇴자까지 모두가 알아야 할 기본적인 재태크, 투자 필수 용어 모음집 입니다.", + category = CategoryType.fromCode(0)!!.name, + createdAt = LocalDateTime.now(), + writerDetails = listOf( + WriterDetail(1L, "안나포", URL("http://localhost:8080/api/v1/users/1")), + WriterDetail(2L, "퓨퓨", URL("http://localhost:8080/api/v1/users/2")), + WriterDetail(3L, "프레소", URL("http://localhost:8080/api/v1/users/3")) + ), + subscriptionCount = 100 ) ) ) + `when`(browseWorkBooksUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when mockMvc.perform( get(uri) - .header("Authorization", "Bearer thisisaccesstoken") + .header("Authorization", "Bearer $token") .contentType(MediaType.APPLICATION_JSON) ).andExpect( status().is2xxSuccessful @@ -190,9 +169,9 @@ class WorkBookControllerTest : ControllerTestSpec() { PayloadDocumentation.fieldWithPath("data.workbooks[].writers[].name") .fieldWithString("워크북 작가 이름"), PayloadDocumentation.fieldWithPath("data.workbooks[].writers[].url") - .fieldWithString("워크북 작가 링크"), + .fieldWithString("워크북 작가 외부 링크"), PayloadDocumentation.fieldWithPath("data.workbooks[].subscriberCount") - .fieldWithNumber("워크북 구독자 수") + .fieldWithNumber("워크북 현재 구독자 수") ) ) ).build() @@ -210,34 +189,40 @@ class WorkBookControllerTest : ControllerTestSpec() { .path("$BASE_URL/{workbookId}") .build() .toUriString() - // set usecase mock + val workbookId = 1L - `when`(readWorkbookUseCase.execute(ReadWorkbookUseCaseIn(workbookId))).thenReturn( - ReadWorkbookUseCaseOut( - id = 1L, - mainImageUrl = URL("http://localhost:8080/api/v1/workbooks/1/image"), - title = "재태크, 투자 필수 용어 모음집", - description = "사회 초년생부터, 직장인, 은퇴자까지 모두가 알아야 할 기본적인 재태크, 투자 필수 용어 모음집 입니다.", - category = CategoryType.fromCode(0)!!.name, - createdAt = LocalDateTime.now(), - writers = listOf( - WriterDetail(1L, "안나포", URL("http://localhost:8080/api/v1/users/1")), - WriterDetail(2L, "퓨퓨", URL("http://localhost:8080/api/v1/users/2")), - WriterDetail(3L, "프레소", URL("http://localhost:8080/api/v1/users/3")) - ), - articles = listOf(ArticleDetail(1L, "ISA(개인종합자산관리계좌)란?"), ArticleDetail(2L, "ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란?")) + val useCaseIn = ReadWorkbookUseCaseIn(workbookId) + val useCaseOut = ReadWorkbookUseCaseOut( + id = 1L, + mainImageUrl = URL("http://localhost:8080/api/v1/workbooks/1/image"), + title = "재태크, 투자 필수 용어 모음집", + description = "사회 초년생부터, 직장인, 은퇴자까지 모두가 알아야 할 기본적인 재태크, 투자 필수 용어 모음집 입니다.", + category = CategoryType.fromCode(0)!!.name, + createdAt = LocalDateTime.now(), + writers = listOf( + WriterDetail(1L, "안나포", URL("http://localhost:8080/api/v1/users/1")), + WriterDetail(2L, "퓨퓨", URL("http://localhost:8080/api/v1/users/2")), + WriterDetail(3L, "프레소", URL("http://localhost:8080/api/v1/users/3")) + ), + articles = listOf( + ArticleDetail(1L, "ISA(개인종합자산관리계좌)란?"), + ArticleDetail( + 2L, + "ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란? ISA(개인종합자산관리계좌)란?" + ) ) ) + `when`(readWorkbookUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when - this.webTestClient.get() - .uri(uri, 1) - .accept(MediaType.APPLICATION_JSON) - .exchange().expectStatus().isOk() - .expectBody().consumeWith( - WebTestClientRestDocumentation.document( + mockMvc.perform( + get(uri, workbookId) + .contentType(MediaType.APPLICATION_JSON) + ).andExpect(status().is2xxSuccessful) + .andDo( + document( api.toIdentifier(), - ResourceDocumentation.resource( + resource( ResourceSnippetParameters.builder() .description("학습지 Id를 입력하여 학습지 정보를 조회합니다.") .summary(api.toIdentifier()) diff --git a/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt b/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt index c3d610b72..0ce3c75c2 100644 --- a/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt +++ b/api/src/test/kotlin/com/few/api/web/controller/workbook/article/WorkBookArticleControllerTest.kt @@ -11,20 +11,13 @@ import com.few.api.web.controller.ControllerTestSpec import com.few.api.web.controller.description.Description import com.few.api.web.controller.helper.* import com.few.data.common.code.CategoryType -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test import org.mockito.Mockito.`when` -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.codec.json.Jackson2JsonDecoder -import org.springframework.http.codec.json.Jackson2JsonEncoder -import org.springframework.restdocs.RestDocumentationContextProvider import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get import org.springframework.restdocs.payload.PayloadDocumentation -import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation import org.springframework.security.test.context.support.WithUserDetails -import org.springframework.test.web.reactive.server.WebTestClient import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.web.util.UriComponentsBuilder import java.net.URL @@ -32,25 +25,9 @@ import java.time.LocalDateTime class WorkBookArticleControllerTest : ControllerTestSpec() { - @Autowired - lateinit var workBookArticleController: WorkBookArticleController - companion object { - private val BASE_URL = "/api/v1/workbooks/{workbookId}/articles" - private val TAG = "WorkBookArticleController" - } - - @BeforeEach - fun setUp(restDocumentation: RestDocumentationContextProvider) { - webTestClient = WebTestClient - .bindToController(workBookArticleController) - .controllerAdvice(super.apiControllerExceptionHandler).httpMessageCodecs { - it.defaultCodecs().jackson2JsonEncoder(Jackson2JsonEncoder(objectMapper)) - it.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) - } - .configureClient() - .filter(WebTestClientRestDocumentation.documentationConfiguration(restDocumentation)) - .build() + private const val BASE_URL = "/api/v1/workbooks/{workbookId}/articles" + private const val TAG = "WorkBookArticleController" } @Test @@ -59,36 +36,39 @@ class WorkBookArticleControllerTest : ControllerTestSpec() { fun readWorkBookArticle() { // given val api = "ReadWorkBookArticle" + val token = "thisisaccesstoken" val uri = UriComponentsBuilder.newInstance() .path("$BASE_URL/{articleId}") .build() .toUriString() - // set usecase mock + + val memberId = 1L + `when`(tokenResolver.resolveId(token)).thenReturn(memberId) + val workbookId = 1L val articleId = 1L - val memberId = 1L - `when`(tokenResolver.resolveId("thisisaccesstoken")).thenReturn(memberId) - `when`(readWorkBookArticleUseCase.execute(ReadWorkBookArticleUseCaseIn(workbookId, articleId, memberId = memberId))).thenReturn( - ReadWorkBookArticleOut( + val useCaseIn = + ReadWorkBookArticleUseCaseIn(workbookId, articleId, memberId = memberId) + val useCaseOut = ReadWorkBookArticleOut( + id = 1L, + writer = WriterDetail( id = 1L, - writer = WriterDetail( - id = 1L, - name = "안나포", - url = URL("http://localhost:8080/api/v1/writers/1") - ), - title = "ETF(상장 지수 펀드)란? 모르면 손해라고?", - content = "content", - problemIds = listOf(1L, 2L, 3L), - category = CategoryType.fromCode(0)!!.name, - createdAt = LocalDateTime.now(), - day = 1L - ) + name = "안나포", + url = URL("http://localhost:8080/api/v1/writers/1") + ), + title = "ETF(상장 지수 펀드)란? 모르면 손해라고?", + content = "content", + problemIds = listOf(1L, 2L, 3L), + category = CategoryType.fromCode(0)!!.name, + createdAt = LocalDateTime.now(), + day = 1L ) + `when`(readWorkBookArticleUseCase.execute(useCaseIn)).thenReturn(useCaseOut) // when mockMvc.perform( get(uri, workbookId, articleId) - .header("Authorization", "Bearer thisisaccesstoken") + .header("Authorization", "Bearer $token") ).andExpect(MockMvcResultMatchers.status().is2xxSuccessful) .andDo( MockMvcRestDocumentation.document(