Skip to content

Commit 4bb299b

Browse files
committed
test(users): add coverage for UserManager's coroutine-native methods
scheduleDuplicateAccountsForDeletionSuspend() was the only method with tests; currentUser, deleteUserSuspend, scheduleUserForDeletionWithIdSuspend, setUserAsActiveSuspend, storeProfileSuspend and other suspend methods introduced by the coroutines migration had none. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
1 parent 7c35ab2 commit 4bb299b

1 file changed

Lines changed: 284 additions & 0 deletions

File tree

app/src/test/java/com/nextcloud/talk/users/UserManagerTest.kt

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ package com.nextcloud.talk.users
88

99
import com.nextcloud.talk.data.user.UsersRepository
1010
import com.nextcloud.talk.data.user.model.User
11+
import com.nextcloud.talk.models.ExternalSignalingServer
12+
import kotlinx.coroutines.flow.emptyFlow
1113
import kotlinx.coroutines.test.runTest
1214
import org.junit.Assert.assertEquals
1315
import org.junit.Assert.assertFalse
16+
import org.junit.Assert.assertNull
1417
import org.junit.Assert.assertTrue
18+
import org.junit.Assert.fail
1519
import org.junit.Before
1620
import org.junit.Test
21+
import org.mockito.kotlin.any
22+
import org.mockito.kotlin.check
1723
import org.mockito.kotlin.mock
24+
import org.mockito.kotlin.never
1825
import org.mockito.kotlin.verify
26+
import org.mockito.kotlin.verifyBlocking
1927
import 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

Comments
 (0)