From e79a980f2cec0366e0063cedb8c0cf22144cba3d Mon Sep 17 00:00:00 2001 From: 0x330a <92654767+0x330a@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:18:56 +1100 Subject: [PATCH] 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 --- app/build.gradle | 6 +- app/src/androidTest/AndroidManifest.xml | 6 +- .../loki/messenger/CreateGroupTests.kt | 63 ++++++++++++++ .../loki/messenger/HomeActivityTests.kt | 81 ++++-------------- .../network/loki/messenger/LibSessionTests.kt | 31 ++----- .../loki/messenger/util/LoginAutomation.kt | 82 +++++++++++++++++++ .../loki/messenger/util/StorageUtility.kt | 31 +++++++ .../securesms/groups/CreateGroupFragment.kt | 25 +++++- app/src/main/res/values/ids.xml | 5 ++ app/src/main/res/values/strings.xml | 3 + .../libsession_util/InstrumentedTests.kt | 47 +++++++++++ libsignal/protobuf/SignalService.proto | 81 ++++++++++++++++++ 12 files changed, 364 insertions(+), 97 deletions(-) create mode 100644 app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt create mode 100644 app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt create mode 100644 app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt diff --git a/app/build.gradle b/app/build.gradle index 56f2a4ce1c..333031759d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml index deab87dd62..be464ad75a 100644 --- a/app/src/androidTest/AndroidManifest.xml +++ b/app/src/androidTest/AndroidManifest.xml @@ -1,8 +1,10 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> + + + \ No newline at end of file diff --git a/app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt b/app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt new file mode 100644 index 0000000000..04b8dab95b --- /dev/null +++ b/app/src/androidTest/java/network/loki/messenger/CreateGroupTests.kt @@ -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)) + + } + +} \ No newline at end of file diff --git a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt index a20a3a2a67..822564b4ff 100644 --- a/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt +++ b/app/src/androidTest/java/network/loki/messenger/HomeActivityTests.kt @@ -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(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? { - return isRoot() - } - - override fun getDescription(): String = "Wait for $millis milliseconds." - - override fun perform(uiController: UiController, view: View?) { - uiController.loopMainThreadForAtLeast(millis) - } - } - } - } \ No newline at end of file diff --git a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt index 59cb8ede08..e58f1db5f2 100644 --- a/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt +++ b/app/src/androidTest/java/network/loki/messenger/LibSessionTests.kt @@ -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? { - 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): 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)) } } \ No newline at end of file diff --git a/app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt b/app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt new file mode 100644 index 0000000000..e7a3ce107c --- /dev/null +++ b/app/src/androidTest/java/network/loki/messenger/util/LoginAutomation.kt @@ -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(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? { + return ViewMatchers.isRoot() + } + + override fun getDescription(): String = "Wait for $millis milliseconds." + + override fun perform(uiController: UiController, view: View?) { + uiController.loopMainThreadForAtLeast(millis) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt b/app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt new file mode 100644 index 0000000000..7b02efad68 --- /dev/null +++ b/app/src/androidTest/java/network/loki/messenger/util/StorageUtility.kt @@ -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? { + 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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt index 9a5cba044c..aa5e7a14de 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/CreateGroupFragment.kt @@ -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) ) diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index cb9392f697..b64936bea8 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -3,4 +3,9 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8d1e2dd230..dd28bef23f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,6 +80,9 @@ Done Mentions list Contact mentions + Group name + Group description + Create group Call button Settings diff --git a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt index f4e2672791..2534ac1605 100644 --- a/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt +++ b/libsession-util/src/androidTest/java/network/loki/messenger/libsession_util/InstrumentedTests.kt @@ -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())) + } + } \ No newline at end of file diff --git a/libsignal/protobuf/SignalService.proto b/libsignal/protobuf/SignalService.proto index eb5313ae12..924c3e6aec 100644 --- a/libsignal/protobuf/SignalService.proto +++ b/libsignal/protobuf/SignalService.proto @@ -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 {