pakstr 0.27.0 → 0.28.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/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +201 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/signer/local/LocalAccountStoreTest.kt +194 -0
- package/android-template/app/src/androidTest/java/com/pakstr/app/utils/SignerPickerBottomSheetTest.kt +131 -0
- package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +87 -11
- package/android-template/app/src/main/java/com/pakstr/app/signer/LocalSigner.kt +100 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +6 -6
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +110 -3
- package/android-template/app/src/main/java/com/pakstr/app/signer/SignerType.kt +1 -0
- package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountStore.kt +245 -0
- package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +19 -9
- package/android-template/app/src/main/res/layout/dialog_picker.xml +18 -0
- package/android-template/app/src/main/res/values/strings.xml +3 -1
- package/android-template/app/src/main/res/xml/backup_rules.xml +4 -11
- package/android-template/app/src/main/res/xml/data_extraction_rules.xml +7 -13
- package/android-template/app/src/test/java/com/pakstr/app/signer/LocalSignerTest.kt +110 -0
- package/package.json +1 -1
package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt
CHANGED
|
@@ -11,8 +11,14 @@ import com.pakstr.app.crypto.Nip46ClientKey
|
|
|
11
11
|
import com.pakstr.app.signer.bunker.BunkerConfig
|
|
12
12
|
import com.pakstr.app.signer.bunker.BunkerConnectionStore
|
|
13
13
|
import com.pakstr.app.signer.bunker.Nip46Client
|
|
14
|
+
import com.pakstr.app.signer.exceptions.SignerUnavailableException
|
|
14
15
|
import com.pakstr.app.signer.interfaces.NostrSigner
|
|
16
|
+
import com.pakstr.app.signer.local.LocalAccountStore
|
|
17
|
+
import kotlinx.coroutines.CoroutineStart
|
|
18
|
+
import kotlinx.coroutines.async
|
|
15
19
|
import kotlinx.coroutines.runBlocking
|
|
20
|
+
import java.security.KeyStore
|
|
21
|
+
import org.junit.After
|
|
16
22
|
import org.junit.Assert.assertEquals
|
|
17
23
|
import org.junit.Assert.assertFalse
|
|
18
24
|
import org.junit.Assert.assertNull
|
|
@@ -24,6 +30,146 @@ import org.junit.runner.RunWith
|
|
|
24
30
|
@RunWith(AndroidJUnit4::class)
|
|
25
31
|
class SignerManagerTest {
|
|
26
32
|
|
|
33
|
+
private val context
|
|
34
|
+
get() = ApplicationProvider.getApplicationContext<Context>()
|
|
35
|
+
|
|
36
|
+
@After
|
|
37
|
+
fun clearSignerPreferences() {
|
|
38
|
+
context.getSharedPreferences("signer", Context.MODE_PRIVATE)
|
|
39
|
+
.edit()
|
|
40
|
+
.clear()
|
|
41
|
+
.commit()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun fresh_manager_defaults_to_amber_but_is_not_configured() {
|
|
46
|
+
clearSignerPreferences()
|
|
47
|
+
val manager = SignerManager(context, NoOpLauncher())
|
|
48
|
+
|
|
49
|
+
assertEquals(SignerType.AMBER, manager.getActiveType())
|
|
50
|
+
assertFalse(manager.isSignerConfigured())
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Test
|
|
54
|
+
fun explicit_selection_persists_configuration_across_recreation() =
|
|
55
|
+
runBlocking {
|
|
56
|
+
clearSignerPreferences()
|
|
57
|
+
var pickerRequests = 0
|
|
58
|
+
val first = SignerManager(context, NoOpLauncher())
|
|
59
|
+
first.setActiveType(SignerType.FAKE)
|
|
60
|
+
val restored =
|
|
61
|
+
SignerManager(
|
|
62
|
+
context,
|
|
63
|
+
NoOpLauncher(),
|
|
64
|
+
signerPickerRequestHandler = { pickerRequests++ }
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assertTrue(restored.isSignerConfigured())
|
|
68
|
+
assertEquals(SignerType.FAKE, restored.getActiveType())
|
|
69
|
+
assertTrue(restored.getConfiguredSigner() is FakeSigner)
|
|
70
|
+
assertEquals(0, pickerRequests)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun local_account_recreation_reuses_existing_identity() = runBlocking {
|
|
75
|
+
clearLocalAccount(context)
|
|
76
|
+
val first = SignerManager(context, NoOpLauncher())
|
|
77
|
+
try {
|
|
78
|
+
first.createLocalAccount()
|
|
79
|
+
val firstPublicKey = first.getCurrentUserPubkey()
|
|
80
|
+
val restored = SignerManager(context, NoOpLauncher())
|
|
81
|
+
try {
|
|
82
|
+
assertTrue(restored.isSignerConfigured())
|
|
83
|
+
assertEquals(SignerType.LOCAL, restored.getActiveType())
|
|
84
|
+
assertEquals(firstPublicKey, restored.getCurrentUserPubkey())
|
|
85
|
+
} finally {
|
|
86
|
+
restored.close()
|
|
87
|
+
}
|
|
88
|
+
} finally {
|
|
89
|
+
first.close()
|
|
90
|
+
clearLocalAccount(context)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Test
|
|
95
|
+
fun legacy_completed_setup_migrates_configuration() {
|
|
96
|
+
clearSignerPreferences()
|
|
97
|
+
val manager = SignerManager(context, NoOpLauncher())
|
|
98
|
+
|
|
99
|
+
manager.migrateLegacySignerConfiguration(setupCompleted = true)
|
|
100
|
+
|
|
101
|
+
assertTrue(manager.isSignerConfigured())
|
|
102
|
+
assertEquals(SignerType.AMBER, manager.getActiveType())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
fun configured_signer_waits_for_one_picker_and_resumes_all_requests() =
|
|
107
|
+
runBlocking {
|
|
108
|
+
clearSignerPreferences()
|
|
109
|
+
var pickerRequests = 0
|
|
110
|
+
val manager =
|
|
111
|
+
SignerManager(
|
|
112
|
+
context,
|
|
113
|
+
NoOpLauncher(),
|
|
114
|
+
signerPickerRequestHandler = { pickerRequests++ }
|
|
115
|
+
)
|
|
116
|
+
val first =
|
|
117
|
+
async(start = CoroutineStart.UNDISPATCHED) {
|
|
118
|
+
manager.getConfiguredSigner()
|
|
119
|
+
}
|
|
120
|
+
val second =
|
|
121
|
+
async(start = CoroutineStart.UNDISPATCHED) {
|
|
122
|
+
manager.getConfiguredSigner()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
assertEquals(1, pickerRequests)
|
|
126
|
+
assertFalse(first.isCompleted)
|
|
127
|
+
assertFalse(second.isCompleted)
|
|
128
|
+
|
|
129
|
+
manager.setActiveType(SignerType.FAKE)
|
|
130
|
+
|
|
131
|
+
assertTrue(first.await() is FakeSigner)
|
|
132
|
+
assertTrue(second.await() is FakeSigner)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Test
|
|
136
|
+
fun dismissed_picker_returns_signer_unavailable() = runBlocking {
|
|
137
|
+
clearSignerPreferences()
|
|
138
|
+
val manager =
|
|
139
|
+
SignerManager(
|
|
140
|
+
context,
|
|
141
|
+
NoOpLauncher(),
|
|
142
|
+
signerPickerRequestHandler = {}
|
|
143
|
+
)
|
|
144
|
+
val request =
|
|
145
|
+
async(start = CoroutineStart.UNDISPATCHED) {
|
|
146
|
+
runCatching { manager.getConfiguredSigner() }.exceptionOrNull()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
manager.cancelSignerSelection()
|
|
150
|
+
|
|
151
|
+
assertTrue(request.await() is SignerUnavailableException)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Test
|
|
155
|
+
fun close_returns_signer_unavailable_to_pending_request() = runBlocking {
|
|
156
|
+
clearSignerPreferences()
|
|
157
|
+
val manager =
|
|
158
|
+
SignerManager(
|
|
159
|
+
context,
|
|
160
|
+
NoOpLauncher(),
|
|
161
|
+
signerPickerRequestHandler = {}
|
|
162
|
+
)
|
|
163
|
+
val request =
|
|
164
|
+
async(start = CoroutineStart.UNDISPATCHED) {
|
|
165
|
+
runCatching { manager.getConfiguredSigner() }.exceptionOrNull()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
manager.close()
|
|
169
|
+
|
|
170
|
+
assertTrue(request.await() is SignerUnavailableException)
|
|
171
|
+
}
|
|
172
|
+
|
|
27
173
|
@Test
|
|
28
174
|
fun signer_change_clears_cached_user_pubkey() = runBlocking {
|
|
29
175
|
val context =
|
|
@@ -102,6 +248,47 @@ class SignerManagerTest {
|
|
|
102
248
|
}
|
|
103
249
|
}
|
|
104
250
|
|
|
251
|
+
@Test
|
|
252
|
+
fun local_account_survives_manager_recreation() = runBlocking {
|
|
253
|
+
val context =
|
|
254
|
+
ApplicationProvider.getApplicationContext<Context>()
|
|
255
|
+
clearLocalAccount(context)
|
|
256
|
+
val first = SignerManager(context, NoOpLauncher())
|
|
257
|
+
val restored = SignerManager(context, NoOpLauncher())
|
|
258
|
+
try {
|
|
259
|
+
first.createLocalAccount()
|
|
260
|
+
val firstPublicKey = first.getCurrentUserPubkey()
|
|
261
|
+
|
|
262
|
+
assertEquals(SignerType.LOCAL, first.getActiveType())
|
|
263
|
+
assertEquals(SignerType.LOCAL, restored.getActiveType())
|
|
264
|
+
assertEquals(firstPublicKey, restored.getCurrentUserPubkey())
|
|
265
|
+
} finally {
|
|
266
|
+
first.close()
|
|
267
|
+
restored.close()
|
|
268
|
+
clearLocalAccount(context)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@Test
|
|
273
|
+
fun repeated_local_account_creation_reuses_identity() = runBlocking {
|
|
274
|
+
val context =
|
|
275
|
+
ApplicationProvider.getApplicationContext<Context>()
|
|
276
|
+
clearLocalAccount(context)
|
|
277
|
+
val manager = SignerManager(context, NoOpLauncher())
|
|
278
|
+
try {
|
|
279
|
+
manager.createLocalAccount()
|
|
280
|
+
val firstPublicKey = manager.getCurrentUserPubkey()
|
|
281
|
+
|
|
282
|
+
manager.createLocalAccount()
|
|
283
|
+
|
|
284
|
+
assertEquals(firstPublicKey, manager.getCurrentUserPubkey())
|
|
285
|
+
assertEquals(SignerType.LOCAL, manager.getActiveType())
|
|
286
|
+
} finally {
|
|
287
|
+
manager.close()
|
|
288
|
+
clearLocalAccount(context)
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
105
292
|
@Test
|
|
106
293
|
fun switching_from_bunker_disconnects_and_closes_client() {
|
|
107
294
|
val context =
|
|
@@ -149,6 +336,20 @@ class SignerManagerTest {
|
|
|
149
336
|
}
|
|
150
337
|
}
|
|
151
338
|
|
|
339
|
+
private fun clearLocalAccount(context: Context) {
|
|
340
|
+
context.getSharedPreferences(
|
|
341
|
+
LocalAccountStore.PREFERENCES_NAME,
|
|
342
|
+
Context.MODE_PRIVATE
|
|
343
|
+
).edit().clear().commit()
|
|
344
|
+
KeyStore.getInstance("AndroidKeyStore")
|
|
345
|
+
.apply { load(null) }
|
|
346
|
+
.deleteEntry(LocalAccountStore.KEY_ALIAS)
|
|
347
|
+
context.getSharedPreferences("signer", Context.MODE_PRIVATE)
|
|
348
|
+
.edit()
|
|
349
|
+
.clear()
|
|
350
|
+
.commit()
|
|
351
|
+
}
|
|
352
|
+
|
|
152
353
|
private fun getPendingPairing(manager: SignerManager): Any? {
|
|
153
354
|
return SignerManager::class.java
|
|
154
355
|
.getDeclaredField("pendingPairing")
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
package com.pakstr.app.signer.local
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Base64
|
|
5
|
+
import androidx.test.core.app.ApplicationProvider
|
|
6
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
7
|
+
import org.junit.After
|
|
8
|
+
import org.junit.Assert.assertEquals
|
|
9
|
+
import org.junit.Assert.assertFalse
|
|
10
|
+
import org.junit.Assert.assertNotEquals
|
|
11
|
+
import org.junit.Assert.assertThrows
|
|
12
|
+
import org.junit.Assert.assertTrue
|
|
13
|
+
import org.junit.Before
|
|
14
|
+
import org.junit.Test
|
|
15
|
+
import org.junit.runner.RunWith
|
|
16
|
+
import java.security.KeyStore
|
|
17
|
+
|
|
18
|
+
@RunWith(AndroidJUnit4::class)
|
|
19
|
+
class LocalAccountStoreTest {
|
|
20
|
+
private lateinit var context: Context
|
|
21
|
+
private lateinit var store: LocalAccountStore
|
|
22
|
+
|
|
23
|
+
@Before
|
|
24
|
+
fun setup() {
|
|
25
|
+
context = ApplicationProvider.getApplicationContext()
|
|
26
|
+
clearState()
|
|
27
|
+
store =
|
|
28
|
+
LocalAccountStore(
|
|
29
|
+
context,
|
|
30
|
+
PREFERENCES_NAME,
|
|
31
|
+
KEY_ALIAS
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@After
|
|
36
|
+
fun teardown() {
|
|
37
|
+
clearState()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@Test
|
|
41
|
+
fun account_survives_store_recreation() {
|
|
42
|
+
val createdPublicKey = store.createAccount()
|
|
43
|
+
val restored =
|
|
44
|
+
LocalAccountStore(
|
|
45
|
+
context,
|
|
46
|
+
PREFERENCES_NAME,
|
|
47
|
+
KEY_ALIAS
|
|
48
|
+
).loadKey()
|
|
49
|
+
|
|
50
|
+
requireNotNull(restored).use { key ->
|
|
51
|
+
assertEquals(createdPublicKey, key.publicKeyHex)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun repeated_creation_reuses_existing_account() {
|
|
57
|
+
val first = store.createAccount()
|
|
58
|
+
val second = store.createAccount()
|
|
59
|
+
|
|
60
|
+
assertEquals(first, second)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@Test
|
|
64
|
+
fun persisted_value_is_not_plaintext_private_key() {
|
|
65
|
+
store.createAccount()
|
|
66
|
+
val privateKey =
|
|
67
|
+
requireNotNull(store.loadKey()).use { key ->
|
|
68
|
+
key.withPrivateKey(ByteArray::copyOf)
|
|
69
|
+
}
|
|
70
|
+
val preferences = preferences()
|
|
71
|
+
val ciphertext =
|
|
72
|
+
requireNotNull(
|
|
73
|
+
preferences.getString(
|
|
74
|
+
LocalAccountStore.KEY_CIPHERTEXT,
|
|
75
|
+
null
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
assertNotEquals(
|
|
81
|
+
Base64.encodeToString(privateKey, Base64.NO_WRAP),
|
|
82
|
+
ciphertext
|
|
83
|
+
)
|
|
84
|
+
assertFalse(
|
|
85
|
+
preferences.all.values.any { value ->
|
|
86
|
+
value == Base64.encodeToString(
|
|
87
|
+
privateKey,
|
|
88
|
+
Base64.NO_WRAP
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
} finally {
|
|
93
|
+
privateKey.fill(0)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun incomplete_storage_fails_without_being_cleared() {
|
|
99
|
+
preferences().edit()
|
|
100
|
+
.putString(LocalAccountStore.KEY_IV, "AA==")
|
|
101
|
+
.commit()
|
|
102
|
+
|
|
103
|
+
assertThrows(LocalAccountStorageException::class.java) {
|
|
104
|
+
store.createAccount()
|
|
105
|
+
}
|
|
106
|
+
assertTrue(preferences().contains(LocalAccountStore.KEY_IV))
|
|
107
|
+
assertFalse(
|
|
108
|
+
preferences().contains(
|
|
109
|
+
LocalAccountStore.KEY_CIPHERTEXT
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@Test
|
|
115
|
+
fun tampered_ciphertext_fails_without_replacement() {
|
|
116
|
+
store.createAccount()
|
|
117
|
+
val preferences = preferences()
|
|
118
|
+
val original =
|
|
119
|
+
requireNotNull(
|
|
120
|
+
preferences.getString(
|
|
121
|
+
LocalAccountStore.KEY_CIPHERTEXT,
|
|
122
|
+
null
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
val tampered = Base64.decode(original, Base64.NO_WRAP)
|
|
126
|
+
tampered[0] = (tampered[0].toInt() xor 1).toByte()
|
|
127
|
+
val encodedTampered =
|
|
128
|
+
Base64.encodeToString(tampered, Base64.NO_WRAP)
|
|
129
|
+
tampered.fill(0)
|
|
130
|
+
preferences.edit()
|
|
131
|
+
.putString(
|
|
132
|
+
LocalAccountStore.KEY_CIPHERTEXT,
|
|
133
|
+
encodedTampered
|
|
134
|
+
)
|
|
135
|
+
.commit()
|
|
136
|
+
|
|
137
|
+
assertThrows(LocalAccountStorageException::class.java) {
|
|
138
|
+
store.createAccount()
|
|
139
|
+
}
|
|
140
|
+
assertEquals(
|
|
141
|
+
encodedTampered,
|
|
142
|
+
preferences.getString(
|
|
143
|
+
LocalAccountStore.KEY_CIPHERTEXT,
|
|
144
|
+
null
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@Test
|
|
150
|
+
fun missing_wrapping_key_does_not_generate_replacement_account() {
|
|
151
|
+
store.createAccount()
|
|
152
|
+
val ciphertext =
|
|
153
|
+
preferences().getString(
|
|
154
|
+
LocalAccountStore.KEY_CIPHERTEXT,
|
|
155
|
+
null
|
|
156
|
+
)
|
|
157
|
+
deleteKey()
|
|
158
|
+
|
|
159
|
+
assertThrows(LocalAccountStorageException::class.java) {
|
|
160
|
+
store.createAccount()
|
|
161
|
+
}
|
|
162
|
+
assertEquals(
|
|
163
|
+
ciphertext,
|
|
164
|
+
preferences().getString(
|
|
165
|
+
LocalAccountStore.KEY_CIPHERTEXT,
|
|
166
|
+
null
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private fun preferences() =
|
|
172
|
+
context.getSharedPreferences(
|
|
173
|
+
PREFERENCES_NAME,
|
|
174
|
+
Context.MODE_PRIVATE
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
private fun clearState() {
|
|
178
|
+
if (::context.isInitialized) {
|
|
179
|
+
preferences().edit().clear().commit()
|
|
180
|
+
deleteKey()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun deleteKey() {
|
|
185
|
+
KeyStore.getInstance("AndroidKeyStore")
|
|
186
|
+
.apply { load(null) }
|
|
187
|
+
.deleteEntry(KEY_ALIAS)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private companion object {
|
|
191
|
+
const val PREFERENCES_NAME = "local_signer_account_test"
|
|
192
|
+
const val KEY_ALIAS = "pakstr_local_signer_wrapping_key_test"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
package com.pakstr.app.utils
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.test.core.app.ActivityScenario
|
|
5
|
+
import androidx.test.core.app.ApplicationProvider
|
|
6
|
+
import androidx.test.espresso.Espresso.onView
|
|
7
|
+
import androidx.test.espresso.action.ViewActions.click
|
|
8
|
+
import androidx.test.espresso.assertion.ViewAssertions.matches
|
|
9
|
+
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
|
10
|
+
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
|
|
11
|
+
import androidx.test.espresso.matcher.ViewMatchers.withId
|
|
12
|
+
import androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE
|
|
13
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
14
|
+
import com.pakstr.app.MainActivity
|
|
15
|
+
import com.pakstr.app.R
|
|
16
|
+
import com.pakstr.app.signer.SignerManager
|
|
17
|
+
import com.pakstr.app.signer.SignerType
|
|
18
|
+
import com.pakstr.app.signer.local.LocalAccountStore
|
|
19
|
+
import org.junit.After
|
|
20
|
+
import org.junit.Assert.assertEquals
|
|
21
|
+
import org.junit.Assert.assertNull
|
|
22
|
+
import org.junit.Assert.assertTrue
|
|
23
|
+
import org.junit.Before
|
|
24
|
+
import org.junit.Test
|
|
25
|
+
import org.junit.runner.RunWith
|
|
26
|
+
import java.security.KeyStore
|
|
27
|
+
|
|
28
|
+
@RunWith(AndroidJUnit4::class)
|
|
29
|
+
class SignerPickerBottomSheetTest {
|
|
30
|
+
private lateinit var context: Context
|
|
31
|
+
|
|
32
|
+
@Before
|
|
33
|
+
fun setup() {
|
|
34
|
+
context = ApplicationProvider.getApplicationContext()
|
|
35
|
+
AppPreferences.init(context)
|
|
36
|
+
AppPreferences.saveBoolean("isFirstRun", false)
|
|
37
|
+
clearLocalAccount()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@After
|
|
41
|
+
fun teardown() {
|
|
42
|
+
clearLocalAccount()
|
|
43
|
+
context.getSharedPreferences("signer", Context.MODE_PRIVATE)
|
|
44
|
+
.edit()
|
|
45
|
+
.clear()
|
|
46
|
+
.commit()
|
|
47
|
+
AppPreferences.saveBoolean("isFirstRun", true)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Test
|
|
51
|
+
fun cold_start_does_not_show_signer_picker() {
|
|
52
|
+
AppPreferences.saveBoolean("isFirstRun", true)
|
|
53
|
+
|
|
54
|
+
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
|
55
|
+
scenario.onActivity { activity ->
|
|
56
|
+
assertNull(
|
|
57
|
+
activity.supportFragmentManager
|
|
58
|
+
.findFragmentByTag("SignerPicker")
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@Test
|
|
65
|
+
fun completed_legacy_setup_migrates_signer_configuration() {
|
|
66
|
+
AppPreferences.saveBoolean("isFirstRun", false)
|
|
67
|
+
|
|
68
|
+
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
|
69
|
+
scenario.onActivity { activity ->
|
|
70
|
+
assertTrue(signerManager(activity).isSignerConfigured())
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
fun local_option_creates_and_selects_local_signer() {
|
|
77
|
+
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
|
|
78
|
+
scenario.onActivity { activity ->
|
|
79
|
+
MainActivity::class.java
|
|
80
|
+
.getDeclaredMethod("showSignerPicker")
|
|
81
|
+
.apply { isAccessible = true }
|
|
82
|
+
.invoke(activity)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onView(withId(R.id.radio_option_three))
|
|
86
|
+
.check(matches(isDisplayed()))
|
|
87
|
+
.perform(click())
|
|
88
|
+
onView(withId(R.id.bunker_uri_container))
|
|
89
|
+
.check(matches(withEffectiveVisibility(GONE)))
|
|
90
|
+
onView(withId(R.id.button_share_pairing))
|
|
91
|
+
.check(matches(withEffectiveVisibility(GONE)))
|
|
92
|
+
onView(withId(R.id.button_confirm)).perform(click())
|
|
93
|
+
|
|
94
|
+
waitForLocalSelection(scenario)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private fun waitForLocalSelection(
|
|
99
|
+
scenario: ActivityScenario<MainActivity>
|
|
100
|
+
) {
|
|
101
|
+
repeat(50) {
|
|
102
|
+
var activeType: SignerType? = null
|
|
103
|
+
scenario.onActivity { activity ->
|
|
104
|
+
activeType = signerManager(activity).getActiveType()
|
|
105
|
+
}
|
|
106
|
+
if (activeType == SignerType.LOCAL) {
|
|
107
|
+
assertEquals(SignerType.LOCAL, activeType)
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
Thread.sleep(100)
|
|
111
|
+
}
|
|
112
|
+
throw AssertionError("LOCAL signer was not selected")
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private fun signerManager(activity: MainActivity): SignerManager {
|
|
116
|
+
return MainActivity::class.java
|
|
117
|
+
.getDeclaredField("signerManager")
|
|
118
|
+
.apply { isAccessible = true }
|
|
119
|
+
.get(activity) as SignerManager
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private fun clearLocalAccount() {
|
|
123
|
+
context.getSharedPreferences(
|
|
124
|
+
LocalAccountStore.PREFERENCES_NAME,
|
|
125
|
+
Context.MODE_PRIVATE
|
|
126
|
+
).edit().clear().commit()
|
|
127
|
+
KeyStore.getInstance("AndroidKeyStore")
|
|
128
|
+
.apply { load(null) }
|
|
129
|
+
.deleteEntry(LocalAccountStore.KEY_ALIAS)
|
|
130
|
+
}
|
|
131
|
+
}
|