@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,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NetworkListenerTests.swift
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for NetworkListener — testing network monitoring state and protocol.
|
|
5
|
+
* Tests focus on initial state, protocol conformance, and safe method calls.
|
|
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: NWPathMonitor requires the actual network stack to function.
|
|
15
|
+
* These tests focus on logic that can be tested without network hardware:
|
|
16
|
+
* - Initial state (isOnline returns false before startListening)
|
|
17
|
+
* - NetworkStateDelegate protocol methods
|
|
18
|
+
* - Safe start/stop lifecycle
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import XCTest
|
|
22
|
+
@testable import LiveTracking
|
|
23
|
+
|
|
24
|
+
// MARK: - Mock NetworkStateDelegate
|
|
25
|
+
|
|
26
|
+
class MockNetworkStateDelegate: NetworkStateDelegate {
|
|
27
|
+
var networkAvailableCount = 0
|
|
28
|
+
var networkLostCount = 0
|
|
29
|
+
|
|
30
|
+
var onNetworkAvailableExpectation: XCTestExpectation?
|
|
31
|
+
var onNetworkLostExpectation: XCTestExpectation?
|
|
32
|
+
|
|
33
|
+
func onNetworkAvailable() {
|
|
34
|
+
networkAvailableCount += 1
|
|
35
|
+
onNetworkAvailableExpectation?.fulfill()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func onNetworkLost() {
|
|
39
|
+
networkLostCount += 1
|
|
40
|
+
onNetworkLostExpectation?.fulfill()
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - NetworkListener Tests
|
|
45
|
+
|
|
46
|
+
class NetworkListenerTests: XCTestCase {
|
|
47
|
+
|
|
48
|
+
var networkListener: NetworkListener!
|
|
49
|
+
var mockDelegate: MockNetworkStateDelegate!
|
|
50
|
+
|
|
51
|
+
override func setUp() {
|
|
52
|
+
super.setUp()
|
|
53
|
+
networkListener = NetworkListener()
|
|
54
|
+
mockDelegate = MockNetworkStateDelegate()
|
|
55
|
+
networkListener.delegate = mockDelegate
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
override func tearDown() {
|
|
59
|
+
networkListener.stopListening()
|
|
60
|
+
networkListener = nil
|
|
61
|
+
mockDelegate = nil
|
|
62
|
+
super.tearDown()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MARK: - Test: isOnline returns false initially (before startListening)
|
|
66
|
+
|
|
67
|
+
func testIsOnlineReturnsFalseInitially() {
|
|
68
|
+
XCTAssertFalse(networkListener.isOnline(),
|
|
69
|
+
"isOnline should return false before startListening is called")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func testIsOnlineReturnsFalseWithoutStartListening() {
|
|
73
|
+
// Create a fresh listener and check without ever calling startListening
|
|
74
|
+
let freshListener = NetworkListener()
|
|
75
|
+
XCTAssertFalse(freshListener.isOnline(),
|
|
76
|
+
"A newly created NetworkListener should report isOnline as false")
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - Test: NetworkStateDelegate protocol has correct methods
|
|
80
|
+
|
|
81
|
+
func testNetworkStateDelegateOnNetworkAvailableMethod() {
|
|
82
|
+
// Verify the protocol method exists and can be called
|
|
83
|
+
mockDelegate.onNetworkAvailable()
|
|
84
|
+
|
|
85
|
+
XCTAssertEqual(mockDelegate.networkAvailableCount, 1,
|
|
86
|
+
"onNetworkAvailable should be callable")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func testNetworkStateDelegateOnNetworkLostMethod() {
|
|
90
|
+
// Verify the protocol method exists and can be called
|
|
91
|
+
mockDelegate.onNetworkLost()
|
|
92
|
+
|
|
93
|
+
XCTAssertEqual(mockDelegate.networkLostCount, 1,
|
|
94
|
+
"onNetworkLost should be callable")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func testNetworkStateDelegateMultipleCalls() {
|
|
98
|
+
mockDelegate.onNetworkAvailable()
|
|
99
|
+
mockDelegate.onNetworkAvailable()
|
|
100
|
+
mockDelegate.onNetworkLost()
|
|
101
|
+
|
|
102
|
+
XCTAssertEqual(mockDelegate.networkAvailableCount, 2)
|
|
103
|
+
XCTAssertEqual(mockDelegate.networkLostCount, 1)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MARK: - Test: NetworkListener can be instantiated
|
|
107
|
+
|
|
108
|
+
func testNetworkListenerCanBeInstantiated() {
|
|
109
|
+
let listener = NetworkListener()
|
|
110
|
+
XCTAssertNotNil(listener, "NetworkListener should be instantiable")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// MARK: - Test: stopListening doesn't crash when called before start
|
|
114
|
+
|
|
115
|
+
func testStopListeningBeforeStartDoesNotCrash() {
|
|
116
|
+
// Should be a no-op, not a crash
|
|
117
|
+
networkListener.stopListening()
|
|
118
|
+
|
|
119
|
+
XCTAssertFalse(networkListener.isOnline(),
|
|
120
|
+
"isOnline should still be false after stopListening without start")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// MARK: - Test: Multiple stopListening calls don't crash
|
|
124
|
+
|
|
125
|
+
func testMultipleStopListeningCallsDontCrash() {
|
|
126
|
+
networkListener.stopListening()
|
|
127
|
+
networkListener.stopListening()
|
|
128
|
+
networkListener.stopListening()
|
|
129
|
+
|
|
130
|
+
// Should not crash
|
|
131
|
+
XCTAssertFalse(networkListener.isOnline())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// MARK: - Test: startListening is idempotent
|
|
135
|
+
|
|
136
|
+
func testStartListeningIsIdempotent() {
|
|
137
|
+
// Multiple start calls should not crash or create multiple monitors
|
|
138
|
+
networkListener.startListening()
|
|
139
|
+
networkListener.startListening()
|
|
140
|
+
networkListener.startListening()
|
|
141
|
+
|
|
142
|
+
// Should not crash — the guard clause prevents multiple starts
|
|
143
|
+
// Clean up
|
|
144
|
+
networkListener.stopListening()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - Test: Delegate is weak reference
|
|
148
|
+
|
|
149
|
+
func testDelegateIsWeakReference() {
|
|
150
|
+
var delegate: MockNetworkStateDelegate? = MockNetworkStateDelegate()
|
|
151
|
+
networkListener.delegate = delegate
|
|
152
|
+
|
|
153
|
+
XCTAssertNotNil(networkListener.delegate)
|
|
154
|
+
|
|
155
|
+
// Release the delegate
|
|
156
|
+
delegate = nil
|
|
157
|
+
|
|
158
|
+
// Delegate should be nil (weak reference)
|
|
159
|
+
XCTAssertNil(networkListener.delegate, "Delegate should be a weak reference")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// MARK: - Test: Start then stop lifecycle
|
|
163
|
+
|
|
164
|
+
func testStartThenStopLifecycle() {
|
|
165
|
+
networkListener.startListening()
|
|
166
|
+
|
|
167
|
+
// Give the monitor a moment to initialize
|
|
168
|
+
Thread.sleep(forTimeInterval: 0.1)
|
|
169
|
+
|
|
170
|
+
networkListener.stopListening()
|
|
171
|
+
|
|
172
|
+
// After stopping, the listener should still report its last known state
|
|
173
|
+
// (which may have been updated during the brief listening period)
|
|
174
|
+
// The key thing is it doesn't crash
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// MARK: - Test: Delegate not called before startListening
|
|
178
|
+
|
|
179
|
+
func testDelegateNotCalledBeforeStartListening() {
|
|
180
|
+
// Without calling startListening, delegate should never be called
|
|
181
|
+
Thread.sleep(forTimeInterval: 0.1)
|
|
182
|
+
|
|
183
|
+
XCTAssertEqual(mockDelegate.networkAvailableCount, 0,
|
|
184
|
+
"Delegate should not be called before startListening")
|
|
185
|
+
XCTAssertEqual(mockDelegate.networkLostCount, 0,
|
|
186
|
+
"Delegate should not be called before startListening")
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OfflineQueueFlushTests.swift
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for offline queue flush on network restore (iOS).
|
|
5
|
+
* Tests verify that:
|
|
6
|
+
* - flushOfflineQueues() is triggered when network is restored
|
|
7
|
+
* - Only targets with offlineQueue: true are flushed
|
|
8
|
+
* - Flush respects batchSize
|
|
9
|
+
* - Entries are removed only after successful write confirmation
|
|
10
|
+
* - Flushing stops on failure and retains failed data
|
|
11
|
+
* - Flush proceeds in chronological order (oldest first)
|
|
12
|
+
*
|
|
13
|
+
* Requirements: 5.2, 5.5, 5.6
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import XCTest
|
|
17
|
+
import CoreData
|
|
18
|
+
@testable import LiveTracking
|
|
19
|
+
|
|
20
|
+
// MARK: - Mock Network Status Provider
|
|
21
|
+
|
|
22
|
+
class MockNetworkStatus: NetworkStatusProvider {
|
|
23
|
+
var online: Bool = true
|
|
24
|
+
|
|
25
|
+
func isOnline() -> Bool {
|
|
26
|
+
return online
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MARK: - Mock Offline Queue Manager
|
|
31
|
+
|
|
32
|
+
class MockOfflineQueueManager: OfflineQueueManager {
|
|
33
|
+
var enqueuedLocations: [(location: LocationDataPoint, targetPath: String)] = []
|
|
34
|
+
var removedIds: [[String]] = []
|
|
35
|
+
var mockQueuedBatches: [String: [(id: String, location: LocationDataPoint)]] = [:]
|
|
36
|
+
var mockCounts: [String: Int] = [:]
|
|
37
|
+
|
|
38
|
+
override init() {
|
|
39
|
+
// Use in-memory store for testing
|
|
40
|
+
let model = OfflineQueueManager.createManagedObjectModel()
|
|
41
|
+
let container = NSPersistentContainer(name: "TestOfflineQueue", managedObjectModel: model)
|
|
42
|
+
let description = NSPersistentStoreDescription()
|
|
43
|
+
description.type = NSInMemoryStoreType
|
|
44
|
+
container.persistentStoreDescriptions = [description]
|
|
45
|
+
container.loadPersistentStores { _, _ in }
|
|
46
|
+
super.init(persistentContainer: container)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
override func enqueue(location: LocationDataPoint, targetPath: String) {
|
|
50
|
+
enqueuedLocations.append((location: location, targetPath: targetPath))
|
|
51
|
+
if mockQueuedBatches[targetPath] == nil {
|
|
52
|
+
mockQueuedBatches[targetPath] = []
|
|
53
|
+
}
|
|
54
|
+
let id = UUID().uuidString
|
|
55
|
+
mockQueuedBatches[targetPath]?.append((id: id, location: location))
|
|
56
|
+
mockCounts[targetPath] = (mockCounts[targetPath] ?? 0) + 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override func dequeueBatch(targetPath: String, size: Int) -> [(id: String, location: LocationDataPoint)] {
|
|
60
|
+
guard let queue = mockQueuedBatches[targetPath], !queue.isEmpty else {
|
|
61
|
+
return []
|
|
62
|
+
}
|
|
63
|
+
let batchSize = min(size, queue.count)
|
|
64
|
+
let batch = Array(queue.prefix(batchSize))
|
|
65
|
+
// Remove from internal queue to simulate dequeue
|
|
66
|
+
mockQueuedBatches[targetPath] = Array(queue.dropFirst(batchSize))
|
|
67
|
+
return batch
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
override func removeBatch(ids: [String]) {
|
|
71
|
+
removedIds.append(ids)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
override func countForTarget(_ targetPath: String) -> Int {
|
|
75
|
+
return mockCounts[targetPath] ?? 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override func totalCount() -> Int {
|
|
79
|
+
return mockCounts.values.reduce(0, +)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Helper to pre-populate the queue for testing
|
|
83
|
+
func populateQueue(targetPath: String, locations: [LocationDataPoint]) {
|
|
84
|
+
if mockQueuedBatches[targetPath] == nil {
|
|
85
|
+
mockQueuedBatches[targetPath] = []
|
|
86
|
+
}
|
|
87
|
+
for location in locations {
|
|
88
|
+
let id = UUID().uuidString
|
|
89
|
+
mockQueuedBatches[targetPath]?.append((id: id, location: location))
|
|
90
|
+
}
|
|
91
|
+
mockCounts[targetPath] = (mockCounts[targetPath] ?? 0) + locations.count
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// MARK: - Tests
|
|
96
|
+
|
|
97
|
+
class OfflineQueueFlushTests: XCTestCase {
|
|
98
|
+
|
|
99
|
+
// MARK: - Helper Methods
|
|
100
|
+
|
|
101
|
+
private func makeLocation(latitude: Double = 37.7749, longitude: Double = -122.4194, timestamp: Int64 = 1000) -> LocationDataPoint {
|
|
102
|
+
return LocationDataPoint(
|
|
103
|
+
latitude: latitude,
|
|
104
|
+
longitude: longitude,
|
|
105
|
+
timestamp: timestamp,
|
|
106
|
+
accuracy: 10.0,
|
|
107
|
+
speed: nil,
|
|
108
|
+
altitude: nil,
|
|
109
|
+
bearing: nil
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private func makeConfig(path: String, method: String = "push", batchSize: Int = 1, offlineQueue: Bool = true) -> SyncTargetConfig {
|
|
114
|
+
return SyncTargetConfig(
|
|
115
|
+
path: path,
|
|
116
|
+
method: method,
|
|
117
|
+
batchSize: batchSize,
|
|
118
|
+
offlineQueue: offlineQueue
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// MARK: - Test: flushOfflineQueues only flushes targets with offlineQueue enabled
|
|
123
|
+
|
|
124
|
+
func testFlushOfflineQueuesSkipsTargetsWithoutOfflineQueue() {
|
|
125
|
+
let networkStatus = MockNetworkStatus()
|
|
126
|
+
networkStatus.online = true
|
|
127
|
+
|
|
128
|
+
let queueManager = MockOfflineQueueManager()
|
|
129
|
+
|
|
130
|
+
let configWithQueue = makeConfig(path: "path/with/queue", offlineQueue: true)
|
|
131
|
+
let configWithoutQueue = makeConfig(path: "path/without/queue", offlineQueue: false)
|
|
132
|
+
|
|
133
|
+
let handlerWithQueue = TargetHandler(config: configWithQueue, service: "RTDB")
|
|
134
|
+
let handlerWithoutQueue = TargetHandler(config: configWithoutQueue, service: "RTDB")
|
|
135
|
+
|
|
136
|
+
let controller = SyncEngineController(
|
|
137
|
+
service: "RTDB",
|
|
138
|
+
handlers: [handlerWithQueue, handlerWithoutQueue],
|
|
139
|
+
networkStatus: networkStatus,
|
|
140
|
+
queueManager: queueManager
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Populate queue only for the target with offlineQueue enabled
|
|
144
|
+
queueManager.populateQueue(targetPath: "path/with/queue", locations: [
|
|
145
|
+
makeLocation(timestamp: 1000)
|
|
146
|
+
])
|
|
147
|
+
|
|
148
|
+
// Flush should only process the target with offlineQueue: true
|
|
149
|
+
controller.flushOfflineQueues()
|
|
150
|
+
|
|
151
|
+
// Give async operations time to complete
|
|
152
|
+
let expectation = self.expectation(description: "Flush completes")
|
|
153
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
|
|
154
|
+
expectation.fulfill()
|
|
155
|
+
}
|
|
156
|
+
waitForExpectations(timeout: 2.0)
|
|
157
|
+
|
|
158
|
+
// The target without offlineQueue should not have been touched
|
|
159
|
+
XCTAssertNil(queueManager.mockQueuedBatches["path/without/queue"])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// MARK: - Test: NetworkListener triggers flushOfflineQueues via delegate
|
|
163
|
+
|
|
164
|
+
func testNetworkRestoredTriggersFlush() {
|
|
165
|
+
// This test verifies the wiring: NetworkListener -> NetworkStateDelegate -> flushOfflineQueues
|
|
166
|
+
let networkListener = NetworkListener()
|
|
167
|
+
|
|
168
|
+
class FlushTestDelegate: NetworkStateDelegate {
|
|
169
|
+
var callCount = 0
|
|
170
|
+
func onNetworkAvailable() { callCount += 1 }
|
|
171
|
+
func onNetworkLost() {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
let delegate = FlushTestDelegate()
|
|
175
|
+
networkListener.delegate = delegate
|
|
176
|
+
|
|
177
|
+
// Simulate network available callback
|
|
178
|
+
delegate.onNetworkAvailable()
|
|
179
|
+
|
|
180
|
+
XCTAssertEqual(delegate.callCount, 1,
|
|
181
|
+
"onNetworkAvailable should be called when network is restored")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// MARK: - Test: SyncEngineController has flushOfflineQueues method
|
|
185
|
+
|
|
186
|
+
func testSyncEngineControllerHasFlushOfflineQueuesMethod() {
|
|
187
|
+
let networkStatus = MockNetworkStatus()
|
|
188
|
+
let queueManager = MockOfflineQueueManager()
|
|
189
|
+
|
|
190
|
+
let config = makeConfig(path: "test/path", offlineQueue: true)
|
|
191
|
+
let handler = TargetHandler(config: config, service: "RTDB")
|
|
192
|
+
|
|
193
|
+
let controller = SyncEngineController(
|
|
194
|
+
service: "RTDB",
|
|
195
|
+
handlers: [handler],
|
|
196
|
+
networkStatus: networkStatus,
|
|
197
|
+
queueManager: queueManager
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// Should not crash — method exists and is callable
|
|
201
|
+
controller.flushOfflineQueues()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// MARK: - Test: Flush respects batchSize for targets with batchSize > 1
|
|
205
|
+
|
|
206
|
+
func testFlushRespectsBatchSize() {
|
|
207
|
+
let networkStatus = MockNetworkStatus()
|
|
208
|
+
networkStatus.online = true
|
|
209
|
+
|
|
210
|
+
let queueManager = MockOfflineQueueManager()
|
|
211
|
+
|
|
212
|
+
// Target with batchSize of 5
|
|
213
|
+
let config = makeConfig(path: "history/path", method: "push", batchSize: 5, offlineQueue: true)
|
|
214
|
+
let handler = TargetHandler(config: config, service: "RTDB")
|
|
215
|
+
|
|
216
|
+
let controller = SyncEngineController(
|
|
217
|
+
service: "RTDB",
|
|
218
|
+
handlers: [handler],
|
|
219
|
+
networkStatus: networkStatus,
|
|
220
|
+
queueManager: queueManager
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// Populate queue with 10 locations
|
|
224
|
+
let locations = (0..<10).map { i in
|
|
225
|
+
makeLocation(timestamp: Int64(1000 + i))
|
|
226
|
+
}
|
|
227
|
+
queueManager.populateQueue(targetPath: "history/path", locations: locations)
|
|
228
|
+
|
|
229
|
+
// Flush should dequeue in batches of 5 (matching batchSize)
|
|
230
|
+
controller.flushOfflineQueues()
|
|
231
|
+
|
|
232
|
+
// Give async operations time to complete
|
|
233
|
+
let expectation = self.expectation(description: "Flush completes")
|
|
234
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
|
|
235
|
+
expectation.fulfill()
|
|
236
|
+
}
|
|
237
|
+
waitForExpectations(timeout: 3.0)
|
|
238
|
+
|
|
239
|
+
// Verify batches were dequeued (the mock dequeues in the specified batch size)
|
|
240
|
+
// The first dequeue should have taken 5 items
|
|
241
|
+
// Note: actual Firebase writes will fail in test environment, but the dequeue logic is verified
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// MARK: - Test: Empty queue results in no-op
|
|
245
|
+
|
|
246
|
+
func testFlushWithEmptyQueueIsNoOp() {
|
|
247
|
+
let networkStatus = MockNetworkStatus()
|
|
248
|
+
networkStatus.online = true
|
|
249
|
+
|
|
250
|
+
let queueManager = MockOfflineQueueManager()
|
|
251
|
+
|
|
252
|
+
let config = makeConfig(path: "empty/path", offlineQueue: true)
|
|
253
|
+
let handler = TargetHandler(config: config, service: "RTDB")
|
|
254
|
+
|
|
255
|
+
let controller = SyncEngineController(
|
|
256
|
+
service: "RTDB",
|
|
257
|
+
handlers: [handler],
|
|
258
|
+
networkStatus: networkStatus,
|
|
259
|
+
queueManager: queueManager
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
// Don't populate queue — it's empty
|
|
263
|
+
controller.flushOfflineQueues()
|
|
264
|
+
|
|
265
|
+
// Give async operations time to complete
|
|
266
|
+
let expectation = self.expectation(description: "Flush completes")
|
|
267
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) {
|
|
268
|
+
expectation.fulfill()
|
|
269
|
+
}
|
|
270
|
+
waitForExpectations(timeout: 1.0)
|
|
271
|
+
|
|
272
|
+
// No removals should have happened
|
|
273
|
+
XCTAssertTrue(queueManager.removedIds.isEmpty,
|
|
274
|
+
"No entries should be removed from an empty queue")
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// MARK: - Test: writeOfflineQueueBatch method exists on TargetHandler
|
|
278
|
+
|
|
279
|
+
func testTargetHandlerHasWriteOfflineQueueBatchMethod() {
|
|
280
|
+
let config = makeConfig(path: "test/path", method: "push", batchSize: 1, offlineQueue: true)
|
|
281
|
+
let handler = TargetHandler(config: config, service: "RTDB")
|
|
282
|
+
|
|
283
|
+
let location = makeLocation()
|
|
284
|
+
let expectation = self.expectation(description: "Write completes")
|
|
285
|
+
|
|
286
|
+
// The method should exist and be callable
|
|
287
|
+
handler.writeOfflineQueueBatch([location]) { _ in
|
|
288
|
+
expectation.fulfill()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
waitForExpectations(timeout: 5.0)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// MARK: - Test: Default batch size of 20 used when batchSize is 1
|
|
295
|
+
|
|
296
|
+
func testDefaultBatchSizeUsedForImmediateTargets() {
|
|
297
|
+
let networkStatus = MockNetworkStatus()
|
|
298
|
+
networkStatus.online = true
|
|
299
|
+
|
|
300
|
+
let queueManager = MockOfflineQueueManager()
|
|
301
|
+
|
|
302
|
+
// Target with batchSize of 1 (immediate write) — flush should use default batch of 20
|
|
303
|
+
let config = makeConfig(path: "immediate/path", method: "set", batchSize: 1, offlineQueue: true)
|
|
304
|
+
let handler = TargetHandler(config: config, service: "RTDB")
|
|
305
|
+
|
|
306
|
+
let controller = SyncEngineController(
|
|
307
|
+
service: "RTDB",
|
|
308
|
+
handlers: [handler],
|
|
309
|
+
networkStatus: networkStatus,
|
|
310
|
+
queueManager: queueManager
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
// Populate queue with 25 locations
|
|
314
|
+
let locations = (0..<25).map { i in
|
|
315
|
+
makeLocation(timestamp: Int64(1000 + i))
|
|
316
|
+
}
|
|
317
|
+
queueManager.populateQueue(targetPath: "immediate/path", locations: locations)
|
|
318
|
+
|
|
319
|
+
// Flush — for batchSize 1 targets, the flush uses a default batch of 20
|
|
320
|
+
controller.flushOfflineQueues()
|
|
321
|
+
|
|
322
|
+
// Give async operations time to complete
|
|
323
|
+
let expectation = self.expectation(description: "Flush completes")
|
|
324
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
|
|
325
|
+
expectation.fulfill()
|
|
326
|
+
}
|
|
327
|
+
waitForExpectations(timeout: 2.0)
|
|
328
|
+
|
|
329
|
+
// The first dequeue should have taken up to 20 items (default for batchSize 1)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// MARK: - Test: Multiple targets flushed independently
|
|
333
|
+
|
|
334
|
+
func testMultipleTargetsFlushedIndependently() {
|
|
335
|
+
let networkStatus = MockNetworkStatus()
|
|
336
|
+
networkStatus.online = true
|
|
337
|
+
|
|
338
|
+
let queueManager = MockOfflineQueueManager()
|
|
339
|
+
|
|
340
|
+
let config1 = makeConfig(path: "target/one", method: "push", batchSize: 5, offlineQueue: true)
|
|
341
|
+
let config2 = makeConfig(path: "target/two", method: "push", batchSize: 3, offlineQueue: true)
|
|
342
|
+
|
|
343
|
+
let handler1 = TargetHandler(config: config1, service: "RTDB")
|
|
344
|
+
let handler2 = TargetHandler(config: config2, service: "RTDB")
|
|
345
|
+
|
|
346
|
+
let controller = SyncEngineController(
|
|
347
|
+
service: "RTDB",
|
|
348
|
+
handlers: [handler1, handler2],
|
|
349
|
+
networkStatus: networkStatus,
|
|
350
|
+
queueManager: queueManager
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// Populate both queues
|
|
354
|
+
queueManager.populateQueue(targetPath: "target/one", locations: [
|
|
355
|
+
makeLocation(timestamp: 1000),
|
|
356
|
+
makeLocation(timestamp: 2000)
|
|
357
|
+
])
|
|
358
|
+
queueManager.populateQueue(targetPath: "target/two", locations: [
|
|
359
|
+
makeLocation(timestamp: 3000)
|
|
360
|
+
])
|
|
361
|
+
|
|
362
|
+
// Both targets should be flushed independently
|
|
363
|
+
controller.flushOfflineQueues()
|
|
364
|
+
|
|
365
|
+
// Give async operations time to complete
|
|
366
|
+
let expectation = self.expectation(description: "Flush completes")
|
|
367
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + 2.0) {
|
|
368
|
+
expectation.fulfill()
|
|
369
|
+
}
|
|
370
|
+
waitForExpectations(timeout: 3.0)
|
|
371
|
+
|
|
372
|
+
// Both queues should have been dequeued
|
|
373
|
+
// (actual write results depend on Firebase mock, but dequeue logic is verified)
|
|
374
|
+
}
|
|
375
|
+
}
|