@@ -8,6 +8,8 @@ import com.formbricks.android.helper.FormbricksConfig
88import com.formbricks.android.logger.Logger
99import com.formbricks.android.manager.SurveyManager
1010import com.formbricks.android.manager.UserManager
11+ import com.formbricks.android.model.user.AttributeValue
12+ import com.formbricks.android.network.queue.UpdateQueue
1113import org.junit.Assert.assertEquals
1214import org.junit.Assert.assertFalse
1315import org.junit.Assert.assertNotEquals
@@ -37,8 +39,12 @@ class FormbricksInstrumentedTest {
3739 fun setUp () {
3840 val appContext = InstrumentationRegistry .getInstrumentation().targetContext
3941 Formbricks .applicationContext = appContext
42+ Formbricks .isInitialized = false
43+ Formbricks .language = " default"
4044 UserManager .logout()
45+ UpdateQueue .reset()
4146 SurveyManager .environmentDataHolder = null
47+ SurveyManager .filteredSurveys.clear()
4248 FormbricksApi .service = MockFormbricksApiService ()
4349 }
4450
@@ -57,7 +63,7 @@ class FormbricksInstrumentedTest {
5763 // Use methods before init should have no effect
5864 Formbricks .setUserId(" userId" )
5965 Formbricks .setLanguage(" de" )
60- Formbricks .setAttributes(mapOf (" testA" to " testB" ))
66+ Formbricks .setAttributes(mapOf (" testA" to AttributeValue .string( " testB" ) ))
6167 Formbricks .setAttribute(" test" , " testKey" )
6268 assertNull(UserManager .userId)
6369 assertEquals(" default" , Formbricks .language)
@@ -73,7 +79,7 @@ class FormbricksInstrumentedTest {
7379 waitForSeconds(1 )
7480
7581 // Should be ignored, becuase we don't have user ID yet
76- Formbricks .setAttributes(mapOf (" testA" to " testB" ))
82+ Formbricks .setAttributes(mapOf (" testA" to AttributeValue .string( " testB" ) ))
7783 Formbricks .setAttribute(" test" , " testKey" )
7884 assertNull(UserManager .userId)
7985
@@ -174,6 +180,187 @@ class FormbricksInstrumentedTest {
174180 assertTrue(SurveyManager .isShowingSurvey)
175181 }
176182
183+ @Test
184+ fun testSetAttributesWithUserId () {
185+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
186+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
187+ waitForSeconds(1 )
188+
189+ // Set userId first, then set attributes - exercises UpdateQueue.setAttributes with a valid userId
190+ Formbricks .setUserId(userId)
191+ waitForSeconds(2 )
192+ assertEquals(userId, UserManager .userId)
193+
194+ Formbricks .setAttributes(mapOf (
195+ " plan" to AttributeValue .string(" premium" ),
196+ " score" to AttributeValue .number(99.5 )
197+ ))
198+ waitForSeconds(1 )
199+
200+ // User should still be synced
201+ assertEquals(userId, UserManager .userId)
202+ }
203+
204+ @Test
205+ fun testAddAttributeWithUserId () {
206+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
207+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
208+ waitForSeconds(1 )
209+
210+ // Set userId first, then add attributes - exercises UpdateQueue.addAttribute with a valid userId
211+ Formbricks .setUserId(userId)
212+ waitForSeconds(2 )
213+ assertEquals(userId, UserManager .userId)
214+
215+ Formbricks .setAttribute(" John" , " name" )
216+ Formbricks .setAttribute(42.0 , " age" )
217+ Formbricks .setAttribute(99 , " level" )
218+ waitForSeconds(1 )
219+
220+ assertEquals(userId, UserManager .userId)
221+ }
222+
223+ @Test
224+ fun testSetLanguageWithUserId () {
225+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
226+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
227+ waitForSeconds(1 )
228+
229+ // Set userId first, then set language - exercises the if-branch in UpdateQueue.setLanguage
230+ Formbricks .setUserId(userId)
231+ waitForSeconds(2 )
232+ assertEquals(userId, UserManager .userId)
233+
234+ Formbricks .setLanguage(" de" )
235+ waitForSeconds(1 )
236+
237+ assertEquals(" de" , Formbricks .language)
238+ assertEquals(userId, UserManager .userId)
239+ }
240+
241+ @Test
242+ fun testSetUserIdSameValueIsNoOp () {
243+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
244+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
245+ waitForSeconds(1 )
246+
247+ Formbricks .setUserId(userId)
248+ waitForSeconds(2 )
249+ assertEquals(userId, UserManager .userId)
250+
251+ // Same userId again — should be a no-op
252+ Formbricks .setUserId(userId)
253+ assertEquals(userId, UserManager .userId)
254+ }
255+
256+ @Test
257+ fun testSetUserIdDifferentValueOverridesPrevious () {
258+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
259+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
260+ waitForSeconds(1 )
261+
262+ Formbricks .setUserId(userId)
263+ waitForSeconds(2 )
264+ assertEquals(userId, UserManager .userId)
265+ assertNotNull(UserManager .expiresAt)
266+
267+ // Different userId — should clean up previous state and re-sync
268+ // (Previously this would error and return without doing anything)
269+ val newUserId = " NEW-USER-ID-12345"
270+ Formbricks .setUserId(newUserId)
271+
272+ // Verify that logout was called: expiresAt should be cleared immediately
273+ assertNull(" expiresAt should be cleared by logout" , UserManager .expiresAt)
274+
275+ // After sync completes, the mock returns the hardcoded userId from User.json,
276+ // so we just verify the SDK is still functional (sync completed without errors)
277+ waitForSeconds(2 )
278+ assertNotNull(" userId should be set after re-sync" , UserManager .userId)
279+ }
280+
281+ @Test
282+ fun testLogoutWithoutUserIdDoesNotError () {
283+ // Mark SDK as initialized without triggering async operations
284+ Formbricks .isInitialized = true
285+
286+ // Logout without ever setting a userId — should not crash
287+ assertNull(UserManager .userId)
288+ Formbricks .logout()
289+ assertNull(UserManager .userId)
290+ }
291+
292+ @Test
293+ fun testSyncUserSetsLanguageFromResponse () {
294+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
295+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
296+ waitForSeconds(1 )
297+
298+ assertEquals(" default" , Formbricks .language)
299+
300+ // Override the mock to return a user response that includes a language
301+ val mockService = FormbricksApi .service as MockFormbricksApiService
302+ val originalUser = mockService.user
303+ mockService.user = originalUser.copy(
304+ data = originalUser.data.copy(
305+ state = originalUser.data.state.copy(
306+ data = originalUser.data.state.data.copy(
307+ language = " fr"
308+ )
309+ )
310+ )
311+ )
312+
313+ // setUserId triggers syncUser, which should pick up the language from the response
314+ Formbricks .setUserId(userId)
315+ waitForSeconds(2 )
316+
317+ assertEquals(userId, UserManager .userId)
318+ assertEquals(" fr" , Formbricks .language)
319+ }
320+
321+ @Test
322+ fun testSyncUserCatchBlockOnApiError () {
323+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
324+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
325+ waitForSeconds(1 )
326+
327+ // Enable error mode so postUser returns a failure, exercising the catch block
328+ (FormbricksApi .service as MockFormbricksApiService ).isErrorResponseNeeded = true
329+
330+ Formbricks .setUserId(userId)
331+ waitForSeconds(2 )
332+
333+ // The sync should have failed gracefully — userId should not be set from the response
334+ assertNull(UserManager .expiresAt)
335+ }
336+
337+ @Test
338+ fun testLogoutClearsAllUserState () {
339+ val appContext = InstrumentationRegistry .getInstrumentation().targetContext
340+ Formbricks .setup(appContext, FormbricksConfig .Builder (appUrl, environmentId).setLoggingEnabled(true ).build())
341+ waitForSeconds(1 )
342+
343+ // Set up a user so we have state to clear
344+ Formbricks .setUserId(userId)
345+ waitForSeconds(2 )
346+ assertEquals(userId, UserManager .userId)
347+ assertNotNull(UserManager .expiresAt)
348+ assertNotNull(UserManager .contactId)
349+
350+ // Set language to something other than default
351+ Formbricks .setLanguage(" de" )
352+ assertEquals(" de" , Formbricks .language)
353+
354+ // Logout should clear all user state
355+ UserManager .logout()
356+
357+ assertNull(UserManager .userId)
358+ assertNull(UserManager .contactId)
359+ assertNull(UserManager .expiresAt)
360+ assertNull(UserManager .lastDisplayedAt)
361+ assertEquals(" default" , Formbricks .language)
362+ }
363+
177364 private fun waitForSeconds (seconds : Long ) {
178365 val latch = CountDownLatch (1 )
179366 latch.await(seconds, TimeUnit .SECONDS )
0 commit comments