@kafitra/react-native-live-tracking 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/android/build.gradle +71 -0
- package/android/gradle.properties +7 -0
- package/android/src/main/AndroidManifest.xml +40 -0
- package/android/src/main/java/com/livetracking/LiveTrackingModuleImpl.kt +728 -0
- package/android/src/main/java/com/livetracking/LiveTrackingPackage.kt +16 -0
- package/android/src/main/java/com/livetracking/location/LocationEngine.kt +93 -0
- package/android/src/main/java/com/livetracking/network/NetworkListener.kt +127 -0
- package/android/src/main/java/com/livetracking/optimizer/ActivityRecognitionHandler.kt +248 -0
- package/android/src/main/java/com/livetracking/optimizer/MotionSleepManager.kt +130 -0
- package/android/src/main/java/com/livetracking/permissions/PermissionHandler.kt +145 -0
- package/android/src/main/java/com/livetracking/queue/QueueEngine.kt +167 -0
- package/android/src/main/java/com/livetracking/queue/QueuedLocation.kt +16 -0
- package/android/src/main/java/com/livetracking/queue/TrackingDatabase.kt +239 -0
- package/android/src/main/java/com/livetracking/receiver/BootReceiver.kt +53 -0
- package/android/src/main/java/com/livetracking/service/TrackingForegroundService.kt +145 -0
- package/android/src/main/java/com/livetracking/sync/FirebaseSyncEngine.kt +277 -0
- package/android/src/main/java/com/livetracking/sync/LocationDataPoint.kt +31 -0
- package/android/src/main/java/com/livetracking/sync/SyncEngineController.kt +220 -0
- package/android/src/main/java/com/livetracking/sync/SyncTargetConfig.kt +20 -0
- package/android/src/main/java/com/livetracking/sync/TargetHandler.kt +601 -0
- package/android/src/newarch/java/com/livetracking/LiveTrackingModule.kt +64 -0
- package/android/src/oldarch/java/com/livetracking/LiveTrackingModule.kt +70 -0
- package/android/src/test/java/com/livetracking/BackoffCalculationTest.kt +216 -0
- package/android/src/test/java/com/livetracking/BatchAccumulatorTest.kt +391 -0
- package/android/src/test/java/com/livetracking/BootReceiverTest.kt +247 -0
- package/android/src/test/java/com/livetracking/FirebaseSyncEngineTest.kt +337 -0
- package/android/src/test/java/com/livetracking/LocationEngineTest.kt +202 -0
- package/android/src/test/java/com/livetracking/MotionSleepManagerTest.kt +420 -0
- package/android/src/test/java/com/livetracking/OfflineQueueTest.kt +462 -0
- package/android/src/test/java/com/livetracking/PermissionHandlerTest.kt +200 -0
- package/android/src/test/java/com/livetracking/QueueEngineTest.kt +335 -0
- package/android/src/test/java/com/livetracking/SyncEngineControllerTest.kt +855 -0
- package/ios/ActivityRecognitionHandler.swift +196 -0
- package/ios/BackgroundModeHelper.swift +132 -0
- package/ios/FirebaseSyncEngine.swift +276 -0
- package/ios/LiveTracking-Bridging-Header.h +2 -0
- package/ios/LiveTracking.m +37 -0
- package/ios/LiveTracking.swift +773 -0
- package/ios/LocationDataPoint.swift +56 -0
- package/ios/LocationEngine.swift +160 -0
- package/ios/MotionSleepManager.swift +151 -0
- package/ios/NetworkListener.swift +105 -0
- package/ios/OfflineQueueManager.swift +503 -0
- package/ios/PermissionHandler.swift +148 -0
- package/ios/QueueEngine.swift +249 -0
- package/ios/SyncEngineController.swift +396 -0
- package/ios/SyncTargetConfig.swift +36 -0
- package/ios/TargetHandler.swift +715 -0
- package/ios/Tests/ActivityRecognitionHandlerTests.swift +259 -0
- package/ios/Tests/FirebaseSyncEngineTests.swift +303 -0
- package/ios/Tests/LocationEngineTests.swift +244 -0
- package/ios/Tests/MotionSleepManagerTests.swift +355 -0
- package/ios/Tests/NetworkListenerTests.swift +188 -0
- package/ios/Tests/OfflineQueueFlushTests.swift +375 -0
- package/ios/Tests/PermissionHandlerTests.swift +238 -0
- package/ios/Tests/QueueEngineTests.swift +346 -0
- package/ios/TrackingCleanup.swift +93 -0
- package/ios/TrackingNotificationManager.swift +187 -0
- package/lib/commonjs/EventEmitter.js +113 -0
- package/lib/commonjs/EventEmitter.js.map +1 -0
- package/lib/commonjs/LiveTracking.js +134 -0
- package/lib/commonjs/LiveTracking.js.map +1 -0
- package/lib/commonjs/NativeLiveTracking.js +21 -0
- package/lib/commonjs/NativeLiveTracking.js.map +1 -0
- package/lib/commonjs/filters/distanceTimeFilter.js +63 -0
- package/lib/commonjs/filters/distanceTimeFilter.js.map +1 -0
- package/lib/commonjs/index.js +103 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/serialization/locationSerializer.js +51 -0
- package/lib/commonjs/serialization/locationSerializer.js.map +1 -0
- package/lib/commonjs/types.js +77 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/utils/distance.js +63 -0
- package/lib/commonjs/utils/distance.js.map +1 -0
- package/lib/commonjs/utils/retry.js +80 -0
- package/lib/commonjs/utils/retry.js.map +1 -0
- package/lib/commonjs/validation.js +463 -0
- package/lib/commonjs/validation.js.map +1 -0
- package/lib/module/EventEmitter.js +105 -0
- package/lib/module/EventEmitter.js.map +1 -0
- package/lib/module/LiveTracking.js +127 -0
- package/lib/module/LiveTracking.js.map +1 -0
- package/lib/module/NativeLiveTracking.js +16 -0
- package/lib/module/NativeLiveTracking.js.map +1 -0
- package/lib/module/filters/distanceTimeFilter.js +58 -0
- package/lib/module/filters/distanceTimeFilter.js.map +1 -0
- package/lib/module/index.js +32 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/serialization/locationSerializer.js +45 -0
- package/lib/module/serialization/locationSerializer.js.map +1 -0
- package/lib/module/types.js +94 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/utils/distance.js +56 -0
- package/lib/module/utils/distance.js.map +1 -0
- package/lib/module/utils/retry.js +72 -0
- package/lib/module/utils/retry.js.map +1 -0
- package/lib/module/validation.js +456 -0
- package/lib/module/validation.js.map +1 -0
- package/lib/typescript/EventEmitter.d.ts +65 -0
- package/lib/typescript/EventEmitter.d.ts.map +1 -0
- package/lib/typescript/LiveTracking.d.ts +23 -0
- package/lib/typescript/LiveTracking.d.ts.map +1 -0
- package/lib/typescript/NativeLiveTracking.d.ts +25 -0
- package/lib/typescript/NativeLiveTracking.d.ts.map +1 -0
- package/lib/typescript/filters/distanceTimeFilter.d.ts +44 -0
- package/lib/typescript/filters/distanceTimeFilter.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +21 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/serialization/locationSerializer.d.ts +39 -0
- package/lib/typescript/serialization/locationSerializer.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +217 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/utils/distance.d.ts +38 -0
- package/lib/typescript/utils/distance.d.ts.map +1 -0
- package/lib/typescript/utils/retry.d.ts +60 -0
- package/lib/typescript/utils/retry.d.ts.map +1 -0
- package/lib/typescript/validation.d.ts +26 -0
- package/lib/typescript/validation.d.ts.map +1 -0
- package/package.json +126 -0
- package/react-native-live-tracking.podspec +47 -0
- package/src/EventEmitter.ts +118 -0
- package/src/LiveTracking.ts +159 -0
- package/src/NativeLiveTracking.ts +29 -0
- package/src/filters/distanceTimeFilter.ts +75 -0
- package/src/index.ts +51 -0
- package/src/serialization/locationSerializer.ts +57 -0
- package/src/types.ts +252 -0
- package/src/utils/distance.ts +68 -0
- package/src/utils/retry.ts +75 -0
- package/src/validation.ts +552 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
package com.livetracking
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.content.SharedPreferences
|
|
6
|
+
import com.livetracking.receiver.BootReceiver
|
|
7
|
+
import com.livetracking.receiver.TrackingStateStore
|
|
8
|
+
import io.mockk.*
|
|
9
|
+
import org.junit.Assert.*
|
|
10
|
+
import org.junit.Before
|
|
11
|
+
import org.junit.Test
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Unit tests for BootReceiver and TrackingStateStore.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: TrackingStateStore uses SharedPreferences which requires Android Context.
|
|
17
|
+
* These tests verify:
|
|
18
|
+
* - TrackingStateStore constants and structure
|
|
19
|
+
* - BootReceiver intent action filtering logic
|
|
20
|
+
* - SharedPreferences interaction patterns (mocked)
|
|
21
|
+
*
|
|
22
|
+
* Full SharedPreferences persistence tests require instrumented tests (androidTest)
|
|
23
|
+
* or Robolectric for simulating Android framework behavior.
|
|
24
|
+
*/
|
|
25
|
+
class BootReceiverTest {
|
|
26
|
+
|
|
27
|
+
private lateinit var mockContext: Context
|
|
28
|
+
private lateinit var mockSharedPreferences: SharedPreferences
|
|
29
|
+
private lateinit var mockEditor: SharedPreferences.Editor
|
|
30
|
+
|
|
31
|
+
@Before
|
|
32
|
+
fun setup() {
|
|
33
|
+
mockContext = mockk(relaxed = true)
|
|
34
|
+
mockSharedPreferences = mockk(relaxed = true)
|
|
35
|
+
mockEditor = mockk(relaxed = true)
|
|
36
|
+
|
|
37
|
+
every {
|
|
38
|
+
mockContext.getSharedPreferences("live_tracking_prefs", Context.MODE_PRIVATE)
|
|
39
|
+
} returns mockSharedPreferences
|
|
40
|
+
|
|
41
|
+
every { mockSharedPreferences.edit() } returns mockEditor
|
|
42
|
+
every { mockEditor.putBoolean(any(), any()) } returns mockEditor
|
|
43
|
+
every { mockEditor.apply() } just Runs
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- TrackingStateStore Constants Tests ---
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
fun `TrackingStateStore uses correct preferences name`() {
|
|
50
|
+
// Verify the prefs name by checking the mock interaction
|
|
51
|
+
TrackingStateStore.saveTrackingActive(mockContext, true)
|
|
52
|
+
|
|
53
|
+
verify {
|
|
54
|
+
mockContext.getSharedPreferences("live_tracking_prefs", Context.MODE_PRIVATE)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
fun `TrackingStateStore uses correct key for tracking state`() {
|
|
60
|
+
TrackingStateStore.saveTrackingActive(mockContext, true)
|
|
61
|
+
|
|
62
|
+
verify {
|
|
63
|
+
mockEditor.putBoolean("is_tracking_active", true)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- TrackingStateStore Save Tests ---
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
fun `saveTrackingActive with true saves true to SharedPreferences`() {
|
|
71
|
+
TrackingStateStore.saveTrackingActive(mockContext, true)
|
|
72
|
+
|
|
73
|
+
verify(exactly = 1) { mockEditor.putBoolean("is_tracking_active", true) }
|
|
74
|
+
verify(exactly = 1) { mockEditor.apply() }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@Test
|
|
78
|
+
fun `saveTrackingActive with false saves false to SharedPreferences`() {
|
|
79
|
+
TrackingStateStore.saveTrackingActive(mockContext, false)
|
|
80
|
+
|
|
81
|
+
verify(exactly = 1) { mockEditor.putBoolean("is_tracking_active", false) }
|
|
82
|
+
verify(exactly = 1) { mockEditor.apply() }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- TrackingStateStore Read Tests ---
|
|
86
|
+
|
|
87
|
+
@Test
|
|
88
|
+
fun `isTrackingActive returns true when stored value is true`() {
|
|
89
|
+
every {
|
|
90
|
+
mockSharedPreferences.getBoolean("is_tracking_active", false)
|
|
91
|
+
} returns true
|
|
92
|
+
|
|
93
|
+
val result = TrackingStateStore.isTrackingActive(mockContext)
|
|
94
|
+
|
|
95
|
+
assertTrue("Should return true when tracking was active", result)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
@Test
|
|
99
|
+
fun `isTrackingActive returns false when stored value is false`() {
|
|
100
|
+
every {
|
|
101
|
+
mockSharedPreferences.getBoolean("is_tracking_active", false)
|
|
102
|
+
} returns false
|
|
103
|
+
|
|
104
|
+
val result = TrackingStateStore.isTrackingActive(mockContext)
|
|
105
|
+
|
|
106
|
+
assertFalse("Should return false when tracking was not active", result)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Test
|
|
110
|
+
fun `isTrackingActive returns false by default when no value stored`() {
|
|
111
|
+
// Default value is false when key doesn't exist
|
|
112
|
+
every {
|
|
113
|
+
mockSharedPreferences.getBoolean("is_tracking_active", false)
|
|
114
|
+
} returns false
|
|
115
|
+
|
|
116
|
+
val result = TrackingStateStore.isTrackingActive(mockContext)
|
|
117
|
+
|
|
118
|
+
assertFalse("Should return false by default", result)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// --- TrackingStateStore Round-Trip Tests ---
|
|
122
|
+
|
|
123
|
+
@Test
|
|
124
|
+
fun `save and read tracking state round-trip with true`() {
|
|
125
|
+
// Simulate save
|
|
126
|
+
TrackingStateStore.saveTrackingActive(mockContext, true)
|
|
127
|
+
verify { mockEditor.putBoolean("is_tracking_active", true) }
|
|
128
|
+
|
|
129
|
+
// Simulate read after save
|
|
130
|
+
every {
|
|
131
|
+
mockSharedPreferences.getBoolean("is_tracking_active", false)
|
|
132
|
+
} returns true
|
|
133
|
+
|
|
134
|
+
val result = TrackingStateStore.isTrackingActive(mockContext)
|
|
135
|
+
assertTrue(result)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
fun `save and read tracking state round-trip with false`() {
|
|
140
|
+
// Simulate save
|
|
141
|
+
TrackingStateStore.saveTrackingActive(mockContext, false)
|
|
142
|
+
verify { mockEditor.putBoolean("is_tracking_active", false) }
|
|
143
|
+
|
|
144
|
+
// Simulate read after save
|
|
145
|
+
every {
|
|
146
|
+
mockSharedPreferences.getBoolean("is_tracking_active", false)
|
|
147
|
+
} returns false
|
|
148
|
+
|
|
149
|
+
val result = TrackingStateStore.isTrackingActive(mockContext)
|
|
150
|
+
assertFalse(result)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- BootReceiver Intent Filtering Tests ---
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
fun `BootReceiver class exists in correct package`() {
|
|
157
|
+
val clazz = BootReceiver::class.java
|
|
158
|
+
assertEquals("com.livetracking.receiver.BootReceiver", clazz.name)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Test
|
|
162
|
+
fun `BootReceiver extends BroadcastReceiver`() {
|
|
163
|
+
val receiver = BootReceiver()
|
|
164
|
+
assertTrue(
|
|
165
|
+
"BootReceiver should extend BroadcastReceiver",
|
|
166
|
+
receiver is android.content.BroadcastReceiver
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@Test
|
|
171
|
+
fun `ACTION_BOOT_COMPLETED constant is correct`() {
|
|
172
|
+
assertEquals(
|
|
173
|
+
"android.intent.action.BOOT_COMPLETED",
|
|
174
|
+
Intent.ACTION_BOOT_COMPLETED
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@Test
|
|
179
|
+
fun `BootReceiver only responds to BOOT_COMPLETED action`() {
|
|
180
|
+
// Verify the intent action check logic
|
|
181
|
+
val bootIntent = mockk<Intent> {
|
|
182
|
+
every { action } returns Intent.ACTION_BOOT_COMPLETED
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
val otherIntent = mockk<Intent> {
|
|
186
|
+
every { action } returns "com.some.other.ACTION"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// The receiver should only process BOOT_COMPLETED
|
|
190
|
+
assertEquals(Intent.ACTION_BOOT_COMPLETED, bootIntent.action)
|
|
191
|
+
assertNotEquals(Intent.ACTION_BOOT_COMPLETED, otherIntent.action)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@Test
|
|
195
|
+
fun `BootReceiver does not start service when tracking is not active`() {
|
|
196
|
+
every {
|
|
197
|
+
mockSharedPreferences.getBoolean("is_tracking_active", false)
|
|
198
|
+
} returns false
|
|
199
|
+
|
|
200
|
+
val isActive = TrackingStateStore.isTrackingActive(mockContext)
|
|
201
|
+
|
|
202
|
+
assertFalse("Should not start service when tracking is inactive", isActive)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Test
|
|
206
|
+
fun `BootReceiver starts service when tracking is active`() {
|
|
207
|
+
every {
|
|
208
|
+
mockSharedPreferences.getBoolean("is_tracking_active", false)
|
|
209
|
+
} returns true
|
|
210
|
+
|
|
211
|
+
val isActive = TrackingStateStore.isTrackingActive(mockContext)
|
|
212
|
+
|
|
213
|
+
assertTrue("Should start service when tracking was active", isActive)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- TrackingStateStore Object Tests ---
|
|
217
|
+
|
|
218
|
+
@Test
|
|
219
|
+
fun `TrackingStateStore is a singleton object`() {
|
|
220
|
+
// Kotlin object declarations are singletons
|
|
221
|
+
val ref1 = TrackingStateStore
|
|
222
|
+
val ref2 = TrackingStateStore
|
|
223
|
+
assertSame(ref1, ref2)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@Test
|
|
227
|
+
fun `TrackingStateStore uses MODE_PRIVATE for SharedPreferences`() {
|
|
228
|
+
TrackingStateStore.isTrackingActive(mockContext)
|
|
229
|
+
|
|
230
|
+
verify {
|
|
231
|
+
mockContext.getSharedPreferences("live_tracking_prefs", Context.MODE_PRIVATE)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/*
|
|
236
|
+
* ============================================================================
|
|
237
|
+
* NOTE: The following tests require a real Android Context and cannot run as
|
|
238
|
+
* local unit tests. They should be implemented as instrumented tests (androidTest):
|
|
239
|
+
*
|
|
240
|
+
* - SharedPreferences actually persists data across reads
|
|
241
|
+
* - TrackingStateStore survives process restart (SharedPreferences persistence)
|
|
242
|
+
* - BootReceiver.onReceive actually starts TrackingForegroundService
|
|
243
|
+
* - BootReceiver handles null intent gracefully on real device
|
|
244
|
+
* - Service restart behavior after actual device reboot
|
|
245
|
+
* ============================================================================
|
|
246
|
+
*/
|
|
247
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
package com.livetracking
|
|
2
|
+
|
|
3
|
+
import com.livetracking.queue.QueuedLocation
|
|
4
|
+
import com.livetracking.sync.FirebaseSyncEngine
|
|
5
|
+
import com.livetracking.sync.SyncCallback
|
|
6
|
+
import io.mockk.*
|
|
7
|
+
import org.junit.Assert.*
|
|
8
|
+
import org.junit.Before
|
|
9
|
+
import org.junit.Test
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Unit tests for FirebaseSyncEngine.
|
|
13
|
+
*
|
|
14
|
+
* Focuses on the pure logic that can be tested without Firebase SDK initialization:
|
|
15
|
+
* - Exponential backoff delay calculation
|
|
16
|
+
* - Null path error handling
|
|
17
|
+
* - Empty batch handling
|
|
18
|
+
*
|
|
19
|
+
* Full Firebase write tests require either:
|
|
20
|
+
* - Instrumented tests with Firebase emulator
|
|
21
|
+
* - Integration tests with a test Firebase project
|
|
22
|
+
*/
|
|
23
|
+
class FirebaseSyncEngineTest {
|
|
24
|
+
|
|
25
|
+
private lateinit var syncEngine: FirebaseSyncEngine
|
|
26
|
+
private lateinit var syncEngineNoCurrentPath: FirebaseSyncEngine
|
|
27
|
+
private lateinit var syncEngineNoHistoryPath: FirebaseSyncEngine
|
|
28
|
+
|
|
29
|
+
@Before
|
|
30
|
+
fun setup() {
|
|
31
|
+
syncEngine = FirebaseSyncEngine(
|
|
32
|
+
service = "RTDB",
|
|
33
|
+
currentLocationPath = "users/test-user/currentLocation",
|
|
34
|
+
historyPath = "users/test-user/history"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
syncEngineNoCurrentPath = FirebaseSyncEngine(
|
|
38
|
+
service = "RTDB",
|
|
39
|
+
currentLocationPath = null,
|
|
40
|
+
historyPath = "users/test-user/history"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
syncEngineNoHistoryPath = FirebaseSyncEngine(
|
|
44
|
+
service = "RTDB",
|
|
45
|
+
currentLocationPath = "users/test-user/currentLocation",
|
|
46
|
+
historyPath = null
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Exponential Backoff Delay Tests ---
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun `calculateBackoffDelay attempt 1 returns approximately 1000ms`() {
|
|
54
|
+
// Formula: 1000 * 2^(1-1) ± 200 = 1000 ± 200
|
|
55
|
+
// Expected range: [800, 1200]
|
|
56
|
+
val delays = (1..100).map { syncEngine.calculateBackoffDelay(1) }
|
|
57
|
+
|
|
58
|
+
delays.forEach { delay ->
|
|
59
|
+
assertTrue(
|
|
60
|
+
"Attempt 1 delay should be between 800 and 1200ms, got $delay",
|
|
61
|
+
delay in 800..1200
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
fun `calculateBackoffDelay attempt 2 returns approximately 2000ms`() {
|
|
68
|
+
// Formula: 1000 * 2^(2-1) ± 200 = 2000 ± 200
|
|
69
|
+
// Expected range: [1800, 2200]
|
|
70
|
+
val delays = (1..100).map { syncEngine.calculateBackoffDelay(2) }
|
|
71
|
+
|
|
72
|
+
delays.forEach { delay ->
|
|
73
|
+
assertTrue(
|
|
74
|
+
"Attempt 2 delay should be between 1800 and 2200ms, got $delay",
|
|
75
|
+
delay in 1800..2200
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Test
|
|
81
|
+
fun `calculateBackoffDelay attempt 3 returns approximately 4000ms`() {
|
|
82
|
+
// Formula: 1000 * 2^(3-1) ± 200 = 4000 ± 200
|
|
83
|
+
// Expected range: [3800, 4200]
|
|
84
|
+
val delays = (1..100).map { syncEngine.calculateBackoffDelay(3) }
|
|
85
|
+
|
|
86
|
+
delays.forEach { delay ->
|
|
87
|
+
assertTrue(
|
|
88
|
+
"Attempt 3 delay should be between 3800 and 4200ms, got $delay",
|
|
89
|
+
delay in 3800..4200
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
fun `calculateBackoffDelay attempt 4 returns approximately 8000ms`() {
|
|
96
|
+
// Formula: 1000 * 2^(4-1) ± 200 = 8000 ± 200
|
|
97
|
+
// Expected range: [7800, 8200]
|
|
98
|
+
val delays = (1..100).map { syncEngine.calculateBackoffDelay(4) }
|
|
99
|
+
|
|
100
|
+
delays.forEach { delay ->
|
|
101
|
+
assertTrue(
|
|
102
|
+
"Attempt 4 delay should be between 7800 and 8200ms, got $delay",
|
|
103
|
+
delay in 7800..8200
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@Test
|
|
109
|
+
fun `calculateBackoffDelay attempt 5 returns approximately 16000ms`() {
|
|
110
|
+
// Formula: 1000 * 2^(5-1) ± 200 = 16000 ± 200
|
|
111
|
+
// Expected range: [15800, 16200]
|
|
112
|
+
val delays = (1..100).map { syncEngine.calculateBackoffDelay(5) }
|
|
113
|
+
|
|
114
|
+
delays.forEach { delay ->
|
|
115
|
+
assertTrue(
|
|
116
|
+
"Attempt 5 delay should be between 15800 and 16200ms, got $delay",
|
|
117
|
+
delay in 15800..16200
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@Test
|
|
123
|
+
fun `calculateBackoffDelay is never negative`() {
|
|
124
|
+
// Test across many attempts to ensure maxOf(0L, ...) guard works
|
|
125
|
+
val delays = (1..10).flatMap { attempt ->
|
|
126
|
+
(1..100).map { syncEngine.calculateBackoffDelay(attempt) }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
delays.forEach { delay ->
|
|
130
|
+
assertTrue("Delay should never be negative, got $delay", delay >= 0)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@Test
|
|
135
|
+
fun `calculateBackoffDelay includes jitter variation`() {
|
|
136
|
+
// Run multiple times for the same attempt and verify we get different values (jitter)
|
|
137
|
+
val delays = (1..50).map { syncEngine.calculateBackoffDelay(1) }.toSet()
|
|
138
|
+
|
|
139
|
+
// With ±200ms jitter, we should get multiple distinct values over 50 runs
|
|
140
|
+
assertTrue(
|
|
141
|
+
"Expected jitter to produce multiple distinct delay values, got ${delays.size}",
|
|
142
|
+
delays.size > 1
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
fun `calculateBackoffDelay grows exponentially`() {
|
|
148
|
+
// Average of many samples should show exponential growth
|
|
149
|
+
val avgDelay1 = (1..100).map { syncEngine.calculateBackoffDelay(1) }.average()
|
|
150
|
+
val avgDelay2 = (1..100).map { syncEngine.calculateBackoffDelay(2) }.average()
|
|
151
|
+
val avgDelay3 = (1..100).map { syncEngine.calculateBackoffDelay(3) }.average()
|
|
152
|
+
|
|
153
|
+
// Each level should be approximately 2x the previous
|
|
154
|
+
assertTrue("Delay 2 should be ~2x delay 1", avgDelay2 > avgDelay1 * 1.5)
|
|
155
|
+
assertTrue("Delay 3 should be ~2x delay 2", avgDelay3 > avgDelay2 * 1.5)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Null Path Error Handling Tests ---
|
|
159
|
+
|
|
160
|
+
@Test
|
|
161
|
+
fun `updateCurrentLocation with null path calls onError`() {
|
|
162
|
+
var errorCode: String? = null
|
|
163
|
+
var errorMessage: String? = null
|
|
164
|
+
|
|
165
|
+
val callback = object : SyncCallback {
|
|
166
|
+
override fun onSuccess() {
|
|
167
|
+
fail("Should not call onSuccess when path is null")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
override fun onError(code: String, message: String) {
|
|
171
|
+
errorCode = code
|
|
172
|
+
errorMessage = message
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
syncEngineNoCurrentPath.updateCurrentLocation(
|
|
177
|
+
latitude = 37.7749,
|
|
178
|
+
longitude = -122.4194,
|
|
179
|
+
timestamp = System.currentTimeMillis(),
|
|
180
|
+
accuracy = 10.0f,
|
|
181
|
+
speed = null,
|
|
182
|
+
callback = callback
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
assertEquals("NO_PATH", errorCode)
|
|
186
|
+
assertNotNull(errorMessage)
|
|
187
|
+
assertTrue(errorMessage!!.contains("currentLocationPath"))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@Test
|
|
191
|
+
fun `pushHistoryBatch with null path calls onError`() {
|
|
192
|
+
var errorCode: String? = null
|
|
193
|
+
var errorMessage: String? = null
|
|
194
|
+
|
|
195
|
+
val callback = object : SyncCallback {
|
|
196
|
+
override fun onSuccess() {
|
|
197
|
+
fail("Should not call onSuccess when path is null")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
override fun onError(code: String, message: String) {
|
|
201
|
+
errorCode = code
|
|
202
|
+
errorMessage = message
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
val locations = listOf(
|
|
207
|
+
QueuedLocation("1", 37.0, -122.0, 1000L, 5.0f, null, null, null, 1000L)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
syncEngineNoHistoryPath.pushHistoryBatch(locations, callback)
|
|
211
|
+
|
|
212
|
+
assertEquals("NO_PATH", errorCode)
|
|
213
|
+
assertNotNull(errorMessage)
|
|
214
|
+
assertTrue(errorMessage!!.contains("historyPath"))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Empty Batch Handling Tests ---
|
|
218
|
+
|
|
219
|
+
@Test
|
|
220
|
+
fun `pushHistoryBatch with empty list calls onSuccess immediately`() {
|
|
221
|
+
var successCalled = false
|
|
222
|
+
|
|
223
|
+
val callback = object : SyncCallback {
|
|
224
|
+
override fun onSuccess() {
|
|
225
|
+
successCalled = true
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
override fun onError(code: String, message: String) {
|
|
229
|
+
fail("Should not call onError for empty list")
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
syncEngine.pushHistoryBatch(emptyList(), callback)
|
|
234
|
+
|
|
235
|
+
assertTrue("onSuccess should be called for empty batch", successCalled)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
@Test
|
|
239
|
+
fun `pushHistoryBatch with empty list and null path calls onError`() {
|
|
240
|
+
// null path check happens before empty list check
|
|
241
|
+
var errorCode: String? = null
|
|
242
|
+
|
|
243
|
+
val callback = object : SyncCallback {
|
|
244
|
+
override fun onSuccess() {
|
|
245
|
+
fail("Should not call onSuccess when path is null")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
override fun onError(code: String, message: String) {
|
|
249
|
+
errorCode = code
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
syncEngineNoHistoryPath.pushHistoryBatch(emptyList(), callback)
|
|
254
|
+
|
|
255
|
+
assertEquals("NO_PATH", errorCode)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// --- SyncCallback Interface Tests ---
|
|
259
|
+
|
|
260
|
+
@Test
|
|
261
|
+
fun `SyncCallback onSuccess can be called`() {
|
|
262
|
+
var called = false
|
|
263
|
+
val callback = object : SyncCallback {
|
|
264
|
+
override fun onSuccess() { called = true }
|
|
265
|
+
override fun onError(code: String, message: String) {}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
callback.onSuccess()
|
|
269
|
+
assertTrue(called)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@Test
|
|
273
|
+
fun `SyncCallback onError provides error code and message`() {
|
|
274
|
+
var receivedCode: String? = null
|
|
275
|
+
var receivedMessage: String? = null
|
|
276
|
+
|
|
277
|
+
val callback = object : SyncCallback {
|
|
278
|
+
override fun onSuccess() {}
|
|
279
|
+
override fun onError(code: String, message: String) {
|
|
280
|
+
receivedCode = code
|
|
281
|
+
receivedMessage = message
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
callback.onError("TEST_ERROR", "Something went wrong")
|
|
286
|
+
|
|
287
|
+
assertEquals("TEST_ERROR", receivedCode)
|
|
288
|
+
assertEquals("Something went wrong", receivedMessage)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- Constructor / Configuration Tests ---
|
|
292
|
+
|
|
293
|
+
@Test
|
|
294
|
+
fun `FirebaseSyncEngine accepts RTDB service type`() {
|
|
295
|
+
val engine = FirebaseSyncEngine("RTDB", "path/current", "path/history")
|
|
296
|
+
assertNotNull(engine)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@Test
|
|
300
|
+
fun `FirebaseSyncEngine accepts Firestore service type`() {
|
|
301
|
+
val engine = FirebaseSyncEngine("Firestore", "collection/doc", "collection")
|
|
302
|
+
assertNotNull(engine)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
@Test
|
|
306
|
+
fun `FirebaseSyncEngine accepts null paths`() {
|
|
307
|
+
val engine = FirebaseSyncEngine("RTDB", null, null)
|
|
308
|
+
assertNotNull(engine)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Backoff Constants Verification ---
|
|
312
|
+
|
|
313
|
+
@Test
|
|
314
|
+
fun `backoff base delay is 1000ms for attempt 1`() {
|
|
315
|
+
// The base delay without jitter should center around 1000ms
|
|
316
|
+
val delays = (1..1000).map { syncEngine.calculateBackoffDelay(1) }
|
|
317
|
+
val average = delays.average()
|
|
318
|
+
|
|
319
|
+
// Average should be very close to 1000 (jitter averages to ~0)
|
|
320
|
+
assertTrue(
|
|
321
|
+
"Average delay for attempt 1 should be near 1000ms, got $average",
|
|
322
|
+
average in 900.0..1100.0
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@Test
|
|
327
|
+
fun `backoff jitter range is within +-200ms`() {
|
|
328
|
+
// For attempt 1, base is 1000ms, so all values should be in [800, 1200]
|
|
329
|
+
val delays = (1..1000).map { syncEngine.calculateBackoffDelay(1) }
|
|
330
|
+
|
|
331
|
+
val min = delays.min()
|
|
332
|
+
val max = delays.max()
|
|
333
|
+
|
|
334
|
+
assertTrue("Min delay should be >= 800, got $min", min >= 800)
|
|
335
|
+
assertTrue("Max delay should be <= 1200, got $max", max <= 1200)
|
|
336
|
+
}
|
|
337
|
+
}
|