1
+ package com.segment.analytics.kotlin.core.utilities
2
+
3
+ import com.segment.analytics.kotlin.core.Configuration
4
+ import com.segment.analytics.kotlin.core.Settings
5
+ import com.segment.analytics.kotlin.core.Storage
6
+ import com.segment.analytics.kotlin.core.System
7
+ import com.segment.analytics.kotlin.core.TrackEvent
8
+ import com.segment.analytics.kotlin.core.UserInfo
9
+ import com.segment.analytics.kotlin.core.emptyJsonObject
10
+ import com.segment.analytics.kotlin.core.utils.testAnalytics
11
+ import kotlinx.coroutines.test.TestScope
12
+ import kotlinx.coroutines.test.UnconfinedTestDispatcher
13
+ import kotlinx.coroutines.test.runTest
14
+ import kotlinx.serialization.encodeToString
15
+ import kotlinx.serialization.json.Json
16
+ import kotlinx.serialization.json.JsonObject
17
+ import kotlinx.serialization.json.buildJsonObject
18
+ import kotlinx.serialization.json.jsonArray
19
+ import kotlinx.serialization.json.put
20
+ import org.junit.jupiter.api.Assertions.assertEquals
21
+ import org.junit.jupiter.api.Assertions.assertNotNull
22
+ import org.junit.jupiter.api.Assertions.assertNull
23
+ import org.junit.jupiter.api.Assertions.assertTrue
24
+ import org.junit.jupiter.api.BeforeEach
25
+ import org.junit.jupiter.api.Nested
26
+ import org.junit.jupiter.api.Test
27
+ import sovran.kotlin.Action
28
+ import sovran.kotlin.Store
29
+ import java.util.Date
30
+
31
+ internal class InMemoryStorageTest {
32
+
33
+ private val epochTimestamp = Date (0 ).toInstant().toString()
34
+
35
+ private val testDispatcher = UnconfinedTestDispatcher ()
36
+
37
+ private val testScope = TestScope (testDispatcher)
38
+
39
+ private lateinit var store: Store
40
+
41
+ private lateinit var storage: StorageImpl
42
+
43
+ @BeforeEach
44
+ fun setup () = runTest {
45
+ val config = Configuration (
46
+ writeKey = " 123" ,
47
+ application = " Test" ,
48
+ apiHost = " local" ,
49
+ )
50
+ val analytics = testAnalytics(config, testScope, testDispatcher)
51
+ store = analytics.store
52
+ storage = InMemoryStorageProvider .createStorage(analytics) as StorageImpl
53
+ storage.initialize()
54
+ }
55
+
56
+
57
+ @Test
58
+ fun `userInfo update calls write` () = runTest {
59
+ val action = object : Action <UserInfo > {
60
+ override fun reduce (state : UserInfo ): UserInfo {
61
+ return UserInfo (
62
+ anonymousId = " newAnonId" ,
63
+ userId = " newUserId" ,
64
+ traits = emptyJsonObject
65
+ )
66
+ }
67
+ }
68
+ store.dispatch(action, UserInfo ::class )
69
+ val userId = storage.read(Storage .Constants .UserId )
70
+ val anonId = storage.read(Storage .Constants .AnonymousId )
71
+ val traits = storage.read(Storage .Constants .Traits )
72
+
73
+ assertEquals(" newAnonId" , anonId)
74
+ assertEquals(" newUserId" , userId)
75
+ assertEquals(" {}" , traits)
76
+ }
77
+
78
+ @Test
79
+ fun `userInfo reset action removes userInfo` () = runTest {
80
+ store.dispatch(UserInfo .ResetAction (), UserInfo ::class )
81
+
82
+ val userId = storage.read(Storage .Constants .UserId )
83
+ val anonId = storage.read(Storage .Constants .AnonymousId )
84
+ val traits = storage.read(Storage .Constants .Traits )
85
+
86
+ assertNotNull(anonId)
87
+ assertEquals(null , userId)
88
+ assertEquals(null , traits)
89
+ }
90
+
91
+ @Test
92
+ fun `system update calls write for settings` () = runTest {
93
+ val action = object : Action <System > {
94
+ override fun reduce (state : System ): System {
95
+ return System (
96
+ configuration = state.configuration,
97
+ settings = Settings (
98
+ integrations = buildJsonObject {
99
+ put(
100
+ " Segment.io" ,
101
+ buildJsonObject {
102
+ put(
103
+ " apiKey" ,
104
+ " 1vNgUqwJeCHmqgI9S1sOm9UHCyfYqbaQ"
105
+ )
106
+ })
107
+ },
108
+ plan = emptyJsonObject,
109
+ edgeFunction = emptyJsonObject,
110
+ middlewareSettings = emptyJsonObject
111
+ ),
112
+ running = false ,
113
+ initializedPlugins = setOf (),
114
+ enabled = true
115
+ )
116
+ }
117
+ }
118
+ store.dispatch(action, System ::class )
119
+ val settings = storage.read(Storage .Constants .Settings ) ? : " "
120
+
121
+ assertEquals(
122
+ Settings (
123
+ integrations = buildJsonObject {
124
+ put(
125
+ " Segment.io" ,
126
+ buildJsonObject { put(" apiKey" , " 1vNgUqwJeCHmqgI9S1sOm9UHCyfYqbaQ" ) })
127
+ },
128
+ plan = emptyJsonObject,
129
+ edgeFunction = emptyJsonObject,
130
+ middlewareSettings = emptyJsonObject
131
+ ), Json .decodeFromString(Settings .serializer(), settings)
132
+ )
133
+ }
134
+
135
+ @Test
136
+ fun `system reset action removes system` () = runTest {
137
+ val action = object : Action <System > {
138
+ override fun reduce (state : System ): System {
139
+ return System (state.configuration, null , state.running, state.initializedPlugins, state.enabled)
140
+ }
141
+ }
142
+ store.dispatch(action, System ::class )
143
+
144
+ val settings = storage.read(Storage .Constants .Settings )
145
+
146
+ assertEquals(null , settings)
147
+ }
148
+
149
+ @Nested
150
+ inner class EventsStorage () {
151
+
152
+ @Test
153
+ fun `writing events writes to eventsFile` () = runTest {
154
+ val event = TrackEvent (
155
+ event = " clicked" ,
156
+ properties = buildJsonObject { put(" behaviour" , " good" ) })
157
+ .apply {
158
+ messageId = " qwerty-1234"
159
+ anonymousId = " anonId"
160
+ integrations = emptyJsonObject
161
+ context = emptyJsonObject
162
+ timestamp = epochTimestamp
163
+ }
164
+ val stringified: String = Json .encodeToString(event)
165
+ storage.write(Storage .Constants .Events , stringified)
166
+ storage.rollover()
167
+ val storagePath = storage.eventStream.read()[0 ]
168
+ val storageContents = (storage.eventStream as InMemoryEventStream ).readAsStream(storagePath)
169
+ assertNotNull(storageContents)
170
+ val jsonFormat = Json .decodeFromString(JsonObject .serializer(), storageContents!! .bufferedReader().use { it.readText() })
171
+ assertEquals(1 , jsonFormat[" batch" ]!! .jsonArray.size)
172
+ }
173
+
174
+ @Test
175
+ fun `cannot write more than 32kb as event` () = runTest {
176
+ val stringified: String = " A" .repeat(32002 )
177
+ val exception = try {
178
+ storage.write(
179
+ Storage .Constants .Events ,
180
+ stringified
181
+ )
182
+ null
183
+ }
184
+ catch (e : Exception ) {
185
+ e
186
+ }
187
+ assertNotNull(exception)
188
+ assertTrue(storage.eventStream.read().isEmpty())
189
+ }
190
+
191
+ @Test
192
+ fun `reading events returns a non-null file handle with correct events` () = runTest {
193
+ val event = TrackEvent (
194
+ event = " clicked" ,
195
+ properties = buildJsonObject { put(" behaviour" , " good" ) })
196
+ .apply {
197
+ messageId = " qwerty-1234"
198
+ anonymousId = " anonId"
199
+ integrations = emptyJsonObject
200
+ context = emptyJsonObject
201
+ timestamp = epochTimestamp
202
+ }
203
+ val stringified: String = Json .encodeToString(event)
204
+ storage.write(Storage .Constants .Events , stringified)
205
+
206
+ storage.rollover()
207
+ val fileUrl = storage.read(Storage .Constants .Events )
208
+ assertNotNull(fileUrl)
209
+ fileUrl!! .let {
210
+ val storageContents = (storage.eventStream as InMemoryEventStream ).readAsStream(it)
211
+ assertNotNull(storageContents)
212
+ val contentsStr = storageContents!! .bufferedReader().use { it.readText() }
213
+ val contentsJson: JsonObject = Json .decodeFromString(contentsStr)
214
+ assertEquals(3 , contentsJson.size) // batch, sentAt, writeKey
215
+ assertTrue(contentsJson.containsKey(" batch" ))
216
+ assertTrue(contentsJson.containsKey(" sentAt" ))
217
+ assertTrue(contentsJson.containsKey(" writeKey" ))
218
+ assertEquals(1 , contentsJson[" batch" ]?.jsonArray?.size)
219
+ val eventInFile = contentsJson[" batch" ]?.jsonArray?.get(0 )
220
+ val eventInFile2 = Json .decodeFromString(
221
+ TrackEvent .serializer(),
222
+ Json .encodeToString(eventInFile)
223
+ )
224
+ assertEquals(event, eventInFile2)
225
+ }
226
+ }
227
+
228
+ @Test
229
+ fun `reading events with empty storage return empty list` () {
230
+ val fileUrls = storage.read(Storage .Constants .Events )
231
+ assertTrue(fileUrls!! .isEmpty())
232
+ }
233
+
234
+ @Test
235
+ fun `can write and read multiple events` () = runTest {
236
+ val event1 = TrackEvent (
237
+ event = " clicked" ,
238
+ properties = buildJsonObject { put(" behaviour" , " good" ) })
239
+ .apply {
240
+ messageId = " qwerty-1234"
241
+ anonymousId = " anonId"
242
+ integrations = emptyJsonObject
243
+ context = emptyJsonObject
244
+ timestamp = epochTimestamp
245
+ }
246
+ val event2 = TrackEvent (
247
+ event = " clicked2" ,
248
+ properties = buildJsonObject { put(" behaviour" , " bad" ) })
249
+ .apply {
250
+ messageId = " qwerty-12345"
251
+ anonymousId = " anonId"
252
+ integrations = emptyJsonObject
253
+ context = emptyJsonObject
254
+ timestamp = epochTimestamp
255
+ }
256
+ val stringified1: String = Json .encodeToString(event1)
257
+ val stringified2: String = Json .encodeToString(event2)
258
+ storage.write(Storage .Constants .Events , stringified1)
259
+ storage.write(Storage .Constants .Events , stringified2)
260
+
261
+ storage.rollover()
262
+ val fileUrl = storage.read(Storage .Constants .Events )
263
+ assertNotNull(fileUrl)
264
+ fileUrl!! .let {
265
+ val storageContents = (storage.eventStream as InMemoryEventStream ).readAsStream(it)
266
+ assertNotNull(storageContents)
267
+ val contentsStr = storageContents!! .bufferedReader().use { it.readText() }
268
+ val contentsJson: JsonObject = Json .decodeFromString(contentsStr)
269
+ assertEquals(3 , contentsJson.size) // batch, sentAt, writeKey
270
+ assertTrue(contentsJson.containsKey(" batch" ))
271
+ assertTrue(contentsJson.containsKey(" sentAt" ))
272
+ assertTrue(contentsJson.containsKey(" writeKey" ))
273
+ assertEquals(2 , contentsJson[" batch" ]?.jsonArray?.size)
274
+ val eventInFile = contentsJson[" batch" ]?.jsonArray?.get(0 )
275
+ val eventInFile2 = Json .decodeFromString(
276
+ TrackEvent .serializer(),
277
+ Json .encodeToString(eventInFile)
278
+ )
279
+ assertEquals(event1, eventInFile2)
280
+
281
+ val event2InFile = contentsJson[" batch" ]?.jsonArray?.get(1 )
282
+ val event2InFile2 = Json .decodeFromString(
283
+ TrackEvent .serializer(),
284
+ Json .encodeToString(event2InFile)
285
+ )
286
+ assertEquals(event2, event2InFile2)
287
+ }
288
+ }
289
+
290
+ @Test
291
+ fun remove () = runTest {
292
+ val action = object : Action <UserInfo > {
293
+ override fun reduce (state : UserInfo ): UserInfo {
294
+ return UserInfo (
295
+ anonymousId = " newAnonId" ,
296
+ userId = " newUserId" ,
297
+ traits = emptyJsonObject
298
+ )
299
+ }
300
+ }
301
+ store.dispatch(action, UserInfo ::class )
302
+
303
+ val userId = storage.read(Storage .Constants .UserId )
304
+ assertEquals(" newUserId" , userId)
305
+
306
+ storage.remove(Storage .Constants .UserId )
307
+ assertNull(storage.read(Storage .Constants .UserId ))
308
+ assertTrue(storage.remove(Storage .Constants .Events ))
309
+ }
310
+ }
311
+
312
+ }
0 commit comments