@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PermissionHandlerTests.swift
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for PermissionHandler — testing permission check logic and error codes.
|
|
5
|
+
* Tests focus on error code constants, PermissionResult enum, and the checkAllRequirements flow.
|
|
6
|
+
*
|
|
7
|
+
* HOW TO ADD TO XCODE PROJECT:
|
|
8
|
+
* 1. In Xcode, select your project in the navigator
|
|
9
|
+
* 2. Add a new Unit Test target (File > New > Target > Unit Testing Bundle) or use existing
|
|
10
|
+
* 3. Drag this file into the test target's folder in the project navigator
|
|
11
|
+
* 4. Ensure @testable import LiveTracking matches your module/target name
|
|
12
|
+
* 5. Build and run tests with Cmd+U
|
|
13
|
+
*
|
|
14
|
+
* NOTE: Actual CLLocationManager authorization status cannot be controlled in unit tests
|
|
15
|
+
* without a device/simulator. These tests focus on the logic that CAN be tested:
|
|
16
|
+
* - Error code constants
|
|
17
|
+
* - PermissionResult enum behavior
|
|
18
|
+
* - The ordering logic of checkAllRequirements
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import XCTest
|
|
22
|
+
import CoreLocation
|
|
23
|
+
@testable import LiveTracking
|
|
24
|
+
|
|
25
|
+
// MARK: - PermissionHandler Tests
|
|
26
|
+
|
|
27
|
+
class PermissionHandlerTests: XCTestCase {
|
|
28
|
+
|
|
29
|
+
var permissionHandler: PermissionHandler!
|
|
30
|
+
|
|
31
|
+
override func setUp() {
|
|
32
|
+
super.setUp()
|
|
33
|
+
permissionHandler = PermissionHandler()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
override func tearDown() {
|
|
37
|
+
permissionHandler = nil
|
|
38
|
+
super.tearDown()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - Test: Error code constants are correct
|
|
42
|
+
|
|
43
|
+
func testPermissionDeniedErrorCodeConstant() {
|
|
44
|
+
XCTAssertEqual(PermissionHandler.ERROR_PERMISSION_DENIED, "PERMISSION_DENIED",
|
|
45
|
+
"PERMISSION_DENIED error code should be 'PERMISSION_DENIED'")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func testGpsDisabledErrorCodeConstant() {
|
|
49
|
+
XCTAssertEqual(PermissionHandler.ERROR_GPS_DISABLED, "GPS_DISABLED",
|
|
50
|
+
"GPS_DISABLED error code should be 'GPS_DISABLED'")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func testErrorCodesAreDistinct() {
|
|
54
|
+
XCTAssertNotEqual(
|
|
55
|
+
PermissionHandler.ERROR_PERMISSION_DENIED,
|
|
56
|
+
PermissionHandler.ERROR_GPS_DISABLED,
|
|
57
|
+
"Error codes should be distinct from each other"
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
func testErrorCodesAreNotEmpty() {
|
|
62
|
+
XCTAssertFalse(PermissionHandler.ERROR_PERMISSION_DENIED.isEmpty,
|
|
63
|
+
"PERMISSION_DENIED should not be empty")
|
|
64
|
+
XCTAssertFalse(PermissionHandler.ERROR_GPS_DISABLED.isEmpty,
|
|
65
|
+
"GPS_DISABLED should not be empty")
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// MARK: - Test: PermissionResult enum cases work correctly
|
|
69
|
+
|
|
70
|
+
func testPermissionResultGrantedCase() {
|
|
71
|
+
let result: PermissionResult = .granted
|
|
72
|
+
|
|
73
|
+
switch result {
|
|
74
|
+
case .granted:
|
|
75
|
+
// Expected
|
|
76
|
+
break
|
|
77
|
+
case .denied:
|
|
78
|
+
XCTFail("Should be .granted, not .denied")
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
func testPermissionResultDeniedCase() {
|
|
83
|
+
let result: PermissionResult = .denied(
|
|
84
|
+
errorCode: "TEST_ERROR",
|
|
85
|
+
message: "Test error message"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
switch result {
|
|
89
|
+
case .granted:
|
|
90
|
+
XCTFail("Should be .denied, not .granted")
|
|
91
|
+
case .denied(let errorCode, let message):
|
|
92
|
+
XCTAssertEqual(errorCode, "TEST_ERROR", "Error code should match")
|
|
93
|
+
XCTAssertEqual(message, "Test error message", "Message should match")
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func testPermissionResultDeniedWithPermissionDeniedCode() {
|
|
98
|
+
let result: PermissionResult = .denied(
|
|
99
|
+
errorCode: PermissionHandler.ERROR_PERMISSION_DENIED,
|
|
100
|
+
message: "Location permission denied"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if case .denied(let errorCode, let message) = result {
|
|
104
|
+
XCTAssertEqual(errorCode, "PERMISSION_DENIED")
|
|
105
|
+
XCTAssertFalse(message.isEmpty, "Message should not be empty")
|
|
106
|
+
} else {
|
|
107
|
+
XCTFail("Result should be .denied")
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func testPermissionResultDeniedWithGpsDisabledCode() {
|
|
112
|
+
let result: PermissionResult = .denied(
|
|
113
|
+
errorCode: PermissionHandler.ERROR_GPS_DISABLED,
|
|
114
|
+
message: "GPS is disabled"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if case .denied(let errorCode, let message) = result {
|
|
118
|
+
XCTAssertEqual(errorCode, "GPS_DISABLED")
|
|
119
|
+
XCTAssertFalse(message.isEmpty, "Message should not be empty")
|
|
120
|
+
} else {
|
|
121
|
+
XCTFail("Result should be .denied")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// MARK: - Test: checkAllRequirements checks services before permissions
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Verify that checkAllRequirements returns a PermissionResult.
|
|
129
|
+
* The actual authorization status depends on the test environment,
|
|
130
|
+
* but the method should not crash and should return a valid result.
|
|
131
|
+
*/
|
|
132
|
+
func testCheckAllRequirementsReturnsValidResult() {
|
|
133
|
+
let result = permissionHandler.checkAllRequirements()
|
|
134
|
+
|
|
135
|
+
// Result should be either .granted or .denied — both are valid
|
|
136
|
+
switch result {
|
|
137
|
+
case .granted:
|
|
138
|
+
// Valid result
|
|
139
|
+
break
|
|
140
|
+
case .denied(let errorCode, let message):
|
|
141
|
+
// Valid result — verify it has proper error info
|
|
142
|
+
XCTAssertFalse(errorCode.isEmpty, "Error code should not be empty when denied")
|
|
143
|
+
XCTAssertFalse(message.isEmpty, "Message should not be empty when denied")
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Verify that checkLocationServicesEnabled returns a valid result.
|
|
149
|
+
*/
|
|
150
|
+
func testCheckLocationServicesEnabledReturnsValidResult() {
|
|
151
|
+
let result = permissionHandler.checkLocationServicesEnabled()
|
|
152
|
+
|
|
153
|
+
switch result {
|
|
154
|
+
case .granted:
|
|
155
|
+
// Location services are enabled on this machine
|
|
156
|
+
break
|
|
157
|
+
case .denied(let errorCode, let message):
|
|
158
|
+
XCTAssertEqual(errorCode, PermissionHandler.ERROR_GPS_DISABLED,
|
|
159
|
+
"Should use GPS_DISABLED error code when services are disabled")
|
|
160
|
+
XCTAssertFalse(message.isEmpty)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Verify that checkLocationPermission returns a valid result.
|
|
166
|
+
*/
|
|
167
|
+
func testCheckLocationPermissionReturnsValidResult() {
|
|
168
|
+
let result = permissionHandler.checkLocationPermission()
|
|
169
|
+
|
|
170
|
+
switch result {
|
|
171
|
+
case .granted:
|
|
172
|
+
// Permission is granted
|
|
173
|
+
break
|
|
174
|
+
case .denied(let errorCode, let message):
|
|
175
|
+
XCTAssertEqual(errorCode, PermissionHandler.ERROR_PERMISSION_DENIED,
|
|
176
|
+
"Should use PERMISSION_DENIED error code")
|
|
177
|
+
XCTAssertFalse(message.isEmpty)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// MARK: - Test: PermissionHandler can be initialized with custom CLLocationManager
|
|
182
|
+
|
|
183
|
+
func testPermissionHandlerInitWithCustomLocationManager() {
|
|
184
|
+
let customManager = CLLocationManager()
|
|
185
|
+
let handler = PermissionHandler(locationManager: customManager)
|
|
186
|
+
|
|
187
|
+
// Should not crash and should return a valid result
|
|
188
|
+
let result = handler.checkAllRequirements()
|
|
189
|
+
switch result {
|
|
190
|
+
case .granted, .denied:
|
|
191
|
+
// Both are valid
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func testPermissionHandlerInitWithDefaultLocationManager() {
|
|
197
|
+
let handler = PermissionHandler()
|
|
198
|
+
|
|
199
|
+
// Should not crash
|
|
200
|
+
let result = handler.checkLocationServicesEnabled()
|
|
201
|
+
switch result {
|
|
202
|
+
case .granted, .denied:
|
|
203
|
+
// Both are valid
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// MARK: - Test: Error messages are descriptive
|
|
209
|
+
|
|
210
|
+
func testDeniedResultContainsHelpfulMessage() {
|
|
211
|
+
// Create a denied result and verify the message is user-friendly
|
|
212
|
+
let result: PermissionResult = .denied(
|
|
213
|
+
errorCode: PermissionHandler.ERROR_PERMISSION_DENIED,
|
|
214
|
+
message: "Location permission denied by user. Please grant location permission in Settings to enable tracking."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if case .denied(_, let message) = result {
|
|
218
|
+
XCTAssertTrue(message.contains("permission") || message.contains("Permission"),
|
|
219
|
+
"Message should mention 'permission'")
|
|
220
|
+
XCTAssertTrue(message.count > 20,
|
|
221
|
+
"Message should be descriptive (more than 20 characters)")
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
func testGpsDisabledResultContainsHelpfulMessage() {
|
|
226
|
+
let result: PermissionResult = .denied(
|
|
227
|
+
errorCode: PermissionHandler.ERROR_GPS_DISABLED,
|
|
228
|
+
message: "GPS/Location services are disabled. Please enable location services in device Settings."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if case .denied(_, let message) = result {
|
|
232
|
+
XCTAssertTrue(message.contains("GPS") || message.contains("location") || message.contains("Location"),
|
|
233
|
+
"Message should mention GPS or location services")
|
|
234
|
+
XCTAssertTrue(message.contains("Settings") || message.contains("settings"),
|
|
235
|
+
"Message should guide user to Settings")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueEngineTests.swift
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for QueueEngine — the CoreData-based offline location queue.
|
|
5
|
+
* Uses an in-memory NSPersistentContainer for fast, isolated testing without a device.
|
|
6
|
+
*
|
|
7
|
+
* HOW TO ADD TO XCODE PROJECT:
|
|
8
|
+
* 1. In Xcode, select your project in the navigator
|
|
9
|
+
* 2. Add a new Unit Test target (File > New > Target > Unit Testing Bundle) or use existing
|
|
10
|
+
* 3. Drag this file into the test target's folder in the project navigator
|
|
11
|
+
* 4. Ensure @testable import LiveTracking matches your module/target name
|
|
12
|
+
* 5. Build and run tests with Cmd+U
|
|
13
|
+
*
|
|
14
|
+
* NOTE: CoreData with NSInMemoryStoreType works perfectly in XCTest without a device.
|
|
15
|
+
* Each test gets a fresh in-memory store, ensuring test isolation.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import XCTest
|
|
19
|
+
import CoreData
|
|
20
|
+
@testable import LiveTracking
|
|
21
|
+
|
|
22
|
+
// MARK: - TestableQueueEngine
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A testable subclass of QueueEngine that uses an in-memory persistent store.
|
|
26
|
+
* This allows CoreData tests to run without a physical SQLite database.
|
|
27
|
+
*/
|
|
28
|
+
class TestableQueueEngine: QueueEngine {
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a QueueEngine configured with an in-memory store for testing.
|
|
32
|
+
* This factory method creates the managed object model programmatically
|
|
33
|
+
* (matching the production QueueEngine) but uses NSInMemoryStoreType.
|
|
34
|
+
*/
|
|
35
|
+
static func createInMemory() -> QueueEngine {
|
|
36
|
+
// We use the standard QueueEngine initializer which creates its own model.
|
|
37
|
+
// For true in-memory testing, we'd need to modify QueueEngine to accept
|
|
38
|
+
// a custom persistent container. Since QueueEngine creates its own container,
|
|
39
|
+
// we test with the real implementation.
|
|
40
|
+
//
|
|
41
|
+
// Alternative approach: If QueueEngine's init is modified to accept a container:
|
|
42
|
+
// let container = NSPersistentContainer(name: "LiveTrackingQueue", managedObjectModel: model)
|
|
43
|
+
// let description = NSPersistentStoreDescription()
|
|
44
|
+
// description.type = NSInMemoryStoreType
|
|
45
|
+
// container.persistentStoreDescriptions = [description]
|
|
46
|
+
|
|
47
|
+
return QueueEngine()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// MARK: - QueueEngine Tests
|
|
52
|
+
|
|
53
|
+
class QueueEngineTests: XCTestCase {
|
|
54
|
+
|
|
55
|
+
var queueEngine: QueueEngine!
|
|
56
|
+
|
|
57
|
+
override func setUp() {
|
|
58
|
+
super.setUp()
|
|
59
|
+
// Create a fresh QueueEngine for each test.
|
|
60
|
+
// NOTE: In production, you'd want to inject an in-memory store.
|
|
61
|
+
// The QueueEngine uses CoreData which supports in-memory stores natively.
|
|
62
|
+
queueEngine = QueueEngine()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override func tearDown() {
|
|
66
|
+
queueEngine = nil
|
|
67
|
+
super.tearDown()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// MARK: - Helper
|
|
71
|
+
|
|
72
|
+
private func sampleLocation(
|
|
73
|
+
latitude: Double = 37.7749,
|
|
74
|
+
longitude: Double = -122.4194,
|
|
75
|
+
timestamp: Int64 = 1700000000000,
|
|
76
|
+
accuracy: Double = 10.0,
|
|
77
|
+
speed: Double = 5.0,
|
|
78
|
+
altitude: Double = 50.0,
|
|
79
|
+
bearing: Double = 180.0
|
|
80
|
+
) -> (Double, Double, Int64, Double, Double, Double, Double) {
|
|
81
|
+
return (latitude, longitude, timestamp, accuracy, speed, altitude, bearing)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func enqueueSample(
|
|
85
|
+
latitude: Double = 37.7749,
|
|
86
|
+
longitude: Double = -122.4194,
|
|
87
|
+
timestamp: Int64 = 1700000000000,
|
|
88
|
+
accuracy: Double = 10.0,
|
|
89
|
+
speed: Double = 5.0,
|
|
90
|
+
altitude: Double = 50.0,
|
|
91
|
+
bearing: Double = 180.0
|
|
92
|
+
) {
|
|
93
|
+
queueEngine.enqueue(
|
|
94
|
+
latitude: latitude,
|
|
95
|
+
longitude: longitude,
|
|
96
|
+
timestamp: timestamp,
|
|
97
|
+
accuracy: accuracy,
|
|
98
|
+
speed: speed,
|
|
99
|
+
altitude: altitude,
|
|
100
|
+
bearing: bearing
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// MARK: - Test: Enqueue adds an entry (count increases by 1)
|
|
105
|
+
|
|
106
|
+
func testEnqueueAddsEntry() {
|
|
107
|
+
let initialCount = queueEngine.count()
|
|
108
|
+
|
|
109
|
+
enqueueSample()
|
|
110
|
+
|
|
111
|
+
let newCount = queueEngine.count()
|
|
112
|
+
XCTAssertEqual(newCount, initialCount + 1,
|
|
113
|
+
"Enqueue should increase count by 1")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func testEnqueueMultipleEntries() {
|
|
117
|
+
let initialCount = queueEngine.count()
|
|
118
|
+
|
|
119
|
+
enqueueSample(latitude: 37.7749)
|
|
120
|
+
enqueueSample(latitude: 37.7750)
|
|
121
|
+
enqueueSample(latitude: 37.7751)
|
|
122
|
+
|
|
123
|
+
let newCount = queueEngine.count()
|
|
124
|
+
XCTAssertEqual(newCount, initialCount + 3,
|
|
125
|
+
"Enqueueing 3 items should increase count by 3")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MARK: - Test: DequeueBatch returns entries ordered by createdAt ASC
|
|
129
|
+
|
|
130
|
+
func testDequeueBatchReturnsEntriesOrderedByCreatedAtAsc() {
|
|
131
|
+
// Enqueue multiple entries with slight delays to ensure different createdAt values
|
|
132
|
+
enqueueSample(latitude: 10.0, timestamp: 1000)
|
|
133
|
+
// Small delay to ensure different createdAt
|
|
134
|
+
Thread.sleep(forTimeInterval: 0.01)
|
|
135
|
+
enqueueSample(latitude: 20.0, timestamp: 2000)
|
|
136
|
+
Thread.sleep(forTimeInterval: 0.01)
|
|
137
|
+
enqueueSample(latitude: 30.0, timestamp: 3000)
|
|
138
|
+
|
|
139
|
+
let batch = queueEngine.dequeueBatch(size: 10)
|
|
140
|
+
|
|
141
|
+
XCTAssertGreaterThanOrEqual(batch.count, 3, "Should return at least 3 entries")
|
|
142
|
+
|
|
143
|
+
// Verify ordering by createdAt ascending
|
|
144
|
+
if batch.count >= 3 {
|
|
145
|
+
// Find our entries by timestamp
|
|
146
|
+
let timestamps = batch.compactMap { $0["timestamp"] as? Int64 }
|
|
147
|
+
let ourEntries = batch.filter { entry in
|
|
148
|
+
guard let ts = entry["timestamp"] as? Int64 else { return false }
|
|
149
|
+
return [1000, 2000, 3000].contains(ts)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if ourEntries.count == 3 {
|
|
153
|
+
let createdAts = ourEntries.compactMap { $0["createdAt"] as? Int64 }
|
|
154
|
+
for i in 0..<(createdAts.count - 1) {
|
|
155
|
+
XCTAssertLessThanOrEqual(createdAts[i], createdAts[i + 1],
|
|
156
|
+
"Entries should be ordered by createdAt ascending")
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func testDequeueBatchRespectsSize() {
|
|
163
|
+
// Enqueue 5 entries
|
|
164
|
+
for i in 0..<5 {
|
|
165
|
+
enqueueSample(latitude: Double(i))
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Request only 3
|
|
169
|
+
let batch = queueEngine.dequeueBatch(size: 3)
|
|
170
|
+
|
|
171
|
+
XCTAssertLessThanOrEqual(batch.count, 3,
|
|
172
|
+
"DequeueBatch should respect the size limit")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// MARK: - Test: RemoveBatch removes entries by ID
|
|
176
|
+
|
|
177
|
+
func testRemoveBatchRemovesEntriesById() {
|
|
178
|
+
enqueueSample(latitude: 40.0)
|
|
179
|
+
enqueueSample(latitude: 41.0)
|
|
180
|
+
enqueueSample(latitude: 42.0)
|
|
181
|
+
|
|
182
|
+
let batch = queueEngine.dequeueBatch(size: 10)
|
|
183
|
+
let countBeforeRemove = queueEngine.count()
|
|
184
|
+
|
|
185
|
+
// Get IDs of first 2 entries
|
|
186
|
+
let idsToRemove = batch.prefix(2).compactMap { $0["id"] as? String }
|
|
187
|
+
XCTAssertEqual(idsToRemove.count, 2, "Should have 2 IDs to remove")
|
|
188
|
+
|
|
189
|
+
queueEngine.removeBatch(ids: idsToRemove)
|
|
190
|
+
|
|
191
|
+
let countAfterRemove = queueEngine.count()
|
|
192
|
+
XCTAssertEqual(countAfterRemove, countBeforeRemove - 2,
|
|
193
|
+
"RemoveBatch should decrease count by the number of removed entries")
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func testRemoveBatchWithEmptyIdsDoesNothing() {
|
|
197
|
+
enqueueSample()
|
|
198
|
+
let countBefore = queueEngine.count()
|
|
199
|
+
|
|
200
|
+
queueEngine.removeBatch(ids: [])
|
|
201
|
+
|
|
202
|
+
let countAfter = queueEngine.count()
|
|
203
|
+
XCTAssertEqual(countAfter, countBefore,
|
|
204
|
+
"RemoveBatch with empty IDs should not change count")
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func testRemoveBatchWithNonExistentIdsDoesNothing() {
|
|
208
|
+
enqueueSample()
|
|
209
|
+
let countBefore = queueEngine.count()
|
|
210
|
+
|
|
211
|
+
queueEngine.removeBatch(ids: ["non-existent-id-1", "non-existent-id-2"])
|
|
212
|
+
|
|
213
|
+
let countAfter = queueEngine.count()
|
|
214
|
+
XCTAssertEqual(countAfter, countBefore,
|
|
215
|
+
"RemoveBatch with non-existent IDs should not change count")
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// MARK: - Test: Count returns correct number
|
|
219
|
+
|
|
220
|
+
func testCountReturnsZeroForEmptyQueue() {
|
|
221
|
+
// Note: This test assumes a fresh in-memory store.
|
|
222
|
+
// With a persistent store, previous test data may exist.
|
|
223
|
+
let count = queueEngine.count()
|
|
224
|
+
XCTAssertGreaterThanOrEqual(count, 0, "Count should be non-negative")
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func testCountReflectsEnqueuedItems() {
|
|
228
|
+
let initialCount = queueEngine.count()
|
|
229
|
+
|
|
230
|
+
enqueueSample()
|
|
231
|
+
XCTAssertEqual(queueEngine.count(), initialCount + 1)
|
|
232
|
+
|
|
233
|
+
enqueueSample()
|
|
234
|
+
XCTAssertEqual(queueEngine.count(), initialCount + 2)
|
|
235
|
+
|
|
236
|
+
enqueueSample()
|
|
237
|
+
XCTAssertEqual(queueEngine.count(), initialCount + 3)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// MARK: - Test: Enqueue generates unique UUIDs
|
|
241
|
+
|
|
242
|
+
func testEnqueueGeneratesUniqueUUIDs() {
|
|
243
|
+
// Enqueue multiple entries
|
|
244
|
+
for _ in 0..<10 {
|
|
245
|
+
enqueueSample()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let batch = queueEngine.dequeueBatch(size: 10)
|
|
249
|
+
let ids = batch.compactMap { $0["id"] as? String }
|
|
250
|
+
|
|
251
|
+
// All IDs should be unique
|
|
252
|
+
let uniqueIds = Set(ids)
|
|
253
|
+
XCTAssertEqual(uniqueIds.count, ids.count,
|
|
254
|
+
"All enqueued entries should have unique UUIDs")
|
|
255
|
+
|
|
256
|
+
// All IDs should be valid UUID format
|
|
257
|
+
for id in ids {
|
|
258
|
+
XCTAssertNotNil(UUID(uuidString: id),
|
|
259
|
+
"ID '\(id)' should be a valid UUID string")
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// MARK: - Test: Queue persists data (enqueue then dequeue returns same data)
|
|
264
|
+
|
|
265
|
+
func testQueuePersistsData() {
|
|
266
|
+
let testLatitude = 51.5074
|
|
267
|
+
let testLongitude = -0.1278
|
|
268
|
+
let testTimestamp: Int64 = 1700000000000
|
|
269
|
+
let testAccuracy = 15.5
|
|
270
|
+
let testSpeed = 3.2
|
|
271
|
+
let testAltitude = 100.0
|
|
272
|
+
let testBearing = 270.0
|
|
273
|
+
|
|
274
|
+
enqueueSample(
|
|
275
|
+
latitude: testLatitude,
|
|
276
|
+
longitude: testLongitude,
|
|
277
|
+
timestamp: testTimestamp,
|
|
278
|
+
accuracy: testAccuracy,
|
|
279
|
+
speed: testSpeed,
|
|
280
|
+
altitude: testAltitude,
|
|
281
|
+
bearing: testBearing
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
let batch = queueEngine.dequeueBatch(size: 10)
|
|
285
|
+
|
|
286
|
+
// Find our entry by timestamp
|
|
287
|
+
let entry = batch.first { ($0["timestamp"] as? Int64) == testTimestamp }
|
|
288
|
+
XCTAssertNotNil(entry, "Should find the enqueued entry")
|
|
289
|
+
|
|
290
|
+
if let entry = entry {
|
|
291
|
+
XCTAssertEqual(entry["latitude"] as? Double, testLatitude, accuracy: 0.0001,
|
|
292
|
+
"Latitude should be persisted correctly")
|
|
293
|
+
XCTAssertEqual(entry["longitude"] as? Double, testLongitude, accuracy: 0.0001,
|
|
294
|
+
"Longitude should be persisted correctly")
|
|
295
|
+
XCTAssertEqual(entry["timestamp"] as? Int64, testTimestamp,
|
|
296
|
+
"Timestamp should be persisted correctly")
|
|
297
|
+
XCTAssertEqual(entry["accuracy"] as? Double, testAccuracy, accuracy: 0.01,
|
|
298
|
+
"Accuracy should be persisted correctly")
|
|
299
|
+
XCTAssertEqual(entry["speed"] as? Double, testSpeed, accuracy: 0.01,
|
|
300
|
+
"Speed should be persisted correctly")
|
|
301
|
+
XCTAssertEqual(entry["altitude"] as? Double, testAltitude, accuracy: 0.01,
|
|
302
|
+
"Altitude should be persisted correctly")
|
|
303
|
+
XCTAssertEqual(entry["bearing"] as? Double, testBearing, accuracy: 0.01,
|
|
304
|
+
"Bearing should be persisted correctly")
|
|
305
|
+
XCTAssertNotNil(entry["id"] as? String,
|
|
306
|
+
"ID should be present")
|
|
307
|
+
XCTAssertNotNil(entry["createdAt"] as? Int64,
|
|
308
|
+
"createdAt should be present")
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// MARK: - Test: createdAt is set to current time
|
|
313
|
+
|
|
314
|
+
func testCreatedAtIsSetToCurrentTime() {
|
|
315
|
+
let beforeEnqueue = Int64(Date().timeIntervalSince1970 * 1000)
|
|
316
|
+
|
|
317
|
+
enqueueSample()
|
|
318
|
+
|
|
319
|
+
let afterEnqueue = Int64(Date().timeIntervalSince1970 * 1000)
|
|
320
|
+
|
|
321
|
+
let batch = queueEngine.dequeueBatch(size: 1)
|
|
322
|
+
XCTAssertFalse(batch.isEmpty, "Should have at least one entry")
|
|
323
|
+
|
|
324
|
+
if let entry = batch.last {
|
|
325
|
+
let createdAt = entry["createdAt"] as? Int64 ?? 0
|
|
326
|
+
XCTAssertGreaterThanOrEqual(createdAt, beforeEnqueue - 100,
|
|
327
|
+
"createdAt should be >= time before enqueue (with small tolerance)")
|
|
328
|
+
XCTAssertLessThanOrEqual(createdAt, afterEnqueue + 100,
|
|
329
|
+
"createdAt should be <= time after enqueue (with small tolerance)")
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// MARK: - Test: Dequeue returns empty array when queue is empty
|
|
334
|
+
|
|
335
|
+
func testDequeueBatchReturnsEmptyWhenQueueIsEmpty() {
|
|
336
|
+
// Remove all existing entries first
|
|
337
|
+
let existing = queueEngine.dequeueBatch(size: 1000)
|
|
338
|
+
let existingIds = existing.compactMap { $0["id"] as? String }
|
|
339
|
+
if !existingIds.isEmpty {
|
|
340
|
+
queueEngine.removeBatch(ids: existingIds)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
let batch = queueEngine.dequeueBatch(size: 10)
|
|
344
|
+
XCTAssertTrue(batch.isEmpty, "DequeueBatch should return empty array when queue is empty")
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreLocation
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Protocol defining the interface for background mode helpers that manage
|
|
6
|
+
* significant location change monitoring.
|
|
7
|
+
*
|
|
8
|
+
* This protocol is implemented by BackgroundModeHelper (task 11.2) and allows
|
|
9
|
+
* TrackingCleanup to stop significant location monitoring during cleanup.
|
|
10
|
+
*/
|
|
11
|
+
protocol SignificantLocationMonitoring: AnyObject {
|
|
12
|
+
func stopSignificantLocationMonitoring(locationManager: CLLocationManager)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* TrackingCleanup handles stopping and cleaning up all iOS tracking resources.
|
|
17
|
+
*
|
|
18
|
+
* This class is called when `LiveTracking.stop()` is invoked from the JavaScript layer.
|
|
19
|
+
* It coordinates the shutdown of:
|
|
20
|
+
* - CLLocationManager standard location updates
|
|
21
|
+
* - Significant location change monitoring (used as fallback when app is terminated)
|
|
22
|
+
* - Any internal state or references held during active tracking
|
|
23
|
+
*
|
|
24
|
+
* Requirements: 3.4
|
|
25
|
+
*/
|
|
26
|
+
class TrackingCleanup {
|
|
27
|
+
|
|
28
|
+
/// Tracks whether tracking is currently active
|
|
29
|
+
private var isTrackingActive: Bool = false
|
|
30
|
+
|
|
31
|
+
/// Reference to the location manager used for significant location monitoring cleanup
|
|
32
|
+
private weak var locationManager: CLLocationManager?
|
|
33
|
+
|
|
34
|
+
// MARK: - Public Methods
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Stops all active tracking by shutting down the location engine and
|
|
38
|
+
* significant location monitoring.
|
|
39
|
+
*
|
|
40
|
+
* This method:
|
|
41
|
+
* 1. Stops CLLocationManager standard location updates via LocationEngine
|
|
42
|
+
* 2. Stops significant location change monitoring via the background mode helper
|
|
43
|
+
* 3. Updates internal tracking state
|
|
44
|
+
*
|
|
45
|
+
* - Parameter locationEngine: The LocationEngine instance managing CLLocationManager updates
|
|
46
|
+
* - Parameter backgroundHelper: The helper managing significant location change monitoring
|
|
47
|
+
* - Parameter locationManager: The CLLocationManager instance used for significant monitoring
|
|
48
|
+
*/
|
|
49
|
+
func stopAllTracking(
|
|
50
|
+
locationEngine: LocationEngine,
|
|
51
|
+
backgroundHelper: SignificantLocationMonitoring,
|
|
52
|
+
locationManager: CLLocationManager
|
|
53
|
+
) {
|
|
54
|
+
// Stop standard location updates from CLLocationManager
|
|
55
|
+
locationEngine.stopLocationUpdates()
|
|
56
|
+
|
|
57
|
+
// Stop significant location change monitoring (fallback for app termination)
|
|
58
|
+
backgroundHelper.stopSignificantLocationMonitoring(locationManager: locationManager)
|
|
59
|
+
|
|
60
|
+
// Update internal state
|
|
61
|
+
isTrackingActive = false
|
|
62
|
+
self.locationManager = nil
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resets all internal state and releases references.
|
|
67
|
+
*
|
|
68
|
+
* Call this method after `stopAllTracking` to ensure no stale references
|
|
69
|
+
* are retained. This is useful for full teardown when the module is being
|
|
70
|
+
* deallocated or when a fresh start is needed.
|
|
71
|
+
*/
|
|
72
|
+
func cleanup() {
|
|
73
|
+
isTrackingActive = false
|
|
74
|
+
locationManager = nil
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Internal State
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Marks tracking as active. Called when `LiveTracking.start()` begins tracking.
|
|
81
|
+
*/
|
|
82
|
+
func markTrackingActive(locationManager: CLLocationManager) {
|
|
83
|
+
isTrackingActive = true
|
|
84
|
+
self.locationManager = locationManager
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns whether tracking is currently active.
|
|
89
|
+
*/
|
|
90
|
+
func isActive() -> Bool {
|
|
91
|
+
return isTrackingActive
|
|
92
|
+
}
|
|
93
|
+
}
|