Skip to content

Commit 6173900

Browse files
authored
feat(be) : Clean up code and add tests (#74)
* refactor(be): Rename method and fix import path * feat(be): compute updatedAt in Asia/Seoul * feat(be):Add Message,Room Test
1 parent e3f713c commit 6173900

File tree

10 files changed

+310
-45
lines changed

10 files changed

+310
-45
lines changed

.env.example

Lines changed: 0 additions & 32 deletions
This file was deleted.

src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/dto/ChatMessageResponse.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package com.back.koreaTravelGuide.domain.userChat.chatmessage.dto
22

33
import com.back.koreaTravelGuide.domain.userChat.chatmessage.entity.ChatMessage
4-
import java.time.Instant
4+
import java.time.ZoneId
5+
import java.time.ZonedDateTime
56

67
data class ChatMessageResponse(
78
val id: Long?,
89
val roomId: Long,
910
val senderId: Long,
1011
val content: String,
11-
val createdAt: Instant,
12+
val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
1213
) {
1314
companion object {
1415
fun from(message: ChatMessage): ChatMessageResponse {

src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatmessage/entity/ChatMessage.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import jakarta.persistence.GenerationType
77
import jakarta.persistence.Id
88
import jakarta.persistence.Index
99
import jakarta.persistence.Table
10-
import java.time.Instant
10+
import java.time.ZoneId
11+
import java.time.ZonedDateTime
1112

1213
@Entity
1314
@Table(
@@ -24,5 +25,5 @@ data class ChatMessage(
2425
@Column(nullable = false, columnDefinition = "text")
2526
val content: String,
2627
@Column(name = "created_at", nullable = false)
27-
val createdAt: Instant = Instant.now(),
28+
val createdAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
2829
)

src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/controller/ChatRoomController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class ChatRoomController(
2727
@RequestBody req: ChatRoomStartRequest,
2828
): ResponseEntity<ApiResponse<ChatRoomResponse>> {
2929
val authenticatedId = requesterId ?: throw AccessDeniedException("인증이 필요합니다.")
30-
val room = roomService.checkOneToOneRoom(req.guideId, req.userId, authenticatedId)
30+
val room = roomService.createOneToOneRoom(req.guideId, req.userId, authenticatedId)
3131
return ResponseEntity.ok(ApiResponse(msg = "채팅방 시작", data = ChatRoomResponse.from(room)))
3232
}
3333

src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/dto/ChatRoomResponse.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package com.back.koreaTravelGuide.domain.userChat.chatroom.dto
22

33
import com.back.koreaTravelGuide.domain.userChat.chatroom.entity.ChatRoom
4-
import java.time.Instant
4+
import java.time.ZoneId
5+
import java.time.ZonedDateTime
56

67
data class ChatRoomResponse(
78
val id: Long?,
89
val title: String,
910
val guideId: Long,
1011
val userId: Long,
11-
val updatedAt: Instant,
12+
val updatedAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
1213
val lastMessageId: Long?,
1314
) {
1415
companion object {

src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/entity/ChatRoom.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import jakarta.persistence.GenerationType
77
import jakarta.persistence.Id
88
import jakarta.persistence.Index
99
import jakarta.persistence.Table
10-
import java.time.Instant
10+
import java.time.ZoneId
11+
import java.time.ZonedDateTime
1112

1213
@Entity
1314
@Table(
@@ -24,7 +25,7 @@ data class ChatRoom(
2425
@Column(name = "user_id", nullable = false)
2526
val userId: Long,
2627
@Column(name = "updated_at", nullable = false)
27-
val updatedAt: Instant = Instant.now(),
28+
val updatedAt: ZonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
2829
@Column(name = "last_message_id")
2930
val lastMessageId: Long? = null,
3031
)

src/main/kotlin/com/back/koreaTravelGuide/domain/userChat/chatroom/service/ChatRoomService.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import com.back.koreaTravelGuide.domain.userChat.chatroom.repository.ChatRoomRep
66
import org.springframework.security.access.AccessDeniedException
77
import org.springframework.stereotype.Service
88
import org.springframework.transaction.annotation.Transactional
9-
import java.time.Instant
9+
import java.time.ZoneId
10+
import java.time.ZonedDateTime
1011
import java.util.NoSuchElementException
1112

1213
@Service
@@ -15,7 +16,7 @@ class ChatRoomService(
1516
private val messageRepository: ChatMessageRepository,
1617
) {
1718
@Transactional
18-
fun checkOneToOneRoom(
19+
fun createOneToOneRoom(
1920
guideId: Long,
2021
userId: Long,
2122
requesterId: Long,
@@ -27,7 +28,12 @@ class ChatRoomService(
2728
// 2) 없으면 생성 (동시요청은 DB 유니크 인덱스로 가드)
2829
val title = "Guide-$guideId · User-$userId"
2930
return roomRepository.save(
30-
ChatRoom(title = title, guideId = guideId, userId = userId, updatedAt = Instant.now()),
31+
ChatRoom(
32+
title = title,
33+
guideId = guideId,
34+
userId = userId,
35+
updatedAt = ZonedDateTime.now(ZoneId.of("Asia/Seoul")),
36+
),
3137
)
3238
}
3339

src/main/resources/application.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ springdoc:
130130
# Weather API 설정
131131
weather:
132132
api:
133-
key: ${WEATHER__API__KEY}
133+
key: ${WEATHER_API_KEY}
134134
base-url: https://apihub.kma.go.kr/api/typ02/openApi/MidFcstInfoService
135135

136136
# Tour API 설정
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package com.back.koreaTravelGuide.domain.userChat.chatmessage.controller
2+
3+
import com.back.koreaTravelGuide.common.security.JwtTokenProvider
4+
import com.back.koreaTravelGuide.domain.user.entity.User
5+
import com.back.koreaTravelGuide.domain.user.enums.UserRole
6+
import com.back.koreaTravelGuide.domain.user.repository.UserRepository
7+
import com.back.koreaTravelGuide.domain.userChat.chatmessage.dto.ChatMessageSendRequest
8+
import com.back.koreaTravelGuide.domain.userChat.chatmessage.entity.ChatMessage
9+
import com.back.koreaTravelGuide.domain.userChat.chatmessage.repository.ChatMessageRepository
10+
import com.back.koreaTravelGuide.domain.userChat.chatroom.entity.ChatRoom
11+
import com.back.koreaTravelGuide.domain.userChat.chatroom.repository.ChatRoomRepository
12+
import com.fasterxml.jackson.databind.ObjectMapper
13+
import org.junit.jupiter.api.Assertions.assertEquals
14+
import org.junit.jupiter.api.BeforeEach
15+
import org.junit.jupiter.api.DisplayName
16+
import org.junit.jupiter.api.Test
17+
import org.springframework.beans.factory.annotation.Autowired
18+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
19+
import org.springframework.boot.test.context.SpringBootTest
20+
import org.springframework.http.MediaType
21+
import org.springframework.test.context.ActiveProfiles
22+
import org.springframework.test.web.servlet.MockMvc
23+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
24+
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
25+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
26+
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
27+
import org.springframework.transaction.annotation.Transactional
28+
29+
@ActiveProfiles("test")
30+
@SpringBootTest
31+
@AutoConfigureMockMvc
32+
@Transactional
33+
class ChatMessageControllerTest {
34+
@Autowired
35+
private lateinit var mockMvc: MockMvc
36+
37+
@Autowired
38+
private lateinit var chatMessageRepository: ChatMessageRepository
39+
40+
@Autowired
41+
private lateinit var chatRoomRepository: ChatRoomRepository
42+
43+
@Autowired
44+
private lateinit var userRepository: UserRepository
45+
46+
@Autowired
47+
private lateinit var jwtTokenProvider: JwtTokenProvider
48+
49+
@Autowired
50+
private lateinit var objectMapper: ObjectMapper
51+
52+
private lateinit var guide: User
53+
private lateinit var guest: User
54+
private lateinit var guideToken: String
55+
private lateinit var guestToken: String
56+
private lateinit var room: ChatRoom
57+
private lateinit var firstMessage: ChatMessage
58+
59+
@BeforeEach
60+
fun setUp() {
61+
chatMessageRepository.deleteAll()
62+
chatRoomRepository.deleteAll()
63+
userRepository.deleteAll()
64+
guide =
65+
userRepository.save(
66+
User(
67+
email = "[email protected]",
68+
nickname = "guideTwo",
69+
role = UserRole.GUIDE,
70+
oauthProvider = "test",
71+
oauthId = "guide456",
72+
),
73+
)
74+
guest =
75+
userRepository.save(
76+
User(
77+
email = "[email protected]",
78+
nickname = "guestTwo",
79+
role = UserRole.USER,
80+
oauthProvider = "test",
81+
oauthId = "guest456",
82+
),
83+
)
84+
guideToken = jwtTokenProvider.createAccessToken(guide.id!!, guide.role)
85+
guestToken = jwtTokenProvider.createAccessToken(guest.id!!, guest.role)
86+
room =
87+
chatRoomRepository.save(
88+
ChatRoom(
89+
title = "Guide-${guide.id} · User-${guest.id}",
90+
guideId = guide.id!!,
91+
userId = guest.id!!,
92+
),
93+
)
94+
firstMessage =
95+
chatMessageRepository.save(
96+
ChatMessage(
97+
roomId = room.id!!,
98+
senderId = guide.id!!,
99+
content = "첫 메세지",
100+
),
101+
)
102+
}
103+
104+
@Test
105+
@DisplayName("listMessages returns latest batch when after is missing")
106+
fun listMessagesReturnsLatest() {
107+
mockMvc.perform(
108+
get("/api/userchat/rooms/${room.id}/messages")
109+
.header("Authorization", "Bearer $guestToken"),
110+
)
111+
.andExpect(status().isOk)
112+
.andExpect(jsonPath("$.data[0].id").value(firstMessage.id!!.toInt()))
113+
.andExpect(jsonPath("$.data[0].content").value(firstMessage.content))
114+
}
115+
116+
@Test
117+
@DisplayName("listMessages filters newer messages when after provided")
118+
fun listMessagesFiltersNewer() {
119+
val newer =
120+
chatMessageRepository.save(
121+
ChatMessage(
122+
roomId = room.id!!,
123+
senderId = guest.id!!,
124+
content = "두 번째",
125+
),
126+
)
127+
128+
mockMvc.perform(
129+
get("/api/userchat/rooms/${room.id}/messages")
130+
.param("after", firstMessage.id!!.toString())
131+
.header("Authorization", "Bearer $guestToken"),
132+
)
133+
.andExpect(status().isOk)
134+
.andExpect(jsonPath("$.data.length()").value(1))
135+
.andExpect(jsonPath("$.data[0].id").value(newer.id!!.toInt()))
136+
.andExpect(jsonPath("$.data[0].content").value(newer.content))
137+
}
138+
139+
@Test
140+
@DisplayName("sendMessage stores message and updates room summary")
141+
fun sendMessageStoresMessage() {
142+
val before = chatMessageRepository.count()
143+
val body = objectMapper.writeValueAsString(ChatMessageSendRequest("새 메세지"))
144+
145+
mockMvc.perform(
146+
post("/api/userchat/rooms/${room.id}/messages")
147+
.header("Authorization", "Bearer $guestToken")
148+
.contentType(MediaType.APPLICATION_JSON)
149+
.content(body),
150+
)
151+
.andExpect(status().isCreated)
152+
.andExpect(jsonPath("$.data.content").value("새 메세지"))
153+
154+
val messages = chatMessageRepository.findAll()
155+
assertEquals(before + 1, messages.size.toLong())
156+
val latest = messages.maxByOrNull { it.id ?: 0L }!!
157+
assertEquals(latest.id, chatRoomRepository.findById(room.id!!).get().lastMessageId)
158+
}
159+
}

0 commit comments

Comments
 (0)