Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/consumer-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
-keep class com.formbricks.android.Formbricks { *; }
-keep class com.formbricks.android.helper.FormbricksConfig { *; }
-keep class com.formbricks.android.model.error.SDKError { *; }
-keep class com.formbricks.android.model.user.AttributeValue { *; }
-keep class com.formbricks.android.model.user.AttributeValue$* { *; }
-keep interface com.formbricks.android.FormbricksCallback { *; }
4 changes: 3 additions & 1 deletion android/src/androidTest/assets/User.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
"userId": "6CCCE716-6783-4D0F-8344-9C7DFA43D8F7"
},
"expiresAt": "2035-03-06T10:59:32.359Z"
}
},
"messages": ["User synced successfully"],
"errors": ["Unknown attribute key: invalidKey"]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import com.formbricks.android.helper.FormbricksConfig
import com.formbricks.android.logger.Logger
import com.formbricks.android.manager.SurveyManager
import com.formbricks.android.manager.UserManager
import com.formbricks.android.model.user.AttributeValue
import com.formbricks.android.network.queue.UpdateQueue
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
Expand Down Expand Up @@ -37,8 +39,12 @@ class FormbricksInstrumentedTest {
fun setUp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.applicationContext = appContext
Formbricks.isInitialized = false
Formbricks.language = "default"
UserManager.logout()
UpdateQueue.reset()
SurveyManager.environmentDataHolder = null
SurveyManager.filteredSurveys.clear()
FormbricksApi.service = MockFormbricksApiService()
}

Expand All @@ -57,7 +63,7 @@ class FormbricksInstrumentedTest {
// Use methods before init should have no effect
Formbricks.setUserId("userId")
Formbricks.setLanguage("de")
Formbricks.setAttributes(mapOf("testA" to "testB"))
Formbricks.setAttributes(mapOf("testA" to AttributeValue.string("testB")))
Formbricks.setAttribute("test", "testKey")
assertNull(UserManager.userId)
assertEquals("default", Formbricks.language)
Expand All @@ -73,7 +79,7 @@ class FormbricksInstrumentedTest {
waitForSeconds(1)

// Should be ignored, becuase we don't have user ID yet
Formbricks.setAttributes(mapOf("testA" to "testB"))
Formbricks.setAttributes(mapOf("testA" to AttributeValue.string("testB")))
Formbricks.setAttribute("test", "testKey")
assertNull(UserManager.userId)

Expand Down Expand Up @@ -174,6 +180,187 @@ class FormbricksInstrumentedTest {
assertTrue(SurveyManager.isShowingSurvey)
}

@Test
fun testSetAttributesWithUserId() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

// Set userId first, then set attributes - exercises UpdateQueue.setAttributes with a valid userId
Formbricks.setUserId(userId)
waitForSeconds(2)
assertEquals(userId, UserManager.userId)

Formbricks.setAttributes(mapOf(
"plan" to AttributeValue.string("premium"),
"score" to AttributeValue.number(99.5)
))
waitForSeconds(1)

// User should still be synced
assertEquals(userId, UserManager.userId)
}

@Test
fun testAddAttributeWithUserId() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

// Set userId first, then add attributes - exercises UpdateQueue.addAttribute with a valid userId
Formbricks.setUserId(userId)
waitForSeconds(2)
assertEquals(userId, UserManager.userId)

Formbricks.setAttribute("John", "name")
Formbricks.setAttribute(42.0, "age")
Formbricks.setAttribute(99, "level")
waitForSeconds(1)

assertEquals(userId, UserManager.userId)
}

@Test
fun testSetLanguageWithUserId() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

// Set userId first, then set language - exercises the if-branch in UpdateQueue.setLanguage
Formbricks.setUserId(userId)
waitForSeconds(2)
assertEquals(userId, UserManager.userId)

Formbricks.setLanguage("de")
waitForSeconds(1)

assertEquals("de", Formbricks.language)
assertEquals(userId, UserManager.userId)
}

@Test
fun testSetUserIdSameValueIsNoOp() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

Formbricks.setUserId(userId)
waitForSeconds(2)
assertEquals(userId, UserManager.userId)

// Same userId again — should be a no-op
Formbricks.setUserId(userId)
assertEquals(userId, UserManager.userId)
}

@Test
fun testSetUserIdDifferentValueOverridesPrevious() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

Formbricks.setUserId(userId)
waitForSeconds(2)
assertEquals(userId, UserManager.userId)
assertNotNull(UserManager.expiresAt)

// Different userId — should clean up previous state and re-sync
// (Previously this would error and return without doing anything)
val newUserId = "NEW-USER-ID-12345"
Formbricks.setUserId(newUserId)

// Verify that logout was called: expiresAt should be cleared immediately
assertNull("expiresAt should be cleared by logout", UserManager.expiresAt)

// After sync completes, the mock returns the hardcoded userId from User.json,
// so we just verify the SDK is still functional (sync completed without errors)
waitForSeconds(2)
assertNotNull("userId should be set after re-sync", UserManager.userId)
}

@Test
fun testLogoutWithoutUserIdDoesNotError() {
// Mark SDK as initialized without triggering async operations
Formbricks.isInitialized = true

// Logout without ever setting a userId — should not crash
assertNull(UserManager.userId)
Formbricks.logout()
assertNull(UserManager.userId)
}

@Test
fun testSyncUserSetsLanguageFromResponse() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

assertEquals("default", Formbricks.language)

// Override the mock to return a user response that includes a language
val mockService = FormbricksApi.service as MockFormbricksApiService
val originalUser = mockService.user
mockService.user = originalUser.copy(
data = originalUser.data.copy(
state = originalUser.data.state.copy(
data = originalUser.data.state.data.copy(
language = "fr"
)
)
)
)

// setUserId triggers syncUser, which should pick up the language from the response
Formbricks.setUserId(userId)
waitForSeconds(2)

assertEquals(userId, UserManager.userId)
assertEquals("fr", Formbricks.language)
}

@Test
fun testSyncUserCatchBlockOnApiError() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

// Enable error mode so postUser returns a failure, exercising the catch block
(FormbricksApi.service as MockFormbricksApiService).isErrorResponseNeeded = true

Formbricks.setUserId(userId)
waitForSeconds(2)

// The sync should have failed gracefully — userId should not be set from the response
assertNull(UserManager.expiresAt)
}

@Test
fun testLogoutClearsAllUserState() {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Formbricks.setup(appContext, FormbricksConfig.Builder(appUrl, environmentId).setLoggingEnabled(true).build())
waitForSeconds(1)

// Set up a user so we have state to clear
Formbricks.setUserId(userId)
waitForSeconds(2)
assertEquals(userId, UserManager.userId)
assertNotNull(UserManager.expiresAt)
assertNotNull(UserManager.contactId)

// Set language to something other than default
Formbricks.setLanguage("de")
assertEquals("de", Formbricks.language)

// Logout should clear all user state
UserManager.logout()

assertNull(UserManager.userId)
assertNull(UserManager.contactId)
assertNull(UserManager.expiresAt)
assertNull(UserManager.lastDisplayedAt)
assertEquals("default", Formbricks.language)
}

private fun waitForSeconds(seconds: Long) {
val latch = CountDownLatch(1)
latch.await(seconds, TimeUnit.SECONDS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import com.formbricks.android.model.error.SDKError
class MockFormbricksApiService: FormbricksApiService() {
private val gson = Gson()
private val environment: EnvironmentResponse
private val user: UserResponse
internal var user: UserResponse
var isErrorResponseNeeded = false

init {
Expand Down
Loading
Loading