@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,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivityRecognitionHandlerTests.swift
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for ActivityRecognitionHandler — testing activity detection state and defaults.
|
|
5
|
+
* Tests focus on enum cases, default values, and initial state.
|
|
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: CMMotionActivityManager requires device hardware (M-series coprocessor).
|
|
15
|
+
* These tests focus on logic that can be tested without hardware:
|
|
16
|
+
* - ActivityType enum cases
|
|
17
|
+
* - Default threshold values
|
|
18
|
+
* - Initial state of the handler
|
|
19
|
+
* - ActivityStateDelegate protocol conformance
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import XCTest
|
|
23
|
+
@testable import LiveTracking
|
|
24
|
+
|
|
25
|
+
// MARK: - Mock ActivityStateDelegate
|
|
26
|
+
|
|
27
|
+
class MockActivityStateDelegate: ActivityStateDelegate {
|
|
28
|
+
var activityChangedCount = 0
|
|
29
|
+
var lastActivity: ActivityType?
|
|
30
|
+
var stationaryDurationExceededCount = 0
|
|
31
|
+
var lastDurationMs: Int64?
|
|
32
|
+
|
|
33
|
+
func onActivityChanged(activity: ActivityType) {
|
|
34
|
+
activityChangedCount += 1
|
|
35
|
+
lastActivity = activity
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func onStationaryDurationExceeded(durationMs: Int64) {
|
|
39
|
+
stationaryDurationExceededCount += 1
|
|
40
|
+
lastDurationMs = durationMs
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - ActivityRecognitionHandler Tests
|
|
45
|
+
|
|
46
|
+
class ActivityRecognitionHandlerTests: XCTestCase {
|
|
47
|
+
|
|
48
|
+
var handler: ActivityRecognitionHandler!
|
|
49
|
+
var mockDelegate: MockActivityStateDelegate!
|
|
50
|
+
|
|
51
|
+
override func setUp() {
|
|
52
|
+
super.setUp()
|
|
53
|
+
handler = ActivityRecognitionHandler()
|
|
54
|
+
mockDelegate = MockActivityStateDelegate()
|
|
55
|
+
handler.delegate = mockDelegate
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override func tearDown() {
|
|
59
|
+
handler = nil
|
|
60
|
+
mockDelegate = nil
|
|
61
|
+
super.tearDown()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MARK: - Test: ActivityType enum has correct cases
|
|
65
|
+
|
|
66
|
+
func testActivityTypeHasStationaryCase() {
|
|
67
|
+
let activity: ActivityType = .stationary
|
|
68
|
+
switch activity {
|
|
69
|
+
case .stationary:
|
|
70
|
+
// Expected
|
|
71
|
+
break
|
|
72
|
+
default:
|
|
73
|
+
XCTFail("Should match .stationary case")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
func testActivityTypeHasWalkingCase() {
|
|
78
|
+
let activity: ActivityType = .walking
|
|
79
|
+
switch activity {
|
|
80
|
+
case .walking:
|
|
81
|
+
// Expected
|
|
82
|
+
break
|
|
83
|
+
default:
|
|
84
|
+
XCTFail("Should match .walking case")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
func testActivityTypeHasAutomotiveCase() {
|
|
89
|
+
let activity: ActivityType = .automotive
|
|
90
|
+
switch activity {
|
|
91
|
+
case .automotive:
|
|
92
|
+
// Expected
|
|
93
|
+
break
|
|
94
|
+
default:
|
|
95
|
+
XCTFail("Should match .automotive case")
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
func testActivityTypeHasUnknownCase() {
|
|
100
|
+
let activity: ActivityType = .unknown
|
|
101
|
+
switch activity {
|
|
102
|
+
case .unknown:
|
|
103
|
+
// Expected
|
|
104
|
+
break
|
|
105
|
+
default:
|
|
106
|
+
XCTFail("Should match .unknown case")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
func testActivityTypeAllCasesAreDistinct() {
|
|
111
|
+
// Verify all cases can be distinguished via switch
|
|
112
|
+
let activities: [ActivityType] = [.stationary, .walking, .automotive, .unknown]
|
|
113
|
+
|
|
114
|
+
var stationaryCount = 0
|
|
115
|
+
var walkingCount = 0
|
|
116
|
+
var automotiveCount = 0
|
|
117
|
+
var unknownCount = 0
|
|
118
|
+
|
|
119
|
+
for activity in activities {
|
|
120
|
+
switch activity {
|
|
121
|
+
case .stationary: stationaryCount += 1
|
|
122
|
+
case .walking: walkingCount += 1
|
|
123
|
+
case .automotive: automotiveCount += 1
|
|
124
|
+
case .unknown: unknownCount += 1
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
XCTAssertEqual(stationaryCount, 1, "Should have exactly one .stationary")
|
|
129
|
+
XCTAssertEqual(walkingCount, 1, "Should have exactly one .walking")
|
|
130
|
+
XCTAssertEqual(automotiveCount, 1, "Should have exactly one .automotive")
|
|
131
|
+
XCTAssertEqual(unknownCount, 1, "Should have exactly one .unknown")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MARK: - Test: stationaryThresholdMs default is 180_000
|
|
135
|
+
|
|
136
|
+
func testStationaryThresholdMsDefaultIs180000() {
|
|
137
|
+
XCTAssertEqual(handler.stationaryThresholdMs, 180_000,
|
|
138
|
+
"Default stationaryThresholdMs should be 180,000 ms (3 minutes)")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
func testStationaryThresholdMsCanBeModified() {
|
|
142
|
+
handler.stationaryThresholdMs = 60_000 // 1 minute
|
|
143
|
+
|
|
144
|
+
XCTAssertEqual(handler.stationaryThresholdMs, 60_000,
|
|
145
|
+
"stationaryThresholdMs should be modifiable")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// MARK: - Test: isDeviceStationary returns false initially
|
|
149
|
+
|
|
150
|
+
func testIsDeviceStationaryReturnsFalseInitially() {
|
|
151
|
+
XCTAssertFalse(handler.isDeviceStationary(),
|
|
152
|
+
"isDeviceStationary should return false before any activity updates")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// MARK: - Test: getCurrentActivity returns unknown initially
|
|
156
|
+
|
|
157
|
+
func testGetCurrentActivityReturnsUnknownInitially() {
|
|
158
|
+
let activity = handler.getCurrentActivity()
|
|
159
|
+
|
|
160
|
+
switch activity {
|
|
161
|
+
case .unknown:
|
|
162
|
+
// Expected
|
|
163
|
+
break
|
|
164
|
+
default:
|
|
165
|
+
XCTFail("getCurrentActivity should return .unknown initially, got \(activity)")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// MARK: - Test: getStationaryDurationMs returns 0 initially
|
|
170
|
+
|
|
171
|
+
func testGetStationaryDurationMsReturnsZeroInitially() {
|
|
172
|
+
let duration = handler.getStationaryDurationMs()
|
|
173
|
+
XCTAssertEqual(duration, 0,
|
|
174
|
+
"getStationaryDurationMs should return 0 when not stationary")
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// MARK: - Test: ActivityStateDelegate protocol has correct methods
|
|
178
|
+
|
|
179
|
+
func testActivityStateDelegateOnActivityChangedMethod() {
|
|
180
|
+
// Verify the delegate protocol method exists and can be called
|
|
181
|
+
mockDelegate.onActivityChanged(activity: .walking)
|
|
182
|
+
|
|
183
|
+
XCTAssertEqual(mockDelegate.activityChangedCount, 1)
|
|
184
|
+
XCTAssertNotNil(mockDelegate.lastActivity)
|
|
185
|
+
|
|
186
|
+
if let lastActivity = mockDelegate.lastActivity {
|
|
187
|
+
switch lastActivity {
|
|
188
|
+
case .walking:
|
|
189
|
+
// Expected
|
|
190
|
+
break
|
|
191
|
+
default:
|
|
192
|
+
XCTFail("Delegate should receive .walking activity")
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func testActivityStateDelegateOnStationaryDurationExceededMethod() {
|
|
198
|
+
// Verify the delegate protocol method exists and can be called
|
|
199
|
+
mockDelegate.onStationaryDurationExceeded(durationMs: 200_000)
|
|
200
|
+
|
|
201
|
+
XCTAssertEqual(mockDelegate.stationaryDurationExceededCount, 1)
|
|
202
|
+
XCTAssertEqual(mockDelegate.lastDurationMs, 200_000)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// MARK: - Test: Handler can be instantiated
|
|
206
|
+
|
|
207
|
+
func testHandlerCanBeInstantiated() {
|
|
208
|
+
let newHandler = ActivityRecognitionHandler()
|
|
209
|
+
XCTAssertNotNil(newHandler, "ActivityRecognitionHandler should be instantiable")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// MARK: - Test: stopActivityRecognition resets state
|
|
213
|
+
|
|
214
|
+
func testStopActivityRecognitionResetsState() {
|
|
215
|
+
// Stop should reset internal state without crashing
|
|
216
|
+
handler.stopActivityRecognition()
|
|
217
|
+
|
|
218
|
+
XCTAssertFalse(handler.isDeviceStationary(),
|
|
219
|
+
"isDeviceStationary should be false after stop")
|
|
220
|
+
XCTAssertEqual(handler.getStationaryDurationMs(), 0,
|
|
221
|
+
"Stationary duration should be 0 after stop")
|
|
222
|
+
|
|
223
|
+
let activity = handler.getCurrentActivity()
|
|
224
|
+
switch activity {
|
|
225
|
+
case .unknown:
|
|
226
|
+
// Expected after reset
|
|
227
|
+
break
|
|
228
|
+
default:
|
|
229
|
+
XCTFail("getCurrentActivity should return .unknown after stop")
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// MARK: - Test: Multiple stop calls don't crash
|
|
234
|
+
|
|
235
|
+
func testMultipleStopCallsDontCrash() {
|
|
236
|
+
handler.stopActivityRecognition()
|
|
237
|
+
handler.stopActivityRecognition()
|
|
238
|
+
handler.stopActivityRecognition()
|
|
239
|
+
|
|
240
|
+
// Should not crash
|
|
241
|
+
XCTAssertFalse(handler.isDeviceStationary())
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// MARK: - Test: Delegate is weak reference
|
|
245
|
+
|
|
246
|
+
func testDelegateIsWeakReference() {
|
|
247
|
+
var delegate: MockActivityStateDelegate? = MockActivityStateDelegate()
|
|
248
|
+
handler.delegate = delegate
|
|
249
|
+
|
|
250
|
+
// Verify delegate is set
|
|
251
|
+
XCTAssertNotNil(handler.delegate)
|
|
252
|
+
|
|
253
|
+
// Release the delegate
|
|
254
|
+
delegate = nil
|
|
255
|
+
|
|
256
|
+
// Delegate should be nil (weak reference)
|
|
257
|
+
XCTAssertNil(handler.delegate, "Delegate should be a weak reference")
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FirebaseSyncEngineTests.swift
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for FirebaseSyncEngine — focusing on retry/backoff logic and path validation.
|
|
5
|
+
* The calculateBackoffDelay method is `internal` and accessible within the same module via @testable import.
|
|
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: Firebase SDK calls are NOT mocked here — these tests focus on the backoff calculation
|
|
15
|
+
* and path validation logic which don't require Firebase connectivity.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import XCTest
|
|
19
|
+
@testable import LiveTracking
|
|
20
|
+
|
|
21
|
+
// MARK: - Mock SyncCallback
|
|
22
|
+
|
|
23
|
+
class MockSyncCallback: SyncCallback {
|
|
24
|
+
var successCount = 0
|
|
25
|
+
var errorCount = 0
|
|
26
|
+
var lastErrorCode: String?
|
|
27
|
+
var lastErrorMessage: String?
|
|
28
|
+
|
|
29
|
+
var onSuccessExpectation: XCTestExpectation?
|
|
30
|
+
var onErrorExpectation: XCTestExpectation?
|
|
31
|
+
|
|
32
|
+
func onSuccess() {
|
|
33
|
+
successCount += 1
|
|
34
|
+
onSuccessExpectation?.fulfill()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func onError(errorCode: String, message: String) {
|
|
38
|
+
errorCount += 1
|
|
39
|
+
lastErrorCode = errorCode
|
|
40
|
+
lastErrorMessage = message
|
|
41
|
+
onErrorExpectation?.fulfill()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// MARK: - FirebaseSyncEngine Tests
|
|
46
|
+
|
|
47
|
+
class FirebaseSyncEngineTests: XCTestCase {
|
|
48
|
+
|
|
49
|
+
var syncEngine: FirebaseSyncEngine!
|
|
50
|
+
var mockCallback: MockSyncCallback!
|
|
51
|
+
|
|
52
|
+
override func setUp() {
|
|
53
|
+
super.setUp()
|
|
54
|
+
mockCallback = MockSyncCallback()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
override func tearDown() {
|
|
58
|
+
syncEngine = nil
|
|
59
|
+
mockCallback = nil
|
|
60
|
+
super.tearDown()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Backoff Delay Tests
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Test: calculateBackoffDelay(attempt: 1) ≈ 1000ms ±200ms
|
|
67
|
+
*
|
|
68
|
+
* Formula: baseDelay(1000) × 2^(1-1) ± jitter(200)
|
|
69
|
+
* = 1000 × 1 ± 200
|
|
70
|
+
* = 800 to 1200
|
|
71
|
+
*/
|
|
72
|
+
func testCalculateBackoffDelayAttempt1() {
|
|
73
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
74
|
+
|
|
75
|
+
let delay = syncEngine.calculateBackoffDelay(attempt: 1)
|
|
76
|
+
|
|
77
|
+
XCTAssertGreaterThanOrEqual(delay, 800,
|
|
78
|
+
"Attempt 1 delay should be >= 800ms (1000 - 200 jitter)")
|
|
79
|
+
XCTAssertLessThanOrEqual(delay, 1200,
|
|
80
|
+
"Attempt 1 delay should be <= 1200ms (1000 + 200 jitter)")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Test: calculateBackoffDelay(attempt: 2) ≈ 2000ms ±200ms
|
|
85
|
+
*
|
|
86
|
+
* Formula: baseDelay(1000) × 2^(2-1) ± jitter(200)
|
|
87
|
+
* = 1000 × 2 ± 200
|
|
88
|
+
* = 1800 to 2200
|
|
89
|
+
*/
|
|
90
|
+
func testCalculateBackoffDelayAttempt2() {
|
|
91
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
92
|
+
|
|
93
|
+
let delay = syncEngine.calculateBackoffDelay(attempt: 2)
|
|
94
|
+
|
|
95
|
+
XCTAssertGreaterThanOrEqual(delay, 1800,
|
|
96
|
+
"Attempt 2 delay should be >= 1800ms (2000 - 200 jitter)")
|
|
97
|
+
XCTAssertLessThanOrEqual(delay, 2200,
|
|
98
|
+
"Attempt 2 delay should be <= 2200ms (2000 + 200 jitter)")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Test: calculateBackoffDelay(attempt: 3) ≈ 4000ms ±200ms
|
|
103
|
+
*
|
|
104
|
+
* Formula: baseDelay(1000) × 2^(3-1) ± jitter(200)
|
|
105
|
+
* = 1000 × 4 ± 200
|
|
106
|
+
* = 3800 to 4200
|
|
107
|
+
*/
|
|
108
|
+
func testCalculateBackoffDelayAttempt3() {
|
|
109
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
110
|
+
|
|
111
|
+
let delay = syncEngine.calculateBackoffDelay(attempt: 3)
|
|
112
|
+
|
|
113
|
+
XCTAssertGreaterThanOrEqual(delay, 3800,
|
|
114
|
+
"Attempt 3 delay should be >= 3800ms (4000 - 200 jitter)")
|
|
115
|
+
XCTAssertLessThanOrEqual(delay, 4200,
|
|
116
|
+
"Attempt 3 delay should be <= 4200ms (4000 + 200 jitter)")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Test: delay is never negative
|
|
121
|
+
*
|
|
122
|
+
* The implementation uses max(0, ...) to ensure non-negative delays.
|
|
123
|
+
* Even with negative jitter on attempt 1, the result should be >= 0.
|
|
124
|
+
*/
|
|
125
|
+
func testDelayIsNeverNegative() {
|
|
126
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
127
|
+
|
|
128
|
+
// Run multiple times to account for random jitter
|
|
129
|
+
for attempt in 1...10 {
|
|
130
|
+
for _ in 0..<100 {
|
|
131
|
+
let delay = syncEngine.calculateBackoffDelay(attempt: attempt)
|
|
132
|
+
XCTAssertGreaterThanOrEqual(delay, 0,
|
|
133
|
+
"Delay should never be negative for attempt \(attempt)")
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Test: updateCurrentLocation with nil path calls onError
|
|
140
|
+
*
|
|
141
|
+
* When currentLocationPath is nil, the engine should immediately call onError
|
|
142
|
+
* with "NO_PATH" error code.
|
|
143
|
+
*/
|
|
144
|
+
func testUpdateCurrentLocationWithNilPathCallsOnError() {
|
|
145
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: nil, historyPath: "/history")
|
|
146
|
+
|
|
147
|
+
let expectation = self.expectation(description: "onError should be called")
|
|
148
|
+
mockCallback.onErrorExpectation = expectation
|
|
149
|
+
|
|
150
|
+
syncEngine.updateCurrentLocation(
|
|
151
|
+
latitude: 37.7749,
|
|
152
|
+
longitude: -122.4194,
|
|
153
|
+
timestamp: 1700000000000,
|
|
154
|
+
accuracy: 10.0,
|
|
155
|
+
speed: 5.0,
|
|
156
|
+
callback: mockCallback
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
waitForExpectations(timeout: 1.0) { error in
|
|
160
|
+
XCTAssertNil(error)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
XCTAssertEqual(mockCallback.errorCount, 1, "onError should be called exactly once")
|
|
164
|
+
XCTAssertEqual(mockCallback.lastErrorCode, "NO_PATH", "Error code should be NO_PATH")
|
|
165
|
+
XCTAssertEqual(mockCallback.successCount, 0, "onSuccess should not be called")
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Test: pushHistoryBatch with empty array calls onSuccess
|
|
170
|
+
*
|
|
171
|
+
* When locations array is empty, the engine should immediately call onSuccess
|
|
172
|
+
* without attempting a Firebase write.
|
|
173
|
+
*/
|
|
174
|
+
func testPushHistoryBatchWithEmptyArrayCallsOnSuccess() {
|
|
175
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/current", historyPath: "/history")
|
|
176
|
+
|
|
177
|
+
let expectation = self.expectation(description: "onSuccess should be called")
|
|
178
|
+
mockCallback.onSuccessExpectation = expectation
|
|
179
|
+
|
|
180
|
+
syncEngine.pushHistoryBatch(locations: [], callback: mockCallback)
|
|
181
|
+
|
|
182
|
+
waitForExpectations(timeout: 1.0) { error in
|
|
183
|
+
XCTAssertNil(error)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
XCTAssertEqual(mockCallback.successCount, 1, "onSuccess should be called for empty batch")
|
|
187
|
+
XCTAssertEqual(mockCallback.errorCount, 0, "onError should not be called for empty batch")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Test: pushHistoryBatch with nil path calls onError
|
|
192
|
+
*
|
|
193
|
+
* When historyPath is nil, the engine should immediately call onError
|
|
194
|
+
* with "NO_PATH" error code.
|
|
195
|
+
*/
|
|
196
|
+
func testPushHistoryBatchWithNilPathCallsOnError() {
|
|
197
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/current", historyPath: nil)
|
|
198
|
+
|
|
199
|
+
let expectation = self.expectation(description: "onError should be called")
|
|
200
|
+
mockCallback.onErrorExpectation = expectation
|
|
201
|
+
|
|
202
|
+
let locations: [[String: Any]] = [
|
|
203
|
+
["latitude": 37.7749, "longitude": -122.4194, "timestamp": 1700000000000]
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
syncEngine.pushHistoryBatch(locations: locations, callback: mockCallback)
|
|
207
|
+
|
|
208
|
+
waitForExpectations(timeout: 1.0) { error in
|
|
209
|
+
XCTAssertNil(error)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
XCTAssertEqual(mockCallback.errorCount, 1, "onError should be called exactly once")
|
|
213
|
+
XCTAssertEqual(mockCallback.lastErrorCode, "NO_PATH", "Error code should be NO_PATH")
|
|
214
|
+
XCTAssertEqual(mockCallback.successCount, 0, "onSuccess should not be called")
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Test: jitter produces variation across multiple calls
|
|
219
|
+
*
|
|
220
|
+
* Running calculateBackoffDelay multiple times for the same attempt should produce
|
|
221
|
+
* different values due to random jitter (±200ms range).
|
|
222
|
+
*/
|
|
223
|
+
func testJitterProducesVariation() {
|
|
224
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
225
|
+
|
|
226
|
+
var delays: Set<Int> = []
|
|
227
|
+
|
|
228
|
+
// Run 50 times — with ±200ms jitter range (401 possible values), we should see variation
|
|
229
|
+
for _ in 0..<50 {
|
|
230
|
+
let delay = syncEngine.calculateBackoffDelay(attempt: 1)
|
|
231
|
+
delays.insert(delay)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// With 401 possible values and 50 samples, we should get at least 2 distinct values
|
|
235
|
+
XCTAssertGreaterThan(delays.count, 1,
|
|
236
|
+
"Jitter should produce variation across multiple calls. Got \(delays.count) unique values from 50 samples")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Test: backoff grows exponentially
|
|
241
|
+
*
|
|
242
|
+
* Verify that higher attempts produce larger delays (on average).
|
|
243
|
+
*/
|
|
244
|
+
func testBackoffGrowsExponentially() {
|
|
245
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
246
|
+
|
|
247
|
+
// Calculate average delay for each attempt over multiple samples
|
|
248
|
+
let samples = 100
|
|
249
|
+
var avgDelay1: Double = 0
|
|
250
|
+
var avgDelay2: Double = 0
|
|
251
|
+
var avgDelay3: Double = 0
|
|
252
|
+
|
|
253
|
+
for _ in 0..<samples {
|
|
254
|
+
avgDelay1 += Double(syncEngine.calculateBackoffDelay(attempt: 1))
|
|
255
|
+
avgDelay2 += Double(syncEngine.calculateBackoffDelay(attempt: 2))
|
|
256
|
+
avgDelay3 += Double(syncEngine.calculateBackoffDelay(attempt: 3))
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
avgDelay1 /= Double(samples)
|
|
260
|
+
avgDelay2 /= Double(samples)
|
|
261
|
+
avgDelay3 /= Double(samples)
|
|
262
|
+
|
|
263
|
+
XCTAssertLessThan(avgDelay1, avgDelay2, "Attempt 2 should have higher average delay than attempt 1")
|
|
264
|
+
XCTAssertLessThan(avgDelay2, avgDelay3, "Attempt 3 should have higher average delay than attempt 2")
|
|
265
|
+
|
|
266
|
+
// Verify approximate 2x growth
|
|
267
|
+
let ratio2to1 = avgDelay2 / avgDelay1
|
|
268
|
+
let ratio3to2 = avgDelay3 / avgDelay2
|
|
269
|
+
|
|
270
|
+
XCTAssertGreaterThan(ratio2to1, 1.5, "Delay should roughly double between attempts 1 and 2")
|
|
271
|
+
XCTAssertLessThan(ratio2to1, 2.5, "Delay should roughly double between attempts 1 and 2")
|
|
272
|
+
XCTAssertGreaterThan(ratio3to2, 1.5, "Delay should roughly double between attempts 2 and 3")
|
|
273
|
+
XCTAssertLessThan(ratio3to2, 2.5, "Delay should roughly double between attempts 2 and 3")
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Test: attempt 4 delay ≈ 8000ms ±200ms
|
|
278
|
+
*
|
|
279
|
+
* Formula: 1000 × 2^(4-1) = 8000 ± 200
|
|
280
|
+
*/
|
|
281
|
+
func testCalculateBackoffDelayAttempt4() {
|
|
282
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
283
|
+
|
|
284
|
+
let delay = syncEngine.calculateBackoffDelay(attempt: 4)
|
|
285
|
+
|
|
286
|
+
XCTAssertGreaterThanOrEqual(delay, 7800, "Attempt 4 delay should be >= 7800ms")
|
|
287
|
+
XCTAssertLessThanOrEqual(delay, 8200, "Attempt 4 delay should be <= 8200ms")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Test: attempt 5 delay ≈ 16000ms ±200ms
|
|
292
|
+
*
|
|
293
|
+
* Formula: 1000 × 2^(5-1) = 16000 ± 200
|
|
294
|
+
*/
|
|
295
|
+
func testCalculateBackoffDelayAttempt5() {
|
|
296
|
+
syncEngine = FirebaseSyncEngine(service: "RTDB", currentLocationPath: "/test", historyPath: "/history")
|
|
297
|
+
|
|
298
|
+
let delay = syncEngine.calculateBackoffDelay(attempt: 5)
|
|
299
|
+
|
|
300
|
+
XCTAssertGreaterThanOrEqual(delay, 15800, "Attempt 5 delay should be >= 15800ms")
|
|
301
|
+
XCTAssertLessThanOrEqual(delay, 16200, "Attempt 5 delay should be <= 16200ms")
|
|
302
|
+
}
|
|
303
|
+
}
|