@ledgerhq/device-transport-kit-react-native-hid 0.0.0-rn-hid-20250221112139 → 0.0.0-rnhid-transport-20250411151739
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/README.md +1 -1
- package/android/build.gradle +101 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +1 -0
- package/android/gradlew +252 -0
- package/android/gradlew.bat +94 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/BridgeEvents.kt +42 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidModule.kt +241 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/TransportHidPackage.kt +25 -0
- package/android/src/main/kotlin/com/ledger/androidtransporthid/bridge/serialization.kt +124 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/AndroidUsbTransport.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/DefaultAndroidUsbTransport.kt +298 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/UsbPermissionRequester.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/connection/AndroidUsbApduSender.kt +133 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbAttachedReceiverController.kt +59 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbDetachedReceiverController.kt +58 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/controller/UsbPermissionReceiver.kt +92 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/LedgerUsbDevice.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/ProductId.kt +11 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbPermissionEvent.kt +14 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/UsbState.kt +16 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/model/VendorId.kt +11 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbDeviceMapper.kt +46 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMain/transport/usb/utils/UsbMapper.kt +56 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/UsbConst.android.kt +8 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceApduSender.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnection.kt +95 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachine.kt +314 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/Apdu.kt +44 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduBuilder.kt +88 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduParser.kt +37 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/ApduUtils.kt +37 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/apdu/SendApduResult.kt +47 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectedDevice.kt +25 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/connection/ConnectionResult.kt +45 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/BleInformation.kt +8 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/LedgerDevice.kt +89 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/device/UsbInfo.kt +7 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/disconnection/DisconnectionResult.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/ConnectivityType.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryDevice.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/discovery/DiscoveryResult.kt +28 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/ByteArrayExtension.kt +116 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/api/utils/StringExtension.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectedDevice.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/connection/InternalConnectionResult.kt +41 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScope.kt +25 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/coroutine/SDKScopeHandler.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/event/SdkEventDispatcher.kt +19 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/DisableLoggerService.kt +12 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogInfo.kt +52 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LogLevel.kt +13 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/service/logger/LoggerService.kt +10 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/Transport.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/TransportEvent.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerService.kt +210 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/FramerUtils.kt +35 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduConst.kt +9 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFrame.kt +66 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/ApduFramerHeader.kt +74 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/framer/model/FramerConst.kt +14 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/ByteExtension.kt +21 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/transport/utils/InternalByteArrayExtension.kt +18 -0
- package/android/src/main/kotlin/com/ledger/devicesdk/shared/internal/utils/Controller.kt +12 -0
- package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionStateMachineTest.kt +713 -0
- package/android/src/test/kotlin/com/ledger/devicesdk/shared/androidMainInternal/transport/deviceconnection/DeviceConnectionTest.kt +218 -0
- package/lib/cjs/package.json +2 -1
- package/lib/esm/package.json +2 -1
- package/lib/types/tsconfig.prod.tsbuildinfo +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
package com.ledger.devicesdk.shared.androidMainInternal.transport.deviceconnection
|
|
2
|
+
|
|
3
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduFailureReason
|
|
4
|
+
import com.ledger.devicesdk.shared.api.apdu.SendApduResult
|
|
5
|
+
import com.ledger.devicesdk.shared.internal.service.logger.LogInfo
|
|
6
|
+
import com.ledger.devicesdk.shared.internal.service.logger.LoggerService
|
|
7
|
+
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
8
|
+
import kotlinx.coroutines.test.StandardTestDispatcher
|
|
9
|
+
import kotlinx.coroutines.test.advanceTimeBy
|
|
10
|
+
import kotlinx.coroutines.test.runTest
|
|
11
|
+
import kotlin.time.Duration
|
|
12
|
+
import kotlin.time.Duration.Companion.seconds
|
|
13
|
+
import kotlin.test.Test
|
|
14
|
+
import org.junit.Assert.*
|
|
15
|
+
import kotlin.time.Duration.Companion.milliseconds
|
|
16
|
+
|
|
17
|
+
@OptIn(ExperimentalCoroutinesApi::class)
|
|
18
|
+
class DeviceConnectionStateMachineTest {
|
|
19
|
+
@Test
|
|
20
|
+
fun `GIVEN the device connection state machine in Connected state WHEN an APDU is sent THEN the APDU is processed`() =
|
|
21
|
+
runTest {
|
|
22
|
+
var sendApduCalled: ByteArray? = null
|
|
23
|
+
var terminated = false
|
|
24
|
+
var error: Throwable? = null
|
|
25
|
+
var sendApduResult: SendApduResult? = null
|
|
26
|
+
|
|
27
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
28
|
+
sendApduFn = { apdu -> sendApduCalled = apdu },
|
|
29
|
+
onTerminated = { terminated = true },
|
|
30
|
+
isFatalSendApduFailure = { false },
|
|
31
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
32
|
+
onError = { error = it },
|
|
33
|
+
loggerService = FakeLoggerService(),
|
|
34
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// Request sending an APDU
|
|
38
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
39
|
+
apdu = mockedRequestApduA,
|
|
40
|
+
triggersDisconnection = false,
|
|
41
|
+
resultCallback = { result -> sendApduResult = result }
|
|
42
|
+
))
|
|
43
|
+
assertArrayEquals(mockedRequestApduA, sendApduCalled)
|
|
44
|
+
|
|
45
|
+
// Simulate a successful response
|
|
46
|
+
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
47
|
+
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
48
|
+
|
|
49
|
+
// Then
|
|
50
|
+
assertEquals(mockedSuccessApduResult, sendApduResult)
|
|
51
|
+
assertFalse(terminated)
|
|
52
|
+
assertNull(error)
|
|
53
|
+
|
|
54
|
+
// In Connected state now; closing connection should terminate without an error.
|
|
55
|
+
stateMachine.requestCloseConnection()
|
|
56
|
+
assertTrue(terminated)
|
|
57
|
+
assertNull(error)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
@Test
|
|
61
|
+
fun `GIVEN the device connection state machine in SendingApdu state WHEN a fatal failure occurs THEN the machine terminates and the APDU request fails`() =
|
|
62
|
+
runTest {
|
|
63
|
+
var terminated = false
|
|
64
|
+
var sendApduResult: SendApduResult? = null
|
|
65
|
+
var error: Throwable? = null
|
|
66
|
+
|
|
67
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
68
|
+
sendApduFn = { },
|
|
69
|
+
onTerminated = { terminated = true },
|
|
70
|
+
isFatalSendApduFailure = { true }, // All failures are fatal
|
|
71
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
72
|
+
onError = { error = it },
|
|
73
|
+
loggerService = FakeLoggerService(),
|
|
74
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
78
|
+
apdu = mockedRequestApduA,
|
|
79
|
+
triggersDisconnection = false,
|
|
80
|
+
resultCallback = { result -> sendApduResult = result }
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
// Simulate a failure
|
|
84
|
+
val mockedFailureResult =
|
|
85
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceDisconnected)
|
|
86
|
+
stateMachine.handleApduResult(mockedFailureResult)
|
|
87
|
+
|
|
88
|
+
// Then
|
|
89
|
+
assertEquals(sendApduResult, mockedFailureResult)
|
|
90
|
+
assertTrue(terminated)
|
|
91
|
+
assertNull(error)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
fun `GIVEN the device connection state machine in SendingApdu state WHEN a nonfatal failure occurs THEN the machine returns to Connected state`() =
|
|
96
|
+
runTest {
|
|
97
|
+
var terminated = false
|
|
98
|
+
var sendApduResult: SendApduResult? = null
|
|
99
|
+
var error: Throwable? = null
|
|
100
|
+
|
|
101
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
102
|
+
sendApduFn = { },
|
|
103
|
+
onTerminated = { terminated = true },
|
|
104
|
+
isFatalSendApduFailure = { false },
|
|
105
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
106
|
+
onError = { error = it },
|
|
107
|
+
loggerService = FakeLoggerService(),
|
|
108
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// Request sending APDU
|
|
112
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
113
|
+
apdu = mockedRequestApduA,
|
|
114
|
+
triggersDisconnection = false,
|
|
115
|
+
resultCallback = { result -> sendApduResult = result }
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
// Simulate a failure
|
|
119
|
+
val mockedFailureResult =
|
|
120
|
+
SendApduResult.Failure(SendApduFailureReason.ApduNotWellFormatted)
|
|
121
|
+
stateMachine.handleApduResult(mockedFailureResult)
|
|
122
|
+
|
|
123
|
+
// Then
|
|
124
|
+
assertEquals(mockedFailureResult, sendApduResult)
|
|
125
|
+
assertFalse(terminated)
|
|
126
|
+
|
|
127
|
+
// In Connected state now; closing connection should terminate without an error.
|
|
128
|
+
stateMachine.requestCloseConnection()
|
|
129
|
+
assertTrue(terminated)
|
|
130
|
+
assertNull(error)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Test
|
|
134
|
+
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent THEN the machine moves to WaitingForReconnection and recovers when device reconnects`() =
|
|
135
|
+
runTest {
|
|
136
|
+
var sendApduCalled: ByteArray? = null
|
|
137
|
+
var sendApduResult: SendApduResult? = null
|
|
138
|
+
var terminated = false
|
|
139
|
+
var error: Throwable? = null
|
|
140
|
+
|
|
141
|
+
val dispatcher = StandardTestDispatcher(testScheduler)
|
|
142
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
143
|
+
sendApduFn = { apdu -> sendApduCalled = apdu },
|
|
144
|
+
onTerminated = { terminated = true },
|
|
145
|
+
isFatalSendApduFailure = { false },
|
|
146
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
147
|
+
onError = { error = it },
|
|
148
|
+
loggerService = FakeLoggerService(),
|
|
149
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
// Request sending an APDU (with triggersDisconnection = true)
|
|
153
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
154
|
+
apdu = mockedRequestApduA,
|
|
155
|
+
triggersDisconnection = true,
|
|
156
|
+
resultCallback = { result -> sendApduResult = result }
|
|
157
|
+
))
|
|
158
|
+
|
|
159
|
+
// Send APDU should have been called
|
|
160
|
+
assertArrayEquals(mockedRequestApduA, sendApduCalled)
|
|
161
|
+
|
|
162
|
+
// Simulate a successful response
|
|
163
|
+
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
164
|
+
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
165
|
+
|
|
166
|
+
// Result should have been returned
|
|
167
|
+
assertEquals(mockedSuccessApduResult, sendApduResult)
|
|
168
|
+
|
|
169
|
+
// Should be in waiting state
|
|
170
|
+
assertEquals(
|
|
171
|
+
DeviceConnectionStateMachine.State.WaitingForReconnection,
|
|
172
|
+
stateMachine.getState()
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// Now in WaitingForReconnection; simulate reconnection.
|
|
176
|
+
stateMachine.handleDeviceConnected()
|
|
177
|
+
|
|
178
|
+
// A new APDU request in Connected state should call sendApduFn.
|
|
179
|
+
var secondSendApduResult: SendApduResult? = null
|
|
180
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
181
|
+
apdu = mockedRequestApduB,
|
|
182
|
+
triggersDisconnection = false,
|
|
183
|
+
resultCallback = { secondSendApduResult = it }
|
|
184
|
+
))
|
|
185
|
+
|
|
186
|
+
// Send APDU should have been called
|
|
187
|
+
assertArrayEquals(mockedRequestApduB, sendApduCalled)
|
|
188
|
+
|
|
189
|
+
// Simulate a successful response
|
|
190
|
+
val mockedSuccessApduResultB = SendApduResult.Success(mockedResultApduSuccessB)
|
|
191
|
+
stateMachine.handleApduResult(mockedSuccessApduResultB)
|
|
192
|
+
|
|
193
|
+
// Result should have been returned
|
|
194
|
+
assertEquals(mockedSuccessApduResultB, secondSendApduResult)
|
|
195
|
+
|
|
196
|
+
assertFalse(terminated)
|
|
197
|
+
assertNull(error)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@Test
|
|
201
|
+
fun `GIVEN the device connection state machine WHEN an APDU with triggersDisconnection is sent THEN the machine moves to WaitingForReconnection and the next APDU is queued until reconnection`() =
|
|
202
|
+
runTest {
|
|
203
|
+
val sendApduCalled: MutableList<ByteArray> = mutableListOf()
|
|
204
|
+
var terminated = false
|
|
205
|
+
var error: Throwable? = null
|
|
206
|
+
|
|
207
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
208
|
+
sendApduFn = { apdu -> sendApduCalled += apdu },
|
|
209
|
+
onTerminated = { terminated = true },
|
|
210
|
+
isFatalSendApduFailure = { false },
|
|
211
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
212
|
+
onError = { error = it },
|
|
213
|
+
loggerService = FakeLoggerService(),
|
|
214
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
// Request sending an APDU (with triggersDisconnection = true)
|
|
218
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
219
|
+
apdu = mockedRequestApduA,
|
|
220
|
+
triggersDisconnection = true,
|
|
221
|
+
resultCallback = { }
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
// Simulate a successful response
|
|
225
|
+
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
226
|
+
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
227
|
+
|
|
228
|
+
// sendApduFn should have been called once
|
|
229
|
+
assertEquals(1, sendApduCalled.size)
|
|
230
|
+
|
|
231
|
+
// Should be in waiting state
|
|
232
|
+
assertEquals(
|
|
233
|
+
DeviceConnectionStateMachine.State.WaitingForReconnection,
|
|
234
|
+
stateMachine.getState()
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Now in WaitingForReconnection; simulate new APDU request
|
|
238
|
+
var secondSendApduResult: SendApduResult? = null
|
|
239
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
240
|
+
apdu = mockedRequestApduB,
|
|
241
|
+
triggersDisconnection = false,
|
|
242
|
+
resultCallback = { secondSendApduResult = it }
|
|
243
|
+
))
|
|
244
|
+
|
|
245
|
+
// Should be in waiting state
|
|
246
|
+
assertEquals(
|
|
247
|
+
DeviceConnectionStateMachine.State.WaitingForReconnectionWithQueuedApdu::class,
|
|
248
|
+
stateMachine.getState()::class
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// sendApduFn should not have been called one more time
|
|
252
|
+
assertEquals(1, sendApduCalled.size)
|
|
253
|
+
|
|
254
|
+
// Simulate reconnection
|
|
255
|
+
stateMachine.handleDeviceConnected()
|
|
256
|
+
|
|
257
|
+
// Should be in SendingApdu state
|
|
258
|
+
assertEquals(
|
|
259
|
+
DeviceConnectionStateMachine.State.SendingApdu::class,
|
|
260
|
+
stateMachine.getState()::class
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
// Send APDU should have been called a second time, and the result should have been returned
|
|
264
|
+
assertEquals(2, sendApduCalled.size)
|
|
265
|
+
assertArrayEquals(mockedRequestApduB, sendApduCalled[1])
|
|
266
|
+
|
|
267
|
+
// Simulate a successful response
|
|
268
|
+
val mockedSuccessApduResultB = SendApduResult.Success(mockedResultApduSuccessB)
|
|
269
|
+
stateMachine.handleApduResult(mockedSuccessApduResultB)
|
|
270
|
+
|
|
271
|
+
// Should be in Connected state
|
|
272
|
+
assertEquals(
|
|
273
|
+
DeviceConnectionStateMachine.State.Connected,
|
|
274
|
+
stateMachine.getState()
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
// Result should have been returned
|
|
278
|
+
assertEquals(mockedSuccessApduResultB, secondSendApduResult)
|
|
279
|
+
|
|
280
|
+
assertFalse(terminated)
|
|
281
|
+
assertNull(error)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@Test
|
|
285
|
+
fun `GIVEN the device connection state machine in SendingApdu state WHEN the connection is closed THEN the machine terminates and the APDU request fails`() =
|
|
286
|
+
runTest {
|
|
287
|
+
var terminated = false
|
|
288
|
+
var sendApduResult: SendApduResult? = null
|
|
289
|
+
var error: Throwable? = null
|
|
290
|
+
|
|
291
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
292
|
+
sendApduFn = { },
|
|
293
|
+
onTerminated = { terminated = true },
|
|
294
|
+
isFatalSendApduFailure = { false },
|
|
295
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
296
|
+
onError = { error = it },
|
|
297
|
+
loggerService = FakeLoggerService(),
|
|
298
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
// Request sending an APDU
|
|
302
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
303
|
+
apdu = mockedRequestApduA,
|
|
304
|
+
triggersDisconnection = false,
|
|
305
|
+
resultCallback = { result -> sendApduResult = result }
|
|
306
|
+
))
|
|
307
|
+
|
|
308
|
+
// Should be in sending state
|
|
309
|
+
assertEquals(
|
|
310
|
+
DeviceConnectionStateMachine.State.SendingApdu::class,
|
|
311
|
+
stateMachine.getState()::class
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
// Close connection
|
|
315
|
+
stateMachine.requestCloseConnection()
|
|
316
|
+
|
|
317
|
+
// Failure result should have been returned
|
|
318
|
+
assertEquals(
|
|
319
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceDisconnected),
|
|
320
|
+
sendApduResult
|
|
321
|
+
)
|
|
322
|
+
assertTrue(terminated)
|
|
323
|
+
assertNull(error)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
@Test
|
|
327
|
+
fun `GIVEN the device connection state machine in SendingApdu state WHEN the device disconnects THEN the APDU fails and the machine moves to WaitingForReconnection`() =
|
|
328
|
+
runTest {
|
|
329
|
+
var sendApduResult: SendApduResult? = null
|
|
330
|
+
var terminated = false
|
|
331
|
+
var error: Throwable? = null
|
|
332
|
+
|
|
333
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
334
|
+
sendApduFn = { },
|
|
335
|
+
onTerminated = { terminated = true },
|
|
336
|
+
isFatalSendApduFailure = { false },
|
|
337
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
338
|
+
onError = { error = it },
|
|
339
|
+
loggerService = FakeLoggerService(),
|
|
340
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
// Request sending an APDU
|
|
344
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
345
|
+
apdu = mockedRequestApduA,
|
|
346
|
+
triggersDisconnection = false,
|
|
347
|
+
resultCallback = { result -> sendApduResult = result }
|
|
348
|
+
))
|
|
349
|
+
|
|
350
|
+
// Simulate a disconnection
|
|
351
|
+
stateMachine.handleDeviceDisconnected()
|
|
352
|
+
|
|
353
|
+
// Should be in waiting state
|
|
354
|
+
assertEquals(
|
|
355
|
+
DeviceConnectionStateMachine.State.WaitingForReconnection,
|
|
356
|
+
stateMachine.getState()
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
// Failure result should have been returned
|
|
360
|
+
assertEquals(
|
|
361
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceDisconnected),
|
|
362
|
+
sendApduResult
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
// Should be in waiting state
|
|
366
|
+
assertEquals(
|
|
367
|
+
DeviceConnectionStateMachine.State.WaitingForReconnection,
|
|
368
|
+
stateMachine.getState()
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
// Simulate reconnection to return to normal operation.
|
|
372
|
+
stateMachine.handleDeviceConnected()
|
|
373
|
+
|
|
374
|
+
assertFalse(terminated)
|
|
375
|
+
assertNull(error)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
@Test
|
|
379
|
+
fun `GIVEN the device connection state machine in SendingApdu state WHEN an APDU request is received THEN it returns a DeviceBusy failure`() =
|
|
380
|
+
runTest {
|
|
381
|
+
var firstSendApduResult: SendApduResult? = null
|
|
382
|
+
var secondSendApduResult: SendApduResult? = null
|
|
383
|
+
var terminated = false
|
|
384
|
+
var error: Throwable? = null
|
|
385
|
+
|
|
386
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
387
|
+
sendApduFn = { },
|
|
388
|
+
onTerminated = { terminated = true },
|
|
389
|
+
isFatalSendApduFailure = { false },
|
|
390
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
391
|
+
onError = { error = it },
|
|
392
|
+
loggerService = FakeLoggerService(),
|
|
393
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
// First request enters SendingApdu.
|
|
397
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
398
|
+
apdu = mockedRequestApduA,
|
|
399
|
+
triggersDisconnection = false,
|
|
400
|
+
resultCallback = { firstSendApduResult = it }
|
|
401
|
+
))
|
|
402
|
+
|
|
403
|
+
// Should be in sending state.
|
|
404
|
+
assertEquals(
|
|
405
|
+
DeviceConnectionStateMachine.State.SendingApdu::class,
|
|
406
|
+
stateMachine.getState()::class
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
// Second request while busy.
|
|
410
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
411
|
+
apdu = mockedRequestApduB,
|
|
412
|
+
triggersDisconnection = false,
|
|
413
|
+
resultCallback = { secondSendApduResult = it }
|
|
414
|
+
))
|
|
415
|
+
|
|
416
|
+
assertEquals(
|
|
417
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceBusy),
|
|
418
|
+
secondSendApduResult
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
// Simulate response to first request
|
|
422
|
+
val mockedSuccessApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
423
|
+
stateMachine.handleApduResult(mockedSuccessApduResult)
|
|
424
|
+
|
|
425
|
+
// First request should have succeeded
|
|
426
|
+
assertEquals(mockedSuccessApduResult, firstSendApduResult)
|
|
427
|
+
|
|
428
|
+
assertFalse(terminated)
|
|
429
|
+
assertNull(error)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
@Test
|
|
433
|
+
fun `GIVEN the device connection state machine in WaitingForReconnection state WHEN the device reconnects THEN it transitions to Connected state`() =
|
|
434
|
+
runTest {
|
|
435
|
+
var terminated = false
|
|
436
|
+
|
|
437
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
438
|
+
sendApduFn = { },
|
|
439
|
+
onTerminated = { terminated = true },
|
|
440
|
+
isFatalSendApduFailure = { false },
|
|
441
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
442
|
+
onError = { },
|
|
443
|
+
loggerService = FakeLoggerService(),
|
|
444
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
// Simulate disconnection to move to waiting state.
|
|
448
|
+
stateMachine.handleDeviceDisconnected()
|
|
449
|
+
|
|
450
|
+
// Should be in waiting state.
|
|
451
|
+
assertEquals(
|
|
452
|
+
DeviceConnectionStateMachine.State.WaitingForReconnection,
|
|
453
|
+
stateMachine.getState()
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
// Then simulate reconnection.
|
|
457
|
+
stateMachine.handleDeviceConnected()
|
|
458
|
+
|
|
459
|
+
// Should be in connected state.
|
|
460
|
+
assertEquals(
|
|
461
|
+
DeviceConnectionStateMachine.State.Connected,
|
|
462
|
+
stateMachine.getState()
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
// If no error and no termination, we assume state is Connected.
|
|
466
|
+
assertFalse(terminated)
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
@Test
|
|
470
|
+
fun `GIVEN the device connection state machine in WaitingForReconnection state WHEN the reconnection times out THEN the machine terminates`() =
|
|
471
|
+
runTest {
|
|
472
|
+
var terminated = false
|
|
473
|
+
|
|
474
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
475
|
+
sendApduFn = { },
|
|
476
|
+
onTerminated = { terminated = true },
|
|
477
|
+
isFatalSendApduFailure = { false },
|
|
478
|
+
reconnectionTimeoutDuration = 5.seconds,
|
|
479
|
+
onError = { },
|
|
480
|
+
loggerService = FakeLoggerService(),
|
|
481
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
// Simulate disconnection to move to waiting state.
|
|
485
|
+
stateMachine.handleDeviceDisconnected()
|
|
486
|
+
|
|
487
|
+
// Should be in waiting state.
|
|
488
|
+
assertEquals(
|
|
489
|
+
DeviceConnectionStateMachine.State.WaitingForReconnection,
|
|
490
|
+
stateMachine.getState()
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
// Simulate timeout
|
|
494
|
+
advanceTimeBy(5.seconds)
|
|
495
|
+
assertFalse(terminated)
|
|
496
|
+
|
|
497
|
+
advanceTimeBy(1.milliseconds)
|
|
498
|
+
assertTrue(terminated)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
@Test
|
|
502
|
+
fun `GIVEN the device connection state machine in WaitingForReconnection state WHEN an APDU is queued THEN on device connection the APDU is sent`() =
|
|
503
|
+
runTest {
|
|
504
|
+
var sendApduCalled: ByteArray? = null
|
|
505
|
+
var sendApduResult: SendApduResult? = null
|
|
506
|
+
var terminated = false
|
|
507
|
+
var error: Throwable? = null
|
|
508
|
+
|
|
509
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
510
|
+
sendApduFn = { apdu -> sendApduCalled = apdu },
|
|
511
|
+
onTerminated = { terminated = true },
|
|
512
|
+
isFatalSendApduFailure = { false },
|
|
513
|
+
reconnectionTimeoutDuration = 5.seconds,
|
|
514
|
+
onError = { error = it },
|
|
515
|
+
loggerService = FakeLoggerService(),
|
|
516
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
// Simulate disconnection
|
|
520
|
+
stateMachine.handleDeviceDisconnected()
|
|
521
|
+
|
|
522
|
+
// Request sending an APDU
|
|
523
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
524
|
+
apdu = mockedRequestApduA,
|
|
525
|
+
triggersDisconnection = false,
|
|
526
|
+
resultCallback = { sendApduResult = it }
|
|
527
|
+
))
|
|
528
|
+
|
|
529
|
+
// Send APDU should not have been called
|
|
530
|
+
assertNull(sendApduCalled)
|
|
531
|
+
|
|
532
|
+
// Simulate reconnection
|
|
533
|
+
stateMachine.handleDeviceConnected()
|
|
534
|
+
assertEquals(mockedRequestApduA, sendApduCalled)
|
|
535
|
+
|
|
536
|
+
// Simulate response
|
|
537
|
+
val mockedApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
538
|
+
stateMachine.handleApduResult(mockedApduResult)
|
|
539
|
+
|
|
540
|
+
// Callback should have been called
|
|
541
|
+
assertEquals(sendApduResult, mockedApduResult)
|
|
542
|
+
|
|
543
|
+
assertNull(error)
|
|
544
|
+
assertFalse(terminated)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@Test
|
|
548
|
+
fun `GIVEN the device connection state machine with a queued APDU WHEN the connection is closed THEN the machine terminates and the APDU request fails`() =
|
|
549
|
+
runTest {
|
|
550
|
+
var terminated = false
|
|
551
|
+
var sendApduResult: SendApduResult? = null
|
|
552
|
+
var error: Throwable? = null
|
|
553
|
+
|
|
554
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
555
|
+
sendApduFn = { },
|
|
556
|
+
onTerminated = { terminated = true },
|
|
557
|
+
isFatalSendApduFailure = { false },
|
|
558
|
+
reconnectionTimeoutDuration = 5.seconds,
|
|
559
|
+
onError = { error = it },
|
|
560
|
+
loggerService = FakeLoggerService(),
|
|
561
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
// Simulate disconnection
|
|
565
|
+
stateMachine.handleDeviceDisconnected()
|
|
566
|
+
|
|
567
|
+
// Request sending an APDU
|
|
568
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
569
|
+
apdu = mockedRequestApduA,
|
|
570
|
+
triggersDisconnection = false,
|
|
571
|
+
resultCallback = { result -> sendApduResult = result }
|
|
572
|
+
))
|
|
573
|
+
|
|
574
|
+
// Request closing the connection
|
|
575
|
+
stateMachine.requestCloseConnection()
|
|
576
|
+
|
|
577
|
+
// APDU request should have failed
|
|
578
|
+
assertEquals(
|
|
579
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceDisconnected),
|
|
580
|
+
sendApduResult
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
assertTrue(terminated)
|
|
584
|
+
assertNull(error)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
@Test
|
|
588
|
+
fun `GIVEN the device connection state machine with a queued APDU WHEN the reconnection times out THEN the machine terminates and the APDU request fails`() =
|
|
589
|
+
runTest {
|
|
590
|
+
var sendApduResult: SendApduResult? = null
|
|
591
|
+
var terminated = false
|
|
592
|
+
var error: Throwable? = null
|
|
593
|
+
|
|
594
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
595
|
+
sendApduFn = { },
|
|
596
|
+
onTerminated = { terminated = true },
|
|
597
|
+
isFatalSendApduFailure = { false },
|
|
598
|
+
reconnectionTimeoutDuration = 5.seconds,
|
|
599
|
+
onError = { error = it },
|
|
600
|
+
loggerService = FakeLoggerService(),
|
|
601
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
// Simulate disconnection
|
|
605
|
+
stateMachine.handleDeviceDisconnected()
|
|
606
|
+
|
|
607
|
+
// Request sending an APDU
|
|
608
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
609
|
+
apdu = byteArrayOf(0x0A),
|
|
610
|
+
triggersDisconnection = false,
|
|
611
|
+
resultCallback = { result -> sendApduResult = result }
|
|
612
|
+
))
|
|
613
|
+
|
|
614
|
+
// Simulate timeout
|
|
615
|
+
advanceTimeBy(5.seconds + 1.milliseconds)
|
|
616
|
+
assertEquals(
|
|
617
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceDisconnected),
|
|
618
|
+
sendApduResult
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
assertTrue(terminated)
|
|
622
|
+
assertNull(error)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@Test
|
|
626
|
+
fun `GIVEN the device connection state machine in queued state WHEN a new APDU request is received THEN it returns a DeviceBusy failure`() =
|
|
627
|
+
runTest {
|
|
628
|
+
var firstSendApduResult: SendApduResult? = null
|
|
629
|
+
var secondSendApduResult: SendApduResult? = null
|
|
630
|
+
var terminated = false
|
|
631
|
+
var error: Throwable? = null
|
|
632
|
+
|
|
633
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
634
|
+
sendApduFn = { },
|
|
635
|
+
onTerminated = { terminated = true },
|
|
636
|
+
isFatalSendApduFailure = { false },
|
|
637
|
+
reconnectionTimeoutDuration = 5.seconds,
|
|
638
|
+
onError = { error = it },
|
|
639
|
+
loggerService = FakeLoggerService(),
|
|
640
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
// Simulate disconnection
|
|
644
|
+
stateMachine.handleDeviceDisconnected()
|
|
645
|
+
|
|
646
|
+
// Request sending an APDU
|
|
647
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
648
|
+
apdu = byteArrayOf(0x0A),
|
|
649
|
+
triggersDisconnection = false,
|
|
650
|
+
resultCallback = { firstSendApduResult = it }
|
|
651
|
+
))
|
|
652
|
+
|
|
653
|
+
// Second APDU sending request
|
|
654
|
+
stateMachine.requestSendApdu(DeviceConnectionStateMachine.SendApduRequestContent(
|
|
655
|
+
apdu = byteArrayOf(0x0B),
|
|
656
|
+
triggersDisconnection = false,
|
|
657
|
+
resultCallback = { secondSendApduResult = it }
|
|
658
|
+
))
|
|
659
|
+
|
|
660
|
+
// Second request should immediately return busy.
|
|
661
|
+
assertEquals(
|
|
662
|
+
SendApduResult.Failure(SendApduFailureReason.DeviceBusy),
|
|
663
|
+
secondSendApduResult
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
// Simulate reconnection
|
|
667
|
+
stateMachine.handleDeviceConnected()
|
|
668
|
+
|
|
669
|
+
// Simulate response (to first request)
|
|
670
|
+
val mockedApduResult = SendApduResult.Success(mockedResultApduSuccessA)
|
|
671
|
+
stateMachine.handleApduResult(mockedApduResult)
|
|
672
|
+
|
|
673
|
+
// First Request should have received a result
|
|
674
|
+
assertEquals(mockedApduResult, firstSendApduResult)
|
|
675
|
+
|
|
676
|
+
assertFalse(terminated)
|
|
677
|
+
assertNull(error)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
@Test
|
|
681
|
+
fun `GIVEN the device connection state machine in Terminated state WHEN any event occurs THEN it triggers onError`() =
|
|
682
|
+
runTest {
|
|
683
|
+
var errorCalled: Throwable? = null
|
|
684
|
+
|
|
685
|
+
val stateMachine = DeviceConnectionStateMachine(
|
|
686
|
+
sendApduFn = { },
|
|
687
|
+
onTerminated = { },
|
|
688
|
+
isFatalSendApduFailure = { false },
|
|
689
|
+
reconnectionTimeoutDuration = reconnectionTimeout,
|
|
690
|
+
onError = { errorCalled = it },
|
|
691
|
+
loggerService = FakeLoggerService(),
|
|
692
|
+
coroutineDispatcher = StandardTestDispatcher(testScheduler)
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
// Terminate machine.
|
|
696
|
+
stateMachine.requestCloseConnection()
|
|
697
|
+
// Any subsequent event in Terminated state (e.g. device connected) is unhandled.
|
|
698
|
+
stateMachine.handleDeviceConnected()
|
|
699
|
+
assertNotNull(errorCalled)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
companion object {
|
|
703
|
+
internal class FakeLoggerService : LoggerService {
|
|
704
|
+
override fun log(info: LogInfo) {}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
val reconnectionTimeout: Duration = 5.seconds
|
|
708
|
+
val mockedRequestApduA: ByteArray = byteArrayOf(0x01, 0x02)
|
|
709
|
+
val mockedRequestApduB: ByteArray = byteArrayOf(0x03, 0x04)
|
|
710
|
+
val mockedResultApduSuccessA: ByteArray = byteArrayOf(0x05, 0x06, 0x90.toByte(), 0x00)
|
|
711
|
+
val mockedResultApduSuccessB: ByteArray = byteArrayOf(0x07, 0x08, 0x90.toByte(), 0x00)
|
|
712
|
+
}
|
|
713
|
+
}
|