@@ -8,14 +8,22 @@ package com.nextcloud.talk.users
88
99import com.nextcloud.talk.data.user.UsersRepository
1010import com.nextcloud.talk.data.user.model.User
11+ import com.nextcloud.talk.models.ExternalSignalingServer
12+ import kotlinx.coroutines.flow.emptyFlow
1113import kotlinx.coroutines.test.runTest
1214import org.junit.Assert.assertEquals
1315import org.junit.Assert.assertFalse
16+ import org.junit.Assert.assertNull
1417import org.junit.Assert.assertTrue
18+ import org.junit.Assert.fail
1519import org.junit.Before
1620import org.junit.Test
21+ import org.mockito.kotlin.any
22+ import org.mockito.kotlin.check
1723import org.mockito.kotlin.mock
24+ import org.mockito.kotlin.never
1825import org.mockito.kotlin.verify
26+ import org.mockito.kotlin.verifyBlocking
1927import org.mockito.kotlin.wheneverBlocking
2028
2129@Suppress(" DEPRECATION" )
@@ -33,6 +41,9 @@ class UserManagerTest {
3341 // scheduleDuplicateAccountsForDeletion() falls back to the `current` flag / oldest row,
3442 // matching the behavior asserted by the tests below that don't care about this priority.
3543 wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(null )
44+ // activeUserSubject/activeUserStateFlow lazily collect this on init; an empty flow lets
45+ // that background collection finish without racing the synchronous updates asserted below.
46+ wheneverBlocking { usersRepository.getActiveUserFlow() }.thenReturn(emptyFlow())
3647 }
3748
3849 @Test
@@ -185,4 +196,277 @@ class UserManagerTest {
185196 assertEquals(1 , scheduledCount)
186197 assertTrue(duplicate.scheduledForDeletion)
187198 }
199+
200+ @Test
201+ fun `currentUser returns the active user without touching any fallback` () {
202+ val active = user(id = 1 , username = " userA" , baseUrl = " https://example.com" , current = true )
203+ wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(active)
204+
205+ val result = userManager.currentUser.blockingGet()
206+
207+ assertEquals(active, result)
208+ verifyBlocking(usersRepository, never()) { getUsersNotScheduledForDeletion() }
209+ }
210+
211+ @Test
212+ fun `currentUser falls back to any non-deleted user and sets it active when none is active` () {
213+ val fallback = user(id = 1 , username = " userA" , baseUrl = " https://example.com" )
214+ wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(listOf (fallback))
215+ wheneverBlocking { usersRepository.setUserAsActiveWithId(fallback.id!! ) }.thenReturn(true )
216+ // getActiveUser() is re-queried after setUserAsActiveWithId() succeeds, simulating the DB
217+ // now reporting the freshly-activated row.
218+ wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(null , fallback)
219+
220+ val result = userManager.currentUser.blockingGet()
221+
222+ assertEquals(fallback, result)
223+ verifyBlocking(usersRepository) { setUserAsActiveWithId(fallback.id!! ) }
224+ }
225+
226+ @Test
227+ fun `currentUser is empty when there is no active user and none to fall back to` () {
228+ wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(emptyList())
229+
230+ assertTrue(userManager.currentUser.isEmpty.blockingGet())
231+ }
232+
233+ @Test
234+ fun `deleteUserSuspend does nothing and returns 0 when the user does not exist` () =
235+ runTest {
236+ wheneverBlocking { usersRepository.getUserWithId(42L ) }.thenReturn(null )
237+
238+ val result = userManager.deleteUserSuspend(42L )
239+
240+ assertEquals(0 , result)
241+ verify(usersRepository, never()).deleteUser(any())
242+ }
243+
244+ @Test
245+ fun `deleteUserSuspend deletes the user when it exists` () =
246+ runTest {
247+ val existing = user(id = 42 , username = " userA" , baseUrl = " https://example.com" )
248+ wheneverBlocking { usersRepository.getUserWithId(42L ) }.thenReturn(existing)
249+ wheneverBlocking { usersRepository.deleteUser(existing) }.thenReturn(1 )
250+
251+ val result = userManager.deleteUserSuspend(42L )
252+
253+ assertEquals(1 , result)
254+ verify(usersRepository).deleteUser(existing)
255+ }
256+
257+ @Test
258+ fun `checkIfUserIsScheduledForDeletionSuspend reflects the matching user's flag` () =
259+ runTest {
260+ val scheduled = user(id = 1 , username = " userA" , baseUrl = " https://example.com" )
261+ .apply { scheduledForDeletion = true }
262+ wheneverBlocking {
263+ usersRepository.getUserWithUsernameAndServer(" userA" , " https://example.com" )
264+ }.thenReturn(scheduled)
265+
266+ assertTrue(userManager.checkIfUserIsScheduledForDeletionSuspend(" userA" , " https://example.com" ))
267+ }
268+
269+ @Test
270+ fun `checkIfUserIsScheduledForDeletionSuspend is false when the user does not exist` () =
271+ runTest {
272+ wheneverBlocking {
273+ usersRepository.getUserWithUsernameAndServer(" userA" , " https://example.com" )
274+ }.thenReturn(null )
275+
276+ assertFalse(userManager.checkIfUserIsScheduledForDeletionSuspend(" userA" , " https://example.com" ))
277+ }
278+
279+ @Test
280+ fun `checkIfUserExistsSuspend is true only when a matching user is found` () =
281+ runTest {
282+ wheneverBlocking {
283+ usersRepository.getUserWithUsernameAndServer(" userA" , " https://example.com" )
284+ }.thenReturn(user(id = 1 , username = " userA" , baseUrl = " https://example.com" ))
285+ wheneverBlocking {
286+ usersRepository.getUserWithUsernameAndServer(" userB" , " https://example.com" )
287+ }.thenReturn(null )
288+
289+ assertTrue(userManager.checkIfUserExistsSuspend(" userA" , " https://example.com" ))
290+ assertFalse(userManager.checkIfUserExistsSuspend(" userB" , " https://example.com" ))
291+ }
292+
293+ @Test
294+ fun `scheduleUserForDeletionWithIdSuspend returns false when the user does not exist` () =
295+ runTest {
296+ wheneverBlocking { usersRepository.getUserWithId(99L ) }.thenReturn(null )
297+
298+ assertFalse(userManager.scheduleUserForDeletionWithIdSuspend(99L ))
299+ verify(usersRepository, never()).updateUser(any())
300+ }
301+
302+ @Test
303+ fun `scheduleUserForDeletionWithIdSuspend marks the user deleted and returns false with nobody left to activate` () =
304+ runTest {
305+ val target = user(id = 1 , username = " userA" , baseUrl = " https://example.com" , current = true )
306+ wheneverBlocking { usersRepository.getUserWithId(1L ) }.thenReturn(target)
307+ wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(emptyList())
308+
309+ val result = userManager.scheduleUserForDeletionWithIdSuspend(1L )
310+
311+ assertFalse(result)
312+ assertTrue(target.scheduledForDeletion)
313+ assertFalse(target.current)
314+ verify(usersRepository).updateUser(target)
315+ }
316+
317+ @Test
318+ fun `scheduleUserForDeletionWithIdSuspend returns true and activates another user when one remains` () =
319+ runTest {
320+ val target = user(id = 1 , username = " userA" , baseUrl = " https://example.com" , current = true )
321+ val other = user(id = 2 , username = " userB" , baseUrl = " https://example.com" )
322+ wheneverBlocking { usersRepository.getUserWithId(1L ) }.thenReturn(target)
323+ wheneverBlocking { usersRepository.getUsersNotScheduledForDeletion() }.thenReturn(listOf (other))
324+ wheneverBlocking { usersRepository.setUserAsActiveWithId(other.id!! ) }.thenReturn(true )
325+ wheneverBlocking { usersRepository.getActiveUser() }.thenReturn(other)
326+
327+ val result = userManager.scheduleUserForDeletionWithIdSuspend(1L )
328+
329+ assertTrue(result)
330+ assertTrue(target.scheduledForDeletion)
331+ verify(usersRepository).setUserAsActiveWithId(other.id!! )
332+ }
333+
334+ @Test
335+ fun `updateExternalSignalingServerSuspend throws when the user does not exist` () =
336+ runTest {
337+ wheneverBlocking { usersRepository.getUserWithId(7L ) }.thenReturn(null )
338+
339+ try {
340+ userManager.updateExternalSignalingServerSuspend(7L , ExternalSignalingServer ())
341+ fail(" Expected NoSuchElementException" )
342+ } catch (expected: NoSuchElementException ) {
343+ // expected
344+ }
345+ }
346+
347+ @Test
348+ fun `updateExternalSignalingServerSuspend updates the matching user` () =
349+ runTest {
350+ val existing = user(id = 7 , username = " userA" , baseUrl = " https://example.com" )
351+ val server = ExternalSignalingServer (externalSignalingServer = " https://signaling.example.com" )
352+ wheneverBlocking { usersRepository.getUserWithId(7L ) }.thenReturn(existing)
353+ wheneverBlocking { usersRepository.updateUser(existing) }.thenReturn(1 )
354+
355+ val result = userManager.updateExternalSignalingServerSuspend(7L , server)
356+
357+ assertEquals(1 , result)
358+ assertEquals(server, existing.externalSignalingServer)
359+ }
360+
361+ @Test
362+ fun `updateOrCreateUserSuspend inserts a user without an id` () =
363+ runTest {
364+ val newUser = User (id = null , username = " userA" , baseUrl = " https://example.com" )
365+ wheneverBlocking { usersRepository.insertUser(newUser) }.thenReturn(5L )
366+
367+ val result = userManager.updateOrCreateUserSuspend(newUser)
368+
369+ assertEquals(5 , result)
370+ verify(usersRepository, never()).updateUser(any())
371+ }
372+
373+ @Test
374+ fun `updateOrCreateUserSuspend updates a user that already has an id` () =
375+ runTest {
376+ val existing = user(id = 3 , username = " userA" , baseUrl = " https://example.com" )
377+ wheneverBlocking { usersRepository.updateUser(existing) }.thenReturn(1 )
378+
379+ val result = userManager.updateOrCreateUserSuspend(existing)
380+
381+ assertEquals(1 , result)
382+ verify(usersRepository, never()).insertUser(any())
383+ }
384+
385+ @Test
386+ fun `setUserAsActiveSuspend publishes the new user on currentUserFlow only when it succeeds` () =
387+ runTest {
388+ val target = user(id = 1 , username = " userA" , baseUrl = " https://example.com" )
389+ wheneverBlocking { usersRepository.setUserAsActiveWithId(1L ) }.thenReturn(true )
390+
391+ val result = userManager.setUserAsActiveSuspend(target)
392+
393+ assertTrue(result)
394+ assertEquals(target, userManager.currentUserFlow.value)
395+ }
396+
397+ @Test
398+ fun `setUserAsActiveSuspend leaves currentUserFlow untouched when it fails` () =
399+ runTest {
400+ val target = user(id = 1 , username = " userA" , baseUrl = " https://example.com" )
401+ wheneverBlocking { usersRepository.setUserAsActiveWithId(1L ) }.thenReturn(false )
402+
403+ val result = userManager.setUserAsActiveSuspend(target)
404+
405+ assertFalse(result)
406+ assertNull(userManager.currentUserFlow.value)
407+ }
408+
409+ @Test
410+ fun `storeProfileSuspend creates a new user when the attributes carry no id` () =
411+ runTest {
412+ val attributes = UserManager .UserAttributes (
413+ id = null ,
414+ serverUrl = " https://example.com" ,
415+ currentUser = true ,
416+ userId = " userId" ,
417+ token = " token" ,
418+ displayName = " Display Name" ,
419+ pushConfigurationState = null ,
420+ // createUser() guards these with TextUtils.isEmpty(), which this project's unit
421+ // tests stub to always return false (testOptions.unitTests.isReturnDefaultValues),
422+ // so a null value here would still hit LoganSquare.parse(null, ...) and NPE.
423+ capabilities = " {}" ,
424+ serverVersion = " {}" ,
425+ certificateAlias = null ,
426+ externalSignalingServer = " {}"
427+ )
428+ val stored = user(id = 10 , username = " userA" , baseUrl = " https://example.com" )
429+ wheneverBlocking { usersRepository.insertUser(any()) }.thenReturn(10L )
430+ wheneverBlocking { usersRepository.getUserWithId(10L ) }.thenReturn(stored)
431+
432+ val result = userManager.storeProfileSuspend(" userA" , attributes)
433+
434+ assertEquals(stored, result)
435+ verify(usersRepository).insertUser(
436+ check {
437+ assertEquals(" userA" , it.username)
438+ assertEquals(" https://example.com" , it.baseUrl)
439+ assertEquals(" token" , it.token)
440+ assertEquals(" Display Name" , it.displayName)
441+ }
442+ )
443+ }
444+
445+ @Test
446+ fun `storeProfileSuspend updates the existing user resolved from the attributes' id` () =
447+ runTest {
448+ val existing = user(id = 10 , username = " userA" , baseUrl = " https://old.example.com" )
449+ val attributes = UserManager .UserAttributes (
450+ id = 10 ,
451+ serverUrl = " https://new.example.com" ,
452+ currentUser = true ,
453+ userId = " userId" ,
454+ token = " newToken" ,
455+ displayName = " New Display Name" ,
456+ pushConfigurationState = null ,
457+ capabilities = null ,
458+ serverVersion = null ,
459+ certificateAlias = null ,
460+ externalSignalingServer = null
461+ )
462+ wheneverBlocking { usersRepository.getUserWithId(10L ) }.thenReturn(existing)
463+ wheneverBlocking { usersRepository.insertUser(existing) }.thenReturn(10L )
464+
465+ val result = userManager.storeProfileSuspend(" userA" , attributes)
466+
467+ assertEquals(" https://new.example.com" , existing.baseUrl)
468+ assertEquals(" newToken" , existing.token)
469+ assertEquals(" New Display Name" , existing.displayName)
470+ assertEquals(existing, result)
471+ }
188472}
0 commit comments