@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,462 @@
|
|
|
1
|
+
package com.livetracking
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.test.core.app.ApplicationProvider
|
|
5
|
+
import com.livetracking.queue.QueuedLocation
|
|
6
|
+
import com.livetracking.queue.TrackingDatabase
|
|
7
|
+
import org.junit.After
|
|
8
|
+
import org.junit.Assert.*
|
|
9
|
+
import org.junit.Before
|
|
10
|
+
import org.junit.Test
|
|
11
|
+
import org.junit.runner.RunWith
|
|
12
|
+
import org.robolectric.RobolectricTestRunner
|
|
13
|
+
import org.robolectric.annotation.Config
|
|
14
|
+
import java.util.UUID
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Unit tests for the offline queue (TrackingDatabase per-target methods).
|
|
18
|
+
* Uses Robolectric to provide an in-memory SQLite database.
|
|
19
|
+
*
|
|
20
|
+
* **Validates: Requirements 5.1, 5.2, 5.4, 5.7, 10.1, 10.2, 10.3**
|
|
21
|
+
* **Properties 12, 13, 14, 15**
|
|
22
|
+
*/
|
|
23
|
+
@RunWith(RobolectricTestRunner::class)
|
|
24
|
+
@Config(sdk = [28], manifest = Config.NONE)
|
|
25
|
+
class OfflineQueueTest {
|
|
26
|
+
|
|
27
|
+
private lateinit var db: TrackingDatabase
|
|
28
|
+
|
|
29
|
+
@Before
|
|
30
|
+
fun setup() {
|
|
31
|
+
val context = ApplicationProvider.getApplicationContext<Context>()
|
|
32
|
+
// Create a fresh database instance for each test
|
|
33
|
+
db = TrackingDatabase(context)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@After
|
|
37
|
+
fun teardown() {
|
|
38
|
+
db.close()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- Helper ---
|
|
42
|
+
|
|
43
|
+
private fun createLocation(
|
|
44
|
+
id: String = UUID.randomUUID().toString(),
|
|
45
|
+
latitude: Double = 37.7749,
|
|
46
|
+
longitude: Double = -122.4194,
|
|
47
|
+
timestamp: Long = System.currentTimeMillis(),
|
|
48
|
+
accuracy: Float = 5.0f,
|
|
49
|
+
speed: Float? = null,
|
|
50
|
+
altitude: Double? = null,
|
|
51
|
+
bearing: Float? = null,
|
|
52
|
+
createdAt: Long = System.currentTimeMillis()
|
|
53
|
+
): QueuedLocation {
|
|
54
|
+
return QueuedLocation(
|
|
55
|
+
id = id,
|
|
56
|
+
latitude = latitude,
|
|
57
|
+
longitude = longitude,
|
|
58
|
+
timestamp = timestamp,
|
|
59
|
+
accuracy = accuracy,
|
|
60
|
+
speed = speed,
|
|
61
|
+
altitude = altitude,
|
|
62
|
+
bearing = bearing,
|
|
63
|
+
createdAt = createdAt
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// =========================================================================
|
|
68
|
+
// Property 12: Offline queue size invariant
|
|
69
|
+
// For any SyncTarget with offlineQueue: true, the number of queued data
|
|
70
|
+
// points for that target SHALL never exceed 10,000. When the queue is at
|
|
71
|
+
// capacity and a new point arrives, the oldest point SHALL be evicted.
|
|
72
|
+
// Validates: Requirements 5.1, 5.7
|
|
73
|
+
// =========================================================================
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
fun `insertForTarget enforces 10000 cap by evicting oldest`() {
|
|
77
|
+
val targetPath = "drivers/abc/location"
|
|
78
|
+
|
|
79
|
+
// Insert exactly MAX_QUEUE_SIZE_PER_TARGET items
|
|
80
|
+
for (i in 1..TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET) {
|
|
81
|
+
val location = createLocation(
|
|
82
|
+
id = "loc-$i",
|
|
83
|
+
createdAt = i.toLong()
|
|
84
|
+
)
|
|
85
|
+
db.insertForTarget(location, targetPath)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
assertEquals(TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET, db.getCountForTarget(targetPath))
|
|
89
|
+
|
|
90
|
+
// Insert one more — should evict the oldest (createdAt = 1)
|
|
91
|
+
val newLocation = createLocation(
|
|
92
|
+
id = "loc-new",
|
|
93
|
+
createdAt = (TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET + 1).toLong()
|
|
94
|
+
)
|
|
95
|
+
db.insertForTarget(newLocation, targetPath)
|
|
96
|
+
|
|
97
|
+
// Count should still be at the cap
|
|
98
|
+
assertEquals(TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET, db.getCountForTarget(targetPath))
|
|
99
|
+
|
|
100
|
+
// The oldest item (createdAt = 1) should have been evicted
|
|
101
|
+
val oldest = db.getOldestBatchForTarget(targetPath, 1)
|
|
102
|
+
assertEquals(1, oldest.size)
|
|
103
|
+
// The oldest remaining should be createdAt = 2 (the first was evicted)
|
|
104
|
+
assertEquals(2L, oldest[0].createdAt)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Test
|
|
108
|
+
fun `insertForTarget evicts oldest when at capacity - new item is preserved`() {
|
|
109
|
+
val targetPath = "trips/xyz/history"
|
|
110
|
+
|
|
111
|
+
// Fill to capacity
|
|
112
|
+
for (i in 1..TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET) {
|
|
113
|
+
db.insertForTarget(createLocation(id = "item-$i", createdAt = i.toLong()), targetPath)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Insert a new item
|
|
117
|
+
val newId = "newest-item"
|
|
118
|
+
db.insertForTarget(
|
|
119
|
+
createLocation(id = newId, createdAt = 99999L),
|
|
120
|
+
targetPath
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// The new item should be in the queue
|
|
124
|
+
val newest = db.getOldestBatchForTarget(targetPath, TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET)
|
|
125
|
+
assertTrue(newest.any { it.id == newId })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@Test
|
|
129
|
+
fun `queue never exceeds 10000 after multiple inserts beyond cap`() {
|
|
130
|
+
val targetPath = "test/overflow"
|
|
131
|
+
|
|
132
|
+
// Insert 10,050 items (50 over the cap)
|
|
133
|
+
for (i in 1..10_050) {
|
|
134
|
+
db.insertForTarget(createLocation(id = "item-$i", createdAt = i.toLong()), targetPath)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Count should be exactly at the cap
|
|
138
|
+
assertEquals(TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET, db.getCountForTarget(targetPath))
|
|
139
|
+
|
|
140
|
+
// The oldest remaining should be item 51 (items 1-50 were evicted)
|
|
141
|
+
val oldest = db.getOldestBatchForTarget(targetPath, 1)
|
|
142
|
+
assertEquals(51L, oldest[0].createdAt)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// =========================================================================
|
|
146
|
+
// Property 13: Chronological flush ordering
|
|
147
|
+
// When connectivity is restored, queued data points SHALL be flushed in
|
|
148
|
+
// chronological order (oldest first).
|
|
149
|
+
// Validates: Requirements 5.2
|
|
150
|
+
// =========================================================================
|
|
151
|
+
|
|
152
|
+
@Test
|
|
153
|
+
fun `getOldestBatchForTarget returns items in chronological order`() {
|
|
154
|
+
val targetPath = "drivers/abc/location"
|
|
155
|
+
|
|
156
|
+
// Insert items in non-sequential order
|
|
157
|
+
db.insertForTarget(createLocation(id = "third", createdAt = 300L), targetPath)
|
|
158
|
+
db.insertForTarget(createLocation(id = "first", createdAt = 100L), targetPath)
|
|
159
|
+
db.insertForTarget(createLocation(id = "second", createdAt = 200L), targetPath)
|
|
160
|
+
|
|
161
|
+
val batch = db.getOldestBatchForTarget(targetPath, 3)
|
|
162
|
+
|
|
163
|
+
assertEquals(3, batch.size)
|
|
164
|
+
assertEquals("first", batch[0].id)
|
|
165
|
+
assertEquals("second", batch[1].id)
|
|
166
|
+
assertEquals("third", batch[2].id)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
fun `getOldestBatchForTarget respects limit parameter`() {
|
|
171
|
+
val targetPath = "test/batch"
|
|
172
|
+
|
|
173
|
+
for (i in 1..10) {
|
|
174
|
+
db.insertForTarget(createLocation(id = "item-$i", createdAt = i.toLong()), targetPath)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
val batch = db.getOldestBatchForTarget(targetPath, 5)
|
|
178
|
+
|
|
179
|
+
assertEquals(5, batch.size)
|
|
180
|
+
// Should be the 5 oldest
|
|
181
|
+
assertEquals(1L, batch[0].createdAt)
|
|
182
|
+
assertEquals(5L, batch[4].createdAt)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@Test
|
|
186
|
+
fun `deleteByIds removes items and subsequent dequeue skips them`() {
|
|
187
|
+
val targetPath = "test/flush"
|
|
188
|
+
|
|
189
|
+
// Insert 5 items
|
|
190
|
+
for (i in 1..5) {
|
|
191
|
+
db.insertForTarget(createLocation(id = "item-$i", createdAt = i.toLong()), targetPath)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Simulate flush: dequeue first 2, then delete them
|
|
195
|
+
val firstBatch = db.getOldestBatchForTarget(targetPath, 2)
|
|
196
|
+
assertEquals(2, firstBatch.size)
|
|
197
|
+
db.deleteByIds(firstBatch.map { it.id })
|
|
198
|
+
|
|
199
|
+
// Next dequeue should start from item-3
|
|
200
|
+
val secondBatch = db.getOldestBatchForTarget(targetPath, 2)
|
|
201
|
+
assertEquals(2, secondBatch.size)
|
|
202
|
+
assertEquals("item-3", secondBatch[0].id)
|
|
203
|
+
assertEquals("item-4", secondBatch[1].id)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Test
|
|
207
|
+
fun `chronological ordering is maintained after eviction`() {
|
|
208
|
+
val targetPath = "test/order-after-eviction"
|
|
209
|
+
|
|
210
|
+
// Fill to capacity with items createdAt = 1..10000
|
|
211
|
+
for (i in 1..TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET) {
|
|
212
|
+
db.insertForTarget(createLocation(id = "item-$i", createdAt = i.toLong()), targetPath)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Insert 5 more (evicts items 1-5)
|
|
216
|
+
for (i in 1..5) {
|
|
217
|
+
db.insertForTarget(
|
|
218
|
+
createLocation(id = "new-$i", createdAt = (10000 + i).toLong()),
|
|
219
|
+
targetPath
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Dequeue should still be in chronological order
|
|
224
|
+
val batch = db.getOldestBatchForTarget(targetPath, 5)
|
|
225
|
+
assertEquals(5, batch.size)
|
|
226
|
+
// Oldest remaining should be item-6 (createdAt = 6)
|
|
227
|
+
assertEquals(6L, batch[0].createdAt)
|
|
228
|
+
assertEquals(7L, batch[1].createdAt)
|
|
229
|
+
assertEquals(8L, batch[2].createdAt)
|
|
230
|
+
assertEquals(9L, batch[3].createdAt)
|
|
231
|
+
assertEquals(10L, batch[4].createdAt)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// =========================================================================
|
|
235
|
+
// Property 14: Queue count aggregation
|
|
236
|
+
// getQueuedLocations() SHALL return c1 + c2 + ... + cn, and
|
|
237
|
+
// getQueuedLocationsByTarget() SHALL return a record mapping each target
|
|
238
|
+
// path to its respective count.
|
|
239
|
+
// Validates: Requirements 10.1, 10.2
|
|
240
|
+
// =========================================================================
|
|
241
|
+
|
|
242
|
+
@Test
|
|
243
|
+
fun `getCountsByTarget returns correct per-target counts`() {
|
|
244
|
+
val target1 = "drivers/abc/location"
|
|
245
|
+
val target2 = "trips/xyz/history"
|
|
246
|
+
val target3 = "fleet/status"
|
|
247
|
+
|
|
248
|
+
// Insert different counts per target
|
|
249
|
+
for (i in 1..3) {
|
|
250
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), target1)
|
|
251
|
+
}
|
|
252
|
+
for (i in 1..7) {
|
|
253
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), target2)
|
|
254
|
+
}
|
|
255
|
+
for (i in 1..2) {
|
|
256
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), target3)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
val counts = db.getCountsByTarget()
|
|
260
|
+
|
|
261
|
+
assertEquals(3, counts[target1])
|
|
262
|
+
assertEquals(7, counts[target2])
|
|
263
|
+
assertEquals(2, counts[target3])
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@Test
|
|
267
|
+
fun `getCountsByTarget aggregation sums to total count`() {
|
|
268
|
+
val target1 = "path/a"
|
|
269
|
+
val target2 = "path/b"
|
|
270
|
+
|
|
271
|
+
for (i in 1..5) {
|
|
272
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), target1)
|
|
273
|
+
}
|
|
274
|
+
for (i in 1..8) {
|
|
275
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), target2)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
val counts = db.getCountsByTarget()
|
|
279
|
+
val totalFromAggregation = counts.values.sum()
|
|
280
|
+
val totalFromCount = db.getCount()
|
|
281
|
+
|
|
282
|
+
assertEquals(totalFromCount, totalFromAggregation)
|
|
283
|
+
assertEquals(13, totalFromAggregation)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
@Test
|
|
287
|
+
fun `getCountForTarget returns correct count for specific target`() {
|
|
288
|
+
val target1 = "target/one"
|
|
289
|
+
val target2 = "target/two"
|
|
290
|
+
|
|
291
|
+
for (i in 1..4) {
|
|
292
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), target1)
|
|
293
|
+
}
|
|
294
|
+
for (i in 1..6) {
|
|
295
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), target2)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
assertEquals(4, db.getCountForTarget(target1))
|
|
299
|
+
assertEquals(6, db.getCountForTarget(target2))
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
@Test
|
|
303
|
+
fun `getCountsByTarget returns empty map when no items queued`() {
|
|
304
|
+
val counts = db.getCountsByTarget()
|
|
305
|
+
assertTrue(counts.isEmpty())
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@Test
|
|
309
|
+
fun `getCountsByTarget updates after deletions`() {
|
|
310
|
+
val target = "test/delete"
|
|
311
|
+
|
|
312
|
+
for (i in 1..5) {
|
|
313
|
+
db.insertForTarget(createLocation(id = "del-$i", createdAt = i.toLong()), target)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Delete 2 items
|
|
317
|
+
db.deleteByIds(listOf("del-1", "del-2"))
|
|
318
|
+
|
|
319
|
+
val counts = db.getCountsByTarget()
|
|
320
|
+
assertEquals(3, counts[target])
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// =========================================================================
|
|
324
|
+
// Property 15: Non-queuing targets report zero
|
|
325
|
+
// For any SyncTarget with offlineQueue set to false or undefined,
|
|
326
|
+
// getQueuedLocationsByTarget() SHALL report 0 queued locations for that
|
|
327
|
+
// target's path.
|
|
328
|
+
// Validates: Requirements 10.3
|
|
329
|
+
// =========================================================================
|
|
330
|
+
|
|
331
|
+
@Test
|
|
332
|
+
fun `getCountForTarget returns zero for target with no queued items`() {
|
|
333
|
+
// A target that has never had items queued (simulates offlineQueue: false)
|
|
334
|
+
val nonQueuingTarget = "realtime/current"
|
|
335
|
+
|
|
336
|
+
assertEquals(0, db.getCountForTarget(nonQueuingTarget))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
@Test
|
|
340
|
+
fun `getCountsByTarget does not include targets with zero items`() {
|
|
341
|
+
val queuingTarget = "history/path"
|
|
342
|
+
val nonQueuingTarget = "realtime/path"
|
|
343
|
+
|
|
344
|
+
// Only insert for the queuing target
|
|
345
|
+
db.insertForTarget(createLocation(createdAt = 1L), queuingTarget)
|
|
346
|
+
|
|
347
|
+
val counts = db.getCountsByTarget()
|
|
348
|
+
|
|
349
|
+
// The queuing target should be present
|
|
350
|
+
assertEquals(1, counts[queuingTarget])
|
|
351
|
+
// The non-queuing target should not appear (or be absent from the map)
|
|
352
|
+
assertNull(counts[nonQueuingTarget])
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@Test
|
|
356
|
+
fun `non-queuing target reports zero regardless of other targets having items`() {
|
|
357
|
+
val queuingTarget1 = "target/queued1"
|
|
358
|
+
val queuingTarget2 = "target/queued2"
|
|
359
|
+
val nonQueuingTarget = "target/no-queue"
|
|
360
|
+
|
|
361
|
+
// Insert items for queuing targets
|
|
362
|
+
for (i in 1..10) {
|
|
363
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), queuingTarget1)
|
|
364
|
+
}
|
|
365
|
+
for (i in 1..5) {
|
|
366
|
+
db.insertForTarget(createLocation(createdAt = i.toLong()), queuingTarget2)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Non-queuing target should report zero
|
|
370
|
+
assertEquals(0, db.getCountForTarget(nonQueuingTarget))
|
|
371
|
+
|
|
372
|
+
// Verify queuing targets have their counts
|
|
373
|
+
assertEquals(10, db.getCountForTarget(queuingTarget1))
|
|
374
|
+
assertEquals(5, db.getCountForTarget(queuingTarget2))
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// =========================================================================
|
|
378
|
+
// Per-target queue isolation (Property 12 extension)
|
|
379
|
+
// Validates: Requirements 5.4
|
|
380
|
+
// =========================================================================
|
|
381
|
+
|
|
382
|
+
@Test
|
|
383
|
+
fun `per-target queues are isolated - insert to one does not affect another`() {
|
|
384
|
+
val target1 = "drivers/abc/location"
|
|
385
|
+
val target2 = "trips/xyz/history"
|
|
386
|
+
|
|
387
|
+
for (i in 1..5) {
|
|
388
|
+
db.insertForTarget(createLocation(id = "t1-$i", createdAt = i.toLong()), target1)
|
|
389
|
+
}
|
|
390
|
+
for (i in 1..3) {
|
|
391
|
+
db.insertForTarget(createLocation(id = "t2-$i", createdAt = i.toLong()), target2)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
assertEquals(5, db.getCountForTarget(target1))
|
|
395
|
+
assertEquals(3, db.getCountForTarget(target2))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
@Test
|
|
399
|
+
fun `per-target queues are isolated - dequeue from one does not affect another`() {
|
|
400
|
+
val target1 = "path/a"
|
|
401
|
+
val target2 = "path/b"
|
|
402
|
+
|
|
403
|
+
for (i in 1..5) {
|
|
404
|
+
db.insertForTarget(createLocation(id = "a-$i", createdAt = i.toLong()), target1)
|
|
405
|
+
}
|
|
406
|
+
for (i in 1..5) {
|
|
407
|
+
db.insertForTarget(createLocation(id = "b-$i", createdAt = i.toLong()), target2)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Dequeue from target1
|
|
411
|
+
val batch = db.getOldestBatchForTarget(target1, 5)
|
|
412
|
+
assertEquals(5, batch.size)
|
|
413
|
+
// All items should belong to target1
|
|
414
|
+
batch.forEach { assertTrue(it.id.startsWith("a-")) }
|
|
415
|
+
|
|
416
|
+
// target2 should be unaffected
|
|
417
|
+
assertEquals(5, db.getCountForTarget(target2))
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@Test
|
|
421
|
+
fun `per-target cap enforcement is isolated - overflow in one does not evict from another`() {
|
|
422
|
+
val target1 = "target/capped"
|
|
423
|
+
val target2 = "target/safe"
|
|
424
|
+
|
|
425
|
+
// Fill target2 with 100 items
|
|
426
|
+
for (i in 1..100) {
|
|
427
|
+
db.insertForTarget(createLocation(id = "safe-$i", createdAt = i.toLong()), target2)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Fill target1 to capacity and overflow
|
|
431
|
+
for (i in 1..(TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET + 10)) {
|
|
432
|
+
db.insertForTarget(createLocation(id = "capped-$i", createdAt = i.toLong()), target1)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// target1 should be at cap
|
|
436
|
+
assertEquals(TrackingDatabase.MAX_QUEUE_SIZE_PER_TARGET, db.getCountForTarget(target1))
|
|
437
|
+
// target2 should be completely unaffected
|
|
438
|
+
assertEquals(100, db.getCountForTarget(target2))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@Test
|
|
442
|
+
fun `evictOldestForTarget only evicts from specified target`() {
|
|
443
|
+
val target1 = "target/evict"
|
|
444
|
+
val target2 = "target/keep"
|
|
445
|
+
|
|
446
|
+
db.insertForTarget(createLocation(id = "evict-1", createdAt = 1L), target1)
|
|
447
|
+
db.insertForTarget(createLocation(id = "evict-2", createdAt = 2L), target1)
|
|
448
|
+
db.insertForTarget(createLocation(id = "keep-1", createdAt = 1L), target2)
|
|
449
|
+
db.insertForTarget(createLocation(id = "keep-2", createdAt = 2L), target2)
|
|
450
|
+
|
|
451
|
+
// Evict oldest from target1
|
|
452
|
+
db.evictOldestForTarget(target1)
|
|
453
|
+
|
|
454
|
+
// target1 should have lost its oldest
|
|
455
|
+
assertEquals(1, db.getCountForTarget(target1))
|
|
456
|
+
val remaining = db.getOldestBatchForTarget(target1, 1)
|
|
457
|
+
assertEquals("evict-2", remaining[0].id)
|
|
458
|
+
|
|
459
|
+
// target2 should be unaffected
|
|
460
|
+
assertEquals(2, db.getCountForTarget(target2))
|
|
461
|
+
}
|
|
462
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
package com.livetracking
|
|
2
|
+
|
|
3
|
+
import com.livetracking.permissions.PermissionHandler
|
|
4
|
+
import com.livetracking.permissions.PermissionResult
|
|
5
|
+
import org.junit.Assert.*
|
|
6
|
+
import org.junit.Before
|
|
7
|
+
import org.junit.Test
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Unit tests for PermissionHandler.
|
|
11
|
+
*
|
|
12
|
+
* NOTE: PermissionHandler relies on ContextCompat.checkSelfPermission() and LocationManager,
|
|
13
|
+
* both of which require Android Context. These tests verify:
|
|
14
|
+
* - Error code constants are correct
|
|
15
|
+
* - PermissionResult sealed class structure
|
|
16
|
+
* - PermissionHandler can be instantiated
|
|
17
|
+
*
|
|
18
|
+
* Full permission check tests require instrumented tests (androidTest) with a real Context,
|
|
19
|
+
* or Robolectric for simulating Android framework behavior.
|
|
20
|
+
*/
|
|
21
|
+
class PermissionHandlerTest {
|
|
22
|
+
|
|
23
|
+
private lateinit var permissionHandler: PermissionHandler
|
|
24
|
+
|
|
25
|
+
@Before
|
|
26
|
+
fun setup() {
|
|
27
|
+
permissionHandler = PermissionHandler()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// --- Error Code Constants Tests ---
|
|
31
|
+
|
|
32
|
+
@Test
|
|
33
|
+
fun `ERROR_PERMISSION_DENIED constant is PERMISSION_DENIED`() {
|
|
34
|
+
assertEquals("PERMISSION_DENIED", PermissionHandler.ERROR_PERMISSION_DENIED)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@Test
|
|
38
|
+
fun `ERROR_GPS_DISABLED constant is GPS_DISABLED`() {
|
|
39
|
+
assertEquals("GPS_DISABLED", PermissionHandler.ERROR_GPS_DISABLED)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@Test
|
|
43
|
+
fun `error codes are distinct`() {
|
|
44
|
+
assertNotEquals(
|
|
45
|
+
PermissionHandler.ERROR_PERMISSION_DENIED,
|
|
46
|
+
PermissionHandler.ERROR_GPS_DISABLED
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- PermissionResult Sealed Class Tests ---
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
fun `PermissionResult Granted is a singleton object`() {
|
|
54
|
+
val result1 = PermissionResult.Granted
|
|
55
|
+
val result2 = PermissionResult.Granted
|
|
56
|
+
assertSame(result1, result2)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun `PermissionResult Denied contains errorCode and message`() {
|
|
61
|
+
val denied = PermissionResult.Denied(
|
|
62
|
+
errorCode = "PERMISSION_DENIED",
|
|
63
|
+
message = "Location permissions denied"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assertEquals("PERMISSION_DENIED", denied.errorCode)
|
|
67
|
+
assertEquals("Location permissions denied", denied.message)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Test
|
|
71
|
+
fun `PermissionResult Denied supports data class equality`() {
|
|
72
|
+
val denied1 = PermissionResult.Denied("CODE", "message")
|
|
73
|
+
val denied2 = PermissionResult.Denied("CODE", "message")
|
|
74
|
+
|
|
75
|
+
assertEquals(denied1, denied2)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@Test
|
|
79
|
+
fun `PermissionResult Denied with different codes are not equal`() {
|
|
80
|
+
val denied1 = PermissionResult.Denied("PERMISSION_DENIED", "msg")
|
|
81
|
+
val denied2 = PermissionResult.Denied("GPS_DISABLED", "msg")
|
|
82
|
+
|
|
83
|
+
assertNotEquals(denied1, denied2)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@Test
|
|
87
|
+
fun `PermissionResult Denied with different messages are not equal`() {
|
|
88
|
+
val denied1 = PermissionResult.Denied("CODE", "message 1")
|
|
89
|
+
val denied2 = PermissionResult.Denied("CODE", "message 2")
|
|
90
|
+
|
|
91
|
+
assertNotEquals(denied1, denied2)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
fun `PermissionResult can be checked with when expression`() {
|
|
96
|
+
val granted: PermissionResult = PermissionResult.Granted
|
|
97
|
+
val denied: PermissionResult = PermissionResult.Denied("CODE", "msg")
|
|
98
|
+
|
|
99
|
+
val grantedResult = when (granted) {
|
|
100
|
+
is PermissionResult.Granted -> "granted"
|
|
101
|
+
is PermissionResult.Denied -> "denied"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
val deniedResult = when (denied) {
|
|
105
|
+
is PermissionResult.Granted -> "granted"
|
|
106
|
+
is PermissionResult.Denied -> "denied"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assertEquals("granted", grantedResult)
|
|
110
|
+
assertEquals("denied", deniedResult)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Test
|
|
114
|
+
fun `PermissionResult Denied can be destructured`() {
|
|
115
|
+
val denied = PermissionResult.Denied("PERMISSION_DENIED", "Access denied")
|
|
116
|
+
val (code, message) = denied
|
|
117
|
+
|
|
118
|
+
assertEquals("PERMISSION_DENIED", code)
|
|
119
|
+
assertEquals("Access denied", message)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- PermissionHandler Instantiation Tests ---
|
|
123
|
+
|
|
124
|
+
@Test
|
|
125
|
+
fun `PermissionHandler can be instantiated`() {
|
|
126
|
+
val handler = PermissionHandler()
|
|
127
|
+
assertNotNull(handler)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Test
|
|
131
|
+
fun `PermissionHandler class exists in correct package`() {
|
|
132
|
+
val clazz = PermissionHandler::class.java
|
|
133
|
+
assertEquals("com.livetracking.permissions.PermissionHandler", clazz.name)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@Test
|
|
137
|
+
fun `PermissionHandler has checkLocationPermissions method`() {
|
|
138
|
+
val method = PermissionHandler::class.java.methods.find { it.name == "checkLocationPermissions" }
|
|
139
|
+
assertNotNull("checkLocationPermissions method should exist", method)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
@Test
|
|
143
|
+
fun `PermissionHandler has checkBackgroundLocationPermission method`() {
|
|
144
|
+
val method = PermissionHandler::class.java.methods.find { it.name == "checkBackgroundLocationPermission" }
|
|
145
|
+
assertNotNull("checkBackgroundLocationPermission method should exist", method)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
fun `PermissionHandler has isGpsEnabled method`() {
|
|
150
|
+
val method = PermissionHandler::class.java.methods.find { it.name == "isGpsEnabled" }
|
|
151
|
+
assertNotNull("isGpsEnabled method should exist", method)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Test
|
|
155
|
+
fun `PermissionHandler has checkAllTrackingRequirements method`() {
|
|
156
|
+
val method = PermissionHandler::class.java.methods.find { it.name == "checkAllTrackingRequirements" }
|
|
157
|
+
assertNotNull("checkAllTrackingRequirements method should exist", method)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- GPS_DISABLED Error Scenario Tests ---
|
|
161
|
+
|
|
162
|
+
@Test
|
|
163
|
+
fun `GPS_DISABLED error result has correct structure`() {
|
|
164
|
+
val result = PermissionResult.Denied(
|
|
165
|
+
errorCode = PermissionHandler.ERROR_GPS_DISABLED,
|
|
166
|
+
message = "GPS/Location services are disabled. Please enable location services in device settings."
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assertEquals("GPS_DISABLED", result.errorCode)
|
|
170
|
+
assertTrue(result.message.contains("GPS"))
|
|
171
|
+
assertTrue(result.message.contains("disabled"))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Test
|
|
175
|
+
fun `PERMISSION_DENIED error result has correct structure`() {
|
|
176
|
+
val result = PermissionResult.Denied(
|
|
177
|
+
errorCode = PermissionHandler.ERROR_PERMISSION_DENIED,
|
|
178
|
+
message = "Location permissions denied: ACCESS_FINE_LOCATION. Please grant location permissions."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
assertEquals("PERMISSION_DENIED", result.errorCode)
|
|
182
|
+
assertTrue(result.message.contains("ACCESS_FINE_LOCATION"))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/*
|
|
186
|
+
* ============================================================================
|
|
187
|
+
* NOTE: The following tests require Android Context and cannot run as local
|
|
188
|
+
* unit tests. They should be implemented as instrumented tests (androidTest):
|
|
189
|
+
*
|
|
190
|
+
* - checkLocationPermissions returns Granted when both permissions are granted
|
|
191
|
+
* - checkLocationPermissions returns Denied when fine location is denied
|
|
192
|
+
* - checkLocationPermissions returns Denied when coarse location is denied
|
|
193
|
+
* - checkBackgroundLocationPermission returns Granted on API < 29
|
|
194
|
+
* - checkBackgroundLocationPermission returns Denied on API >= 29 without permission
|
|
195
|
+
* - isGpsEnabled returns true when GPS provider is enabled
|
|
196
|
+
* - isGpsEnabled returns false when GPS provider is disabled
|
|
197
|
+
* - checkAllTrackingRequirements returns first Denied result encountered
|
|
198
|
+
* ============================================================================
|
|
199
|
+
*/
|
|
200
|
+
}
|