Skip to content

Commit f0b6493

Browse files
committed
[ECO-5458] Added integration tests for object-subscriptions
- Fixed integration tests for LiveMapManager based on returned result
1 parent 9a0ec71 commit f0b6493

File tree

6 files changed

+247
-32
lines changed

6 files changed

+247
-32
lines changed

live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveCounterTest.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package io.ably.lib.objects.integration
33
import io.ably.lib.objects.type.counter.LiveCounter
44
import io.ably.lib.objects.type.map.LiveMap
55
import io.ably.lib.objects.assertWaiter
6+
import io.ably.lib.objects.integration.helpers.ObjectId
7+
import io.ably.lib.objects.integration.helpers.fixtures.createUserEngagementMatrixMap
68
import io.ably.lib.objects.integration.helpers.fixtures.createUserMapWithCountersObject
79
import io.ably.lib.objects.integration.setup.IntegrationTest
810
import kotlinx.coroutines.test.runTest
911
import org.junit.Test
1012
import kotlin.test.assertEquals
1113
import kotlin.test.assertNotNull
14+
import kotlin.test.assertTrue
1215

1316
class DefaultLiveCounterTest: IntegrationTest() {
1417
/**
@@ -202,4 +205,73 @@ class DefaultLiveCounterTest: IntegrationTest() {
202205
assertNotNull(finalCounterCheck, "Counter should still be accessible from root map")
203206
assertEquals(30.0, finalCounterCheck.value(), "Final counter value should be 30 when accessed from root map")
204207
}
208+
209+
@Test
210+
fun testLiveCounterChangesUsingSubscription() = runTest {
211+
val channelName = generateChannelName()
212+
val userEngagementMapId = restObjects.createUserEngagementMatrixMap(channelName)
213+
restObjects.setMapRef(channelName, "root", "userMatrix", userEngagementMapId)
214+
215+
val channel = getRealtimeChannel(channelName)
216+
val rootMap = channel.objects.root
217+
218+
val userEngagementMap = rootMap.get("userMatrix") as LiveMap
219+
assertEquals(4L, userEngagementMap.size(), "User engagement map should contain 4 top-level entries")
220+
221+
val totalReactions = userEngagementMap.get("totalReactions") as LiveCounter
222+
assertEquals(189.0, totalReactions.value(), "Total reactions counter should have initial value of 189")
223+
224+
// Subscribe to changes on the totalReactions counter
225+
val counterUpdates = mutableListOf<Double>()
226+
val totalReactionsSubscription = totalReactions.subscribe { update ->
227+
counterUpdates.add(update.update.amount)
228+
}
229+
230+
// Step 1: Increment the totalReactions counter by 10 (189 + 10 = 199)
231+
restObjects.incrementCounter(channelName, totalReactions.ObjectId, 10.0)
232+
233+
// Wait for the update to be received
234+
assertWaiter { counterUpdates.isNotEmpty() }
235+
236+
// Verify the increment update was received
237+
assertEquals(1, counterUpdates.size, "Should receive one update for increment")
238+
assertEquals(10.0, counterUpdates.first(), "Update should contain increment amount of 10")
239+
assertEquals(199.0, totalReactions.value(), "Counter should be incremented to 199")
240+
241+
// Step 2: Decrement the totalReactions counter by 5 (199 - 5 = 194)
242+
counterUpdates.clear()
243+
restObjects.decrementCounter(channelName, totalReactions.ObjectId, 5.0)
244+
245+
// Wait for the second update
246+
assertWaiter { counterUpdates.isNotEmpty() }
247+
248+
// Verify the decrement update was received
249+
assertEquals(1, counterUpdates.size, "Should receive one update for decrement")
250+
assertEquals(-5.0, counterUpdates.first(), "Update should contain decrement amount of -5")
251+
assertEquals(194.0, totalReactions.value(), "Counter should be decremented to 194")
252+
253+
// Step 3: Increment the totalReactions counter by 15 (194 + 15 = 209)
254+
counterUpdates.clear()
255+
restObjects.incrementCounter(channelName, totalReactions.ObjectId, 15.0)
256+
257+
// Wait for the third update
258+
assertWaiter { counterUpdates.isNotEmpty() }
259+
260+
// Verify the third increment update was received
261+
assertEquals(1, counterUpdates.size, "Should receive one update for third increment")
262+
assertEquals(15.0, counterUpdates.first(), "Update should contain increment amount of 15")
263+
assertEquals(209.0, totalReactions.value(), "Counter should be incremented to 209")
264+
265+
// Clean up subscription
266+
counterUpdates.clear()
267+
totalReactionsSubscription.unsubscribe()
268+
269+
// No updates should be received after unsubscribing
270+
restObjects.incrementCounter(channelName, totalReactions.ObjectId, 20.0)
271+
272+
// Wait for a moment to ensure no updates are received
273+
assertWaiter { totalReactions.value() == 229.0 }
274+
275+
assertTrue(counterUpdates.isEmpty(), "No updates should be received after unsubscribing")
276+
}
205277
}

live-objects/src/test/kotlin/io/ably/lib/objects/integration/DefaultLiveMapTest.kt

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import io.ably.lib.objects.*
44
import io.ably.lib.objects.ObjectData
55
import io.ably.lib.objects.ObjectValue
66
import io.ably.lib.objects.integration.helpers.fixtures.createUserMapObject
7+
import io.ably.lib.objects.integration.helpers.fixtures.createUserProfileMapObject
78
import io.ably.lib.objects.integration.setup.IntegrationTest
89
import io.ably.lib.objects.type.counter.LiveCounter
910
import io.ably.lib.objects.type.map.LiveMap
11+
import io.ably.lib.objects.type.map.LiveMapUpdate
1012
import kotlinx.coroutines.test.runTest
1113
import org.junit.Test
1214
import kotlin.test.assertEquals
1315
import kotlin.test.assertNotNull
1416
import kotlin.test.assertNull
17+
import kotlin.test.assertTrue
1518

1619
class DefaultLiveMapTest: IntegrationTest() {
1720
/**
@@ -212,4 +215,94 @@ class DefaultLiveMapTest: IntegrationTest() {
212215
val finalValues = testMap.values().toSet()
213216
assertEquals(setOf("Bob", false, "bob@example.com"), finalValues, "Final values should match expected set")
214217
}
218+
219+
@Test
220+
fun testLiveMapChangesUsingSubscription() = runTest {
221+
val channelName = generateChannelName()
222+
val userProfileObjectId = restObjects.createUserProfileMapObject(channelName)
223+
restObjects.setMapRef(channelName, "root", "userProfile", userProfileObjectId)
224+
225+
val channel = getRealtimeChannel(channelName)
226+
val rootMap = channel.objects.root
227+
228+
// Get the user profile map object from the root map
229+
val userProfile = rootMap.get("userProfile") as LiveMap
230+
assertNotNull(userProfile, "User profile should be synchronized")
231+
assertEquals(4L, userProfile.size(), "User profile should contain 4 entries")
232+
233+
// Verify initial values
234+
assertEquals("user123", userProfile.get("userId"), "Initial userId should be user123")
235+
assertEquals("John Doe", userProfile.get("name"), "Initial name should be John Doe")
236+
assertEquals("john@example.com", userProfile.get("email"), "Initial email should be john@example.com")
237+
assertEquals(true, userProfile.get("isActive"), "Initial isActive should be true")
238+
239+
// Subscribe to changes in the user profile map
240+
val userProfileUpdates = mutableListOf<LiveMapUpdate>()
241+
val userProfileSubscription = userProfile.subscribe { update -> userProfileUpdates.add(update) }
242+
243+
// Step 1: Update an existing field in the user profile map (change the name)
244+
restObjects.setMapValue(channelName, userProfileObjectId, "name", ObjectValue("Bob Smith"))
245+
246+
// Wait for the update to be received
247+
assertWaiter { userProfileUpdates.isNotEmpty() }
248+
249+
// Verify the update was received
250+
assertEquals(1, userProfileUpdates.size, "Should receive one update")
251+
val firstUpdateMap = userProfileUpdates.first().update
252+
assertEquals(1, firstUpdateMap.size, "Should have one key change")
253+
assertTrue(firstUpdateMap.containsKey("name"), "Update should contain name key")
254+
assertEquals(LiveMapUpdate.Change.UPDATED, firstUpdateMap["name"], "name should be marked as UPDATED")
255+
256+
// Verify the value was actually updated
257+
assertEquals("Bob Smith", userProfile.get("name"), "Name should be updated to Bob Smith")
258+
259+
// Step 2: Update another field in the user profile map (change the email)
260+
userProfileUpdates.clear()
261+
restObjects.setMapValue(channelName, userProfileObjectId, "email", ObjectValue("bob@example.com"))
262+
263+
// Wait for the second update
264+
assertWaiter { userProfileUpdates.isNotEmpty() }
265+
266+
// Verify the second update
267+
assertEquals(1, userProfileUpdates.size, "Should receive one update for the second change")
268+
val secondUpdateMap = userProfileUpdates.first().update
269+
assertEquals(1, secondUpdateMap.size, "Should have one key change")
270+
assertTrue(secondUpdateMap.containsKey("email"), "Update should contain email key")
271+
assertEquals(LiveMapUpdate.Change.UPDATED, secondUpdateMap["email"], "email should be marked as UPDATED")
272+
273+
// Verify the value was actually updated
274+
assertEquals("bob@example.com", userProfile.get("email"), "Email should be updated to bob@example.com")
275+
276+
// Step 3: Remove an existing field from the user profile map (remove isActive)
277+
userProfileUpdates.clear()
278+
restObjects.removeMapValue(channelName, userProfileObjectId, "isActive")
279+
280+
// Wait for the removal update
281+
assertWaiter { userProfileUpdates.isNotEmpty() }
282+
283+
// Verify the removal update
284+
assertEquals(1, userProfileUpdates.size, "Should receive one update for removal")
285+
val removalUpdateMap = userProfileUpdates.first().update
286+
assertEquals(1, removalUpdateMap.size, "Should have one key change")
287+
assertTrue(removalUpdateMap.containsKey("isActive"), "Update should contain isActive key")
288+
assertEquals(LiveMapUpdate.Change.REMOVED, removalUpdateMap["isActive"], "isActive should be marked as REMOVED")
289+
290+
// Verify final state of the user profile map
291+
assertEquals(3L, userProfile.size(), "User profile should have 3 entries after removing isActive")
292+
assertEquals("user123", userProfile.get("userId"), "userId should remain unchanged")
293+
assertEquals("Bob Smith", userProfile.get("name"), "name should remain updated")
294+
assertEquals("bob@example.com", userProfile.get("email"), "email should remain updated")
295+
assertNull(userProfile.get("isActive"), "isActive should be removed")
296+
297+
// Clean up subscription
298+
userProfileUpdates.clear()
299+
userProfileSubscription.unsubscribe()
300+
// No updates should be received after unsubscribing
301+
restObjects.setMapValue(channelName, userProfileObjectId, "country", ObjectValue("uk"))
302+
303+
// Wait for a moment to ensure no updates are received
304+
assertWaiter { userProfile.size() == 4L }
305+
306+
assertTrue(userProfileUpdates.isEmpty(), "No updates should be received after unsubscribing")
307+
}
215308
}

live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/RestObjects.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import io.ably.lib.objects.ObjectData
55
import io.ably.lib.objects.ObjectValue
66
import io.ably.lib.rest.AblyRest
77
import io.ably.lib.http.HttpUtils
8+
import io.ably.lib.objects.integration.helpers.fixtures.DataFixtures
89
import io.ably.lib.types.ClientOptions
910

1011
/**
@@ -37,8 +38,7 @@ internal class RestObjects(options: ClientOptions) {
3738
* Sets an object reference at the specified key in an existing map.
3839
*/
3940
internal fun setMapRef(channelName: String, mapObjectId: String, key: String, refMapObjectId: String) {
40-
val data = ObjectData(objectId = refMapObjectId)
41-
val mapCreateOp = PayloadBuilder.mapSetRestOp(mapObjectId, key, data)
41+
val mapCreateOp = PayloadBuilder.mapSetRestOp(mapObjectId, key, DataFixtures.mapRef(refMapObjectId))
4242
operationRequest(channelName, mapCreateOp)
4343
}
4444

live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/CounterFixtures.kt

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,7 @@ internal fun RestObjects.createUserMapWithCountersObject(channelName: String): S
4242
val loginStreakCounterObjectId = createCounter(channelName, 7.0)
4343

4444
// Create engagement metrics nested map with counters
45-
val engagementMetricsMapObjectId = createMap(
46-
channelName,
47-
data = mapOf(
48-
"totalShares" to DataFixtures.mapRef(createCounter(channelName, 34.0)),
49-
"totalBookmarks" to DataFixtures.mapRef(createCounter(channelName, 67.0)),
50-
"totalReactions" to DataFixtures.mapRef(createCounter(channelName, 189.0)),
51-
"dailyActiveStreak" to DataFixtures.mapRef(createCounter(channelName, 12.0))
52-
)
53-
)
45+
val engagementMetricsMapObjectId = createUserEngagementMatrixMap(channelName)
5446

5547
// Set up the main test map structure with references to all created counters
5648
setMapRef(channelName, testMapObjectId, "profileViews", profileViewsCounterObjectId)
@@ -63,3 +55,34 @@ internal fun RestObjects.createUserMapWithCountersObject(channelName: String): S
6355

6456
return testMapObjectId
6557
}
58+
59+
/**
60+
* Creates a user engagement matrix map object with counter references for testing.
61+
*
62+
* This method creates a simple engagement metrics map containing counter objects
63+
* that track various user engagement metrics. The map contains references to
64+
* counter objects representing different types of user interactions and activities.
65+
*
66+
* **Object Structure:**
67+
* ```
68+
* userEngagementMatrixMap (Map)
69+
* ├── "totalShares" → Counter(value=34)
70+
* ├── "totalBookmarks" → Counter(value=67)
71+
* ├── "totalReactions" → Counter(value=189)
72+
* └── "dailyActiveStreak" → Counter(value=12)
73+
* ```
74+
*
75+
* @param channelName The channel where the user engagement matrix map will be created
76+
* @return The object ID of the created user engagement matrix map
77+
*/
78+
internal fun RestObjects.createUserEngagementMatrixMap(channelName: String): String {
79+
return createMap(
80+
channelName,
81+
data = mapOf(
82+
"totalShares" to DataFixtures.mapRef(createCounter(channelName, 34.0)),
83+
"totalBookmarks" to DataFixtures.mapRef(createCounter(channelName, 67.0)),
84+
"totalReactions" to DataFixtures.mapRef(createCounter(channelName, 189.0)),
85+
"dailyActiveStreak" to DataFixtures.mapRef(createCounter(channelName, 12.0))
86+
)
87+
)
88+
}

live-objects/src/test/kotlin/io/ably/lib/objects/integration/helpers/fixtures/MapFixtures.kt

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -134,17 +134,9 @@ internal fun RestObjects.createUserMapObject(channelName: String): String {
134134
)
135135

136136
// Create a user profile map with mixed data types and references
137-
val userProfileMapObjectId = createMap(
138-
channelName,
139-
data = mapOf(
140-
"userId" to ObjectData(value = ObjectValue("user123")),
141-
"name" to ObjectData(value = ObjectValue("John Doe")),
142-
"email" to ObjectData(value = ObjectValue("john@example.com")),
143-
"isActive" to ObjectData(value = ObjectValue(true)),
144-
"metrics" to DataFixtures.mapRef(metricsMapObjectId),
145-
"preferences" to DataFixtures.mapRef(preferencesMapObjectId)
146-
)
147-
)
137+
val userProfileMapObjectId = createUserProfileMapObject(channelName)
138+
setMapRef(channelName, userProfileMapObjectId, "metrics", metricsMapObjectId)
139+
setMapRef(channelName, userProfileMapObjectId, "preferences", preferencesMapObjectId)
148140

149141
// Set up the main test map structure with references to all created objects
150142
setMapRef(channelName, testMapObjectId, "userProfile", userProfileMapObjectId)
@@ -155,3 +147,38 @@ internal fun RestObjects.createUserMapObject(channelName: String): String {
155147

156148
return testMapObjectId
157149
}
150+
151+
/**
152+
* Creates a user profile map object with basic user information for testing.
153+
*
154+
* This method creates a simple user profile map containing essential user data fields
155+
* that are commonly used in user management systems. The map contains primitive data types
156+
* representing basic user information.
157+
*
158+
* **Object Structure:**
159+
* ```
160+
* userProfileMap (Map)
161+
* ├── "userId" → "user123"
162+
* ├── "name" → "John Doe"
163+
* ├── "email" → "john@example.com"
164+
* └── "isActive" → true
165+
* ```
166+
*
167+
* This structure provides a foundation for testing map operations on user profile data,
168+
* including field updates, additions, and removals. The map contains a mix of string,
169+
* boolean, and numeric data types to test various primitive value handling.
170+
*
171+
* @param channelName The channel where the user profile map will be created
172+
* @return The object ID of the created user profile map
173+
*/
174+
internal fun RestObjects.createUserProfileMapObject(channelName: String): String {
175+
return createMap(
176+
channelName,
177+
data = mapOf(
178+
"userId" to ObjectData(value = ObjectValue("user123")),
179+
"name" to ObjectData(value = ObjectValue("John Doe")),
180+
"email" to ObjectData(value = ObjectValue("john@example.com")),
181+
"isActive" to ObjectData(value = ObjectValue(true)),
182+
)
183+
)
184+
}

live-objects/src/test/kotlin/io/ably/lib/objects/unit/type/livemap/LiveMapManagerTest.kt

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ class LiveMapManagerTest {
5656

5757
// Assert on update field - should show changes from old to new state
5858
val expectedUpdate = mapOf(
59-
"key1" to "updated", // key1 was updated from "oldValue" to "newValue1"
60-
"key2" to "updated" // key2 was added
59+
"key1" to LiveMapUpdate.Change.UPDATED, // key1 was updated from "oldValue" to "newValue1"
60+
"key2" to LiveMapUpdate.Change.UPDATED // key2 was added
6161
)
62-
assertEquals(expectedUpdate, update)
62+
assertEquals(expectedUpdate, update.update)
6363
}
6464

6565
@Test
@@ -90,8 +90,8 @@ class LiveMapManagerTest {
9090
assertEquals(0, liveMap.data.size) // RTLM6c - should be empty map
9191

9292
// Assert on update field - should show that key1 was removed
93-
val expectedUpdate = mapOf("key1" to "removed")
94-
assertEquals(expectedUpdate, update)
93+
val expectedUpdate = mapOf("key1" to LiveMapUpdate.Change.REMOVED)
94+
assertEquals(expectedUpdate, update.update)
9595
}
9696

9797
@Test
@@ -119,8 +119,8 @@ class LiveMapManagerTest {
119119
assertEquals(0, liveMap.data.size) // RTLM6c - should be empty map when map is null
120120

121121
// Assert on update field - should show that key1 was removed
122-
val expectedUpdate = mapOf("key1" to "removed")
123-
assertEquals(expectedUpdate, update)
122+
val expectedUpdate = mapOf("key1" to LiveMapUpdate.Change.REMOVED)
123+
assertEquals(expectedUpdate, update.update)
124124
}
125125

126126
@Test
@@ -178,10 +178,10 @@ class LiveMapManagerTest {
178178

179179
// Assert on update field - should show changes from create operation
180180
val expectedUpdate = mapOf(
181-
"key1" to "updated", // key1 was updated from "existingValue" to "stateValue"
182-
"key2" to "updated" // key2 was added from create operation
181+
"key1" to LiveMapUpdate.Change.UPDATED, // key1 was updated from "existingValue" to "stateValue"
182+
"key2" to LiveMapUpdate.Change.UPDATED // key2 was added from create operation
183183
)
184-
assertEquals(expectedUpdate, update)
184+
assertEquals(expectedUpdate, update.update)
185185
}
186186

187187

0 commit comments

Comments
 (0)