1+ /*
2+ * Copyright 2024-2025 Embabel Software, Inc.
3+ *
4+ * Licensed under the Apache License, Version 2.0 (the "License");
5+ * you may not use this file except in compliance with the License.
6+ * You may obtain a copy of the License at
7+ *
8+ * http://www.apache.org/licenses/LICENSE-2.0
9+ *
10+ * Unless required by applicable law or agreed to in writing, software
11+ * distributed under the License is distributed on an "AS IS" BASIS,
12+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+ * See the License for the specific language governing permissions and
14+ * limitations under the License.
15+ */
16+ package com.embabel.guide.chat.repository
17+
18+ import com.embabel.guide.Neo4jPropertiesInitializer
19+ import com.embabel.guide.chat.service.ThreadService
20+ import com.embabel.guide.domain.GuideUserData
21+ import com.embabel.guide.domain.GuideUserRepository
22+ import com.embabel.guide.domain.WebUserData
23+ import com.embabel.guide.util.UUIDv7
24+ import org.junit.jupiter.api.Assertions.*
25+ import org.junit.jupiter.api.BeforeEach
26+ import org.junit.jupiter.api.Test
27+ import org.springframework.ai.mcp.client.common.autoconfigure.McpClientAutoConfiguration
28+ import org.springframework.beans.factory.annotation.Autowired
29+ import org.springframework.boot.autoconfigure.ImportAutoConfiguration
30+ import org.springframework.boot.test.context.SpringBootTest
31+ import org.springframework.test.context.ActiveProfiles
32+ import org.springframework.test.context.ContextConfiguration
33+ import org.springframework.transaction.annotation.Transactional
34+ import java.util.*
35+
36+ /* *
37+ * Test for ThreadRepositoryImpl using the type-safe DSL.
38+ */
39+ @SpringBootTest
40+ @ActiveProfiles(" test" )
41+ @ContextConfiguration(initializers = [Neo4jPropertiesInitializer ::class ])
42+ @ImportAutoConfiguration(exclude = [McpClientAutoConfiguration ::class ])
43+ @Transactional
44+ class ThreadRepositoryImplTest {
45+
46+ @Autowired
47+ private lateinit var threadRepository: ThreadRepositoryImpl
48+
49+ @Autowired
50+ private lateinit var guideUserRepository: GuideUserRepository
51+
52+ private lateinit var testUser: com.embabel.guide.domain.GuideUser
53+
54+ @BeforeEach
55+ fun setUp () {
56+ // Create a test user for thread authorship
57+ val guideUserData = GuideUserData (
58+ UUID .randomUUID().toString(),
59+ null ,
60+ null
61+ )
62+ val webUserData = WebUserData (
63+ " thread-test-${UUID .randomUUID()} " ,
64+ " Thread Test User" ,
65+ " threadtestuser-${UUID .randomUUID()} " ,
66+ " threadtest@example.com" ,
67+ " hashedpassword" ,
68+ null
69+ )
70+ testUser = guideUserRepository.createWithWebUser(guideUserData, webUserData)
71+ }
72+
73+ @Test
74+ fun `test create thread with message` () {
75+ // Given: A thread ID and message
76+ val threadId = UUIDv7 .generateString()
77+ val message = " Hello, this is a test message"
78+
79+ // When: We create a thread with the message
80+ val created = threadRepository.createWithMessage(
81+ threadId = threadId,
82+ userId = testUser.core.id,
83+ title = " Test Thread" ,
84+ message = message,
85+ role = ThreadService .ROLE_USER
86+ )
87+
88+ // Then: The thread is created with the correct data
89+ assertNotNull(created)
90+ assertEquals(threadId, created.thread.threadId)
91+ assertEquals(" Test Thread" , created.thread.title)
92+ assertNotNull(created.thread.createdAt)
93+
94+ // And: The thread has one turn with the message
95+ assertEquals(1 , created.turns.size)
96+ val turn = created.turns.first()
97+ assertEquals(ThreadService .ROLE_USER , turn.turn.role)
98+ assertEquals(message, turn.current.text)
99+ assertEquals(testUser.core.id, turn.authoredBy?.core?.id)
100+ }
101+
102+ @Test
103+ fun `test create thread with assistant message` () {
104+ // Given: A thread ID and assistant message
105+ val threadId = UUIDv7 .generateString()
106+ val message = " Welcome! How can I help you?"
107+
108+ // When: We create a thread with an assistant message
109+ val created = threadRepository.createWithMessage(
110+ threadId = threadId,
111+ userId = testUser.core.id,
112+ title = " Welcome" ,
113+ message = message,
114+ role = ThreadService .ROLE_ASSISTANT
115+ )
116+
117+ // Then: The turn has the assistant role
118+ assertEquals(1 , created.turns.size)
119+ assertEquals(ThreadService .ROLE_ASSISTANT , created.turns.first().turn.role)
120+ assertEquals(message, created.turns.first().current.text)
121+ }
122+
123+ @Test
124+ fun `test find thread by ID` () {
125+ // Given: We create a thread
126+ val threadId = UUIDv7 .generateString()
127+ threadRepository.createWithMessage(
128+ threadId = threadId,
129+ userId = testUser.core.id,
130+ title = " Findable Thread" ,
131+ message = " Test message" ,
132+ role = ThreadService .ROLE_USER
133+ )
134+
135+ // When: We find by thread ID
136+ val found = threadRepository.findByThreadId(threadId)
137+
138+ // Then: The thread is found
139+ assertTrue(found.isPresent)
140+ assertEquals(threadId, found.get().thread.threadId)
141+ assertEquals(" Findable Thread" , found.get().thread.title)
142+ assertEquals(1 , found.get().turns.size)
143+ }
144+
145+ @Test
146+ fun `test findByThreadId returns empty when not found` () {
147+ // When: We search for a non-existent thread
148+ val found = threadRepository.findByThreadId(" nonexistent-thread-id" )
149+
150+ // Then: An empty Optional is returned
151+ assertFalse(found.isPresent)
152+ }
153+
154+ @Test
155+ fun `test find threads by user ID` () {
156+ // Given: We create multiple threads for the test user
157+ val thread1Id = UUIDv7 .generateString()
158+ val thread2Id = UUIDv7 .generateString()
159+
160+ threadRepository.createWithMessage(
161+ threadId = thread1Id,
162+ userId = testUser.core.id,
163+ title = " User Thread 1" ,
164+ message = " First thread message" ,
165+ role = ThreadService .ROLE_USER
166+ )
167+
168+ threadRepository.createWithMessage(
169+ threadId = thread2Id,
170+ userId = testUser.core.id,
171+ title = " User Thread 2" ,
172+ message = " Second thread message" ,
173+ role = ThreadService .ROLE_USER
174+ )
175+
176+ // When: We find threads by user ID
177+ val threads = threadRepository.findByUserId(testUser.core.id)
178+
179+ // Then: Both threads are found
180+ assertTrue(threads.size >= 2 )
181+ assertTrue(threads.any { it.thread.threadId == thread1Id })
182+ assertTrue(threads.any { it.thread.threadId == thread2Id })
183+ }
184+
185+ @Test
186+ fun `test findByUserId returns empty list when user has no threads` () {
187+ // Given: A user with no threads
188+ val anotherUser = guideUserRepository.createWithWebUser(
189+ GuideUserData (UUID .randomUUID().toString(), null , null ),
190+ WebUserData (
191+ " no-threads-${UUID .randomUUID()} " ,
192+ " No Threads User" ,
193+ " nothreadsuser-${UUID .randomUUID()} " ,
194+ " nothreads@example.com" ,
195+ " hash" ,
196+ null
197+ )
198+ )
199+
200+ // When: We find threads for this user
201+ val threads = threadRepository.findByUserId(anotherUser.core.id)
202+
203+ // Then: Empty list is returned
204+ assertTrue(threads.isEmpty())
205+ }
206+
207+ @Test
208+ fun `test thread has correct timestamps` () {
209+ // Given: We create a thread
210+ val threadId = UUIDv7 .generateString()
211+ val beforeCreate = java.time.Instant .now()
212+
213+ val created = threadRepository.createWithMessage(
214+ threadId = threadId,
215+ userId = testUser.core.id,
216+ title = null ,
217+ message = " Timestamp test" ,
218+ role = ThreadService .ROLE_USER
219+ )
220+
221+ val afterCreate = java.time.Instant .now()
222+
223+ // Then: All timestamps are within the expected range
224+ assertNotNull(created.thread.createdAt)
225+ assertTrue(created.thread.createdAt!! >= beforeCreate.minusMillis(100 ))
226+ assertTrue(created.thread.createdAt!! <= afterCreate.plusMillis(100 ))
227+
228+ val turn = created.turns.first()
229+ assertNotNull(turn.turn.createdAt)
230+ assertNotNull(turn.current.createdAt)
231+ }
232+
233+ @Test
234+ fun `test thread without title` () {
235+ // Given: We create a thread without a title
236+ val threadId = UUIDv7 .generateString()
237+
238+ // When: We create the thread with null title
239+ val created = threadRepository.createWithMessage(
240+ threadId = threadId,
241+ userId = testUser.core.id,
242+ title = null ,
243+ message = " No title thread" ,
244+ role = ThreadService .ROLE_USER
245+ )
246+
247+ // Then: The thread is created with null title
248+ assertNull(created.thread.title)
249+ }
250+
251+ @Test
252+ fun `test deleteAll removes all threads` () {
253+ // Given: We create a thread
254+ val threadId = UUIDv7 .generateString()
255+ threadRepository.createWithMessage(
256+ threadId = threadId,
257+ userId = testUser.core.id,
258+ title = " Delete Test" ,
259+ message = " Will be deleted" ,
260+ role = ThreadService .ROLE_USER
261+ )
262+
263+ // When: We delete all threads
264+ threadRepository.deleteAll()
265+
266+ // Then: The thread is no longer found
267+ val found = threadRepository.findByThreadId(threadId)
268+ assertFalse(found.isPresent)
269+ }
270+
271+ @Test
272+ fun `test turn version has correct editor role` () {
273+ // Given: We create threads with different roles
274+ val userThreadId = UUIDv7 .generateString()
275+ val assistantThreadId = UUIDv7 .generateString()
276+
277+ val userThread = threadRepository.createWithMessage(
278+ threadId = userThreadId,
279+ userId = testUser.core.id,
280+ title = null ,
281+ message = " User message" ,
282+ role = ThreadService .ROLE_USER
283+ )
284+
285+ val assistantThread = threadRepository.createWithMessage(
286+ threadId = assistantThreadId,
287+ userId = testUser.core.id,
288+ title = null ,
289+ message = " Assistant message" ,
290+ role = ThreadService .ROLE_ASSISTANT
291+ )
292+
293+ // Then: The editor role matches the turn role
294+ assertEquals(ThreadService .ROLE_USER , userThread.turns.first().current.editorRole)
295+ assertEquals(ThreadService .ROLE_ASSISTANT , assistantThread.turns.first().current.editorRole)
296+ }
297+
298+ @Test
299+ fun `test createWithMessage throws when user not found` () {
300+ // When/Then: Creating a thread with non-existent user throws
301+ assertThrows(IllegalArgumentException ::class .java) {
302+ threadRepository.createWithMessage(
303+ threadId = UUIDv7 .generateString(),
304+ userId = " nonexistent-user-id" ,
305+ title = null ,
306+ message = " Should fail" ,
307+ role = ThreadService .ROLE_USER
308+ )
309+ }
310+ }
311+ }
0 commit comments