refactor: move test functions to automation and storage utilities, add compose component activity dependency and create group fragment content descriptions, add compose only testing for create group logic, update protos to match latest for chunk 2
This commit is contained in:
parent
f69e3846e3
commit
e79a980f2c
|
@ -299,9 +299,9 @@ dependencies {
|
||||||
implementation "com.opencsv:opencsv:4.6"
|
implementation "com.opencsv:opencsv:4.6"
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
testImplementation 'org.assertj:assertj-core:3.11.1'
|
testImplementation 'org.assertj:assertj-core:3.11.1'
|
||||||
testImplementation "org.mockito:mockito-inline:4.10.0"
|
testImplementation "org.mockito:mockito-inline:4.11.0"
|
||||||
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito-inline:2.28.3'
|
androidTestImplementation "org.mockito:mockito-android:4.11.0"
|
||||||
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
androidTestImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"
|
||||||
testImplementation "androidx.test:core:$testCoreVersion"
|
testImplementation "androidx.test:core:$testCoreVersion"
|
||||||
testImplementation "androidx.arch.core:core-testing:2.2.0"
|
testImplementation "androidx.arch.core:core-testing:2.2.0"
|
||||||
|
@ -330,6 +330,8 @@ dependencies {
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-web:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
|
androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.5.1'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
|
androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.5.1'
|
||||||
|
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.2"
|
||||||
|
debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.2"
|
||||||
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
androidTestUtil 'androidx.test:orchestrator:1.4.2'
|
||||||
|
|
||||||
testImplementation 'org.robolectric:robolectric:4.4'
|
testImplementation 'org.robolectric:robolectric:4.4'
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
<manifest
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
package="network.loki.messenger.test">
|
|
||||||
<application>
|
<application>
|
||||||
<uses-library android:name="android.test.runner"
|
<uses-library android:name="android.test.runner"
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
|
|
||||||
|
<activity android:name="androidx.activity.ComponentActivity"/>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
|
@ -0,0 +1,63 @@
|
||||||
|
package network.loki.messenger
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.hasContentDescriptionExactly
|
||||||
|
import androidx.compose.ui.test.junit4.createComposeRule
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.hamcrest.CoreMatchers.*
|
||||||
|
import org.hamcrest.MatcherAssert.*
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.groups.CreateGroup
|
||||||
|
import org.thoughtcrime.securesms.groups.CreateGroupFragment
|
||||||
|
import org.thoughtcrime.securesms.groups.CreateGroupState
|
||||||
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@SmallTest
|
||||||
|
class CreateGroupTests {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val composeTest = createComposeRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNavigateToCreateGroup() {
|
||||||
|
val application = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||||
|
// Accessibility IDs
|
||||||
|
val nameDesc = application.getString(R.string.AccessibilityId_closed_group_edit_group_name)
|
||||||
|
val buttonDesc = application.getString(R.string.AccessibilityId_create_closed_group_create_button)
|
||||||
|
|
||||||
|
lateinit var postedGroup: CreateGroupState
|
||||||
|
var backPressed = false
|
||||||
|
var closePressed = false
|
||||||
|
|
||||||
|
composeTest.setContent {
|
||||||
|
AppTheme {
|
||||||
|
CreateGroup(
|
||||||
|
viewState = CreateGroupFragment.ViewState.DEFAULT,
|
||||||
|
createGroupState = CreateGroupState("", "", emptySet()),
|
||||||
|
onCreate = { submitted ->
|
||||||
|
postedGroup = submitted
|
||||||
|
},
|
||||||
|
onBack = { backPressed = true },
|
||||||
|
onClose = { closePressed = true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
with(composeTest) {
|
||||||
|
onNode(hasContentDescriptionExactly(nameDesc)).performTextInput("Name")
|
||||||
|
onNode(hasContentDescriptionExactly(buttonDesc)).performClick()
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThat(postedGroup.groupName, equalTo("Name"))
|
||||||
|
assertThat(backPressed, equalTo(false))
|
||||||
|
assertThat(closePressed, equalTo(false))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +1,10 @@
|
||||||
package network.loki.messenger
|
package network.loki.messenger
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Instrumentation
|
import android.app.Instrumentation
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
|
||||||
import androidx.test.espresso.Espresso.onView
|
import androidx.test.espresso.Espresso.onView
|
||||||
import androidx.test.espresso.Espresso.pressBack
|
import androidx.test.espresso.Espresso.pressBack
|
||||||
import androidx.test.espresso.UiController
|
|
||||||
import androidx.test.espresso.ViewAction
|
|
||||||
import androidx.test.espresso.action.ViewActions
|
import androidx.test.espresso.action.ViewActions
|
||||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
|
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
|
||||||
|
@ -20,11 +16,11 @@ import androidx.test.espresso.matcher.ViewMatchers.withSubstring
|
||||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||||
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
import androidx.test.ext.junit.rules.ActivityScenarioRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.SmallTest
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import com.adevinta.android.barista.interaction.PermissionGranter
|
import network.loki.messenger.util.sendMessage
|
||||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
import network.loki.messenger.util.setupLoggedInState
|
||||||
import org.hamcrest.Matcher
|
import network.loki.messenger.util.waitFor
|
||||||
import org.hamcrest.Matchers.allOf
|
import org.hamcrest.Matchers.allOf
|
||||||
import org.hamcrest.Matchers.not
|
import org.hamcrest.Matchers.not
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
@ -36,12 +32,10 @@ import org.session.libsession.messaging.sending_receiving.link_preview.LinkPrevi
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.guava.Optional
|
import org.session.libsignal.utilities.guava.Optional
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
|
||||||
import org.thoughtcrime.securesms.home.HomeActivity
|
import org.thoughtcrime.securesms.home.HomeActivity
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@LargeTest
|
@SmallTest
|
||||||
class HomeActivityTests {
|
class HomeActivityTests {
|
||||||
|
|
||||||
@get:Rule
|
@get:Rule
|
||||||
|
@ -59,38 +53,6 @@ class HomeActivityTests {
|
||||||
InstrumentationRegistry.getInstrumentation().removeMonitor(activityMonitor)
|
InstrumentationRegistry.getInstrumentation().removeMonitor(activityMonitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendMessage(messageToSend: String, linkPreview: LinkPreview? = null) {
|
|
||||||
// assume in chat activity
|
|
||||||
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),withId(R.id.inputBarEditText))).perform(ViewActions.replaceText(messageToSend))
|
|
||||||
if (linkPreview != null) {
|
|
||||||
val activity = activityMonitor.waitForActivity() as ConversationActivityV2
|
|
||||||
val glide = GlideApp.with(activity)
|
|
||||||
activity.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
|
|
||||||
}
|
|
||||||
onView(allOf(isDescendantOfA(withId(R.id.inputBar)),inputButtonWithDrawable(R.drawable.ic_arrow_up))).perform(ViewActions.click())
|
|
||||||
// TODO: text can flaky on cursor reload, figure out a better way to wait for the UI to settle with new data
|
|
||||||
onView(isRoot()).perform(waitFor(500))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupLoggedInState(hasViewedSeed: Boolean = false) {
|
|
||||||
// landing activity
|
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
|
||||||
// session ID - register activity
|
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
|
||||||
// display name selection
|
|
||||||
onView(withId(R.id.displayNameEditText)).perform(ViewActions.typeText("test-user123"))
|
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
|
||||||
// PN select
|
|
||||||
if (hasViewedSeed) {
|
|
||||||
// has viewed seed is set to false after register activity
|
|
||||||
TextSecurePreferences.setHasViewedSeed(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
|
||||||
}
|
|
||||||
onView(withId(R.id.backgroundPollingOptionView)).perform(ViewActions.click())
|
|
||||||
onView(withId(R.id.registerButton)).perform(ViewActions.click())
|
|
||||||
// allow notification permission
|
|
||||||
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun goToMyChat() {
|
private fun goToMyChat() {
|
||||||
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||||
|
@ -134,11 +96,13 @@ class HomeActivityTests {
|
||||||
setupLoggedInState()
|
setupLoggedInState()
|
||||||
goToMyChat()
|
goToMyChat()
|
||||||
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
||||||
sendMessage("howdy")
|
with (activityMonitor.waitForActivity() as ConversationActivityV2) {
|
||||||
sendMessage("test")
|
sendMessage("howdy")
|
||||||
// tests url rewriter doesn't crash
|
sendMessage("test")
|
||||||
sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
|
// tests url rewriter doesn't crash
|
||||||
sendMessage("https://www.ámazon.com")
|
sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
|
||||||
|
sendMessage("https://www.ámazon.com")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -148,7 +112,9 @@ class HomeActivityTests {
|
||||||
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
||||||
// given the link url text
|
// given the link url text
|
||||||
val url = "https://www.ámazon.com"
|
val url = "https://www.ámazon.com"
|
||||||
sendMessage(url, LinkPreview(url, "amazon", Optional.absent()))
|
with (activityMonitor.waitForActivity() as ConversationActivityV2) {
|
||||||
|
sendMessage(url, LinkPreview(url, "amazon", Optional.absent()))
|
||||||
|
}
|
||||||
|
|
||||||
// when the URL span is clicked
|
// when the URL span is clicked
|
||||||
onView(withSubstring(url)).perform(ViewActions.click())
|
onView(withSubstring(url)).perform(ViewActions.click())
|
||||||
|
@ -162,21 +128,4 @@ class HomeActivityTests {
|
||||||
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
onView(withText(dialogPromptText)).check(matches(isDisplayed()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform action of waiting for a specific time.
|
|
||||||
*/
|
|
||||||
fun waitFor(millis: Long): ViewAction {
|
|
||||||
return object : ViewAction {
|
|
||||||
override fun getConstraints(): Matcher<View>? {
|
|
||||||
return isRoot()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getDescription(): String = "Wait for $millis milliseconds."
|
|
||||||
|
|
||||||
override fun perform(uiController: UiController, view: View?) {
|
|
||||||
uiController.loopMainThreadForAtLeast(millis)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -9,12 +9,15 @@ import network.loki.messenger.libsession_util.ConfigBase
|
||||||
import network.loki.messenger.libsession_util.Contacts
|
import network.loki.messenger.libsession_util.Contacts
|
||||||
import network.loki.messenger.libsession_util.util.Contact
|
import network.loki.messenger.libsession_util.util.Contact
|
||||||
import network.loki.messenger.libsession_util.util.ExpiryMode
|
import network.loki.messenger.libsession_util.util.ExpiryMode
|
||||||
|
import network.loki.messenger.util.applySpiedStorage
|
||||||
|
import network.loki.messenger.util.maybeGetUserInfo
|
||||||
|
import network.loki.messenger.util.randomSeedBytes
|
||||||
|
import network.loki.messenger.util.randomSessionId
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.mockito.kotlin.argThat
|
import org.mockito.kotlin.argThat
|
||||||
import org.mockito.kotlin.eq
|
import org.mockito.kotlin.eq
|
||||||
import org.mockito.kotlin.spy
|
|
||||||
import org.mockito.kotlin.verify
|
import org.mockito.kotlin.verify
|
||||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
@ -22,32 +25,15 @@ import org.session.libsignal.utilities.KeyHelper
|
||||||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
import org.thoughtcrime.securesms.ApplicationContext
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
@SmallTest
|
@SmallTest
|
||||||
class LibSessionTests {
|
class LibSessionTests {
|
||||||
|
|
||||||
private fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
|
||||||
private fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
|
|
||||||
private fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
|
|
||||||
|
|
||||||
private var fakeHashI = 0
|
private var fakeHashI = 0
|
||||||
private val nextFakeHash: String
|
private val nextFakeHash: String
|
||||||
get() = "fakehash${fakeHashI++}"
|
get() = "fakehash${fakeHashI++}"
|
||||||
|
|
||||||
private fun maybeGetUserInfo(): Pair<ByteArray, String>? {
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
|
||||||
val prefs = appContext.prefs
|
|
||||||
val localUserPublicKey = prefs.getLocalNumber()
|
|
||||||
val secretKey = with(appContext) {
|
|
||||||
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
|
|
||||||
edKey.secretKey.asBytes
|
|
||||||
}
|
|
||||||
return if (localUserPublicKey == null || secretKey == null) null
|
|
||||||
else secretKey to localUserPublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildContactMessage(contactList: List<Contact>): ByteArray {
|
private fun buildContactMessage(contactList: List<Contact>): ByteArray {
|
||||||
val (key,_) = maybeGetUserInfo()!!
|
val (key,_) = maybeGetUserInfo()!!
|
||||||
val contacts = Contacts.Companion.newInstance(key)
|
val contacts = Contacts.Companion.newInstance(key)
|
||||||
|
@ -80,9 +66,8 @@ class LibSessionTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun migration_one_to_ones() {
|
fun migration_one_to_ones() {
|
||||||
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
val applicationContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||||
val storageSpy = spy(app.storage)
|
val storage = applicationContext.applySpiedStorage()
|
||||||
app.storage = storageSpy
|
|
||||||
|
|
||||||
val newContactId = randomSessionId()
|
val newContactId = randomSessionId()
|
||||||
val singleContact = Contact(
|
val singleContact = Contact(
|
||||||
|
@ -93,10 +78,10 @@ class LibSessionTests {
|
||||||
val newContactMerge = buildContactMessage(listOf(singleContact))
|
val newContactMerge = buildContactMessage(listOf(singleContact))
|
||||||
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
|
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
|
||||||
fakePollNewConfig(contacts, newContactMerge)
|
fakePollNewConfig(contacts, newContactMerge)
|
||||||
verify(storageSpy).addLibSessionContacts(argThat {
|
verify(storage).addLibSessionContacts(argThat {
|
||||||
first().let { it.id == newContactId && it.approved } && size == 1
|
first().let { it.id == newContactId && it.approved } && size == 1
|
||||||
})
|
})
|
||||||
verify(storageSpy).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
|
verify(storage).setRecipientApproved(argThat { address.serialize() == newContactId }, eq(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
package network.loki.messenger.util
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.view.View
|
||||||
|
import androidx.test.espresso.Espresso.onView
|
||||||
|
import androidx.test.espresso.UiController
|
||||||
|
import androidx.test.espresso.ViewAction
|
||||||
|
import androidx.test.espresso.action.ViewActions
|
||||||
|
import androidx.test.espresso.matcher.ViewMatchers
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.adevinta.android.barista.interaction.PermissionGranter
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.hamcrest.Matcher
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
|
||||||
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
|
import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBar
|
||||||
|
import org.thoughtcrime.securesms.mms.GlideApp
|
||||||
|
|
||||||
|
fun setupLoggedInState(hasViewedSeed: Boolean = false) {
|
||||||
|
// landing activity
|
||||||
|
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
|
||||||
|
// session ID - register activity
|
||||||
|
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
|
||||||
|
// display name selection
|
||||||
|
onView(ViewMatchers.withId(R.id.displayNameEditText))
|
||||||
|
.perform(ViewActions.typeText("test-user123"))
|
||||||
|
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
|
||||||
|
// PN select
|
||||||
|
if (hasViewedSeed) {
|
||||||
|
// has viewed seed is set to false after register activity
|
||||||
|
TextSecurePreferences.setHasViewedSeed(
|
||||||
|
InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onView(ViewMatchers.withId(R.id.backgroundPollingOptionView))
|
||||||
|
.perform(ViewActions.click())
|
||||||
|
onView(ViewMatchers.withId(R.id.registerButton)).perform(ViewActions.click())
|
||||||
|
// allow notification permission
|
||||||
|
PermissionGranter.allowPermissionsIfNeeded(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ConversationActivityV2.sendMessage(messageToSend: String, linkPreview: LinkPreview? = null) {
|
||||||
|
// assume in chat activity
|
||||||
|
onView(
|
||||||
|
Matchers.allOf(
|
||||||
|
ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.inputBar)),
|
||||||
|
ViewMatchers.withId(R.id.inputBarEditText)
|
||||||
|
)
|
||||||
|
).perform(ViewActions.replaceText(messageToSend))
|
||||||
|
if (linkPreview != null) {
|
||||||
|
val glide = GlideApp.with(this)
|
||||||
|
this.findViewById<InputBar>(R.id.inputBar).updateLinkPreviewDraft(glide, linkPreview)
|
||||||
|
}
|
||||||
|
onView(
|
||||||
|
Matchers.allOf(
|
||||||
|
ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.inputBar)),
|
||||||
|
InputBarButtonDrawableMatcher.inputButtonWithDrawable(R.drawable.ic_arrow_up)
|
||||||
|
)
|
||||||
|
).perform(ViewActions.click())
|
||||||
|
// TODO: text can flaky on cursor reload, figure out a better way to wait for the UI to settle with new data
|
||||||
|
onView(ViewMatchers.isRoot()).perform(waitFor(500))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform action of waiting for a specific time.
|
||||||
|
*/
|
||||||
|
fun waitFor(millis: Long): ViewAction {
|
||||||
|
return object : ViewAction {
|
||||||
|
override fun getConstraints(): Matcher<View>? {
|
||||||
|
return ViewMatchers.isRoot()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getDescription(): String = "Wait for $millis milliseconds."
|
||||||
|
|
||||||
|
override fun perform(uiController: UiController, view: View?) {
|
||||||
|
uiController.loopMainThreadForAtLeast(millis)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package network.loki.messenger.util
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.mockito.kotlin.spy
|
||||||
|
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
|
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||||
|
import org.thoughtcrime.securesms.database.Storage
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
fun maybeGetUserInfo(): Pair<ByteArray, String>? {
|
||||||
|
val appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||||
|
val prefs = appContext.prefs
|
||||||
|
val localUserPublicKey = prefs.getLocalNumber()
|
||||||
|
val secretKey = with(appContext) {
|
||||||
|
val edKey = KeyPairUtilities.getUserED25519KeyPair(this) ?: return null
|
||||||
|
edKey.secretKey.asBytes
|
||||||
|
}
|
||||||
|
return if (localUserPublicKey == null || secretKey == null) null
|
||||||
|
else secretKey to localUserPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ApplicationContext.applySpiedStorage(): Storage {
|
||||||
|
val storageSpy = spy(storage)!!
|
||||||
|
storage = storageSpy
|
||||||
|
return storageSpy
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomSeedBytes() = (0 until 16).map { Random.nextInt(UByte.MAX_VALUE.toInt()).toByte() }
|
||||||
|
fun randomKeyPair() = KeyPairUtilities.generate(randomSeedBytes().toByteArray())
|
||||||
|
fun randomSessionId() = randomKeyPair().x25519KeyPair.hexEncodedPublicKey
|
|
@ -35,6 +35,8 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
@ -170,33 +172,46 @@ fun CreateGroup(
|
||||||
.padding(top = 16.dp)
|
.padding(top = 16.dp)
|
||||||
)
|
)
|
||||||
// Title
|
// Title
|
||||||
|
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = nameDescription
|
||||||
|
},
|
||||||
)
|
)
|
||||||
// Description
|
// Description
|
||||||
|
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = description,
|
value = description,
|
||||||
onValueChange = { description = it },
|
onValueChange = { description = it },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = descriptionDescription
|
||||||
|
},
|
||||||
)
|
)
|
||||||
// Group list
|
// Group list
|
||||||
MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp))
|
MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp))
|
||||||
}
|
}
|
||||||
// Create button
|
// Create button
|
||||||
|
val createDescription = stringResource(id = R.string.AccessibilityId_create_closed_group_create_button)
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { onCreate(CreateGroupState(name, description, members)) },
|
onClick = { onCreate(CreateGroupState(name, description, members)) },
|
||||||
enabled = name.isNotBlank() && !viewState.isLoading,
|
enabled = name.isNotBlank() && !viewState.isLoading,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally)
|
.align(Alignment.CenterHorizontally)
|
||||||
.padding(16.dp),
|
.padding(16.dp)
|
||||||
|
.semantics {
|
||||||
|
contentDescription = createDescription
|
||||||
|
}
|
||||||
|
,
|
||||||
shape = RoundedCornerShape(32.dp)
|
shape = RoundedCornerShape(32.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
@ -209,7 +224,9 @@ fun CreateGroup(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (viewState.isLoading) {
|
if (viewState.isLoading) {
|
||||||
Box(modifier = modifier.fillMaxSize().background(Color.Gray.copy(alpha = 0.5f))) {
|
Box(modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Gray.copy(alpha = 0.5f))) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.align(Alignment.Center)
|
modifier = Modifier.align(Alignment.Center)
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,4 +3,9 @@
|
||||||
<item type="id" name="holder_tag"/>
|
<item type="id" name="holder_tag"/>
|
||||||
<item type="id" name="contact_info_tag"/>
|
<item type="id" name="contact_info_tag"/>
|
||||||
<item type="id" name="motion_view_edittext"/>
|
<item type="id" name="motion_view_edittext"/>
|
||||||
|
|
||||||
|
<!-- Create closed groups -->
|
||||||
|
<item type="id" name="create_group_name_text_field"/>
|
||||||
|
<item type="id" name="create_group_create_button"/>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -80,6 +80,9 @@
|
||||||
<string name="AccessibilityId_done">Done</string>
|
<string name="AccessibilityId_done">Done</string>
|
||||||
<string name="AccessibilityId_mentions_list">Mentions list</string>
|
<string name="AccessibilityId_mentions_list">Mentions list</string>
|
||||||
<string name="AccessibilityId_contact_mentions">Contact mentions</string>
|
<string name="AccessibilityId_contact_mentions">Contact mentions</string>
|
||||||
|
<string name="AccessibilityId_closed_group_edit_group_name">Group name</string>
|
||||||
|
<string name="AccessibilityId_closed_group_edit_group_description">Group description</string>
|
||||||
|
<string name="AccessibilityId_create_closed_group_create_button">Create group</string>
|
||||||
<!-- Conversation icons -->
|
<!-- Conversation icons -->
|
||||||
<string name="AccessibilityId_call_button">Call button</string>
|
<string name="AccessibilityId_call_button">Call button</string>
|
||||||
<string name="AccessibilityId_settings">Settings</string>
|
<string name="AccessibilityId_settings">Settings</string>
|
||||||
|
|
|
@ -702,4 +702,51 @@ class InstrumentedTests {
|
||||||
assertThat(groupInfo.getDescription(), equalTo("This is a test group"))
|
assertThat(groupInfo.getDescription(), equalTo("This is a test group"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGroupKeyConfig() {
|
||||||
|
val (userPubKey, userSecret) = keyPair
|
||||||
|
val groupConfig = UserGroupsConfig.newInstance(userSecret)
|
||||||
|
val group = groupConfig.createGroup()
|
||||||
|
groupConfig.set(group)
|
||||||
|
val groupInfo = GroupInfoConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey())
|
||||||
|
groupInfo.setName("test")
|
||||||
|
val groupMembers = GroupMembersConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey())
|
||||||
|
groupMembers.set(
|
||||||
|
GroupMember(
|
||||||
|
sessionId = SessionId(IdPrefix.STANDARD, Sodium.ed25519PkToCurve25519(userPubKey)).hexString(),
|
||||||
|
name = "admin",
|
||||||
|
admin = true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val membersDump = groupMembers.dump()
|
||||||
|
val infoDump = groupInfo.dump()
|
||||||
|
|
||||||
|
val ourKeyConfig = GroupKeysConfig.newInstance(
|
||||||
|
userSecret,
|
||||||
|
group.groupSessionId.pubKeyBytes,
|
||||||
|
group.signingKey(),
|
||||||
|
info = groupInfo,
|
||||||
|
members = groupMembers
|
||||||
|
)
|
||||||
|
|
||||||
|
assertThat(ourKeyConfig.needsRekey(), equalTo(false))
|
||||||
|
val pushed = ourKeyConfig.pendingConfig()!!
|
||||||
|
val messageTimestamp = System.currentTimeMillis()
|
||||||
|
ourKeyConfig.loadKey(pushed, "testabc", messageTimestamp, groupInfo, groupMembers)
|
||||||
|
assertThat(ourKeyConfig.needsDump(), equalTo(true))
|
||||||
|
ourKeyConfig.dump()
|
||||||
|
assertThat(ourKeyConfig.needsRekey(), equalTo(false))
|
||||||
|
val mergeInfo = GroupInfoConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey(), infoDump)
|
||||||
|
val mergeMembers = GroupMembersConfig.newInstance(group.groupSessionId.pubKeyBytes, group.signingKey(), membersDump)
|
||||||
|
val mergeConfig = GroupKeysConfig.newInstance(userSecret, group.groupSessionId.pubKeyBytes, group.signingKey(), info = mergeInfo, members = mergeMembers)
|
||||||
|
mergeConfig.loadKey(pushed, "testabc", messageTimestamp, mergeInfo, mergeMembers)
|
||||||
|
assertThat(mergeConfig.needsRekey(), equalTo(false))
|
||||||
|
assertThat(mergeConfig.keys().size, equalTo(1))
|
||||||
|
assertThat(ourKeyConfig.keys().size, equalTo(1))
|
||||||
|
assertThat(mergeConfig.keys().first(), equalTo(ourKeyConfig.keys().first()))
|
||||||
|
assertThat(ourKeyConfig.groupKeys().size, equalTo(1))
|
||||||
|
assertThat(mergeConfig.groupKeys().size, equalTo(1))
|
||||||
|
assertThat(ourKeyConfig.groupKeys().first(), equalTo(mergeConfig.groupKeys().first()))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -127,6 +127,87 @@ message DataMessage {
|
||||||
optional GroupPromoteMessage promoteMessage = 34;
|
optional GroupPromoteMessage promoteMessage = 34;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New closed group update messages
|
||||||
|
message GroupUpdateMessage {
|
||||||
|
optional GroupUpdateInviteMessage inviteMessage = 1;
|
||||||
|
optional GroupUpdateDeleteMessage deleteMessage = 2;
|
||||||
|
optional GroupUpdateInfoChangeMessage infoChangeMessage = 3;
|
||||||
|
optional GroupUpdateMemberChangeMessage memberChangeMessage = 4;
|
||||||
|
optional GroupUpdatePromoteMessage promoteMessage = 5;
|
||||||
|
optional GroupUpdateMemberLeftMessage memberLeftMessage = 6;
|
||||||
|
optional GroupUpdateInviteResponseMessage inviteResponse = 7;
|
||||||
|
optional GroupUpdateDeleteMemberContentMessage deleteMemberContent = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New closed groups
|
||||||
|
message GroupUpdateInviteMessage {
|
||||||
|
// @required
|
||||||
|
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
|
||||||
|
// @required
|
||||||
|
required string name = 2;
|
||||||
|
// @required
|
||||||
|
required bytes memberAuthData = 3;
|
||||||
|
optional bytes profileKey = 4;
|
||||||
|
optional LokiProfile profile = 5;
|
||||||
|
// @required
|
||||||
|
required bytes adminSignature = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUpdateDeleteMessage {
|
||||||
|
// @required
|
||||||
|
required string groupSessionId = 1; // The `groupIdentityPublicKey` with a `03` prefix
|
||||||
|
// @required
|
||||||
|
required bytes adminSignature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUpdatePromoteMessage {
|
||||||
|
// @required
|
||||||
|
required bytes groupIdentitySeed = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUpdateInfoChangeMessage {
|
||||||
|
enum Type {
|
||||||
|
NAME = 1;
|
||||||
|
AVATAR = 2;
|
||||||
|
DISAPPEARING_MESSAGES = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @required
|
||||||
|
required Type type = 1;
|
||||||
|
optional string updatedName = 2;
|
||||||
|
optional uint32 updatedExpiration = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUpdateMemberChangeMessage {
|
||||||
|
enum Type {
|
||||||
|
ADDED = 1;
|
||||||
|
REMOVED = 2;
|
||||||
|
PROMOTED = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @required
|
||||||
|
required Type type = 1;
|
||||||
|
repeated bytes memberPublicKeys = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUpdateMemberLeftMessage {
|
||||||
|
// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop)
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUpdateInviteResponseMessage {
|
||||||
|
// @required
|
||||||
|
required bool isApproved = 1; // Whether the request was approved
|
||||||
|
optional bytes profileKey = 2;
|
||||||
|
optional LokiProfile profile = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GroupUpdateDeleteMemberContentMessage {
|
||||||
|
repeated bytes memberPublicKeys = 1;
|
||||||
|
// @required
|
||||||
|
required bytes adminSignature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
message ClosedGroupControlMessage {
|
message ClosedGroupControlMessage {
|
||||||
|
|
||||||
enum Type {
|
enum Type {
|
||||||
|
|
Loading…
Reference in New Issue