diff --git a/app/build.gradle b/app/build.gradle index 56f2a4ce1..333031759 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 deab87dd6..be464ad75 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 000000000..04b8dab95 --- /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 a20a3a2a6..822564b4f 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 59cb8ede0..e58f1db5f 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 000000000..e7a3ce107 --- /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 000000000..7b02efad6 --- /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 9a5cba044..aa5e7a14d 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 cb9392f69..b64936bea 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 8d1e2dd23..dd28bef23 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 f4e267279..2534ac160 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 eb5313ae1..924c3e6ae 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 {