feat(notice): v2 최근 활성 공지 조회를 추가한다
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.notice
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.notice.ServiceNotice
|
||||
import kr.co.vividnext.sodalive.notice.ServiceServiceNoticeRepository
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate
|
||||
import org.springframework.http.HttpEntity
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.jdbc.core.JdbcTemplate
|
||||
import org.springframework.test.annotation.DirtiesContext
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import java.sql.Timestamp
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@SpringBootTest(
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
properties = [
|
||||
"spring.datasource.url=jdbc:h2:mem:latest-notice-controller-integration;" +
|
||||
"MODE=MySQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=VALUE;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"
|
||||
]
|
||||
)
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
|
||||
class LatestNoticeControllerIntegrationTest @Autowired constructor(
|
||||
private val restTemplate: TestRestTemplate,
|
||||
private val repository: ServiceServiceNoticeRepository,
|
||||
private val jdbcTemplate: JdbcTemplate,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
jdbcTemplate.execute(
|
||||
"CREATE ALIAS IF NOT EXISTS DATE_FORMAT FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.dateFormat'"
|
||||
)
|
||||
jdbcTemplate.execute(
|
||||
"CREATE ALIAS IF NOT EXISTS CONVERT_TZ FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.convertTz'"
|
||||
)
|
||||
repository.deleteAllInBatch()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDown() {
|
||||
repository.deleteAllInBatch()
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("익명 요청은 작성 시각과 무관하게 활성 공지 id 역순 최대 3개와 전체 필드를 반환한다")
|
||||
fun shouldReturnLatestThreeActiveNoticesByIdWithUnchangedFields() {
|
||||
saveNotice("제외될 활성 공지", "오래된 id", "2026-10-30T12:00:00")
|
||||
val second = saveNotice("두 번째 공지", "본문 2", "2026-10-29T12:00:00")
|
||||
val third = saveNotice("세 번째 공지", "본문 3", "2026-10-28T12:00:00")
|
||||
val fourth = saveNotice(
|
||||
"최근 서비스 공지",
|
||||
"서비스 공지\n두 번째 줄: <b>전체 본문</b> & \"인용\"",
|
||||
"2026-10-27T15:00:00"
|
||||
)
|
||||
saveNotice("더 큰 비활성 id", "공개하면 안 되는 본문", "2026-10-31T12:00:00", false)
|
||||
|
||||
assertGet(
|
||||
"/api/v2/notices/latest",
|
||||
"""
|
||||
{"success":true,"message":null,"data":[
|
||||
{"id":$fourth,"title":"최근 서비스 공지",
|
||||
"content":"서비스 공지\n두 번째 줄: <b>전체 본문</b> & \"인용\"","date":"2026-10-28"},
|
||||
{"id":$third,"title":"세 번째 공지","content":"본문 3","date":"2026-10-28"},
|
||||
{"id":$second,"title":"두 번째 공지","content":"본문 2","date":"2026-10-29"}
|
||||
],"errorProperty":null}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource("2026-10-27T14:59:59, 2026-10-27", "2026-10-27T15:00:00, 2026-10-28")
|
||||
@DisplayName("UTC 시각은 기존 KST 자정 경계에 맞춰 날짜 문자열로 반환한다")
|
||||
fun shouldPreserveExistingDateAtKstMidnight(createdAt: String, expectedDate: String) {
|
||||
val noticeId = saveNotice("날짜 경계", "날짜 본문", createdAt)
|
||||
|
||||
assertGet(
|
||||
"/api/v2/notices/latest",
|
||||
"""
|
||||
{"success":true,"message":null,"data":[
|
||||
{"id":$noticeId,"title":"날짜 경계","content":"날짜 본문","date":"$expectedDate"}
|
||||
],"errorProperty":null}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("공지가 없으면 null 대신 빈 배열을 반환한다")
|
||||
fun shouldReturnEmptyArrayWhenThereAreNoNotices() {
|
||||
assertGet(
|
||||
"/api/v2/notices/latest",
|
||||
"""{"success":true,"message":null,"data":[],"errorProperty":null}"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("활성 공지 1개는 원래 필드 그대로 한 항목 배열로 반환한다")
|
||||
fun shouldReturnOneNoticeWithoutPadding() {
|
||||
val noticeId = saveNotice("단일 공지", "단일 본문", "2026-10-27T12:00:00")
|
||||
|
||||
assertGet(
|
||||
"/api/v2/notices/latest",
|
||||
"""
|
||||
{"success":true,"message":null,"data":[
|
||||
{"id":$noticeId,"title":"단일 공지","content":"단일 본문","date":"2026-10-27"}
|
||||
],"errorProperty":null}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("활성 공지 2개는 중복 없이 id 역순으로 반환한다")
|
||||
fun shouldReturnTwoNoticesWithoutPadding() {
|
||||
val first = saveNotice("첫 공지", "첫 본문", "2026-10-28T12:00:00")
|
||||
val second = saveNotice("다음 공지", "다음 본문", "2026-10-27T12:00:00")
|
||||
|
||||
assertGet(
|
||||
"/api/v2/notices/latest",
|
||||
"""
|
||||
{"success":true,"message":null,"data":[
|
||||
{"id":$second,"title":"다음 공지","content":"다음 본문","date":"2026-10-27"},
|
||||
{"id":$first,"title":"첫 공지","content":"첫 본문","date":"2026-10-28"}
|
||||
],"errorProperty":null}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("비활성 공지만 있으면 빈 배열을 반환한다")
|
||||
fun shouldReturnEmptyArrayWhenAllNoticesAreInactive() {
|
||||
saveNotice("비활성 1", "본문 1", "2026-10-27T12:00:00", false)
|
||||
saveNotice("비활성 2", "본문 2", "2026-10-28T12:00:00", false)
|
||||
|
||||
assertGet(
|
||||
"/api/v2/notices/latest",
|
||||
"""{"success":true,"message":null,"data":[],"errorProperty":null}"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("기존 v1 최근 공지는 배열이 아닌 최신 활성 공지 단일 객체를 유지한다")
|
||||
fun shouldKeepLegacyLatestNoticeAsAnObject() {
|
||||
saveNotice("이전 공지", "이전 본문", "2026-10-29T12:00:00")
|
||||
val latest = saveNotice("기존 최근 공지", "기존 전체 본문", "2026-10-27T15:00:00")
|
||||
saveNotice("비활성", "미공개 본문", "2026-10-30T12:00:00", false)
|
||||
|
||||
assertGet(
|
||||
"/notice/latest",
|
||||
"""
|
||||
{"success":true,"message":null,"data":
|
||||
{"id":$latest,"title":"기존 최근 공지","content":"기존 전체 본문","date":"2026-10-28"},
|
||||
"errorProperty":null}
|
||||
""".trimIndent()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("기존 v1 최근 공지는 데이터가 없으면 null을 유지한다")
|
||||
fun shouldKeepLegacyLatestNoticeNullWhenEmpty() {
|
||||
assertGet("/notice/latest", """{"success":true,"message":null,"data":null,"errorProperty":null}""")
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource("POST, /api/v2/notices/latest", "GET, /api/v2/notices/private")
|
||||
@DisplayName("익명 POST 및 인접 보호 경로는 공개하지 않는다")
|
||||
fun shouldRejectAnonymousRequestsOutsideTheExactGetRoute(method: HttpMethod, path: String) {
|
||||
val response = restTemplate.exchange(path, method, HttpEntity.EMPTY, String::class.java)
|
||||
|
||||
println("HTTP $method $path -> ${response.statusCodeValue} ${response.body}")
|
||||
assertEquals(HttpStatus.UNAUTHORIZED, response.statusCode)
|
||||
}
|
||||
|
||||
private fun assertGet(path: String, expectedJson: String) {
|
||||
val response = restTemplate.getForEntity(path, String::class.java)
|
||||
|
||||
println("HTTP GET $path -> ${response.statusCodeValue} ${response.body}")
|
||||
assertEquals(HttpStatus.OK, response.statusCode)
|
||||
assertEquals(objectMapper.readTree(expectedJson), objectMapper.readTree(response.body))
|
||||
}
|
||||
|
||||
private fun saveNotice(title: String, content: String, createdAt: String, isActive: Boolean = true): Long {
|
||||
val noticeId = requireNotNull(repository.saveAndFlush(ServiceNotice(title, content, isActive)).id)
|
||||
assertEquals(
|
||||
1,
|
||||
jdbcTemplate.update(
|
||||
"UPDATE service_notice SET created_at = ? WHERE id = ?",
|
||||
Timestamp.valueOf(LocalDateTime.parse(createdAt)),
|
||||
noticeId
|
||||
)
|
||||
)
|
||||
return noticeId
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user