Skip to content

Commit f7c5c2f

Browse files
authored
fix: Convert user attribute values to string format (#97)
- Handle large double values from the user attributes and convert them to string - Use `BigDecimal` `toPlainString` to convert the type properly - Add unit tests
1 parent 72ae46c commit f7c5c2f

File tree

2 files changed

+215
-1
lines changed

2 files changed

+215
-1
lines changed

src/main/kotlin/com/mparticle/kits/RoktKit.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ class RoktKit :
217217
filterUser?.userAttributes?.let { userAttrs ->
218218
for ((key, value) in userAttrs) {
219219
if (value != null) {
220-
finalAttributes[key] = value.toString()
220+
finalAttributes[key] = convertValueToString(value)
221221
}
222222
}
223223
}
@@ -232,6 +232,16 @@ class RoktKit :
232232
return finalAttributes
233233
}
234234

235+
private fun convertValueToString(value: Any?): String {
236+
return when (value) {
237+
is Double -> BigDecimal.valueOf(value).toPlainString()
238+
is Long -> BigDecimal.valueOf(value).toPlainString()
239+
is Int -> BigDecimal.valueOf(value.toLong()).toPlainString()
240+
is Number -> BigDecimal(value.toString()).toPlainString()
241+
else -> value.toString()
242+
}
243+
}
244+
235245
private fun filterAttributes(attributes: Map<String, String>, kitConfiguration: KitConfiguration): MutableMap<String, String> {
236246
val userAttributes = mutableMapOf<String, String>()
237247
for ((key, value) in attributes) {

src/test/kotlin/com/mparticle/kits/RoktKitTests.kt

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import io.mockk.just
2424
import io.mockk.mockk
2525
import io.mockk.mockkObject
2626
import io.mockk.runs
27+
import io.mockk.slot
2728
import io.mockk.unmockkObject
2829
import io.mockk.verify
2930
import io.mockk.verifyOrder
@@ -122,6 +123,209 @@ class RoktKitTests {
122123
assertEquals("123", result["attr_non_string"])
123124
}
124125

126+
@Test
127+
fun test_prepareFinalAttributes_converts_large_double_without_scientific_notation() {
128+
// Arrange
129+
mockkObject(Rokt)
130+
val capturedAttributesSlot = slot<Map<String, String>>()
131+
every {
132+
Rokt.execute(
133+
any<String>(),
134+
capture(capturedAttributesSlot),
135+
any<Rokt.RoktCallback>(),
136+
null,
137+
null,
138+
null,
139+
)
140+
} just runs
141+
142+
val mockFilterUser = mock(FilteredMParticleUser::class.java)
143+
Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap())
144+
Mockito.`when`(mockFilterUser.id).thenReturn(12345L)
145+
146+
val userAttributes = HashMap<String, Any?>()
147+
// Large Double value that would produce scientific notation with toString()
148+
userAttributes["large_double"] = 50352212112.0
149+
userAttributes["large_double_negative"] = -50352212112.0
150+
userAttributes["double_with_decimal"] = 123.456789
151+
userAttributes["double_zero"] = 0.0
152+
Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes)
153+
154+
roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject()))
155+
156+
// Act
157+
val inputAttributes: Map<String, String> = mapOf("initial_attr" to "initial_value")
158+
roktKit.execute(
159+
viewName = "test_view",
160+
attributes = inputAttributes,
161+
mpRoktEventCallback = null,
162+
placeHolders = null,
163+
fontTypefaces = null,
164+
filterUser = mockFilterUser,
165+
mpRoktConfig = null,
166+
)
167+
168+
// Assert
169+
val capturedAttributes = capturedAttributesSlot.captured
170+
assertEquals("50352212112", capturedAttributes["large_double"])
171+
172+
assertEquals("-50352212112", capturedAttributes["large_double_negative"])
173+
174+
assertTrue(capturedAttributes.containsKey("double_with_decimal"))
175+
assertEquals("123.456789", capturedAttributes["double_with_decimal"])
176+
177+
assertEquals("0.0", capturedAttributes["double_zero"])
178+
179+
// Verify initial attributes are preserved
180+
assertEquals("initial_value", capturedAttributes["initial_attr"])
181+
182+
unmockkObject(Rokt)
183+
}
184+
185+
@Test
186+
fun test_prepareFinalAttributes_converts_long_values() {
187+
// Arrange
188+
mockkObject(Rokt)
189+
val capturedAttributesSlot = slot<Map<String, String>>()
190+
every {
191+
Rokt.execute(
192+
any<String>(),
193+
capture(capturedAttributesSlot),
194+
any<Rokt.RoktCallback>(),
195+
null,
196+
null,
197+
null,
198+
)
199+
} just runs
200+
201+
val mockFilterUser = mock(FilteredMParticleUser::class.java)
202+
Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap())
203+
Mockito.`when`(mockFilterUser.id).thenReturn(12345L)
204+
205+
val userAttributes = HashMap<String, Any?>()
206+
userAttributes["long_value"] = Long.MAX_VALUE // 9223372036854775807L
207+
userAttributes["long_negative"] = Long.MIN_VALUE // -9223372036854775808L
208+
userAttributes["long_zero"] = 0L
209+
Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes)
210+
211+
roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject()))
212+
213+
// Act
214+
roktKit.execute(
215+
viewName = "test_view",
216+
attributes = emptyMap(),
217+
mpRoktEventCallback = null,
218+
placeHolders = null,
219+
fontTypefaces = null,
220+
filterUser = mockFilterUser,
221+
mpRoktConfig = null,
222+
)
223+
224+
// Assert
225+
val capturedAttributes = capturedAttributesSlot.captured
226+
assertEquals("9223372036854775807", capturedAttributes["long_value"])
227+
assertEquals("-9223372036854775808", capturedAttributes["long_negative"])
228+
assertEquals("0", capturedAttributes["long_zero"])
229+
230+
unmockkObject(Rokt)
231+
}
232+
233+
@Test
234+
fun test_prepareFinalAttributes_converts_int_values() {
235+
// Arrange
236+
mockkObject(Rokt)
237+
val capturedAttributesSlot = slot<Map<String, String>>()
238+
every {
239+
Rokt.execute(
240+
any<String>(),
241+
capture(capturedAttributesSlot),
242+
any<Rokt.RoktCallback>(),
243+
null,
244+
null,
245+
null,
246+
)
247+
} just runs
248+
249+
val mockFilterUser = mock(FilteredMParticleUser::class.java)
250+
Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap())
251+
Mockito.`when`(mockFilterUser.id).thenReturn(12345L)
252+
253+
val userAttributes = HashMap<String, Any?>()
254+
userAttributes["int_value"] = Int.MAX_VALUE // 2147483647
255+
userAttributes["int_negative"] = Int.MIN_VALUE // -2147483648
256+
userAttributes["int_zero"] = 0
257+
Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes)
258+
259+
roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject()))
260+
261+
// Act
262+
roktKit.execute(
263+
viewName = "test_view",
264+
attributes = emptyMap(),
265+
mpRoktEventCallback = null,
266+
placeHolders = null,
267+
fontTypefaces = null,
268+
filterUser = mockFilterUser,
269+
mpRoktConfig = null,
270+
)
271+
272+
// Assert
273+
val capturedAttributes = capturedAttributesSlot.captured
274+
assertEquals("2147483647", capturedAttributes["int_value"])
275+
assertEquals("-2147483648", capturedAttributes["int_negative"])
276+
assertEquals("0", capturedAttributes["int_zero"])
277+
278+
unmockkObject(Rokt)
279+
}
280+
281+
@Test
282+
fun test_prepareFinalAttributes_preserves_string_values() {
283+
// Arrange
284+
mockkObject(Rokt)
285+
val capturedAttributesSlot = slot<Map<String, String>>()
286+
every {
287+
Rokt.execute(
288+
any<String>(),
289+
capture(capturedAttributesSlot),
290+
any<Rokt.RoktCallback>(),
291+
null,
292+
null,
293+
null,
294+
)
295+
} just runs
296+
297+
val mockFilterUser = mock(FilteredMParticleUser::class.java)
298+
Mockito.`when`(mockFilterUser.userIdentities).thenReturn(HashMap())
299+
Mockito.`when`(mockFilterUser.id).thenReturn(12345L)
300+
301+
val userAttributes = HashMap<String, Any?>()
302+
userAttributes["string_value"] = "test_string"
303+
userAttributes["string_with_numbers"] = "123abc456"
304+
userAttributes["empty_string"] = ""
305+
Mockito.`when`(mockFilterUser.userAttributes).thenReturn(userAttributes)
306+
307+
roktKit.configuration = MockKitConfiguration.createKitConfiguration(JSONObject().put("hs", JSONObject()))
308+
309+
// Act
310+
roktKit.execute(
311+
viewName = "test_view",
312+
attributes = emptyMap(),
313+
mpRoktEventCallback = null,
314+
placeHolders = null,
315+
fontTypefaces = null,
316+
filterUser = mockFilterUser,
317+
mpRoktConfig = null,
318+
)
319+
320+
// Assert
321+
val capturedAttributes = capturedAttributesSlot.captured
322+
assertEquals("test_string", capturedAttributes["string_value"])
323+
assertEquals("123abc456", capturedAttributes["string_with_numbers"])
324+
assertEquals("", capturedAttributes["empty_string"])
325+
326+
unmockkObject(Rokt)
327+
}
328+
125329
private inner class TestKitManager :
126330
KitManagerImpl(context, null, TestCoreCallbacks(), mock(MParticleOptions::class.java)) {
127331
var attributes = HashMap<String, String>()

0 commit comments

Comments
 (0)