From 947ce3a96ebfdc75a819e9b3a0ac81770dfeb5a8 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Tue, 29 Apr 2025 16:09:57 +0300 Subject: [PATCH 1/6] Fixed DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.| #3026 --- .../java/com/flowcrypt/email/base/BaseTest.kt | 22 +- ...ITestCorrectCreatingAndUpdatingFlowTest.kt | 286 +++++++++++++----- ...aftsGmailAPITestCorrectDeletingFlowTest.kt | 12 +- ...raftsGmailAPITestCorrectSendingFlowTest.kt | 8 +- .../ui/base/BaseDraftsGmailAPIFlowTest.kt | 165 ++++------ .../email/ui/base/BaseGmailApiTest.kt | 2 + 6 files changed, 309 insertions(+), 186 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/base/BaseTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/base/BaseTest.kt index 77226df71d..b0334ffa98 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/base/BaseTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/base/BaseTest.kt @@ -12,6 +12,7 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.os.Bundle +import android.os.SystemClock import android.text.Html import android.view.View import android.widget.Toast @@ -71,6 +72,7 @@ import org.junit.Before import org.junit.Rule import java.io.InputStream import java.util.Properties +import java.util.concurrent.TimeUnit /** * The base test implementation. @@ -86,7 +88,7 @@ abstract class BaseTest : BaseActivityTestImplementation { val flowCryptTestSettingsRule = FlowCryptTestSettingsRule() private val useIntents - get() = flowCryptTestSettingsRule.flowCryptTestSettings?.useIntents ?: false + get() = flowCryptTestSettingsRule.flowCryptTestSettings?.useIntents == true protected val decorView: View? get() { @@ -385,6 +387,24 @@ abstract class BaseTest : BaseActivityTestImplementation { } } + protected fun waitUntil( + timeoutInMilliseconds: Long = TimeUnit.SECONDS.toMillis(10), + predicate: () -> Boolean + ) { + val startTime = SystemClock.uptimeMillis() + val interval = 100L + + var elapsedTime: Long = 0 + while (!predicate()) { + if (elapsedTime >= timeoutInMilliseconds) { + break + } + + SystemClock.sleep(interval) + elapsedTime = SystemClock.uptimeMillis() - startTime + } + } + companion object{ const val NOTIFICATION_RESOURCES_NAME = "com.android.systemui:id/expandableNotificationRow" diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt index b5c9244710..031c59adaf 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt @@ -5,16 +5,18 @@ package com.flowcrypt.email.ui -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.closeSoftKeyboard import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.scrollTo import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.matcher.ViewMatchers.hasSibling +import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText @@ -23,55 +25,65 @@ import androidx.test.filters.MediumTest import com.flowcrypt.email.R import com.flowcrypt.email.TestConstants import com.flowcrypt.email.jetpack.viewmodel.DraftViewModel +import com.flowcrypt.email.junit.annotations.FlowCryptTestSettings import com.flowcrypt.email.matchers.CustomMatchers.Companion.withRecyclerViewItemCount import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.FlowCryptMockWebServerRule import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule -import com.flowcrypt.email.ui.DraftsGmailAPITestCorrectDeletingFlowTest.Companion.HISTORY_ID_FIRST import com.flowcrypt.email.ui.base.BaseDraftsGmailAPIFlowTest +import com.flowcrypt.email.viewaction.ClickOnViewInRecyclerViewItem import com.google.api.client.json.Json +import com.google.api.client.json.gson.GsonFactory +import com.google.api.services.gmail.model.BatchModifyMessagesRequest +import com.google.api.services.gmail.model.Draft +import com.google.api.services.gmail.model.ListDraftsResponse import com.google.api.services.gmail.model.Message import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest -import org.hamcrest.CoreMatchers.allOf +import okio.GzipSource +import okio.buffer +import org.hamcrest.Matchers +import org.hamcrest.core.AllOf.allOf import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.RunWith import java.net.HttpURLConnection +import java.util.concurrent.TimeUnit /** * https://github.com/FlowCrypt/flowcrypt-android/issues/2050 */ @MediumTest +@FlowCryptTestSettings(useCommonIdling = false) @RunWith(AndroidJUnit4::class) -@Ignore("Should be re-looked after threads will be completed") class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIFlowTest() { override val mockWebServerRule = FlowCryptMockWebServerRule(TestConstants.MOCK_WEB_SERVER_PORT, object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { - when { + return when { request.method == "PUT" && request.path == "/gmail/v1/users/me/drafts/$DRAFT_ID_FIRST" -> { val (draft, mimeMessage) = getDraftAndMimeMessageFromRequest(request) - val existingDraftInCache = draftsCache.firstOrNull { it.id == DRAFT_ID_FIRST } - - return if (existingDraftInCache != null && mimeMessage.subject == MESSAGE_SUBJECT_FIRST_EDITED) { - val existingMessage = existingDraftInCache.message - existingDraftInCache.message = Message().apply { - id = existingMessage.id - threadId = existingMessage.threadId - labelIds = existingMessage.labelIds - raw = draft.message.raw - } + val existingDraftInCache = draftsCache[DRAFT_ID_FIRST] + if (existingDraftInCache != null && mimeMessage.subject == MESSAGE_SUBJECT_FIRST_EDITED) { + val updatedDraft = draft.clone().apply { + id = existingDraftInCache.id + message = Message().apply { + id = existingDraftInCache.message.id + threadId = existingDraftInCache.message.threadId + labelIds = existingDraftInCache.message.labelIds + raw = message.raw + } + } + draftsCache.put(DRAFT_ID_FIRST, updatedDraft) MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(existingDraftInCache.toString()) + .setBody(updatedDraft.toString()) } else { MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) } @@ -80,7 +92,7 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF request.method == "POST" && request.path == "/gmail/v1/users/me/drafts" -> { val (draft, mimeMessage) = getDraftAndMimeMessageFromRequest(request) - return when (mimeMessage.subject) { + when (mimeMessage.subject) { MESSAGE_SUBJECT_FIRST -> { val newDraft = prepareDraft( draftId = DRAFT_ID_FIRST, @@ -88,20 +100,7 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF messageThreadId = THREAD_ID_FIRST, rawMsg = draft.message.raw ) - draftsCache.add(newDraft) - - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(newDraft.toString()) - } - - MESSAGE_SUBJECT_SECOND -> { - val newDraft = prepareDraft( - draftId = DRAFT_ID_SECOND, - messageId = MESSAGE_ID_SECOND, - messageThreadId = THREAD_ID_SECOND, - rawMsg = draft.message.raw - ) - draftsCache.add(newDraft) + draftsCache.put(DRAFT_ID_FIRST, newDraft) MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .setBody(newDraft.toString()) @@ -114,28 +113,92 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF } request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_FIRST}?fields=id,threadId,historyId&format=full" -> { - return genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) + genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) } - request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_SECOND}?fields=id,threadId,historyId&format=full" -> { - return genMsgDetailsMockResponse(MESSAGE_ID_SECOND, THREAD_ID_SECOND) + request.method == "GET" && request.path?.matches(REGEX_USER_THREADS_GET_FORMAT_FULL) == true -> { + val path = request.path ?: "" + val threadId = + REGEX_USER_THREADS_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() + when (threadId) { + THREAD_ID_FIRST -> { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + .setBody( + com.google.api.services.gmail.model.Thread().apply { + factory = GsonFactory.getDefaultInstance() + id = THREAD_ID_FIRST + messages = listOf( + genMessage( + messageId = MESSAGE_ID_FIRST, + messageThreadId = THREAD_ID_FIRST, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject + ?: "", + historyIdValue = HISTORY_ID_FIRST + ) + ) + }.toString() + ) + } + + else -> { + MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + } } request.method == "GET" && request.path == genPathForGmailMessages(MESSAGE_ID_FIRST) -> { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .setHeader("Content-Type", Json.MEDIA_TYPE) .setBody( genMessage( messageId = MESSAGE_ID_FIRST, messageThreadId = THREAD_ID_FIRST, - subject = MESSAGE_SUBJECT_FIRST, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", historyIdValue = HISTORY_ID_FIRST - ) + ).toString() ) } - else -> return handleCommonAPICalls(request) + request.method == "GET" && request.path?.matches(REGEX_DRAFT_BY_RFC822MSGID) == true -> { + genListDraftsResponseForRfc822msgidSearch(request.path ?: "") + } + + request.method == "GET" && request.path?.matches(REGEX_USER_MESSAGES_GET_FORMAT_FULL) == true -> { + val path = request.path ?: "" + val messageId = + REGEX_USER_MESSAGES_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() + if (messageId in listOf(MESSAGE_ID_FIRST)) { + genUserMessagesGetFormatFullResponseInternal(path) + } else { + handleCommonAPICalls(request) + } + } + + request.path == "/gmail/v1/users/me/messages/5555555555500001?fields=raw&format=raw" -> { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + .setBody(draftsCache[DRAFT_ID_FIRST]?.message?.raw ?: error("Draft not found")) + } + + request.method == "POST" && request.path == "/gmail/v1/users/me/messages/batchModify" -> { + val source = GzipSource(request.body) + val batchModifyMessagesRequest = GsonFactory.getDefaultInstance().fromInputStream( + source.buffer().inputStream(), + BatchModifyMessagesRequest::class.java + ) + + val handledIds = arrayOf(MESSAGE_ID_FIRST) + + if (handledIds.any { batchModifyMessagesRequest.ids.contains(it) }) { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + } else { + MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + } + + else -> handleCommonAPICalls(request) } } }) @@ -159,41 +222,55 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF onView(withId(R.id.recyclerViewMsgs)) .check(matches(withRecyclerViewItemCount(0))) - openComposeScreenAndTypeSubject(MESSAGE_SUBJECT_FIRST) - Thread.sleep(DraftViewModel.DELAY_TIMEOUT * 2) + //create a draft + openComposeScreenAndTypeData(MESSAGE_SUBJECT_FIRST) + //switch to standard mode + openActionBarOverflowOrOptionsMenu(getTargetContext()) + onView(withText(R.string.switch_to_standard_email)) + .check(matches(isDisplayed())) + .perform(click()) + waitUntil(DraftViewModel.DELAY_TIMEOUT * 2) { draftsCache.isNotEmpty() } pressBack() + waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(10)) - //check that the first draft was created + //check that the a thread with a single draft was created assertEquals(1, draftsCache.size) onView(withId(R.id.recyclerViewMsgs)) .check(matches(withRecyclerViewItemCount(1))) - val mimeMessageFirst = getMimeMessageFromCache(0) + val mimeMessageFirst = getMimeMessageFromCache(DRAFT_ID_FIRST) assertEquals(MESSAGE_SUBJECT_FIRST, mimeMessageFirst.subject) - openComposeScreenAndTypeSubject(MESSAGE_SUBJECT_SECOND) - Thread.sleep(DraftViewModel.DELAY_TIMEOUT * 2) - pressBack() + //open the created thread and wait rendering + onView(allOf(withId(R.id.recyclerViewMsgs), isDisplayed())).perform( + actionOnItemAtPosition(0, click()) + ) + waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(10)) - //check that the second draft was created - assertEquals(2, draftsCache.size) - onView(withId(R.id.recyclerViewMsgs)) - .check(matches(withRecyclerViewItemCount(2))) - val mimeMessageSecond = getMimeMessageFromCache(1) - assertEquals(MESSAGE_SUBJECT_SECOND, mimeMessageSecond.subject) + onView(Matchers.allOf(withId(R.id.recyclerViewMessages), isDisplayed())) + .perform( + RecyclerViewActions.scrollTo( + hasDescendant( + allOf( + withId(R.id.textViewSubject), + withText(MESSAGE_SUBJECT_FIRST) + ) + ) + ) + ) - //open the first draft and modify it - onView(withId(R.id.recyclerViewMsgs)) - .perform(actionOnItemAtPosition(1, click())) - //wait for the message details - Thread.sleep(2000) - onView( - allOf( - //as we have viewpager at this stage need to add additional selector - hasSibling(allOf(withId(R.id.textViewSubject), withText(MESSAGE_SUBJECT_FIRST))), - withId(R.id.imageButtonEditDraft) + //click to edit a draft + onView(Matchers.allOf(withId(R.id.recyclerViewMessages), isDisplayed())) + .perform( + actionOnItemAtPosition( + 1, + ClickOnViewInRecyclerViewItem(R.id.imageButtonEditDraft) + ) ) - ).check(matches(isDisplayed())) - .perform(click()) + + //wait rendering a draft on the compose message screen + waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(10)) + + //update the draft subject onView(withId(R.id.editTextEmailSubject)) .check(matches(isDisplayed())) .perform( @@ -203,20 +280,81 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF closeSoftKeyboard() ) - Thread.sleep(DraftViewModel.DELAY_TIMEOUT * 2) - pressBack()//back to the message details screen - pressBack()//back to the messages list screen + //back to the message details screen and check if content was updated + pressBack() + waitForObjectWithText(MESSAGE_SUBJECT_FIRST_EDITED, TimeUnit.SECONDS.toMillis(10)) + onView(Matchers.allOf(withId(R.id.recyclerViewMessages), isDisplayed())) + .perform( + RecyclerViewActions.scrollTo( + hasDescendant( + allOf( + withId(R.id.textViewSubject), + withText(MESSAGE_SUBJECT_FIRST_EDITED) + ) + ) + ) + ) - //check if 1st draft is updated correctly and 2nd draft remains same - assertEquals(2, draftsCache.size) + //back to the messages list screen and check if content was updated + pressBack() + assertEquals(1, draftsCache.size) onView(withId(R.id.recyclerViewMsgs)) - .check(matches(withRecyclerViewItemCount(2))) - val mimeMessageFirstEdited = getMimeMessageFromCache(0) + .check(matches(withRecyclerViewItemCount(1))) + val mimeMessageFirstEdited = getMimeMessageFromCache(DRAFT_ID_FIRST) assertEquals(MESSAGE_SUBJECT_FIRST_EDITED, mimeMessageFirstEdited.subject) onView(withText(MESSAGE_SUBJECT_FIRST_EDITED)) .check(matches(isDisplayed())) - val mimeMessageSecondAfterEditingFirst = getMimeMessageFromCache(1) - assertEquals(MESSAGE_SUBJECT_SECOND, mimeMessageSecondAfterEditingFirst.subject) + } + + private fun genListDraftsResponseForRfc822msgidSearch(path: String): MockResponse { + val messageId = + REGEX_DRAFT_BY_RFC822MSGID.find(path)?.groups?.get(1)?.value?.trim() + + val draft = when (messageId) { + MESSAGE_ID_FIRST -> + Draft().apply { + id = DRAFT_ID_FIRST + message = Message().apply { + id = MESSAGE_ID_FIRST + } + } + + else -> null + } + + return if (draft != null) { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + .setBody( + ListDraftsResponse().apply { + factory = GsonFactory.getDefaultInstance() + drafts = listOf(draft) + }.toString() + ) + } else { + MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + } + + private fun genUserMessagesGetFormatFullResponseInternal(path: String): MockResponse { + val messageId = REGEX_USER_MESSAGES_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() + val baseResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + + return when (messageId) { + MESSAGE_ID_FIRST -> { + baseResponse.setBody( + genMessage( + messageId = MESSAGE_ID_FIRST, + messageThreadId = THREAD_ID_FIRST, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", + historyIdValue = HISTORY_ID_FIRST + ).toString() + ) + } + + else -> MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } } companion object { diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt index b9b05cfbfe..20ab5b3654 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt @@ -88,7 +88,7 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { messageThreadId = THREAD_ID_FIRST, subject = MESSAGE_SUBJECT_FIRST, historyIdValue = HISTORY_ID_FIRST - ) + ).toString() ) } @@ -102,7 +102,7 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { messageThreadId = THREAD_ID_SECOND, subject = MESSAGE_SUBJECT_SECOND, historyIdValue = HISTORY_ID_SECOND - ) + ).toString() ) } @@ -155,7 +155,7 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { messageThreadId = THREAD_ID_FIRST, rawMsg = genRawMimeWithSubject(MESSAGE_SUBJECT_FIRST) ) - draftsCache.add(firstDraft) + draftsCache.put(DRAFT_ID_FIRST, firstDraft) val secondDraft = prepareDraft( draftId = DRAFT_ID_SECOND, @@ -163,7 +163,7 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { messageThreadId = THREAD_ID_SECOND, rawMsg = genRawMimeWithSubject(MESSAGE_SUBJECT_SECOND) ) - draftsCache.add(secondDraft) + draftsCache.put(DRAFT_ID_SECOND, secondDraft) } @Test @@ -260,7 +260,7 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { } companion object { - val HISTORY_ID_FIRST = BigInteger("4444444") - val HISTORY_ID_SECOND = BigInteger("5555555") + private val HISTORY_ID_FIRST = BigInteger("4444444") + private val HISTORY_ID_SECOND = BigInteger("5555555") } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt index 677ca7c38a..7083eeb1d7 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt @@ -98,7 +98,7 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { request.method == "PUT" && request.path == "/gmail/v1/users/me/drafts/$DRAFT_ID_FIRST" -> { val (draft, _) = getDraftAndMimeMessageFromRequest(request) - val existingDraftInCache = draftsCache.firstOrNull { it.id == DRAFT_ID_FIRST } + val existingDraftInCache = draftsCache[DRAFT_ID_FIRST] return if (existingDraftInCache != null) { val existingMessage = existingDraftInCache.message @@ -125,7 +125,7 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { messageThreadId = THREAD_ID_FIRST, rawMsg = draft.message.raw ) - draftsCache.add(newDraft) + draftsCache.put(DRAFT_ID_FIRST, newDraft) MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .setBody(newDraft.toString()) } else { @@ -172,7 +172,7 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { moveToDraftFolder() //create a new draft - openComposeScreenAndTypeSubject(MESSAGE_SUBJECT_FIRST) + openComposeScreenAndTypeData(MESSAGE_SUBJECT_FIRST) openActionBarOverflowOrOptionsMenu(getTargetContext()) onView(withText(R.string.switch_to_standard_email)) @@ -195,7 +195,7 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { //check that draft was created assertEquals(1, draftsCache.size) - val mimeMessage = getMimeMessageFromCache(0) + val mimeMessage = getMimeMessageFromCache(DRAFT_ID_FIRST) assertEquals(MESSAGE_SUBJECT_FIRST, mimeMessage.subject) assertEquals( TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER, diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt index 5ea866ae2f..2c67f81a4c 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt @@ -19,20 +19,10 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.rules.activityScenarioRule import com.flowcrypt.email.R -import com.flowcrypt.email.TestConstants -import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.JavaEmailConstants -import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.api.retrofit.ApiHelper import com.flowcrypt.email.api.retrofit.response.api.EkmPrivateKeysResponse -import com.flowcrypt.email.api.retrofit.response.model.ClientConfiguration import com.flowcrypt.email.api.retrofit.response.model.Key -import com.flowcrypt.email.database.entity.AccountEntity -import com.flowcrypt.email.rules.AddAccountToDatabaseRule -import com.flowcrypt.email.rules.AddLabelsToDatabaseRule -import com.flowcrypt.email.rules.AddPrivateKeyToDatabaseRule -import com.flowcrypt.email.rules.FlowCryptMockWebServerRule -import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.ui.activity.MainActivity import com.flowcrypt.email.util.AccountDaoManager import com.flowcrypt.email.viewaction.CustomViewActions.clickOnFolderWithName @@ -43,14 +33,14 @@ import com.google.api.client.json.JsonObjectParser import com.google.api.client.json.gson.GsonFactory import com.google.api.services.gmail.model.Draft import com.google.api.services.gmail.model.Label -import com.google.api.services.gmail.model.ListDraftsResponse import com.google.api.services.gmail.model.ListLabelsResponse -import com.google.api.services.gmail.model.ListMessagesResponse import com.google.api.services.gmail.model.ListSendAsResponse +import com.google.api.services.gmail.model.ListThreadsResponse import com.google.api.services.gmail.model.Message import com.google.api.services.gmail.model.MessagePart import com.google.api.services.gmail.model.MessagePartBody import com.google.api.services.gmail.model.MessagePartHeader +import com.google.api.services.gmail.model.Thread import jakarta.activation.DataSource import jakarta.mail.Session import jakarta.mail.internet.ContentType @@ -64,7 +54,6 @@ import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest import org.junit.Before -import org.pgpainless.util.Passphrase import rawhttp.core.RawHttp import java.io.ByteArrayOutputStream import java.io.InputStream @@ -79,51 +68,11 @@ import kotlin.random.Random /** * @author Denys Bondarenko */ -abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { - abstract val mockWebServerRule: FlowCryptMockWebServerRule +abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( + accountEntity = BASE_ACCOUNT_ENTITY.copy(useConversationMode = true) +) { override val activityScenarioRule = activityScenarioRule() - protected val draftsCache = mutableListOf() - - private val accountEntity = AccountDaoManager.getDefaultAccountDao().copy( - accountType = AccountEntity.ACCOUNT_TYPE_GOOGLE, clientConfiguration = ClientConfiguration( - flags = listOf( - ClientConfiguration.ConfigurationProperty.NO_PRV_CREATE, - ClientConfiguration.ConfigurationProperty.NO_PRV_BACKUP, - ClientConfiguration.ConfigurationProperty.NO_ATTESTER_SUBMIT, - ClientConfiguration.ConfigurationProperty.PRV_AUTOIMPORT_OR_AUTOGEN, - ClientConfiguration.ConfigurationProperty.FORBID_STORING_PASS_PHRASE, - ClientConfiguration.ConfigurationProperty.RESTRICT_ANDROID_ATTACHMENT_HANDLING, - ), - keyManagerUrl = "https://flowcrypt.test/", - ), useAPI = true, useCustomerFesUrl = true - ) - - final override val addAccountToDatabaseRule: AddAccountToDatabaseRule = - AddAccountToDatabaseRule(accountEntity) - - protected val addPrivateKeyToDatabaseRule = - AddPrivateKeyToDatabaseRule(addAccountToDatabaseRule.account) - - protected val addLabelsToDatabaseRule = AddLabelsToDatabaseRule( - account = addAccountToDatabaseRule.account, folders = listOf( - LocalFolder( - account = addAccountToDatabaseRule.account.email, - fullName = JavaEmailConstants.FOLDER_DRAFT, - folderAlias = JavaEmailConstants.FOLDER_DRAFT, - attributes = listOf("\\HasNoChildren", "\\Draft") - ), LocalFolder( - account = addAccountToDatabaseRule.account.email, - fullName = JavaEmailConstants.FOLDER_INBOX, - folderAlias = JavaEmailConstants.FOLDER_INBOX, - attributes = listOf("\\HasNoChildren") - ) - ) - ) - - protected val decryptedPrivateKey = PgpKey.decryptKey( - requireNotNull(addPrivateKeyToDatabaseRule.pgpKeyRingDetails.privateKey), - Passphrase.fromPassword(TestConstants.DEFAULT_STRONG_PASSWORD) - ) + protected val draftsCache = mutableMapOf() @Before fun clearCache() { @@ -144,10 +93,18 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { return Pair(draft, mimeMessage) } - protected fun genMsgDetailsMockResponse( - messageId: String, - messageThreadId: String, - ) = + protected fun getMimeMessageFromDraft(draft: Draft?): MimeMessage? { + if (draft?.message?.raw == null) { + return null + } + + val rawMimeMessageAsByteArray = Base64.decode( + draft.message.raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + ) + return MimeMessage(Session.getInstance(Properties()), rawMimeMessageAsByteArray.inputStream()) + } + + protected fun genMsgDetailsMockResponse(messageId: String, messageThreadId: String) = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .setBody(Message().apply { factory = GsonFactory.getDefaultInstance() @@ -175,7 +132,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { } } - protected fun openComposeScreenAndTypeSubject(subject: String) { + protected fun openComposeScreenAndTypeData(text: String) { //open the compose screen onView(withId(R.id.floatActionButtonCompose)) .check(matches(isDisplayed())) @@ -187,7 +144,17 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { .perform( scrollTo(), click(), - typeText(subject), + typeText(text), + closeSoftKeyboard() + ) + + //type some text in the message + onView(withId(R.id.editTextEmailMessage)) + .check(matches(isDisplayed())) + .perform( + scrollTo(), + click(), + typeText(text), closeSoftKeyboard() ) } @@ -200,10 +167,10 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { onView(withId(R.id.navigationView)) .perform(clickOnFolderWithName(JavaEmailConstants.FOLDER_DRAFT)) - Thread.sleep(1000) + java.lang.Thread.sleep(1000) } - protected fun handleCommonAPICalls(request: RecordedRequest): MockResponse { + override fun handleCommonAPICalls(request: RecordedRequest): MockResponse { when { request.path == "/v1/keys/private" -> { return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( @@ -235,43 +202,31 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { ) } - request.path == "/gmail/v1/users/me/messages?labelIds=${JavaEmailConstants.FOLDER_INBOX}&maxResults=45" -> { + request.method == "GET" && request.path == "/gmail/v1/users/me/threads?labelIds=${JavaEmailConstants.FOLDER_INBOX}&maxResults=45" -> { return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( - ListMessagesResponse().apply { + ListThreadsResponse().apply { factory = GsonFactory.getDefaultInstance() - messages = emptyList() + threads = emptyList() }.toString() ) } - request.method == "GET" && request.path?.matches("/gmail/v1/users/me/drafts\\S*".toRegex()) == true -> { + request.method == "GET" && request.path == "/gmail/v1/users/me/threads?labelIds=${JavaEmailConstants.FOLDER_DRAFT}&maxResults=45" -> { return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( - ListDraftsResponse().apply { + ListThreadsResponse().apply { factory = GsonFactory.getDefaultInstance() - val requestUrl = requireNotNull(request.requestUrl) - val queryParameterField = requestUrl.queryParameter("fields") - val isThreadIdAllowed = queryParameterField == null || - queryParameterField.contains("drafts/message/threadId") - drafts = draftsCache.map { draft -> - Draft().apply { - id = draft.id - message = Message().apply { - id = draft.message.id - if (isThreadIdAllowed) { - threadId = draft.message.threadId - } - } - } - } + threads = + draftsCache.values.map { it.message.threadId }.toSet() + .map { Thread().apply { id = it } } }.toString() ) } request.method == "DELETE" && request.path?.matches("/gmail/v1/users/me/drafts/\\S*".toRegex()) == true -> { val draftId = request.requestUrl?.encodedPathSegments?.last() - val cachedDraft = draftsCache.firstOrNull { it.id == draftId } + val cachedDraft = draftsCache[draftId] if (cachedDraft != null) { - draftsCache.remove(cachedDraft) + draftsCache.remove(draftId) return MockResponse().setResponseCode(HttpURLConnection.HTTP_NO_CONTENT) } else { return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) @@ -371,7 +326,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { } else -> { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + return super.handleCommonAPICalls(request) } } } @@ -387,9 +342,11 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { }.toByteArray() ) - protected fun getMimeMessageFromCache(msgPosition: Int): MimeMessage { + protected fun getMimeMessageFromCache(draftId: String): MimeMessage { + val raw = draftsCache[draftId]?.message?.raw ?: error("Draft not found") + val rawMimeMessageAsByteArrayOfSecondMsg = Base64.decode( - draftsCache[msgPosition].message.raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP ) return MimeMessage( Session.getInstance(Properties()), rawMimeMessageAsByteArrayOfSecondMsg.inputStream() @@ -413,7 +370,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { partId = "" mimeType = "multipart/alternative" filename = "" - headers = prepareMessageHeaders(subject) + headers = prepareMessageHeaders(messageId, subject) body = MessagePartBody().apply { setSize(0) } @@ -426,11 +383,15 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { name = "Content-Type" value = "text/plain" }) - body = MessagePartBody().apply { setSize(130) } + body = MessagePartBody().apply { + setSize(subject.length) + data = java.util.Base64.getEncoder() + .encodeToString(subject.toByteArray()) + } } ) } - }.toString() + } protected fun genPathForGmailMessages(subPath: String) = "/gmail/v1/users/me/messages/$subPath?" + "fields=id,threadId,labelIds,snippet,sizeEstimate,historyId,internalDate," + @@ -438,7 +399,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { "payload/body,payload/parts(partId,mimeType,filename,headers,body/size,body/attachmentId)" + "&format=full" - private fun prepareMessageHeaders(subject: String) = listOf( + private fun prepareMessageHeaders(messageId: String, subject: String) = listOf( MessagePartHeader().apply { name = "MIME-Version" value = "1.0" @@ -449,7 +410,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { }, MessagePartHeader().apply { name = "Message-ID" - value = EmailUtil.generateContentId() + value = messageId }, MessagePartHeader().apply { name = "Subject" @@ -466,14 +427,16 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseComposeScreenTest() { ) companion object { - const val DRAFT_ID_FIRST = "r5555555555555555551" - const val MESSAGE_ID_FIRST = "5555555555555551" - const val THREAD_ID_FIRST = "1111111111111111" + const val DRAFT_ID_FIRST = "r5555555555555500001" + const val MESSAGE_ID_FIRST = "5555555555500001" + const val THREAD_ID_FIRST = "1111111111100001" const val MESSAGE_SUBJECT_FIRST = "first" + val HISTORY_ID_FIRST = BigInteger("1111111") - const val DRAFT_ID_SECOND = "r5555555555555555552" - const val MESSAGE_ID_SECOND = "5555555555555552" - const val THREAD_ID_SECOND = "1111111111111112" + const val DRAFT_ID_SECOND = "r5555555555555500002" + const val MESSAGE_ID_SECOND = "5555555555500002" + const val THREAD_ID_SECOND = "1111111111100002" const val MESSAGE_SUBJECT_SECOND = "second" + val HISTORY_ID_SECOND = BigInteger("2222222") } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt index 1d47f31fe1..7bedb9837f 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt @@ -1745,6 +1745,8 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ ("/gmail/v1/users/me/messages/(.{16})\\?fields=raw&format=raw").toRegex() val REGEX_USER_THREADS_GET_FORMAT_FULL = ("/gmail/v1/users/me/threads/(.{16})\\?format=full").toRegex() + val REGEX_DRAFT_BY_RFC822MSGID = + ("/gmail/v1/users/me/drafts\\?fields=drafts/id,drafts/message/id\\&q=rfc822msgid:(.{16})").toRegex() const val ATTACHMENT_NAME_1 = "text.txt" const val ATTACHMENT_NAME_2 = "text1.txt" From 0969daad2534f8de83172c30fb218a46abafaef8 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Thu, 1 May 2025 14:17:52 +0300 Subject: [PATCH 2/6] Fixed DraftsGmailAPITestCorrectDeletingFlowTest. Refactored code.| #3026 --- ...ITestCorrectCreatingAndUpdatingFlowTest.kt | 142 +-------- ...aftsGmailAPITestCorrectDeletingFlowTest.kt | 181 +++--------- .../ui/base/BaseDraftsGmailAPIFlowTest.kt | 270 ++++++++++++++++-- .../email/ui/base/BaseGmailApiTest.kt | 93 +++--- 4 files changed, 332 insertions(+), 354 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt index 031c59adaf..a7ae68d5b8 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt @@ -34,17 +34,10 @@ import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.base.BaseDraftsGmailAPIFlowTest import com.flowcrypt.email.viewaction.ClickOnViewInRecyclerViewItem -import com.google.api.client.json.Json -import com.google.api.client.json.gson.GsonFactory -import com.google.api.services.gmail.model.BatchModifyMessagesRequest -import com.google.api.services.gmail.model.Draft -import com.google.api.services.gmail.model.ListDraftsResponse import com.google.api.services.gmail.model.Message import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest -import okio.GzipSource -import okio.buffer import org.hamcrest.Matchers import org.hamcrest.core.AllOf.allOf import org.junit.Assert.assertEquals @@ -116,88 +109,6 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) } - request.method == "GET" && request.path?.matches(REGEX_USER_THREADS_GET_FORMAT_FULL) == true -> { - val path = request.path ?: "" - val threadId = - REGEX_USER_THREADS_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() - when (threadId) { - THREAD_ID_FIRST -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", Json.MEDIA_TYPE) - .setBody( - com.google.api.services.gmail.model.Thread().apply { - factory = GsonFactory.getDefaultInstance() - id = THREAD_ID_FIRST - messages = listOf( - genMessage( - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject - ?: "", - historyIdValue = HISTORY_ID_FIRST - ) - ) - }.toString() - ) - } - - else -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - } - } - - request.method == "GET" && request.path == genPathForGmailMessages(MESSAGE_ID_FIRST) -> { - - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", Json.MEDIA_TYPE) - .setBody( - genMessage( - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", - historyIdValue = HISTORY_ID_FIRST - ).toString() - ) - } - - request.method == "GET" && request.path?.matches(REGEX_DRAFT_BY_RFC822MSGID) == true -> { - genListDraftsResponseForRfc822msgidSearch(request.path ?: "") - } - - request.method == "GET" && request.path?.matches(REGEX_USER_MESSAGES_GET_FORMAT_FULL) == true -> { - val path = request.path ?: "" - val messageId = - REGEX_USER_MESSAGES_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() - if (messageId in listOf(MESSAGE_ID_FIRST)) { - genUserMessagesGetFormatFullResponseInternal(path) - } else { - handleCommonAPICalls(request) - } - } - - request.path == "/gmail/v1/users/me/messages/5555555555500001?fields=raw&format=raw" -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", Json.MEDIA_TYPE) - .setBody(draftsCache[DRAFT_ID_FIRST]?.message?.raw ?: error("Draft not found")) - } - - request.method == "POST" && request.path == "/gmail/v1/users/me/messages/batchModify" -> { - val source = GzipSource(request.body) - val batchModifyMessagesRequest = GsonFactory.getDefaultInstance().fromInputStream( - source.buffer().inputStream(), - BatchModifyMessagesRequest::class.java - ) - - val handledIds = arrayOf(MESSAGE_ID_FIRST) - - if (handledIds.any { batchModifyMessagesRequest.ids.contains(it) }) { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - } else { - MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - } - else -> handleCommonAPICalls(request) } } @@ -259,7 +170,7 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF ) //click to edit a draft - onView(Matchers.allOf(withId(R.id.recyclerViewMessages), isDisplayed())) + onView(allOf(withId(R.id.recyclerViewMessages), isDisplayed())) .perform( actionOnItemAtPosition( 1, @@ -306,57 +217,6 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF .check(matches(isDisplayed())) } - private fun genListDraftsResponseForRfc822msgidSearch(path: String): MockResponse { - val messageId = - REGEX_DRAFT_BY_RFC822MSGID.find(path)?.groups?.get(1)?.value?.trim() - - val draft = when (messageId) { - MESSAGE_ID_FIRST -> - Draft().apply { - id = DRAFT_ID_FIRST - message = Message().apply { - id = MESSAGE_ID_FIRST - } - } - - else -> null - } - - return if (draft != null) { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", Json.MEDIA_TYPE) - .setBody( - ListDraftsResponse().apply { - factory = GsonFactory.getDefaultInstance() - drafts = listOf(draft) - }.toString() - ) - } else { - MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - } - - private fun genUserMessagesGetFormatFullResponseInternal(path: String): MockResponse { - val messageId = REGEX_USER_MESSAGES_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() - val baseResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", Json.MEDIA_TYPE) - - return when (messageId) { - MESSAGE_ID_FIRST -> { - baseResponse.setBody( - genMessage( - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", - historyIdValue = HISTORY_ID_FIRST - ).toString() - ) - } - - else -> MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - } - companion object { const val MESSAGE_SUBJECT_FIRST_EDITED = "first edited" } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt index 20ab5b3654..2d95655d63 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectDeletingFlowTest.kt @@ -5,33 +5,23 @@ package com.flowcrypt.email.ui -import android.view.InputDevice -import android.view.MotionEvent -import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.GeneralClickAction -import androidx.test.espresso.action.GeneralLocation -import androidx.test.espresso.action.Press -import androidx.test.espresso.action.Tap -import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition -import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.isDisplayingAtLeast +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.MediumTest import com.flowcrypt.email.R import com.flowcrypt.email.TestConstants -import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.junit.annotations.FlowCryptTestSettings -import com.flowcrypt.email.matchers.CustomMatchers.Companion.withEmptyRecyclerView import com.flowcrypt.email.matchers.CustomMatchers.Companion.withRecyclerViewItemCount import com.flowcrypt.email.rules.ClearAppSettingsRule import com.flowcrypt.email.rules.FlowCryptMockWebServerRule @@ -39,28 +29,20 @@ import com.flowcrypt.email.rules.GrantPermissionRuleChooser import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.base.BaseDraftsGmailAPIFlowTest -import com.flowcrypt.email.viewaction.CustomViewActions.swipeToRefresh -import com.google.api.client.json.Json -import com.google.api.client.json.gson.GsonFactory -import com.google.api.services.gmail.model.History -import com.google.api.services.gmail.model.HistoryMessageDeleted +import com.flowcrypt.email.viewaction.ClickOnViewInRecyclerViewItem import com.google.api.services.gmail.model.ListDraftsResponse -import com.google.api.services.gmail.model.ListHistoryResponse -import com.google.api.services.gmail.model.Message import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest +import org.hamcrest.core.AllOf.allOf import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.RunWith -import java.math.BigInteger -import java.net.HttpURLConnection import java.util.concurrent.TimeUnit @@ -72,66 +54,11 @@ import java.util.concurrent.TimeUnit @MediumTest @RunWith(AndroidJUnit4::class) @FlowCryptTestSettings(useCommonIdling = false) -@Ignore("Should be re-looked after threads will be completed") class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { override val mockWebServerRule: FlowCryptMockWebServerRule = FlowCryptMockWebServerRule( TestConstants.MOCK_WEB_SERVER_PORT, object : Dispatcher() { override fun dispatch(request: RecordedRequest): MockResponse { - return when { - request.method == "GET" && request.path == genPathForGmailMessages(MESSAGE_ID_FIRST) -> { - - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", Json.MEDIA_TYPE) - .setBody( - genMessage( - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - subject = MESSAGE_SUBJECT_FIRST, - historyIdValue = HISTORY_ID_FIRST - ).toString() - ) - } - - request.method == "GET" && request.path == genPathForGmailMessages(MESSAGE_ID_SECOND) -> { - - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", Json.MEDIA_TYPE) - .setBody( - genMessage( - messageId = MESSAGE_ID_SECOND, - messageThreadId = THREAD_ID_SECOND, - subject = MESSAGE_SUBJECT_SECOND, - historyIdValue = HISTORY_ID_SECOND - ).toString() - ) - } - - request.method == "GET" && request.path == "/gmail/v1/users/me/history?" + - "labelId=${JavaEmailConstants.FOLDER_DRAFT}" + - "&startHistoryId=$HISTORY_ID_FIRST" -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(ListHistoryResponse().apply { - factory = GsonFactory.getDefaultInstance() - historyId = BigInteger("40066765") - history = listOf(History().apply { - id = BigInteger("40066715") - messages = listOf(Message().apply { - id = MESSAGE_ID_SECOND - threadId = THREAD_ID_SECOND - }) - messagesDeleted = listOf(HistoryMessageDeleted().apply { - message = Message().apply { - id = MESSAGE_ID_SECOND - threadId = THREAD_ID_SECOND - labelIds = listOf(JavaEmailConstants.FOLDER_DRAFT) - } - }) - }) - }.toString()) - } - - else -> handleCommonAPICalls(request) - } + return handleCommonAPICalls(request) } }) @@ -153,7 +80,7 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { draftId = DRAFT_ID_FIRST, messageId = MESSAGE_ID_FIRST, messageThreadId = THREAD_ID_FIRST, - rawMsg = genRawMimeWithSubject(MESSAGE_SUBJECT_FIRST) + rawMsg = genRawMimeBase64Encoded(MESSAGE_SUBJECT_FIRST) ) draftsCache.put(DRAFT_ID_FIRST, firstDraft) @@ -161,36 +88,47 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { draftId = DRAFT_ID_SECOND, messageId = MESSAGE_ID_SECOND, messageThreadId = THREAD_ID_SECOND, - rawMsg = genRawMimeWithSubject(MESSAGE_SUBJECT_SECOND) + rawMsg = genRawMimeBase64Encoded(MESSAGE_SUBJECT_SECOND) ) draftsCache.put(DRAFT_ID_SECOND, secondDraft) } @Test - fun testCorrectDraftsDeleting() { + fun testCorrectDraftDeleting() { moveToDraftFolder() - - //select the second draft waitForObjectWithText(MESSAGE_SUBJECT_SECOND, TimeUnit.SECONDS.toMillis(5)) - selectDraft() - //delete the second draft - onView(withId(R.id.menuActionDeleteMessage)) - .check(matches(ViewMatchers.isDisplayed())) + //open a thread + onView(allOf(withId(R.id.recyclerViewMsgs), isDisplayed())).perform( + actionOnItemAtPosition(0, click()) + ) + waitForObjectWithText(MESSAGE_SUBJECT_SECOND, TimeUnit.SECONDS.toMillis(10)) + + //delete a draft + onView(allOf(withId(R.id.recyclerViewMessages), isDisplayed())) + .perform( + actionOnItemAtPosition( + 1, + ClickOnViewInRecyclerViewItem(R.id.imageButtonDeleteDraft) + ) + ) + + onView(withText(getResString(R.string.delete))) + .inRoot(isDialog()) + .check(matches(isDisplayed())) .perform(click()) - //swipe down to refresh - onView(withId(R.id.swipeRefreshLayout)) - .perform(swipeToRefresh(swipeDown(), isDisplayingAtLeast(85))) - Thread.sleep(1000) + waitUntil { draftsCache.size == 1 } + + waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(5)) - //check that only the first message exists in the local cache + //check that only the first draft exists in the local cache onView(withId(R.id.recyclerViewMsgs)) .check(matches(withRecyclerViewItemCount(1))) onView(withId(R.id.recyclerViewMsgs)) .perform( // scrollTo will fail the test if no item matches. - RecyclerViewActions.scrollTo( + RecyclerViewActions.scrollTo( hasDescendant(withText(MESSAGE_SUBJECT_FIRST)) ) ) @@ -207,60 +145,5 @@ class DraftsGmailAPITestCorrectDeletingFlowTest : BaseDraftsGmailAPIFlowTest() { } assertEquals(1, (responseAfterDeletingSecondDraft as ListDraftsResponse).drafts.size) - - //############################################################################################ - - //select the first draft - selectDraft() - - //delete the first draft - onView(withId(R.id.menuActionDeleteMessage)) - .check(matches(ViewMatchers.isDisplayed())) - .perform(click()) - - //swipe down to refresh - onView(withId(R.id.swipeRefreshLayout)) - .perform(swipeToRefresh(swipeDown(), isDisplayingAtLeast(85))) - Thread.sleep(1000) - - //check that there is no drafts in the local cache - onView(withId(R.id.recyclerViewMsgs)) - .check(matches(withEmptyRecyclerView())) - - //check that on the server side we have no drafts - val responseAfterDeletingFirstDraft = runBlocking { - GmailApiHelper.loadMsgsBaseInfo( - context = getTargetContext(), - accountEntity = addAccountToDatabaseRule.account, - localFolder = addLabelsToDatabaseRule.folders.first { it.isDrafts }, - fields = listOf("drafts/id", "drafts/message/id"), - maxResult = 500 - ) - } - - assertEquals(0, (responseAfterDeletingFirstDraft as ListDraftsResponse).drafts.size) - } - - private fun selectDraft(position: Int = 0) { - onView(withId(R.id.recyclerViewMsgs)) - .perform( - actionOnItemAtPosition( - position, - ViewActions.actionWithAssertions( - GeneralClickAction( - Tap.LONG, - GeneralLocation.CENTER, - Press.FINGER, - InputDevice.SOURCE_TOUCHSCREEN, - MotionEvent.BUTTON_PRIMARY - ) - ) - ) - ) - } - - companion object { - private val HISTORY_ID_FIRST = BigInteger("4444444") - private val HISTORY_ID_SECOND = BigInteger("5555555") } } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt index 2c67f81a4c..b41cdab9df 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt @@ -31,8 +31,11 @@ import com.google.api.client.googleapis.json.GoogleJsonErrorContainer import com.google.api.client.json.Json import com.google.api.client.json.JsonObjectParser import com.google.api.client.json.gson.GsonFactory +import com.google.api.services.gmail.model.BatchDeleteMessagesRequest +import com.google.api.services.gmail.model.BatchModifyMessagesRequest import com.google.api.services.gmail.model.Draft import com.google.api.services.gmail.model.Label +import com.google.api.services.gmail.model.ListDraftsResponse import com.google.api.services.gmail.model.ListLabelsResponse import com.google.api.services.gmail.model.ListSendAsResponse import com.google.api.services.gmail.model.ListThreadsResponse @@ -53,6 +56,8 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest +import okio.GzipSource +import okio.buffer import org.junit.Before import rawhttp.core.RawHttp import java.io.ByteArrayOutputStream @@ -171,16 +176,16 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( } override fun handleCommonAPICalls(request: RecordedRequest): MockResponse { - when { + return when { request.path == "/v1/keys/private" -> { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( ApiHelper.getInstance(getTargetContext()).gson .toJson(EkmPrivateKeysResponse(privateKeys = listOf(Key(decryptedPrivateKey)))) ) } request.path == "/gmail/v1/users/me/settings/sendAs" -> { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( ListSendAsResponse().apply { factory = GsonFactory.getDefaultInstance() sendAs = emptyList() @@ -189,7 +194,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( } request.path == "/gmail/v1/users/me/labels" -> { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( ListLabelsResponse().apply { factory = GsonFactory.getDefaultInstance() labels = addLabelsToDatabaseRule.folders.map { @@ -203,7 +208,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( } request.method == "GET" && request.path == "/gmail/v1/users/me/threads?labelIds=${JavaEmailConstants.FOLDER_INBOX}&maxResults=45" -> { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( ListThreadsResponse().apply { factory = GsonFactory.getDefaultInstance() threads = emptyList() @@ -212,7 +217,7 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( } request.method == "GET" && request.path == "/gmail/v1/users/me/threads?labelIds=${JavaEmailConstants.FOLDER_DRAFT}&maxResults=45" -> { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( ListThreadsResponse().apply { factory = GsonFactory.getDefaultInstance() threads = @@ -227,9 +232,9 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( val cachedDraft = draftsCache[draftId] if (cachedDraft != null) { draftsCache.remove(draftId) - return MockResponse().setResponseCode(HttpURLConnection.HTTP_NO_CONTENT) + MockResponse().setResponseCode(HttpURLConnection.HTTP_NO_CONTENT) } else { - return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) .setHeader("Content-Type", Json.MEDIA_TYPE) .setBody(GoogleJsonErrorContainer().apply { factory = GsonFactory.getDefaultInstance() @@ -246,6 +251,63 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( } } + request.method == "GET" && request.path?.matches(REGEX_USER_THREADS_GET_FORMAT_FULL) == true -> { + genThreadDetailsMockResponse(request) + } + + request.method == "GET" && request.path?.matches(REGEX_DRAFT_BY_RFC822MSGID) == true -> { + genListDraftsResponseForRfc822msgidSearch(request.path ?: "") + } + + request.method == "GET" && request.path?.matches(REGEX_USER_MESSAGES_GET_FORMAT_FULL) == true -> { + val path = request.path ?: "" + val messageId = + REGEX_USER_MESSAGES_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() + if (messageId in listOf(MESSAGE_ID_FIRST, MESSAGE_ID_SECOND)) { + genUserMessagesGetFormatFullResponseInternal(path) + } else { + super.handleCommonAPICalls(request) + } + } + + request.method == "GET" && request.path?.matches(REGEX_USER_MESSAGES_GET_RAW) == true -> { + genUserMessagesRawResponse(request.path ?: "") + } + + request.method == "POST" && request.path == "/gmail/v1/users/me/messages/batchModify" -> { + val source = GzipSource(request.body) + val batchModifyMessagesRequest = GsonFactory.getDefaultInstance().fromInputStream( + source.buffer().inputStream(), + BatchModifyMessagesRequest::class.java + ) + + val handledIds = arrayOf(MESSAGE_ID_FIRST, MESSAGE_ID_SECOND) + + if (handledIds.any { batchModifyMessagesRequest.ids.contains(it) }) { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + } else { + MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + } + + request.method == "GET" && request.path == "/gmail/v1/users/me/drafts?fields=drafts/id,drafts/message/id&maxResults=500" -> { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + .setBody( + ListDraftsResponse().apply { + factory = GsonFactory.getDefaultInstance() + drafts = draftsCache.values.map { draft -> + Draft().apply { + id = draft.id + message = Message().apply { + id = draft.message.id + } + } + } + }.toString() + ) + } + request.method == "POST" && request.path == "/batch" -> { val mimeMultipart = MimeMultipart(object : DataSource { override fun getInputStream(): InputStream = request.body.inputStream() @@ -320,27 +382,82 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( val content = String(outputStream.toByteArray()) val boundary = (ContentType(responseMimeMultipart.contentType)).getParameter("boundary") - return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .setHeader("Content-Type", "multipart/mixed; boundary=$boundary") .setBody(content) } else -> { - return super.handleCommonAPICalls(request) + super.handleCommonAPICalls(request) } } } - protected fun genRawMimeWithSubject(msgSubject: String) = String( - ByteArrayOutputStream().apply { + override fun genUserMessagesGetWithFieldsFormatFullResponse(path: String): MockResponse { + val messageId = + REGEX_USER_MESSAGES_GET_WITH_FIELDS_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() + + val message = when (messageId) { + MESSAGE_ID_FIRST -> { + genMessage( + messageId = MESSAGE_ID_FIRST, + messageThreadId = THREAD_ID_FIRST, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", + historyIdValue = HISTORY_ID_FIRST + ) + } + + MESSAGE_ID_SECOND -> { + genMessage( + messageId = MESSAGE_ID_SECOND, + messageThreadId = THREAD_ID_SECOND, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_SECOND])?.subject ?: "", + historyIdValue = HISTORY_ID_SECOND + ) + } + + else -> return super.genUserMessagesGetWithFieldsFormatFullResponse(path) + } + + return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + .setBody(message.toString()) + } + + override fun getAllowedIdsForMessagesBatchDelete(): Collection { + return super.getAllowedIdsForMessagesBatchDelete() + listOf( + MESSAGE_ID_FIRST, + MESSAGE_ID_SECOND + ) + } + + override fun handleBatchDeleteMessagesRequest(batchDeleteMessagesRequest: BatchDeleteMessagesRequest) { + super.handleBatchDeleteMessagesRequest(batchDeleteMessagesRequest) + + for (id in batchDeleteMessagesRequest.ids) { + draftsCache.filter { + it.value.message.id == id + }.forEach { + draftsCache.remove(it.key) + } + } + } + + protected fun genRawMimeBase64Encoded(msgSubject: String): String { + val raw = ByteArrayOutputStream().apply { this.use { MimeMessage(Session.getInstance(Properties())).apply { + setFrom(accountEntity.email) subject = msgSubject - setContent(MimeMultipart().apply { addBodyPart(MimeBodyPart().apply { setText("") }) }) + setContent(MimeMultipart().apply { addBodyPart(MimeBodyPart().apply { setText(msgSubject) }) }) }.writeTo(it) } }.toByteArray() - ) + + return Base64.encodeToString( + raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + ) + } protected fun getMimeMessageFromCache(draftId: String): MimeMessage { val raw = draftsCache[draftId]?.message?.raw ?: error("Draft not found") @@ -393,12 +510,6 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( } } - protected fun genPathForGmailMessages(subPath: String) = "/gmail/v1/users/me/messages/$subPath?" + - "fields=id,threadId,labelIds,snippet,sizeEstimate,historyId,internalDate," + - "payload/partId,payload/mimeType,payload/filename,payload/headers," + - "payload/body,payload/parts(partId,mimeType,filename,headers,body/size,body/attachmentId)" + - "&format=full" - private fun prepareMessageHeaders(messageId: String, subject: String) = listOf( MessagePartHeader().apply { name = "MIME-Version" @@ -426,6 +537,125 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( }, ) + private fun genThreadDetailsMockResponse(request: RecordedRequest): MockResponse { + val path = request.path ?: "" + val threadId = + REGEX_USER_THREADS_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() + + val message = when (threadId) { + THREAD_ID_FIRST -> { + genMessage( + messageId = MESSAGE_ID_FIRST, + messageThreadId = THREAD_ID_FIRST, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", + historyIdValue = HISTORY_ID_FIRST + ) + } + + THREAD_ID_SECOND -> { + genMessage( + messageId = MESSAGE_ID_SECOND, + messageThreadId = THREAD_ID_SECOND, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_SECOND])?.subject ?: "", + historyIdValue = HISTORY_ID_SECOND + ) + } + + else -> return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + + return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + .setBody( + Thread().apply { + factory = GsonFactory.getDefaultInstance() + id = threadId + messages = listOf(message) + }.toString() + ) + } + + private fun genListDraftsResponseForRfc822msgidSearch(path: String): MockResponse { + val messageId = + REGEX_DRAFT_BY_RFC822MSGID.find(path)?.groups?.get(1)?.value?.trim() + + val draft = when (messageId) { + MESSAGE_ID_FIRST -> Draft().apply { + id = DRAFT_ID_FIRST + message = Message().apply { + id = MESSAGE_ID_FIRST + } + } + + MESSAGE_ID_SECOND -> Draft().apply { + id = DRAFT_ID_SECOND + message = Message().apply { + id = MESSAGE_ID_SECOND + } + } + + else -> return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + + return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + .setBody( + ListDraftsResponse().apply { + factory = GsonFactory.getDefaultInstance() + drafts = listOf(draft) + }.toString() + ) + } + + private fun genUserMessagesGetFormatFullResponseInternal(path: String): MockResponse { + val messageId = REGEX_USER_MESSAGES_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() + val baseResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + + val message = when (messageId) { + MESSAGE_ID_FIRST -> { + genMessage( + messageId = MESSAGE_ID_FIRST, + messageThreadId = THREAD_ID_FIRST, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", + historyIdValue = HISTORY_ID_FIRST + ) + } + + MESSAGE_ID_SECOND -> { + genMessage( + messageId = MESSAGE_ID_SECOND, + messageThreadId = THREAD_ID_SECOND, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_SECOND])?.subject ?: "", + historyIdValue = HISTORY_ID_SECOND + ) + } + + else -> return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + + return baseResponse.setBody(message.toString()) + } + + private fun genUserMessagesRawResponse(path: String): MockResponse { + val messageId = + REGEX_USER_MESSAGES_GET_RAW.find(path)?.groups?.get(1)?.value?.trim() + + val key = when (messageId) { + MESSAGE_ID_FIRST -> DRAFT_ID_FIRST + MESSAGE_ID_SECOND -> DRAFT_ID_SECOND + else -> return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + + val raw = draftsCache[key]?.message?.raw ?: error("Draft not found") + return MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Content-Type", Json.MEDIA_TYPE) + //ref com.flowcrypt.email.api.email.gmail.api.GMailRawMIMEMessageFilterInputStream + .setBody( + "{\n \"raw\": \"$raw\"\n}\n" + ) + } + companion object { const val DRAFT_ID_FIRST = "r5555555555555500001" const val MESSAGE_ID_FIRST = "5555555555500001" diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt index 7bedb9837f..4723adfa78 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt @@ -360,7 +360,7 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ ) } - request.method == "GET" && request.path?.matches(REGEX_USER_MESSAGES_GET_PGP_MIME_FORMAT_RAW) == true -> { + request.method == "GET" && request.path?.matches(REGEX_USER_MESSAGES_GET_RAW) == true -> { genPgpMimeRawResponse(request.path ?: "") } @@ -405,11 +405,10 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ BatchDeleteMessagesRequest::class.java ) - val handledIds = arrayOf( - MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_3, - ) + val handledIds = getAllowedIdsForMessagesBatchDelete() if (handledIds.any { batchDeleteMessagesRequest.ids.contains(it) }) { + handleBatchDeleteMessagesRequest(batchDeleteMessagesRequest) MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) } else { MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) @@ -517,44 +516,7 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ } } - private fun genPgpMimeRawResponse(path: String): MockResponse { - val messageId = - REGEX_USER_MESSAGES_GET_PGP_MIME_FORMAT_RAW.find(path)?.groups?.get(1)?.value?.trim() - val gmailMessage = when (messageId) { - MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 -> genPGPMimeMessage( - threadId = THREAD_ID_PGP_MIME, - messageId = MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1, - isFullFormat = true - ) - - MESSAGE_ID_EXISTING_PGP_MIME -> genPGPMimeMessage( - threadId = THREAD_ID_EXISTING_PGP_MIME, - messageId = MESSAGE_ID_EXISTING_PGP_MIME, - isFullFormat = true - ) - - else -> null - } - - return if (gmailMessage != null) { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody( - Base64.getEncoder().encodeToString( - ByteArrayOutputStream().apply { - GmaiAPIMimeMessage( - Session.getInstance(Properties()), - gmailMessage - ).writeTo(this) - }.toByteArray() - ) - ) - } else { - MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - } - - - private fun genUserMessagesGetWithFieldsFormatFullResponse(path: String): MockResponse { + protected open fun genUserMessagesGetWithFieldsFormatFullResponse(path: String): MockResponse { val messageId = REGEX_USER_MESSAGES_GET_WITH_FIELDS_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() val baseResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) @@ -744,6 +706,49 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ } } + protected open fun getAllowedIdsForMessagesBatchDelete(): Collection { + return listOf( + MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_3, + ) + } + + protected open fun handleBatchDeleteMessagesRequest(batchDeleteMessagesRequest: BatchDeleteMessagesRequest) {} + + private fun genPgpMimeRawResponse(path: String): MockResponse { + val messageId = REGEX_USER_MESSAGES_GET_RAW.find(path)?.groups?.get(1)?.value?.trim() + val gmailMessage = when (messageId) { + MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1 -> genPGPMimeMessage( + threadId = THREAD_ID_PGP_MIME, + messageId = MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1, + isFullFormat = true + ) + + MESSAGE_ID_EXISTING_PGP_MIME -> genPGPMimeMessage( + threadId = THREAD_ID_EXISTING_PGP_MIME, + messageId = MESSAGE_ID_EXISTING_PGP_MIME, + isFullFormat = true + ) + + else -> null + } + + return if (gmailMessage != null) { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setBody( + Base64.getEncoder().encodeToString( + ByteArrayOutputStream().apply { + GmaiAPIMimeMessage( + Session.getInstance(Properties()), + gmailMessage + ).writeTo(this) + }.toByteArray() + ) + ) + } else { + MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + } + private fun genUserMessagesGetFormatFullResponse(path: String): MockResponse { val messageId = REGEX_USER_MESSAGES_GET_FORMAT_FULL.find(path)?.groups?.get(1)?.value?.trim() val baseResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) @@ -1741,12 +1746,12 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ "body/size,body/attachmentId\\)&format=full").toRegex() val REGEX_USER_MESSAGES_GET_FORMAT_FULL = ("/gmail/v1/users/me/messages/(.{16})\\?format=full").toRegex() - val REGEX_USER_MESSAGES_GET_PGP_MIME_FORMAT_RAW = - ("/gmail/v1/users/me/messages/(.{16})\\?fields=raw&format=raw").toRegex() val REGEX_USER_THREADS_GET_FORMAT_FULL = ("/gmail/v1/users/me/threads/(.{16})\\?format=full").toRegex() val REGEX_DRAFT_BY_RFC822MSGID = ("/gmail/v1/users/me/drafts\\?fields=drafts/id,drafts/message/id\\&q=rfc822msgid:(.{16})").toRegex() + val REGEX_USER_MESSAGES_GET_RAW = + ("/gmail/v1/users/me/messages/(.{16})\\?fields=raw&format=raw").toRegex() const val ATTACHMENT_NAME_1 = "text.txt" const val ATTACHMENT_NAME_2 = "text1.txt" From b9f0d4cf0807caa51afbf4d313b2b85d0c88fca0 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Mon, 5 May 2025 08:41:48 +0300 Subject: [PATCH 3/6] Fixed DraftsGmailAPITestCorrectSendingFlowTest. Refactored code.| #3026 --- ...raftsGmailAPITestCorrectSendingFlowTest.kt | 137 ++++++++---------- 1 file changed, 57 insertions(+), 80 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt index 7083eeb1d7..73b35bf2cb 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt @@ -5,16 +5,13 @@ package com.flowcrypt.email.ui -import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.Espresso +import androidx.recyclerview.widget.RecyclerView.ViewHolder import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.closeSoftKeyboard import androidx.test.espresso.action.ViewActions.pressImeActionButton import androidx.test.espresso.action.ViewActions.replaceText -import androidx.test.espresso.action.ViewActions.scrollTo -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -35,23 +32,26 @@ import com.flowcrypt.email.rules.RetryRule import com.flowcrypt.email.rules.ScreenshotTestRule import com.flowcrypt.email.ui.base.BaseDraftsGmailAPIFlowTest import com.flowcrypt.email.util.TestGeneralUtil +import com.flowcrypt.email.viewaction.ClickOnViewInRecyclerViewItem +import com.google.api.client.json.JsonObjectParser import com.google.api.client.json.gson.GsonFactory -import jakarta.mail.Message -import jakarta.mail.internet.InternetAddress +import com.google.api.services.gmail.model.Draft import jakarta.mail.internet.MimeMultipart import kotlinx.coroutines.runBlocking import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest +import org.hamcrest.core.AllOf.allOf import org.junit.Assert.assertEquals -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.RuleChain import org.junit.rules.TestRule import org.junit.runner.RunWith +import java.io.InputStreamReader import java.net.HttpURLConnection import java.util.concurrent.TimeUnit +import java.util.zip.GZIPInputStream /** * https://github.com/FlowCrypt/flowcrypt-android/issues/2050 @@ -60,7 +60,6 @@ import java.util.concurrent.TimeUnit @MediumTest @RunWith(AndroidJUnit4::class) @FlowCryptTestSettings(useCommonIdling = false) -@Ignore("Should be re-looked after threads will be completed") class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { private val sentCache = mutableListOf() @@ -96,26 +95,6 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { .setBody(message.toString()) } - request.method == "PUT" && request.path == "/gmail/v1/users/me/drafts/$DRAFT_ID_FIRST" -> { - val (draft, _) = getDraftAndMimeMessageFromRequest(request) - val existingDraftInCache = draftsCache[DRAFT_ID_FIRST] - - return if (existingDraftInCache != null) { - val existingMessage = existingDraftInCache.message - existingDraftInCache.message = com.google.api.services.gmail.model.Message().apply { - id = existingMessage.id - threadId = existingMessage.threadId - labelIds = existingMessage.labelIds - raw = draft.message.raw - } - - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(existingDraftInCache.toString()) - } else { - MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - } - request.method == "POST" && request.path == "/gmail/v1/users/me/drafts" -> { val (draft, mimeMessage) = getDraftAndMimeMessageFromRequest(request) if (mimeMessage.subject == MESSAGE_SUBJECT_FIRST) { @@ -137,15 +116,24 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { return genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) } - request.method == "POST" && request.path == "/upload/gmail/v1/users/me/messages/send?uploadType=resumable" -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Location", LOCATION_URL) - .setBody(com.google.api.services.gmail.model.Message().apply { - factory = GsonFactory.getDefaultInstance() - id = MESSAGE_ID_SENT - threadId = THREAD_ID_SENT - labelIds = listOf(JavaEmailConstants.FOLDER_SENT) - }.toString()) + request.method == "POST" && request.path == "/upload/gmail/v1/users/me/drafts/send?uploadType=resumable" -> { + val gzipInputStream = GZIPInputStream(request.body.inputStream()) + val draft = JsonObjectParser(GsonFactory.getDefaultInstance()).parseAndClose( + InputStreamReader(gzipInputStream), Draft::class.java + ) + + if (draft.id == DRAFT_ID_FIRST) { + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setHeader("Location", LOCATION_URL) + .setBody(com.google.api.services.gmail.model.Message().apply { + factory = GsonFactory.getDefaultInstance() + id = MESSAGE_ID_SENT + threadId = THREAD_ID_SENT + labelIds = listOf(JavaEmailConstants.FOLDER_SENT) + }.toString()) + } else { + MockResponse().setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) + } } else -> handleCommonAPICalls(request) @@ -178,71 +166,60 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { onView(withText(R.string.switch_to_standard_email)) .check(matches(isDisplayed())) .perform(click()) - - onView(withId(R.id.editTextEmailMessage)) - .perform( - scrollTo(), - click(), - typeText(MESSAGE), - closeSoftKeyboard() - ) - onView(withId(R.id.editTextEmailAddress)) - .perform( - replaceText(TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER), - pressImeActionButton() - ) - Thread.sleep(DraftViewModel.DELAY_TIMEOUT * 2) + waitUntil(DraftViewModel.DELAY_TIMEOUT * 2) { draftsCache.isNotEmpty() } //check that draft was created assertEquals(1, draftsCache.size) val mimeMessage = getMimeMessageFromCache(DRAFT_ID_FIRST) assertEquals(MESSAGE_SUBJECT_FIRST, mimeMessage.subject) assertEquals( - TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER, - (mimeMessage.getRecipients(Message.RecipientType.TO).first() as InternetAddress).address - ) - assertEquals( - MESSAGE, + MESSAGE_SUBJECT_FIRST, (mimeMessage.content as MimeMultipart).getBodyPart(0).content as String ) //move to the drafts list - Espresso.pressBack() + pressBack() + waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(10)) //open created draft and send - onView(withId(R.id.recyclerViewMsgs)) - .perform(actionOnItemAtPosition(0, click())) - waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(2)) - onView(withId(R.id.imageButtonEditDraft)) - .check(matches(isDisplayed())) - .perform(click()) - //need to wait while the message details will be rendered - Thread.sleep(1000) + onView(allOf(withId(R.id.recyclerViewMsgs), isDisplayed())).perform( + actionOnItemAtPosition(0, click()) + ) + waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(10)) + + //click to edit a draft + onView(allOf(withId(R.id.recyclerViewMessages), isDisplayed())) + .perform( + actionOnItemAtPosition( + 1, + ClickOnViewInRecyclerViewItem(R.id.imageButtonEditDraft) + ) + ) + + //wait rendering a draft on the compose message screen + waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(10)) + + onView(withId(R.id.editTextEmailAddress)) + .perform( + replaceText(TestConstants.RECIPIENT_WITH_PUBLIC_KEY_ON_ATTESTER), + pressImeActionButton() + ) + onView(withId(R.id.menuActionSend)) .check(matches(isDisplayed())) .perform(click()) //need to wait while a message will be sent - var timeToWaitForSending = TimeUnit.SECONDS.toMillis(10) - while (timeToWaitForSending > 0) { + waitUntil { val countOfOutgoingMessages = runBlocking { roomDatabase.msgDao().getOutboxMsgsSuspend(addAccountToDatabaseRule.account.email).size } - if (countOfOutgoingMessages == 0) { - Thread.sleep(TimeUnit.SECONDS.toMillis(1)) - break - } else { - val step = TimeUnit.SECONDS.toMillis(1) - timeToWaitForSending -= step - Thread.sleep(step) - } + countOfOutgoingMessages == 0 } - val finalCountOfOutgoingMessages = runBlocking { + assertEquals(0, runBlocking { roomDatabase.msgDao().getOutboxMsgsSuspend(addAccountToDatabaseRule.account.email).size - } - - assertEquals(0, finalCountOfOutgoingMessages) + }) //check that we have a new sent message in the cache assertEquals(1, sentCache.size) From 0fae040ff3263c96c7e582f62797046fba16c598 Mon Sep 17 00:00:00 2001 From: denbond7 Date: Mon, 5 May 2025 08:50:48 +0300 Subject: [PATCH 4/6] Refactored code.| #3026 --- ...ITestCorrectCreatingAndUpdatingFlowTest.kt | 23 -- ...raftsGmailAPITestCorrectSendingFlowTest.kt | 21 +- .../ui/base/BaseDraftsGmailAPIFlowTest.kt | 215 ++++++++++-------- 3 files changed, 126 insertions(+), 133 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt index a7ae68d5b8..554bd1869e 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt @@ -82,29 +82,6 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF } } - request.method == "POST" && request.path == "/gmail/v1/users/me/drafts" -> { - val (draft, mimeMessage) = getDraftAndMimeMessageFromRequest(request) - - when (mimeMessage.subject) { - MESSAGE_SUBJECT_FIRST -> { - val newDraft = prepareDraft( - draftId = DRAFT_ID_FIRST, - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - rawMsg = draft.message.raw - ) - draftsCache.put(DRAFT_ID_FIRST, newDraft) - - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(newDraft.toString()) - } - - else -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) - } - } - } - request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_FIRST}?fields=id,threadId,historyId&format=full" -> { genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) } diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt index 73b35bf2cb..947b317185 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt @@ -95,23 +95,6 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { .setBody(message.toString()) } - request.method == "POST" && request.path == "/gmail/v1/users/me/drafts" -> { - val (draft, mimeMessage) = getDraftAndMimeMessageFromRequest(request) - if (mimeMessage.subject == MESSAGE_SUBJECT_FIRST) { - val newDraft = prepareDraft( - draftId = DRAFT_ID_FIRST, - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - rawMsg = draft.message.raw - ) - draftsCache.put(DRAFT_ID_FIRST, newDraft) - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(newDraft.toString()) - } else { - MockResponse().setResponseCode(HttpURLConnection.HTTP_BAD_REQUEST) - } - } - request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_FIRST}?fields=id,threadId,historyId&format=full" -> { return genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) } @@ -166,7 +149,9 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { onView(withText(R.string.switch_to_standard_email)) .check(matches(isDisplayed())) .perform(click()) - waitUntil(DraftViewModel.DELAY_TIMEOUT * 2) { draftsCache.isNotEmpty() } + waitUntil(DraftViewModel.DELAY_TIMEOUT * 2) { + draftsCache.isNotEmpty() + } //check that draft was created assertEquals(1, draftsCache.size) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt index b41cdab9df..ba66b7527f 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt @@ -67,6 +67,7 @@ import java.io.OutputStream import java.math.BigInteger import java.net.HttpURLConnection import java.util.Properties +import java.util.concurrent.TimeUnit import java.util.zip.GZIPInputStream import kotlin.random.Random @@ -84,97 +85,6 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( draftsCache.clear() } - protected fun getDraftAndMimeMessageFromRequest(request: RecordedRequest): Pair { - val gzipInputStream = GZIPInputStream(request.body.inputStream()) - val draft = JsonObjectParser(GsonFactory.getDefaultInstance()).parseAndClose( - InputStreamReader(gzipInputStream), Draft::class.java - ) - val rawMimeMessageAsByteArray = Base64.decode( - draft.message.raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP - ) - val mimeMessage = MimeMessage( - Session.getInstance(Properties()), rawMimeMessageAsByteArray.inputStream() - ) - return Pair(draft, mimeMessage) - } - - protected fun getMimeMessageFromDraft(draft: Draft?): MimeMessage? { - if (draft?.message?.raw == null) { - return null - } - - val rawMimeMessageAsByteArray = Base64.decode( - draft.message.raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP - ) - return MimeMessage(Session.getInstance(Properties()), rawMimeMessageAsByteArray.inputStream()) - } - - protected fun genMsgDetailsMockResponse(messageId: String, messageThreadId: String) = - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setBody(Message().apply { - factory = GsonFactory.getDefaultInstance() - id = messageId - threadId = messageThreadId - labelIds = listOf(JavaEmailConstants.FOLDER_DRAFT) - historyId = BigInteger.valueOf(Random.nextLong()) - }.toString()) - - protected fun prepareDraft( - draftId: String, - messageId: String, - messageThreadId: String, - rawMsg: String - ): Draft { - return Draft().apply { - factory = GsonFactory.getDefaultInstance() - id = draftId - message = Message().apply { - id = messageId - threadId = messageThreadId - labelIds = listOf(JavaEmailConstants.FOLDER_DRAFT) - raw = rawMsg - } - } - } - - protected fun openComposeScreenAndTypeData(text: String) { - //open the compose screen - onView(withId(R.id.floatActionButtonCompose)) - .check(matches(isDisplayed())) - .perform(click()) - - //type some text in the subject - onView(withId(R.id.editTextEmailSubject)) - .check(matches(isDisplayed())) - .perform( - scrollTo(), - click(), - typeText(text), - closeSoftKeyboard() - ) - - //type some text in the message - onView(withId(R.id.editTextEmailMessage)) - .check(matches(isDisplayed())) - .perform( - scrollTo(), - click(), - typeText(text), - closeSoftKeyboard() - ) - } - - protected fun moveToDraftFolder() { - onView(withId(R.id.drawer_layout)) - .check(matches(isClosed(Gravity.LEFT))) - .perform(open()) - - onView(withId(R.id.navigationView)) - .perform(clickOnFolderWithName(JavaEmailConstants.FOLDER_DRAFT)) - - java.lang.Thread.sleep(1000) - } - override fun handleCommonAPICalls(request: RecordedRequest): MockResponse { return when { request.path == "/v1/keys/private" -> { @@ -308,6 +218,32 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( ) } + request.method == "POST" && request.path == "/gmail/v1/users/me/drafts" -> { + val (draft, mimeMessage) = getDraftAndMimeMessageFromRequest(request) + + val newDraft = when (mimeMessage.subject) { + MESSAGE_SUBJECT_FIRST -> prepareDraft( + draftId = DRAFT_ID_FIRST, + messageId = MESSAGE_ID_FIRST, + messageThreadId = THREAD_ID_FIRST, + rawMsg = draft.message.raw + ) + + MESSAGE_SUBJECT_SECOND -> prepareDraft( + draftId = DRAFT_ID_SECOND, + messageId = MESSAGE_ID_SECOND, + messageThreadId = THREAD_ID_SECOND, + rawMsg = draft.message.raw + ) + + else -> return super.handleCommonAPICalls(request) + } + + draftsCache.put(newDraft.id, newDraft) + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(newDraft.toString()) + } + request.method == "POST" && request.path == "/batch" -> { val mimeMultipart = MimeMultipart(object : DataSource { override fun getInputStream(): InputStream = request.body.inputStream() @@ -443,13 +379,108 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( } } + protected fun getDraftAndMimeMessageFromRequest(request: RecordedRequest): Pair { + val gzipInputStream = GZIPInputStream(request.body.inputStream()) + val draft = JsonObjectParser(GsonFactory.getDefaultInstance()).parseAndClose( + InputStreamReader(gzipInputStream), Draft::class.java + ) + val rawMimeMessageAsByteArray = Base64.decode( + draft.message.raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + ) + val mimeMessage = MimeMessage( + Session.getInstance(Properties()), rawMimeMessageAsByteArray.inputStream() + ) + return Pair(draft, mimeMessage) + } + + protected fun getMimeMessageFromDraft(draft: Draft?): MimeMessage? { + if (draft?.message?.raw == null) { + return null + } + + val rawMimeMessageAsByteArray = Base64.decode( + draft.message.raw, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + ) + return MimeMessage(Session.getInstance(Properties()), rawMimeMessageAsByteArray.inputStream()) + } + + protected fun genMsgDetailsMockResponse(messageId: String, messageThreadId: String) = + MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(Message().apply { + factory = GsonFactory.getDefaultInstance() + id = messageId + threadId = messageThreadId + labelIds = listOf(JavaEmailConstants.FOLDER_DRAFT) + historyId = BigInteger.valueOf(Random.nextLong()) + }.toString()) + + protected fun prepareDraft( + draftId: String, + messageId: String, + messageThreadId: String, + rawMsg: String + ): Draft { + return Draft().apply { + factory = GsonFactory.getDefaultInstance() + id = draftId + message = Message().apply { + id = messageId + threadId = messageThreadId + labelIds = listOf(JavaEmailConstants.FOLDER_DRAFT) + raw = rawMsg + } + } + } + + protected fun openComposeScreenAndTypeData(text: String) { + //open the compose screen + onView(withId(R.id.floatActionButtonCompose)) + .check(matches(isDisplayed())) + .perform(click()) + + //type some text in the subject + onView(withId(R.id.editTextEmailSubject)) + .check(matches(isDisplayed())) + .perform( + scrollTo(), + click(), + typeText(text), + closeSoftKeyboard() + ) + + //type some text in the message + onView(withId(R.id.editTextEmailMessage)) + .check(matches(isDisplayed())) + .perform( + scrollTo(), + click(), + typeText(text), + closeSoftKeyboard() + ) + } + + protected fun moveToDraftFolder() { + onView(withId(R.id.drawer_layout)) + .check(matches(isClosed(Gravity.LEFT))) + .perform(open()) + + onView(withId(R.id.navigationView)) + .perform(clickOnFolderWithName(JavaEmailConstants.FOLDER_DRAFT)) + + waitForObjectWithText(JavaEmailConstants.FOLDER_DRAFT, TimeUnit.SECONDS.toMillis(2)) + } + protected fun genRawMimeBase64Encoded(msgSubject: String): String { val raw = ByteArrayOutputStream().apply { this.use { MimeMessage(Session.getInstance(Properties())).apply { setFrom(accountEntity.email) subject = msgSubject - setContent(MimeMultipart().apply { addBodyPart(MimeBodyPart().apply { setText(msgSubject) }) }) + setContent(MimeMultipart().apply { + addBodyPart(MimeBodyPart().apply { + setText(msgSubject) + }) + }) }.writeTo(it) } }.toByteArray() From 9afcea55c2576288c247e39a80b110b5e79cbdce Mon Sep 17 00:00:00 2001 From: denbond7 Date: Mon, 5 May 2025 11:34:38 +0300 Subject: [PATCH 5/6] Refactored code.| #3026 --- ...raftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt | 8 +++----- .../email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt | 4 ---- .../flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt | 4 ++++ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt index 554bd1869e..c2c7c218c4 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest.kt @@ -82,10 +82,6 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF } } - request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_FIRST}?fields=id,threadId,historyId&format=full" -> { - genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) - } - else -> handleCommonAPICalls(request) } } @@ -117,7 +113,9 @@ class DraftsGmailAPITestCorrectCreatingAndUpdatingFlowTest : BaseDraftsGmailAPIF onView(withText(R.string.switch_to_standard_email)) .check(matches(isDisplayed())) .perform(click()) - waitUntil(DraftViewModel.DELAY_TIMEOUT * 2) { draftsCache.isNotEmpty() } + waitUntil(DraftViewModel.DELAY_TIMEOUT * 2) { + draftsCache.isNotEmpty() + } pressBack() waitForObjectWithText(MESSAGE_SUBJECT_FIRST, TimeUnit.SECONDS.toMillis(10)) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt index 947b317185..6ae85ad196 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/DraftsGmailAPITestCorrectSendingFlowTest.kt @@ -95,10 +95,6 @@ class DraftsGmailAPITestCorrectSendingFlowTest : BaseDraftsGmailAPIFlowTest() { .setBody(message.toString()) } - request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_FIRST}?fields=id,threadId,historyId&format=full" -> { - return genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) - } - request.method == "POST" && request.path == "/upload/gmail/v1/users/me/drafts/send?uploadType=resumable" -> { val gzipInputStream = GZIPInputStream(request.body.inputStream()) val draft = JsonObjectParser(GsonFactory.getDefaultInstance()).parseAndClose( diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt index ba66b7527f..078e2cdc62 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt @@ -244,6 +244,10 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( .setBody(newDraft.toString()) } + request.path == "/gmail/v1/users/me/messages/${MESSAGE_ID_FIRST}?fields=id,threadId,historyId&format=full" -> { + genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) + } + request.method == "POST" && request.path == "/batch" -> { val mimeMultipart = MimeMultipart(object : DataSource { override fun getInputStream(): InputStream = request.body.inputStream() From 74f84cabf63ec538e0da3ef2d43fef69236bf80c Mon Sep 17 00:00:00 2001 From: denbond7 Date: Mon, 5 May 2025 15:35:20 +0300 Subject: [PATCH 6/6] Refactored code.| #3026 --- .../ui/base/BaseDraftsGmailAPIFlowTest.kt | 215 +++--------------- .../email/ui/base/BaseGmailApiTest.kt | 43 ++-- 2 files changed, 56 insertions(+), 202 deletions(-) diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt index 078e2cdc62..ddb41b934e 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseDraftsGmailAPIFlowTest.kt @@ -20,9 +20,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.ext.junit.rules.activityScenarioRule import com.flowcrypt.email.R import com.flowcrypt.email.api.email.JavaEmailConstants -import com.flowcrypt.email.api.retrofit.ApiHelper -import com.flowcrypt.email.api.retrofit.response.api.EkmPrivateKeysResponse -import com.flowcrypt.email.api.retrofit.response.model.Key import com.flowcrypt.email.ui.activity.MainActivity import com.flowcrypt.email.util.AccountDaoManager import com.flowcrypt.email.viewaction.CustomViewActions.clickOnFolderWithName @@ -32,38 +29,23 @@ import com.google.api.client.json.Json import com.google.api.client.json.JsonObjectParser import com.google.api.client.json.gson.GsonFactory import com.google.api.services.gmail.model.BatchDeleteMessagesRequest -import com.google.api.services.gmail.model.BatchModifyMessagesRequest import com.google.api.services.gmail.model.Draft -import com.google.api.services.gmail.model.Label import com.google.api.services.gmail.model.ListDraftsResponse -import com.google.api.services.gmail.model.ListLabelsResponse -import com.google.api.services.gmail.model.ListSendAsResponse import com.google.api.services.gmail.model.ListThreadsResponse import com.google.api.services.gmail.model.Message import com.google.api.services.gmail.model.MessagePart import com.google.api.services.gmail.model.MessagePartBody import com.google.api.services.gmail.model.MessagePartHeader import com.google.api.services.gmail.model.Thread -import jakarta.activation.DataSource import jakarta.mail.Session -import jakarta.mail.internet.ContentType -import jakarta.mail.internet.InternetHeaders import jakarta.mail.internet.MimeBodyPart import jakarta.mail.internet.MimeMessage import jakarta.mail.internet.MimeMultipart -import okhttp3.Headers -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.RecordedRequest -import okio.GzipSource -import okio.buffer import org.junit.Before -import rawhttp.core.RawHttp import java.io.ByteArrayOutputStream -import java.io.InputStream import java.io.InputStreamReader -import java.io.OutputStream import java.math.BigInteger import java.net.HttpURLConnection import java.util.Properties @@ -87,36 +69,6 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( override fun handleCommonAPICalls(request: RecordedRequest): MockResponse { return when { - request.path == "/v1/keys/private" -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( - ApiHelper.getInstance(getTargetContext()).gson - .toJson(EkmPrivateKeysResponse(privateKeys = listOf(Key(decryptedPrivateKey)))) - ) - } - - request.path == "/gmail/v1/users/me/settings/sendAs" -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( - ListSendAsResponse().apply { - factory = GsonFactory.getDefaultInstance() - sendAs = emptyList() - }.toString() - ) - } - - request.path == "/gmail/v1/users/me/labels" -> { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( - ListLabelsResponse().apply { - factory = GsonFactory.getDefaultInstance() - labels = addLabelsToDatabaseRule.folders.map { - Label().apply { - id = it.fullName - name = it.folderAlias - } - } - }.toString() - ) - } - request.method == "GET" && request.path == "/gmail/v1/users/me/threads?labelIds=${JavaEmailConstants.FOLDER_INBOX}&maxResults=45" -> { MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( ListThreadsResponse().apply { @@ -130,9 +82,12 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody( ListThreadsResponse().apply { factory = GsonFactory.getDefaultInstance() - threads = - draftsCache.values.map { it.message.threadId }.toSet() - .map { Thread().apply { id = it } } + threads = draftsCache.values.map { + it.message.threadId + }.toSet() + .map { + Thread().apply { id = it } + } }.toString() ) } @@ -184,22 +139,6 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( genUserMessagesRawResponse(request.path ?: "") } - request.method == "POST" && request.path == "/gmail/v1/users/me/messages/batchModify" -> { - val source = GzipSource(request.body) - val batchModifyMessagesRequest = GsonFactory.getDefaultInstance().fromInputStream( - source.buffer().inputStream(), - BatchModifyMessagesRequest::class.java - ) - - val handledIds = arrayOf(MESSAGE_ID_FIRST, MESSAGE_ID_SECOND) - - if (handledIds.any { batchModifyMessagesRequest.ids.contains(it) }) { - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - } else { - MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) - } - } - request.method == "GET" && request.path == "/gmail/v1/users/me/drafts?fields=drafts/id,drafts/message/id&maxResults=500" -> { MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) .setHeader("Content-Type", Json.MEDIA_TYPE) @@ -248,85 +187,6 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( genMsgDetailsMockResponse(MESSAGE_ID_FIRST, THREAD_ID_FIRST) } - request.method == "POST" && request.path == "/batch" -> { - val mimeMultipart = MimeMultipart(object : DataSource { - override fun getInputStream(): InputStream = request.body.inputStream() - - override fun getOutputStream(): OutputStream { - throw java.lang.UnsupportedOperationException() - } - - override fun getContentType(): String { - return request.getHeader("Content-Type") ?: throw IllegalArgumentException() - } - - override fun getName(): String = "" - }) - - val count = mimeMultipart.count - val rawHttp = RawHttp() - val responseMimeMultipart = MimeMultipart() - for (i in 0 until count) { - try { - val bodyPart = mimeMultipart.getBodyPart(i) - val rawHttpRequest = rawHttp.parseRequest(bodyPart.inputStream) - val requestBody = if (rawHttpRequest.body.isPresent) { - rawHttpRequest.body.get().asRawBytes().toRequestBody( - contentType = bodyPart.contentType.toMediaTypeOrNull() - ) - } else null - - val okhttp3Request = okhttp3.Request.Builder() - .method( - method = rawHttpRequest.method, - body = requestBody - ) - .url(rawHttpRequest.uri.toURL()) - .headers(Headers.Builder().build()) - .build() - - val response = ApiHelper.getInstance(getTargetContext()) - .retrofit.callFactory().newCall(okhttp3Request).execute() - - val stringBuilder = StringBuilder().apply { - append(response.protocol.toString().uppercase()) - append(" ") - append(response.code) - append(" ") - append(response.message) - append("\n") - - response.headers.forEach { - append(it.first + ": " + it.second + "\n") - } - append("\n") - append(response.body?.string()) - } - - responseMimeMultipart.addBodyPart( - MimeBodyPart( - InternetHeaders(byteArrayOf().inputStream()).apply { - setHeader("Content-Type", "application/http") - setHeader("Content-ID", "response-${i + 1}") - }, - stringBuilder.toString().toByteArray() - ) - ) - } catch (e: Exception) { - e.printStackTrace() - } - } - - val outputStream = ByteArrayOutputStream() - responseMimeMultipart.writeTo(outputStream) - val content = String(outputStream.toByteArray()) - val boundary = (ContentType(responseMimeMultipart.contentType)).getParameter("boundary") - - MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) - .setHeader("Content-Type", "multipart/mixed; boundary=$boundary") - .setBody(content) - } - else -> { super.handleCommonAPICalls(request) } @@ -339,21 +199,11 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( val message = when (messageId) { MESSAGE_ID_FIRST -> { - genMessage( - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", - historyIdValue = HISTORY_ID_FIRST - ) + genFirstMessage() } MESSAGE_ID_SECOND -> { - genMessage( - messageId = MESSAGE_ID_SECOND, - messageThreadId = THREAD_ID_SECOND, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_SECOND])?.subject ?: "", - historyIdValue = HISTORY_ID_SECOND - ) + genSecondMessage() } else -> return super.genUserMessagesGetWithFieldsFormatFullResponse(path) @@ -371,6 +221,13 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( ) } + override fun getAllowedIdsForMessagesBatchModify(): Collection { + return super.getAllowedIdsForMessagesBatchModify() + listOf( + MESSAGE_ID_FIRST, + MESSAGE_ID_SECOND + ) + } + override fun handleBatchDeleteMessagesRequest(batchDeleteMessagesRequest: BatchDeleteMessagesRequest) { super.handleBatchDeleteMessagesRequest(batchDeleteMessagesRequest) @@ -579,21 +436,11 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( val message = when (threadId) { THREAD_ID_FIRST -> { - genMessage( - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", - historyIdValue = HISTORY_ID_FIRST - ) + genFirstMessage() } THREAD_ID_SECOND -> { - genMessage( - messageId = MESSAGE_ID_SECOND, - messageThreadId = THREAD_ID_SECOND, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_SECOND])?.subject ?: "", - historyIdValue = HISTORY_ID_SECOND - ) + genSecondMessage() } else -> return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) @@ -649,21 +496,11 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( val message = when (messageId) { MESSAGE_ID_FIRST -> { - genMessage( - messageId = MESSAGE_ID_FIRST, - messageThreadId = THREAD_ID_FIRST, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", - historyIdValue = HISTORY_ID_FIRST - ) + genFirstMessage() } MESSAGE_ID_SECOND -> { - genMessage( - messageId = MESSAGE_ID_SECOND, - messageThreadId = THREAD_ID_SECOND, - subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_SECOND])?.subject ?: "", - historyIdValue = HISTORY_ID_SECOND - ) + genSecondMessage() } else -> return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) @@ -691,6 +528,20 @@ abstract class BaseDraftsGmailAPIFlowTest : BaseGmailApiTest( ) } + private fun genFirstMessage(): Message = genMessage( + messageId = MESSAGE_ID_FIRST, + messageThreadId = THREAD_ID_FIRST, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_FIRST])?.subject ?: "", + historyIdValue = HISTORY_ID_FIRST + ) + + private fun genSecondMessage(): Message = genMessage( + messageId = MESSAGE_ID_SECOND, + messageThreadId = THREAD_ID_SECOND, + subject = getMimeMessageFromDraft(draftsCache[DRAFT_ID_SECOND])?.subject ?: "", + historyIdValue = HISTORY_ID_SECOND + ) + companion object { const val DRAFT_ID_FIRST = "r5555555555555500001" const val MESSAGE_ID_FIRST = "5555555555500001" diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt index 4723adfa78..a95d42b8f6 100644 --- a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/base/BaseGmailApiTest.kt @@ -371,26 +371,7 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ BatchModifyMessagesRequest::class.java ) - val handledIds = arrayOf( - MESSAGE_ID_EXISTING_STANDARD, - MESSAGE_ID_EXISTING_ENCRYPTED, - MESSAGE_ID_EXISTING_PGP_MIME, - MESSAGE_ID_THREAD_ONLY_STANDARD_1, - MESSAGE_ID_THREAD_ONLY_STANDARD_2, - MESSAGE_ID_THREAD_SINGLE_STANDARD_MESSAGE, - MESSAGE_ID_THREAD_SINGLE_ENCRYPTED_MESSAGE, - MESSAGE_ID_THREAD_ONLY_ENCRYPTED_1, - MESSAGE_ID_THREAD_ONLY_ENCRYPTED_2, - MESSAGE_ID_THREAD_MIXED_MESSAGES_1, - MESSAGE_ID_THREAD_MIXED_MESSAGES_2, - MESSAGE_ID_THREAD_NO_ATTACHMENTS_1, - MESSAGE_ID_THREAD_NO_ATTACHMENTS_2, - MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_1, - MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_2, - MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_3, - MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1, - ) - + val handledIds = getAllowedIdsForMessagesBatchModify() if (handledIds.any { batchModifyMessagesRequest.ids.contains(it) }) { MockResponse().setResponseCode(HttpURLConnection.HTTP_OK) } else { @@ -712,6 +693,28 @@ abstract class BaseGmailApiTest(val accountEntity: AccountEntity = BASE_ACCOUNT_ ) } + protected open fun getAllowedIdsForMessagesBatchModify(): Collection { + return listOf( + MESSAGE_ID_EXISTING_STANDARD, + MESSAGE_ID_EXISTING_ENCRYPTED, + MESSAGE_ID_EXISTING_PGP_MIME, + MESSAGE_ID_THREAD_ONLY_STANDARD_1, + MESSAGE_ID_THREAD_ONLY_STANDARD_2, + MESSAGE_ID_THREAD_SINGLE_STANDARD_MESSAGE, + MESSAGE_ID_THREAD_SINGLE_ENCRYPTED_MESSAGE, + MESSAGE_ID_THREAD_ONLY_ENCRYPTED_1, + MESSAGE_ID_THREAD_ONLY_ENCRYPTED_2, + MESSAGE_ID_THREAD_MIXED_MESSAGES_1, + MESSAGE_ID_THREAD_MIXED_MESSAGES_2, + MESSAGE_ID_THREAD_NO_ATTACHMENTS_1, + MESSAGE_ID_THREAD_NO_ATTACHMENTS_2, + MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_1, + MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_2, + MESSAGE_ID_THREAD_FEW_MESSAGES_WITH_SINGLE_DRAFT_3, + MESSAGE_ID_THREAD_PGP_MIME_MESSAGES_1, + ) + } + protected open fun handleBatchDeleteMessagesRequest(batchDeleteMessagesRequest: BatchDeleteMessagesRequest) {} private fun genPgpMimeRawResponse(path: String): MockResponse {