Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class LeaveMessageBlockContractTest {
val detail =
json
.decodeFromString<ReceivedAfternoteDetailDto>(
"""{"id":1,"category":"GALLERY","leaveMessage":[{"title":null,"body":"사진은 남겨줘"}]}""",
"""{"id":1,"category":"GALLERY","title":"사진첩","leaveMessage":[{"title":null,"body":"사진은 남겨줘"}]}""",
).toDomain()

assertEquals(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ data class ReceivedAfternoteDto(
data class ReceivedAfternoteDetailDto(
@SerialName("id") val id: Long,
@SerialName("category") val category: String? = null,
@SerialName("title") val title: String? = null,
@SerialName("title") val title: String,
@SerialName("actions") val processingMethods: List<String> = emptyList(),
@SerialName("leaveMessage") val leaveMessage: List<LeaveMessageBlockDto>? = null,
@SerialName("senderName") val senderName: String? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.afternote.feature.receiver.data.dto

import com.afternote.core.network.model.BaseResponse
import com.afternote.core.network.model.requireData
import com.afternote.feature.receiver.data.mapper.response.toDomain
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test

/**
* 수신 상세 응답의 `title` 이 **반드시 온다**는 계약 회귀 가드 (#1177).
*
* BE 는 이 값을 비워 둘 수 없다 — 생성 요청이 `@NotBlank`(`AfternoteCreateRequest`), 컬럼이
* `nullable = false`(`Afternote`), 수정 경로는 null 이면 기존 제목을 유지한다(`AfternoteService`).
* 응답 조립부 3종(social·gallery·playlist)이 그 값을 그대로 싣고, 임시저장은 수신자 조회 쿼리가
* `isDraft = false` 로 걸러 낸다. 그래서 제목 없는 수신 상세는 **서버에서 나올 수 없다.**
*
* 이 가드가 지키는 것은 그 계약이 깨졌을 때의 **실패 방향**이다. 예전처럼 `String? = null` 이면
* 키가 빠져도 파싱이 조용히 통과하고 화면이 제목 없이 열려, 서버 결함이 «제목이 비어 보이는»
* 정상 화면으로 위장된다. Json 설정은 `NetworkModule.provideJson` 과 동일 —
* `coerceInputValues` 는 **기본값이 있어야** 동작하므로 이 필드를 구해 주지 않는다.
*/
class ReceivedAfternoteDetailTitleContractTest {
private val json =
Json {
ignoreUnknownKeys = true
coerceInputValues = true
}

@Test
fun `제목이 있는 응답 - 디코드부터 도메인 title 까지 도달`() {
val payload =
"""{"status":200,"code":200,"message":"성공","data":{"id":1,"category":"SOCIAL","title":"인스타그램","actions":["계정 삭제"],"senderName":"김철수","createdAt":"2026-08-26T14:30:00"}}"""

val detail = json.decodeFromString<BaseResponse<ReceivedAfternoteDetailDto>>(payload).requireData().toDomain()

assertEquals("인스타그램", detail.title)
}

@Test
fun `제목 키가 빠진 응답 - 빈 제목으로 성공하지 않고 어느 키인지 남기며 실패한다`() {
val payload =
"""{"status":200,"code":200,"message":"성공","data":{"id":1,"category":"SOCIAL","senderName":"김철수"}}"""

assertTrue(decodeFailureMessage(payload).contains("title"))
}

@Test
fun `제목이 명시적 null 인 응답 - coerceInputValues 가 구해 주지 않는다`() {
val payload =
"""{"status":200,"code":200,"message":"성공","data":{"id":1,"category":"SOCIAL","title":null}}"""

assertTrue(decodeFailureMessage(payload).contains("title"))
}

/** 실패 자체와 «어느 키 때문인지»를 함께 고정한다 — 진단 문구가 사라지면 서버 결함을 못 짚는다. */
private fun decodeFailureMessage(payload: String): String =
assertThrows(SerializationException::class.java) {
json.decodeFromString<BaseResponse<ReceivedAfternoteDetailDto>>(payload)
}.message.orEmpty()
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,25 @@ class ReceivedAfternoteDetailMapperTest {

@Test
fun `toDomain - category null이면 type null`() {
assertNull(ReceivedAfternoteDetailDto(id = 1L, category = null).toDomain().type)
assertNull(ReceivedAfternoteDetailDto(id = 1L, title = "제목", category = null).toDomain().type)
}

@Test
fun `toDomain - createdAt null이면 createdAt null`() {
assertNull(ReceivedAfternoteDetailDto(id = 1L, createdAt = null).toDomain().createdAt)
assertNull(ReceivedAfternoteDetailDto(id = 1L, title = "제목", createdAt = null).toDomain().createdAt)
}

@Test
fun `toDomain - playlist null이면 null`() {
assertNull(ReceivedAfternoteDetailDto(id = 1L, playlist = null).toDomain().playlist)
assertNull(ReceivedAfternoteDetailDto(id = 1L, title = "제목", playlist = null).toDomain().playlist)
}

@Test
fun `toDomain - playlist 있으면 songs·atmosphere·memorialVideo 매핑`() {
val result =
ReceivedAfternoteDetailDto(
id = 1L,
title = "제목",
playlist =
ReceivedPlaylistDto(
atmosphere = "차분",
Expand All @@ -74,6 +75,7 @@ class ReceivedAfternoteDetailMapperTest {
val result =
ReceivedAfternoteDetailDto(
id = 1L,
title = "제목",
credentials = ReceivedCredentialsDto(id = "u", password = "p"),
).toDomain()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data class ReceivedExportBundle(
)

data class ReceivedAfternoteDetail(
val title: String? = null,
val title: String,
val senderName: String? = null,
val createdAt: String? = null,
val category: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ internal fun ReceivedAfternoteDetail.toReceivedDetailContentUiModel(): ReceivedD

private fun ReceivedAfternoteDetail.toReceivedSocialNetworkDetailContent(): ReceivedSocialNetworkDetailContent =
ReceivedSocialNetworkDetailContent(
serviceName = title.orEmpty(),
serviceName = title,
accountId = credentials?.id.orEmpty(),
password = credentials?.password.orEmpty(),
processingMethods = processingMethods,
Expand All @@ -41,7 +41,7 @@ private fun ReceivedAfternoteDetail.toReceivedSocialNetworkDetailContent(): Rece

private fun ReceivedAfternoteDetail.toReceivedGalleryDetailContent(): ReceivedGalleryDetailContent =
ReceivedGalleryDetailContent(
serviceName = title.orEmpty(),
serviceName = title,
finalWriteDate = createdAt.orEmpty(),
processingMethods = processingMethods,
messageBlocks = leaveMessageBlocks.toMessageBlockUiModels(),
Expand Down
Loading