Skip to content

Commit

Permalink
Merge tag 'v7.32.1' into molly-7.32
Browse files Browse the repository at this point in the history
  • Loading branch information
valldrac committed Jan 31, 2025
2 parents 4c0c69a + 6492a90 commit dc5f599
Show file tree
Hide file tree
Showing 232 changed files with 17,004 additions and 9,952 deletions.
7 changes: 5 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ plugins {
id("molly")
}

val canonicalVersionCode = 1506
val canonicalVersionName = "7.31.1"
val canonicalVersionCode = 1508
val canonicalVersionName = "7.32.1"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
val mollyRevision = 1
Expand Down Expand Up @@ -426,6 +426,7 @@ dependencies {
implementation(project(":core-ui"))

implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.fragment.compose)
implementation(libs.androidx.appcompat) {
version {
strictly("1.6.1")
Expand Down Expand Up @@ -562,6 +563,8 @@ dependencies {
testImplementation(testFixtures(project(":libsignal-service")))
testImplementation(testLibs.espresso.core)

androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(testLibs.androidx.test.ext.junit)
androidTestImplementation(testLibs.espresso.core)
androidTestImplementation(testLibs.androidx.test.core)
Expand Down
Binary file modified app/src/androidTest/assets/backupTests/chat_folder_00.binproto
Binary file not shown.
Binary file modified app/src/androidTest/assets/backupTests/chat_folder_01.binproto
Binary file not shown.
Binary file modified app/src/androidTest/assets/backupTests/chat_folder_02.binproto
Binary file not shown.
Binary file modified app/src/androidTest/assets/backupTests/chat_folder_03.binproto
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
3,510 changes: 3,509 additions & 1 deletion app/src/androidTest/assets/inAppPaymentsTests/configuration.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription

import android.content.ClipboardManager
import android.content.Context
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.createEmptyComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToNode
import androidx.core.content.ContextCompat
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.billing.BillingProduct
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.billing.BillingPurchaseState
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.RemoteConfig
import java.math.BigDecimal
import java.util.Currency

@RunWith(AndroidJUnit4::class)
class MessageBackupsCheckoutActivityTest {

@get:Rule val activityRule = SignalActivityRule()

@get:Rule val iapRule = InAppPaymentsRule()

@get:Rule val composeTestRule = createEmptyComposeRule()

private val purchaseResults = MutableSharedFlow<BillingPurchaseResult>()

@Before
fun setUp() {
every { AppDependencies.billingApi.getBillingPurchaseResults() } returns purchaseResults
coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")))
coEvery { AppDependencies.billingApi.launchBillingFlow(any()) } returns Unit

mockkStatic(RemoteConfig::class)
every { RemoteConfig.messageBackups } returns true
}

@Test
fun e2e_paid_happy_path() {
val scenario = launchCheckoutFlow()
val context = InstrumentationRegistry.getInstrumentation().targetContext

e2e_shared_happy_path(context, scenario)

composeTestRule.onNodeWithTag("message-backups-type-selection-screen-lazy-column")
.performScrollToNode(hasText(context.getString(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)))
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__text_plus_all_your_media)).performClick()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).assertIsEnabled()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).performClick()
composeTestRule.waitForIdle()

runBlocking {
purchaseResults.emit(
BillingPurchaseResult.Success(
purchaseState = BillingPurchaseState.PURCHASED,
purchaseToken = "asdf",
isAcknowledged = false,
purchaseTime = System.currentTimeMillis(),
isAutoRenewing = true
)
)
}

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("dialog-circular-progress-indicator").assertIsDisplayed()

val iap = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
assertThat(iap?.state).isEqualTo(InAppPaymentTable.State.PENDING)

SignalDatabase.inAppPayments.update(
inAppPayment = iap!!.copy(
state = InAppPaymentTable.State.END
)
)

composeTestRule.waitForIdle()
composeTestRule.onNodeWithTag("dialog-circular-progress-indicator").assertIsNotDisplayed()
}

@Test
fun e2e_free_happy_path() {
val scenario = launchCheckoutFlow()
val context = InstrumentationRegistry.getInstrumentation().targetContext

e2e_shared_happy_path(context, scenario)

composeTestRule.onNodeWithTag("message-backups-type-selection-screen-lazy-column")
.performScrollToNode(hasText(context.getString(R.string.MessageBackupsTypeSelectionScreen__free)))
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__free)).performClick()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).assertIsEnabled()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).performClick()
composeTestRule.waitForIdle()

assertThat(SignalStore.backup.backupTier).isEqualTo(MessageBackupTier.FREE)
}

private fun e2e_shared_happy_path(context: Context, scenario: ActivityScenario<MessageBackupsCheckoutActivity>) {
assertThat(SignalStore.backup.backupTier).isNull()

// Backup education screen
composeTestRule.onNodeWithText(context.getString(R.string.RemoteBackupsSettingsFragment__signal_backups)).assertIsDisplayed()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsEducationScreen__enable_backups)).performClick()

// Key education screen
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyEducationScreen__your_backup_key)).assertIsDisplayed()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__next)).performClick()

// Key record screen
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__record_your_backup_key)).assertIsDisplayed()
composeTestRule.onNodeWithTag("message-backups-key-record-screen-lazy-column")
.performScrollToNode(hasText(context.getString(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)))
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__copy_to_clipboard)).performClick()

scenario.onActivity {
val backupKeyString = SignalStore.account.accountEntropyPool.value.chunked(4).joinToString(" ")
val clipboardManager = ContextCompat.getSystemService(context, ClipboardManager::class.java)
assertThat(clipboardManager?.primaryClip?.getItemAt(0)?.coerceToText(context)).isEqualTo(backupKeyString)
}

composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__next)).assertIsDisplayed()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__next)).performClick()

// Key record bottom sheet
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__keep_your_key_safe)).assertIsDisplayed()
composeTestRule.onNodeWithTag("message-backups-key-record-screen-sheet-content")
.performScrollToNode(hasText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue)))
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue)).assertIsNotEnabled()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__ive_recorded_my_key)).performClick()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue)).assertIsEnabled()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsKeyRecordScreen__continue)).performClick()

// Type selection screen
composeTestRule.onNodeWithText(context.getString(R.string.MessagesBackupsTypeSelectionScreen__choose_your_backup_plan)).assertIsDisplayed()
composeTestRule.onNodeWithText(context.getString(R.string.MessageBackupsTypeSelectionScreen__next)).assertIsNotEnabled()
}

private fun launchCheckoutFlow(tier: MessageBackupTier? = null): ActivityScenario<MessageBackupsCheckoutActivity> {
return ActivityScenario.launch(
MessageBackupsCheckoutActivity.Contract().createIntent(InstrumentationRegistry.getInstrumentation().targetContext, tier)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -25,24 +24,26 @@ import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDepende
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Delete
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.actions.RecyclerViewScrollToBottomAction
import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.math.BigDecimal
import java.util.Currency
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds

@Ignore("Test fails on small screens, requires scrolling.")
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class CheckoutFlowActivityTest__RecurringDonations {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)

@get:Rule
val iapRule = InAppPaymentsRule()

private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION)

@Test
Expand All @@ -54,25 +55,27 @@ class CheckoutFlowActivityTest__RecurringDonations {
@Test
fun givenNoCurrentDonation_whenILoadScreen_thenIExpectContinueButton() {
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withId(R.id.recycler)).perform(RecyclerViewScrollToBottomAction)
onView(withText("Continue")).check(matches(isDisplayed()))
}

@Test
fun givenACurrentDonation_whenILoadScreen_thenIExpectUpgradeButton() {
initialiseConfigurationResponse()
initialiseActiveSubscription()

ActivityScenario.launch<CheckoutFlowActivity>(intent)

onView(withId(R.id.recycler)).perform(RecyclerViewScrollToBottomAction)
onView(withText(R.string.SubscribeFragment__update_subscription)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
}

@Test
fun givenACurrentDonation_whenIPressCancel_thenIExpectCancellationDialog() {
initialiseConfigurationResponse()
initialiseActiveSubscription()

ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withId(R.id.recycler)).perform(RecyclerViewScrollToBottomAction)
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__cancel_subscription)).perform(ViewActions.click())
onView(withText(R.string.SubscribeFragment__confirm_cancellation)).check(matches(isDisplayed()))
Expand All @@ -82,25 +85,14 @@ class CheckoutFlowActivityTest__RecurringDonations {

@Test
fun givenAPendingRecurringDonation_whenILoadScreen_thenIExpectDisabledUpgradeButton() {
initialiseConfigurationResponse()
initialisePendingSubscription()

ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withId(R.id.recycler)).perform(RecyclerViewScrollToBottomAction)
onView(withText(R.string.SubscribeFragment__update_subscription)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__update_subscription)).check(matches(isNotEnabled()))
}

private fun initialiseConfigurationResponse() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/configuration") {
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
}
)
}

private fun initialiseActiveSubscription() {
val currency = Currency.getInstance("USD")
val subscriber = InAppPaymentSubscriberRecord(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

package org.thoughtcrime.securesms.testing

import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import org.junit.rules.ExternalResource
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration

/**
* Sets up some common infrastructure for on-device InAppPayment testing
*/
class InAppPaymentsRule : ExternalResource() {
override fun before() {
initialiseConfigurationResponse()
initialisePutSubscription()
}

private fun initialiseConfigurationResponse() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/configuration") {
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
}
)
}

private fun initialisePutSubscription() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/subscription/") {
MockResponse().success()
}
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/

package org.thoughtcrime.securesms.testing.actions

import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher

/**
* Scrolls the RecyclerView to the bottom position.
*
* Borrowed from [https://stackoverflow.com/a/55990445](https://stackoverflow.com/a/55990445)
*/
object RecyclerViewScrollToBottomAction : ViewAction {
override fun getDescription(): String = "scroll RecyclerView to bottom"

override fun getConstraints(): Matcher<View> = allOf(isAssignableFrom(RecyclerView::class.java), isDisplayed())

override fun perform(uiController: UiController?, view: View?) {
val recyclerView = view as RecyclerView
val itemCount = recyclerView.adapter?.itemCount
val position = itemCount?.minus(1) ?: 0
recyclerView.scrollToPosition(position)
uiController?.loopMainThreadUntilIdle()
}
}
Loading

0 comments on commit dc5f599

Please sign in to comment.