Skip to content

Commit 00f976b

Browse files
authored
fix: attribute data type updates (#47)
* removes native overlay * fixes test * attribute data type * revert https change * fixes errors * tests * test fixes * fixes test * tests
1 parent 78670f0 commit 00f976b

13 files changed

Lines changed: 434 additions & 47 deletions

File tree

android/consumer-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
-keep class com.formbricks.android.Formbricks { *; }
33
-keep class com.formbricks.android.helper.FormbricksConfig { *; }
44
-keep class com.formbricks.android.model.error.SDKError { *; }
5+
-keep class com.formbricks.android.model.user.AttributeValue { *; }
6+
-keep class com.formbricks.android.model.user.AttributeValue$* { *; }
57
-keep interface com.formbricks.android.FormbricksCallback { *; }

android/src/androidTest/assets/User.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"userId": "6CCCE716-6783-4D0F-8344-9C7DFA43D8F7"
1111
},
1212
"expiresAt": "2035-03-06T10:59:32.359Z"
13-
}
13+
},
14+
"messages": ["User synced successfully"],
15+
"errors": ["Unknown attribute key: invalidKey"]
1416
}
1517
}

android/src/androidTest/java/com/formbricks/android/FormbricksInstrumentedTest.kt

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import com.formbricks.android.helper.FormbricksConfig
88
import com.formbricks.android.logger.Logger
99
import com.formbricks.android.manager.SurveyManager
1010
import com.formbricks.android.manager.UserManager
11+
import com.formbricks.android.model.user.AttributeValue
12+
import com.formbricks.android.network.queue.UpdateQueue
1113
import org.junit.Assert.assertEquals
1214
import org.junit.Assert.assertFalse
1315
import 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)

android/src/androidTest/java/com/formbricks/android/MockFormbricksApiService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import com.formbricks.android.model.error.SDKError
1212
class MockFormbricksApiService: FormbricksApiService() {
1313
private val gson = Gson()
1414
private val environment: EnvironmentResponse
15-
private val user: UserResponse
15+
internal var user: UserResponse
1616
var isErrorResponseNeeded = false
1717

1818
init {

0 commit comments

Comments
 (0)