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"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
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"
|
||||
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"
|
||||
testImplementation "androidx.test:core:$testCoreVersion"
|
||||
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.idling:idling-concurrent: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'
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.4'
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="network.loki.messenger.test">
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application>
|
||||
<uses-library android:name="android.test.runner"
|
||||
android:required="false" />
|
||||
|
||||
<activity android:name="androidx.activity.ComponentActivity"/>
|
||||
|
||||
</application>
|
||||
</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
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Instrumentation
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
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.assertion.ViewAssertions.matches
|
||||
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.ext.junit.rules.ActivityScenarioRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.filters.SmallTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.adevinta.android.barista.interaction.PermissionGranter
|
||||
import network.loki.messenger.util.InputBarButtonDrawableMatcher.Companion.inputButtonWithDrawable
|
||||
import org.hamcrest.Matcher
|
||||
import network.loki.messenger.util.sendMessage
|
||||
import network.loki.messenger.util.setupLoggedInState
|
||||
import network.loki.messenger.util.waitFor
|
||||
import org.hamcrest.Matchers.allOf
|
||||
import org.hamcrest.Matchers.not
|
||||
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.libsignal.utilities.guava.Optional
|
||||
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.mms.GlideApp
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@LargeTest
|
||||
@SmallTest
|
||||
class HomeActivityTests {
|
||||
|
||||
@get:Rule
|
||||
|
@ -59,38 +53,6 @@ class HomeActivityTests {
|
|||
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() {
|
||||
onView(withId(R.id.newConversationButton)).perform(ViewActions.click())
|
||||
onView(withId(R.id.createPrivateChatButton)).perform(ViewActions.click())
|
||||
|
@ -134,11 +96,13 @@ class HomeActivityTests {
|
|||
setupLoggedInState()
|
||||
goToMyChat()
|
||||
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
||||
sendMessage("howdy")
|
||||
sendMessage("test")
|
||||
// tests url rewriter doesn't crash
|
||||
sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
|
||||
sendMessage("https://www.ámazon.com")
|
||||
with (activityMonitor.waitForActivity() as ConversationActivityV2) {
|
||||
sendMessage("howdy")
|
||||
sendMessage("test")
|
||||
// tests url rewriter doesn't crash
|
||||
sendMessage("https://www.getsession.org?random_query_parameter=testtesttesttesttesttesttesttest&other_query_parameter=testtesttesttesttesttesttesttest")
|
||||
sendMessage("https://www.ámazon.com")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -148,7 +112,9 @@ class HomeActivityTests {
|
|||
TextSecurePreferences.setLinkPreviewsEnabled(InstrumentationRegistry.getInstrumentation().targetContext, true)
|
||||
// given the link url text
|
||||
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
|
||||
onView(withSubstring(url)).perform(ViewActions.click())
|
||||
|
@ -162,21 +128,4 @@ class HomeActivityTests {
|
|||
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.util.Contact
|
||||
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.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.kotlin.argThat
|
||||
import org.mockito.kotlin.eq
|
||||
import org.mockito.kotlin.spy
|
||||
import org.mockito.kotlin.verify
|
||||
import org.session.libsession.messaging.MessagingModuleConfiguration
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
|
@ -22,32 +25,15 @@ import org.session.libsignal.utilities.KeyHelper
|
|||
import org.session.libsignal.utilities.hexEncodedPublicKey
|
||||
import org.thoughtcrime.securesms.ApplicationContext
|
||||
import org.thoughtcrime.securesms.crypto.KeyPairUtilities
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@SmallTest
|
||||
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 val nextFakeHash: String
|
||||
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 {
|
||||
val (key,_) = maybeGetUserInfo()!!
|
||||
val contacts = Contacts.Companion.newInstance(key)
|
||||
|
@ -80,9 +66,8 @@ class LibSessionTests {
|
|||
|
||||
@Test
|
||||
fun migration_one_to_ones() {
|
||||
val app = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
val storageSpy = spy(app.storage)
|
||||
app.storage = storageSpy
|
||||
val applicationContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as ApplicationContext
|
||||
val storage = applicationContext.applySpiedStorage()
|
||||
|
||||
val newContactId = randomSessionId()
|
||||
val singleContact = Contact(
|
||||
|
@ -93,10 +78,10 @@ class LibSessionTests {
|
|||
val newContactMerge = buildContactMessage(listOf(singleContact))
|
||||
val contacts = MessagingModuleConfiguration.shared.configFactory.contacts!!
|
||||
fakePollNewConfig(contacts, newContactMerge)
|
||||
verify(storageSpy).addLibSessionContacts(argThat {
|
||||
verify(storage).addLibSessionContacts(argThat {
|
||||
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.platform.ComposeView
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
|
@ -170,33 +172,46 @@ fun CreateGroup(
|
|||
.padding(top = 16.dp)
|
||||
)
|
||||
// Title
|
||||
val nameDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_name)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||
.semantics {
|
||||
contentDescription = nameDescription
|
||||
},
|
||||
)
|
||||
// Description
|
||||
val descriptionDescription = stringResource(id = R.string.AccessibilityId_closed_group_edit_group_description)
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp),
|
||||
.padding(vertical = 8.dp, horizontal = 24.dp)
|
||||
.semantics {
|
||||
contentDescription = descriptionDescription
|
||||
},
|
||||
)
|
||||
// Group list
|
||||
MemberList(contacts = members, modifier = Modifier.padding(vertical = 8.dp, horizontal = 24.dp))
|
||||
}
|
||||
// Create button
|
||||
val createDescription = stringResource(id = R.string.AccessibilityId_create_closed_group_create_button)
|
||||
OutlinedButton(
|
||||
onClick = { onCreate(CreateGroupState(name, description, members)) },
|
||||
enabled = name.isNotBlank() && !viewState.isLoading,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(16.dp),
|
||||
.padding(16.dp)
|
||||
.semantics {
|
||||
contentDescription = createDescription
|
||||
}
|
||||
,
|
||||
shape = RoundedCornerShape(32.dp)
|
||||
) {
|
||||
Text(
|
||||
|
@ -209,7 +224,9 @@ fun CreateGroup(
|
|||
}
|
||||
}
|
||||
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(
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
|
|
|
@ -3,4 +3,9 @@
|
|||
<item type="id" name="holder_tag"/>
|
||||
<item type="id" name="contact_info_tag"/>
|
||||
<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>
|
||||
|
|
|
@ -80,6 +80,9 @@
|
|||
<string name="AccessibilityId_done">Done</string>
|
||||
<string name="AccessibilityId_mentions_list">Mentions list</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 -->
|
||||
<string name="AccessibilityId_call_button">Call button</string>
|
||||
<string name="AccessibilityId_settings">Settings</string>
|
||||
|
|
|
@ -702,4 +702,51 @@ class InstrumentedTests {
|
|||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
enum Type {
|
||||
|
|
Loading…
Reference in New Issue