fix(subbanner): 언어 설정과 국가별 공개 필터를 추가한다
This commit is contained in:
+32
-1
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.api.content.recommendation.application
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.v2.common.domain.RecommendationBanner
|
||||
import kr.co.vividnext.sodalive.v2.content.recommendation.application.AudioRecommendationQueryService
|
||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
|
||||
@@ -9,6 +10,8 @@ import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommenda
|
||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.CommentedAudio
|
||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.OriginalSeries
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.application.SubBannerService
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.domain.SubBanner
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.domain.SubBannerPlacement
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
@@ -17,7 +20,35 @@ import org.mockito.Mockito
|
||||
class AudioRecommendationFacadeTest {
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
private val queryService = Mockito.mock(AudioRecommendationQueryService::class.java)
|
||||
private val facade = AudioRecommendationFacade(queryService, Mockito.mock(SubBannerService::class.java), "https://cdn.test")
|
||||
private val subBannerService = Mockito.mock(SubBannerService::class.java)
|
||||
private val facade = AudioRecommendationFacade(queryService, subBannerService, "https://cdn.test")
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 추천은 언어 context 대신 nullable 회원을 서브 배너 조회에 전달하고 공개 결과를 조립한다")
|
||||
fun shouldComposeSubBannersUsingTheViewerRatherThanContextLanguage() {
|
||||
val member = Member(password = "password", nickname = "viewer").apply { id = 900001L }
|
||||
listOf(null, member).forEach { viewer ->
|
||||
val banner = SubBanner(
|
||||
id = 123L,
|
||||
placement = SubBannerPlacement.CONTENT_RECOMMENDATION,
|
||||
imagePath = "sub_banner/ja.png",
|
||||
webUrl = "https://example.test/promo",
|
||||
androidDeepLink = null,
|
||||
iosDeepLink = null,
|
||||
startDate = null,
|
||||
endDate = null,
|
||||
lang = Lang.JA
|
||||
)
|
||||
Mockito.doReturn(domain()).`when`(queryService).getRecommendations(viewer, Lang.EN)
|
||||
Mockito.doReturn(listOf(banner)).`when`(subBannerService)
|
||||
.findVisibleByPlacement(SubBannerPlacement.CONTENT_RECOMMENDATION, viewer)
|
||||
|
||||
val result = facade.getRecommendations(viewer, Lang.EN)
|
||||
|
||||
assertEquals(listOf(123L), result.subBanners.map { it.id })
|
||||
assertEquals("https://cdn.test/sub_banner/ja.png", result.subBanners.single().imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("facade는 도메인 추천 결과를 모든 공개 응답 필드로 변환한다")
|
||||
|
||||
+30
-1
@@ -23,6 +23,8 @@ import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecentlyActiveCreator
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.application.SubBannerService
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.domain.SubBanner
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.domain.SubBannerPlacement
|
||||
import org.junit.jupiter.api.Assertions.assertAll
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
@@ -44,13 +46,40 @@ class HomeRecommendationFacadeTest {
|
||||
creatorCommunityTranslationService = creatorCommunityTranslationService
|
||||
)
|
||||
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||
private val subBannerService = Mockito.mock(SubBannerService::class.java)
|
||||
private val facade = HomeRecommendationFacade(
|
||||
queryService,
|
||||
preferenceService,
|
||||
Mockito.mock(SubBannerService::class.java),
|
||||
subBannerService,
|
||||
"https://cdn.test"
|
||||
)
|
||||
|
||||
@Test
|
||||
@DisplayName("홈은 언어 context 대신 nullable 회원을 서브 배너 조회에 전달하고 공개 결과를 조립한다")
|
||||
fun shouldComposeSubBannersUsingTheViewerRatherThanContextLanguage() {
|
||||
val member = Member(password = "password", nickname = "viewer").apply { id = 900001L }
|
||||
listOf(null, member).forEach { viewer ->
|
||||
val banner = SubBanner(
|
||||
id = 123L,
|
||||
placement = SubBannerPlacement.HOME_RECOMMENDATION,
|
||||
imagePath = "sub_banner/ja.png",
|
||||
webUrl = "https://example.test/promo",
|
||||
androidDeepLink = null,
|
||||
iosDeepLink = null,
|
||||
startDate = null,
|
||||
endDate = null,
|
||||
lang = Lang.JA
|
||||
)
|
||||
Mockito.doReturn(listOf(banner)).`when`(subBannerService)
|
||||
.findVisibleByPlacement(SubBannerPlacement.HOME_RECOMMENDATION, viewer)
|
||||
|
||||
val result = facade.getHomeRecommendations(viewer, Lang.EN)
|
||||
|
||||
assertEquals(listOf(123L), result.subBanners.map { it.id })
|
||||
assertEquals("https://cdn.test/sub_banner/ja.png", result.subBanners.single().imageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("홈 추천은 AI 스냅샷과 상세 및 무작위 보충이나 fallback 조회 없이 빈 AI 목록을 반환한다")
|
||||
fun shouldReturnEmptyAiCharactersWithoutQueryingAiRecommendations() {
|
||||
|
||||
+58
-2
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.adapter.out.persistence.DefaultSubBannerRepository
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.domain.SubBanner
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.port.out.SubBannerRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
@@ -27,6 +28,7 @@ import org.mockito.Mockito
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.boot.test.mock.mockito.SpyBean
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate
|
||||
import org.springframework.boot.test.web.server.LocalServerPort
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
@@ -78,6 +80,9 @@ class AdminSubBannerControllerIntegrationTest @Autowired constructor(
|
||||
@MockBean
|
||||
private lateinit var s3Uploader: S3Uploader
|
||||
|
||||
@SpyBean
|
||||
private lateinit var writeRepository: DefaultSubBannerRepository
|
||||
|
||||
private lateinit var adminToken: String
|
||||
private val memberIds = mutableListOf<Long>()
|
||||
|
||||
@@ -106,7 +111,7 @@ class AdminSubBannerControllerIntegrationTest @Autowired constructor(
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("실제 JWT HTTP CRUD는 관리자 8필드·삭제 null을 유지하며 소프트 삭제 후 수정·재삭제를 거부한다")
|
||||
@DisplayName("실제 JWT HTTP CRUD는 관리자 9필드·삭제 null을 유지하며 소프트 삭제 후 수정·재삭제를 거부한다")
|
||||
fun shouldCreateListUpdateAndSoftDeleteWithExactAdminResponse() {
|
||||
val createdResponse = send(HttpMethod.POST)
|
||||
evidence("POST", createdResponse)
|
||||
@@ -163,6 +168,57 @@ class AdminSubBannerControllerIntegrationTest @Autowired constructor(
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("언어 생략 생성은 KO이고 JA·EN 수정 뒤 생략은 보존하며 관리자 목록은 모든 언어다")
|
||||
fun shouldDefaultCreateLanguageAndPreserveOmittedUpdateAcrossAllAdminLanguages() {
|
||||
val defaultResponse = send(HttpMethod.POST)
|
||||
evidence("language omitted POST", defaultResponse)
|
||||
val default = success(defaultResponse)
|
||||
assertEquals("KO", default.path("lang").asText())
|
||||
val created = listOf(" ja ", "en").map { lang ->
|
||||
val response = send(HttpMethod.POST, fields = baseFields() + ("lang" to lang))
|
||||
evidence("language $lang POST", response)
|
||||
success(response)
|
||||
}
|
||||
assertEquals(listOf("JA", "EN"), created.map { it.path("lang").asText() })
|
||||
created.forEach { original ->
|
||||
val id = original["id"].asLong()
|
||||
val omitted = send(HttpMethod.PUT, "/$id", image = null)
|
||||
evidence("language omitted PUT", omitted)
|
||||
assertEquals(original, success(omitted))
|
||||
assertEquals(original["lang"], objectMapper.valueToTree<JsonNode>(reload(id))["lang"])
|
||||
}
|
||||
val id = default["id"].asLong()
|
||||
listOf("JA", "EN", "KO").forEach { lang ->
|
||||
val updated = send(HttpMethod.PUT, "/$id", baseFields() + ("lang" to lang), image = null)
|
||||
evidence("language $lang PUT", updated)
|
||||
assertEquals(lang, success(updated).path("lang").asText())
|
||||
assertEquals(lang, objectMapper.valueToTree<JsonNode>(reload(id)).path("lang").asText())
|
||||
}
|
||||
val listing = send(HttpMethod.GET, "?placement=HOME_RECOMMENDATION")
|
||||
evidence("language all admin GET", listing)
|
||||
assertEquals(listOf(default) + created, success(listing).toList())
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = ["null", "0", "true", "[]", "{}", "\"\"", "\" \"", "\"FR\""])
|
||||
@DisplayName("명시적 null·비문자열·blank·알 수 없는 언어는 POST·PUT 업로드·저장 호출 전 400이다")
|
||||
fun shouldRejectInvalidLanguageBeforeUploadOrWrite(raw: String) {
|
||||
val id = createScheduled()
|
||||
val existing = reload(id)
|
||||
val before = storedRows()
|
||||
Mockito.clearInvocations(s3Uploader, writeRepository)
|
||||
val fields = baseFields() + ("lang" to objectMapper.readValue(raw, Any::class.java))
|
||||
listOf(HttpMethod.POST to "", HttpMethod.PUT to "/$id").forEach { (method, suffix) ->
|
||||
val response = send(method, suffix, fields)
|
||||
evidence("invalid language $raw $method", response)
|
||||
assertStatus(response, HttpStatus.BAD_REQUEST)
|
||||
assertEquals(before, storedRows())
|
||||
Mockito.verifyNoInteractions(s3Uploader)
|
||||
Mockito.verify(writeRepository, Mockito.never()).save(Mockito.any(SubBanner::class.java) ?: existing)
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = [0, 1, 2, 3, 4, 5, 6, 7])
|
||||
@DisplayName("주소 8개 조합 중 모두 빈 조합만 거부하고 나머지는 생성·수정한다")
|
||||
@@ -519,7 +575,7 @@ class AdminSubBannerControllerIntegrationTest @Autowired constructor(
|
||||
|
||||
private fun assertAdminFields(data: JsonNode) {
|
||||
assertEquals(
|
||||
setOf("id", "placement", "imageUrl", "webUrl", "androidDeepLink", "iosDeepLink", "startDate", "endDate"),
|
||||
setOf("id", "placement", "imageUrl", "webUrl", "androidDeepLink", "iosDeepLink", "startDate", "endDate", "lang"),
|
||||
data.fieldNames().asSequence().toSet()
|
||||
)
|
||||
}
|
||||
|
||||
+42
-5
@@ -6,10 +6,12 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
|
||||
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
|
||||
import kr.co.vividnext.sodalive.content.main.tab.AudioContentMainTab
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.application.SubBannerService
|
||||
@@ -86,6 +88,29 @@ class RecommendationSubBannerIntegrationTest @Autowired constructor(
|
||||
|
||||
private val memberIds = mutableListOf<Long>()
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(SubBannerPlacement::class)
|
||||
@DisplayName("두 공개 추천은 JP·KR·US·국가 누락·강제 매핑에 맞는 언어만 공개 6필드로 반환한다")
|
||||
fun shouldFilterPublicSubBannersByCountryLanguage(placement: SubBannerPlacement) {
|
||||
val korean = save(banner(placement))
|
||||
val response = send(recommendationPath(placement), country = "JP")
|
||||
evidence("language JP $placement", response)
|
||||
assertEquals(emptyList<JsonNode>(), successData(response)["subBanners"].toList())
|
||||
val japanese = save(banner(placement).copy(lang = Lang.JA))
|
||||
save(banner(placement).copy(lang = Lang.EN))
|
||||
listOf("JP" to japanese, "KR" to korean, "US" to korean, null to korean).forEach { (country, expected) ->
|
||||
val result = send(recommendationPath(placement), country = country)
|
||||
evidence("language country=$country $placement", result)
|
||||
assertEquals(listOf(publicTree(expected)), successData(result)["subBanners"].toList())
|
||||
}
|
||||
listOf(29721L to japanese, 16L to korean).forEach { (memberId, expected) ->
|
||||
val country = if (memberId == 29721L) "KR" else "JP"
|
||||
val result = send(recommendationPath(placement), token = tokenFor(MemberRole.USER, memberId), country = country)
|
||||
evidence("language forced=$memberId country=$country $placement", result)
|
||||
assertEquals(listOf(publicTree(expected)), successData(result)["subBanners"].toList())
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jdbcTemplate.update("delete from sub_banner")
|
||||
@@ -107,6 +132,7 @@ class RecommendationSubBannerIntegrationTest @Autowired constructor(
|
||||
jdbcTemplate.update("delete from content_banner")
|
||||
jdbcTemplate.update("delete from audio_content_main_tab")
|
||||
tokenRepository.deleteAll()
|
||||
memberIds.forEach { jdbcTemplate.update("delete from member_content_preference where member_id = ?", it) }
|
||||
memberRepository.deleteAllById(memberIds)
|
||||
memberIds.clear()
|
||||
}
|
||||
@@ -189,7 +215,7 @@ class RecommendationSubBannerIntegrationTest @Autowired constructor(
|
||||
assertEquals(active.map { it.id }, ids(admin))
|
||||
admin.forEach { data ->
|
||||
assertEquals(
|
||||
setOf("id", "placement", "imageUrl", "webUrl", "androidDeepLink", "iosDeepLink", "startDate", "endDate"),
|
||||
setOf("id", "placement", "imageUrl", "webUrl", "androidDeepLink", "iosDeepLink", "startDate", "endDate", "lang"),
|
||||
data.fieldNames().asSequence().toSet()
|
||||
)
|
||||
}
|
||||
@@ -358,13 +384,15 @@ class RecommendationSubBannerIntegrationTest @Autowired constructor(
|
||||
path: String,
|
||||
method: HttpMethod = HttpMethod.GET,
|
||||
token: String? = null,
|
||||
userAgent: String = "sub-banner-http-test"
|
||||
userAgent: String = "sub-banner-http-test",
|
||||
country: String? = null
|
||||
): ResponseEntity<String> = restTemplate.exchange(
|
||||
"http://127.0.0.1:$port$path",
|
||||
method,
|
||||
HttpEntity<Any>(
|
||||
HttpHeaders().apply {
|
||||
token?.let { setBearerAuth(it) }
|
||||
country?.let { set("CloudFront-Viewer-Country", it) }
|
||||
set(HttpHeaders.ACCEPT_LANGUAGE, "en")
|
||||
set(HttpHeaders.USER_AGENT, userAgent)
|
||||
accept = listOf(MediaType.APPLICATION_JSON)
|
||||
@@ -472,11 +500,12 @@ class RecommendationSubBannerIntegrationTest @Autowired constructor(
|
||||
|
||||
private fun storedRows(): List<Map<String, Any>> = jdbcTemplate.queryForList("select * from sub_banner order by id")
|
||||
|
||||
private fun tokenFor(role: MemberRole): String {
|
||||
private fun tokenFor(role: MemberRole, forcedId: Long? = null): String {
|
||||
val member = memberRepository.saveAndFlush(
|
||||
Member(email = "${UUID.randomUUID()}@test.com", password = "password", nickname = "test", role = role)
|
||||
)
|
||||
val id = requireNotNull(member.id)
|
||||
val id = forcedId ?: requireNotNull(member.id)
|
||||
if (forcedId != null) jdbcTemplate.update("update member set id = ? where id = ?", forcedId, member.id)
|
||||
memberIds.add(id)
|
||||
return tokenProvider.createToken(
|
||||
UsernamePasswordAuthenticationToken(member.email, "", listOf(SimpleGrantedAuthority("ROLE_${role.name}"))),
|
||||
@@ -514,8 +543,16 @@ class RecommendationSubBannerIntegrationTest @Autowired constructor(
|
||||
repository: SubBannerRepository,
|
||||
s3Uploader: S3Uploader,
|
||||
objectMapper: ObjectMapper,
|
||||
memberContentPreferenceService: MemberContentPreferenceService,
|
||||
time: TestTime
|
||||
): SubBannerService = SubBannerService(repository, s3Uploader, objectMapper, "test-bucket", time::now)
|
||||
): SubBannerService = SubBannerService(
|
||||
repository,
|
||||
s3Uploader,
|
||||
objectMapper,
|
||||
memberContentPreferenceService,
|
||||
"test-bucket",
|
||||
time::now
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
+56
-8
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.subbanner.adapter.out.persistence
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.i18n.Lang
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.domain.SubBanner
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.domain.SubBannerPlacement
|
||||
import kr.co.vividnext.sodalive.v2.subbanner.port.out.SubBannerRepository
|
||||
@@ -50,6 +51,44 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
jdbcTemplate.execute("alter table sub_banner alter column end_date timestamp(0)")
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(Lang::class)
|
||||
@DisplayName("KO 기본값과 모든 언어는 문자열로 왕복하고 공개 조회만 언어·기간·활성을 함께 제한한다")
|
||||
fun shouldPersistAndUpdateLanguageWhileFilteringOnlyPublicRows(lang: Lang) {
|
||||
assertEquals(Lang.KO, banner().lang)
|
||||
val now = LocalDateTime.of(2026, 10, 29, 23, 59, 59)
|
||||
val placement = SubBannerPlacement.HOME_RECOMMENDATION
|
||||
val visible = Lang.values().map { repository.save(banner().copy(lang = it)) }
|
||||
val future = repository.save(banner().copy(lang = lang, startDate = now.plusSeconds(1)))
|
||||
val expired = repository.save(banner().copy(lang = lang, endDate = now.minusSeconds(1)))
|
||||
val deleted = repository.save(banner().copy(lang = lang))
|
||||
repository.deleteById(requireNotNull(deleted.id))
|
||||
flushAndClear()
|
||||
|
||||
visible.forEach { expected ->
|
||||
assertEquals(expected, repository.findById(requireNotNull(expected.id)))
|
||||
assertEquals(
|
||||
expected.lang.name,
|
||||
entityManager.createNativeQuery("select lang from sub_banner where id = :id")
|
||||
.setParameter("id", expected.id).singleResult
|
||||
)
|
||||
}
|
||||
assertEquals(visible + future + expired, repository.findAllByPlacement(placement))
|
||||
val selected = visible.single { it.lang == lang }
|
||||
assertEquals(listOf(selected), repository.findVisibleByPlacement(placement, lang, now))
|
||||
val changed = selected.copy(lang = Lang.values().first { it != lang })
|
||||
assertEquals(changed, repository.save(changed))
|
||||
flushAndClear()
|
||||
assertEquals(changed, repository.findById(requireNotNull(changed.id)))
|
||||
assertEquals(emptyList<SubBanner>(), repository.findVisibleByPlacement(placement, lang, now))
|
||||
val columns = jdbcTemplate.queryForMap(
|
||||
"select IS_NULLABLE, CHARACTER_MAXIMUM_LENGTH from INFORMATION_SCHEMA.COLUMNS " +
|
||||
"where TABLE_NAME = 'sub_banner' and COLUMN_NAME = 'lang'"
|
||||
)
|
||||
assertEquals("NO", columns["IS_NULLABLE"])
|
||||
assertEquals(10L, (columns["CHARACTER_MAXIMUM_LENGTH"] as Number).toLong())
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = [0, 123456789, 999999999])
|
||||
@DisplayName("nullable UTC 기간은 저장 결과와 DB 재조회 모두 초 정밀도를 정확히 보존한다")
|
||||
@@ -102,7 +141,7 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
fun shouldReturnAllPlacementRowsInIdOrderWithoutLimit(placement: SubBannerPlacement) {
|
||||
val now = LocalDateTime.of(2026, 10, 29, 23, 59, 59)
|
||||
assertEquals(emptyList<SubBanner>(), repository.findAllByPlacement(placement))
|
||||
assertEquals(emptyList<SubBanner>(), repository.findVisibleByPlacement(placement, now))
|
||||
assertEquals(emptyList<SubBanner>(), repository.findVisibleByPlacement(placement, Lang.KO, now))
|
||||
val otherPlacement = SubBannerPlacement.values().first { it != placement }
|
||||
val saved = (1..25).map { index ->
|
||||
repository.save(banner(otherPlacement))
|
||||
@@ -113,7 +152,7 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
flushAndClear()
|
||||
|
||||
assertEquals(saved + future + expired, repository.findAllByPlacement(placement))
|
||||
assertEquals(saved, repository.findVisibleByPlacement(placement, now))
|
||||
assertEquals(saved, repository.findVisibleByPlacement(placement, Lang.KO, now))
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "{0}")
|
||||
@@ -132,7 +171,7 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
|
||||
assertEquals(
|
||||
if (visible) listOf(candidate) else emptyList(),
|
||||
repository.findVisibleByPlacement(SubBannerPlacement.HOME_RECOMMENDATION, now),
|
||||
repository.findVisibleByPlacement(SubBannerPlacement.HOME_RECOMMENDATION, Lang.KO, now),
|
||||
name
|
||||
)
|
||||
}
|
||||
@@ -217,10 +256,10 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
}
|
||||
active.forEach { assertEquals(it, repository.findById(requireNotNull(it.id))) }
|
||||
assertEquals(active, repository.findAllByPlacement(placement))
|
||||
assertEquals(active.take(2), repository.findVisibleByPlacement(placement, now))
|
||||
assertEquals(active.take(2), repository.findVisibleByPlacement(placement, Lang.KO, now))
|
||||
assertEquals(other, repository.findById(requireNotNull(other.id)))
|
||||
assertEquals(listOf(other), repository.findAllByPlacement(other.placement))
|
||||
assertEquals(listOf(other), repository.findVisibleByPlacement(other.placement, now))
|
||||
assertEquals(listOf(other), repository.findVisibleByPlacement(other.placement, Lang.KO, now))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -243,7 +282,7 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
)
|
||||
assertNull(repository.findById(id))
|
||||
assertEquals(emptyList<SubBanner>(), repository.findAllByPlacement(saved.placement))
|
||||
assertEquals(emptyList<SubBanner>(), repository.findVisibleByPlacement(saved.placement, LocalDateTime.now()))
|
||||
assertEquals(emptyList<SubBanner>(), repository.findVisibleByPlacement(saved.placement, Lang.KO, LocalDateTime.now()))
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -297,7 +336,7 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
val readRepository = DefaultSubBannerRepository(readContext, JPAQueryFactory(readContext))
|
||||
assertNull(readRepository.findById(id))
|
||||
assertEquals(emptyList<SubBanner>(), readRepository.findAllByPlacement(saved.placement))
|
||||
assertEquals(emptyList<SubBanner>(), readRepository.findVisibleByPlacement(saved.placement, now))
|
||||
assertEquals(emptyList<SubBanner>(), readRepository.findVisibleByPlacement(saved.placement, Lang.KO, now))
|
||||
readContext.transaction.commit()
|
||||
} finally {
|
||||
listOf(updateContext, deleteContext, readContext).forEach { context ->
|
||||
@@ -341,17 +380,26 @@ class SubBannerPersistenceTest @Autowired constructor(
|
||||
assertTrue(columns.next(), "CREATE DDL must contain is_active")
|
||||
assertEquals("NO", columns.getString("IS_NULLABLE"))
|
||||
}
|
||||
connection.metaData.getColumns(null, null, "sub_banner", "lang").use { columns ->
|
||||
assertTrue(columns.next(), "CREATE DDL must contain lang")
|
||||
assertEquals("NO", columns.getString("IS_NULLABLE"))
|
||||
assertEquals(10, columns.getInt("COLUMN_SIZE"))
|
||||
}
|
||||
statement.executeUpdate(
|
||||
"insert into sub_banner (placement, image_path, web_url) " +
|
||||
"values ('HOME_RECOMMENDATION', 'sub-banner/image.png', 'https://example.com/banner')"
|
||||
)
|
||||
statement.executeQuery("select is_active from sub_banner").use { rows ->
|
||||
statement.executeQuery("select is_active, lang from sub_banner").use { rows ->
|
||||
assertTrue(rows.next())
|
||||
assertTrue(rows.getBoolean("is_active"))
|
||||
assertEquals("KO", rows.getString("lang"))
|
||||
}
|
||||
assertThrows(SQLException::class.java) {
|
||||
statement.executeUpdate("update sub_banner set is_active = null")
|
||||
}
|
||||
assertThrows(SQLException::class.java) {
|
||||
statement.executeUpdate("update sub_banner set lang = null")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package kr.co.vividnext.sodalive.v2.subbanner.domain
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class SubBannerTest {
|
||||
@Test
|
||||
@DisplayName("기존 생성자는 KO 언어를 기본으로 가진다")
|
||||
fun shouldDefaultLanguageToKorean() {
|
||||
val stored = jacksonObjectMapper().valueToTree<JsonNode>(banner("https://example.com", null, null))
|
||||
assertEquals("KO", stored.path("lang").asText())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("세 링크 중 하나 이상이 있는 7개 조합은 허용하고 모두 null이면 거부한다")
|
||||
fun shouldRejectAllEmptyLinksAndAllowSevenNonEmptyCombinations() {
|
||||
|
||||
Reference in New Issue
Block a user