From e482ef4262aef95a9dfa1b9f0ede967032c31e2e Mon Sep 17 00:00:00 2001
From: ariskotsomitopoulos <aris.kotsomitopoulos@gmail.com>
Date: Mon, 3 Jan 2022 16:51:12 +0200
Subject: [PATCH] First local thread integration test

---
 .github/workflows/integration.yml             |   4 +-
 .../android/sdk/common/CommonTestHelper.kt    |  59 +++++++++-
 .../threads/GenerateThreadMessageTests.kt     | 107 ++++++++++++++++++
 .../sdk/session/room/threads/RetryTestRule.kt |  58 ++++++++++
 4 files changed, 224 insertions(+), 4 deletions(-)
 create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
 create mode 100644 matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt

diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml
index 1f087e1ff1..c18ca69fde 100644
--- a/.github/workflows/integration.yml
+++ b/.github/workflows/integration.yml
@@ -69,8 +69,8 @@ jobs:
           python3 -m venv .synapse
           source .synapse/bin/activate
           pip install synapse matrix-synapse
-          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
-#          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh | bash -s --no-rate-limit
+          curl -sL https://raw.githubusercontent.com/matrix-org/synapse/develop/demo/start.sh --no-rate-limit \
+            | sed s/127.0.0.1/0.0.0.0/g | bash
       - name: Run integration tests on API ${{ matrix.api-level }}
         uses: reactivecircus/android-emulator-runner@v2
         with:
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
index 8e21828562..0edd8c52df 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt
@@ -184,18 +184,73 @@ class CommonTestHelper(context: Context) {
     /**
      * Will send nb of messages provided by count parameter but waits a bit every 10 messages to avoid gap in sync
      */
-    private fun sendTextMessagesBatched(room: Room, message: String, count: Int) {
+    private fun sendTextMessagesBatched(room: Room, message: String, count: Int, rootThreadEventId: String? = null) {
         (1 until count + 1)
                 .map { "$message #$it" }
                 .chunked(10)
                 .forEach { batchedMessages ->
                     batchedMessages.forEach { formattedMessage ->
-                        room.sendTextMessage(formattedMessage)
+                        if (rootThreadEventId != null) {
+                            room.replyInThread(
+                                    rootThreadEventId = rootThreadEventId,
+                                    replyInThreadText = formattedMessage)
+                        } else {
+                            room.sendTextMessage(formattedMessage)
+                        }
                     }
                     Thread.sleep(1_000L)
                 }
     }
 
+    /**
+     * Reply in a thread
+     * @param room         the room where to send the messages
+     * @param message      the message to send
+     * @param numberOfMessages the number of time the message will be sent
+     */
+    fun replyInThreadMessage(
+            room: Room,
+            message: String,
+            numberOfMessages: Int,
+            rootThreadEventId: String,
+            timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
+
+        val sentEvents = ArrayList<TimelineEvent>(numberOfMessages)
+        val timeline = room.createTimeline(null, TimelineSettings(10))
+        timeline.start()
+        waitWithLatch(timeout + 1_000L * numberOfMessages) { latch ->
+            val timelineListener = object : Timeline.Listener {
+                override fun onTimelineFailure(throwable: Throwable) {
+                }
+
+                override fun onNewTimelineEvents(eventIds: List<String>) {
+                    // noop
+                }
+
+                override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
+                    val newMessages = snapshot
+                            .filter { it.root.sendState == SendState.SYNCED }
+                            .filter { it.root.getClearType() == EventType.MESSAGE }
+                            .filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
+
+                    Timber.v("New synced message size: ${newMessages.size}")
+                    if (newMessages.size == numberOfMessages) {
+                        sentEvents.addAll(newMessages)
+                        // Remove listener now, if not at the next update sendEvents could change
+                        timeline.removeListener(this)
+                        latch.countDown()
+                    }
+                }
+            }
+            timeline.addListener(timelineListener)
+            sendTextMessagesBatched(room, message, numberOfMessages, rootThreadEventId)
+        }
+        timeline.dispose()
+        // Check that all events has been created
+        assertEquals("Message number do not match $sentEvents", numberOfMessages.toLong(), sentEvents.size.toLong())
+        return sentEvents
+    }
+
     // PRIVATE METHODS *****************************************************************************
 
     /**
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
new file mode 100644
index 0000000000..79f5e8314d
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/GenerateThreadMessageTests.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.session.room.threads
+
+import org.amshove.kluent.shouldBe
+import org.amshove.kluent.shouldBeEqualTo
+import org.amshove.kluent.shouldBeFalse
+import org.amshove.kluent.shouldBeNull
+import org.amshove.kluent.shouldBeTrue
+import org.junit.Assert
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.junit.runners.MethodSorters
+import org.matrix.android.sdk.InstrumentedTest
+import org.matrix.android.sdk.api.session.events.model.getRootThreadEventId
+import org.matrix.android.sdk.api.session.events.model.isTextMessage
+import org.matrix.android.sdk.api.session.events.model.isThread
+import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings
+import org.matrix.android.sdk.common.CommonTestHelper
+import org.matrix.android.sdk.common.CryptoTestHelper
+import timber.log.Timber
+import java.util.concurrent.CountDownLatch
+
+@RunWith(JUnit4::class)
+@FixMethodOrder(MethodSorters.JVM)
+class GenerateThreadMessageTests : InstrumentedTest {
+
+    private val commonTestHelper = CommonTestHelper(context())
+    private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
+
+    private val logPrefix = "---Test--> "
+
+//    @Rule
+//    @JvmField
+//    val mRetryTestRule = RetryTestRule()
+
+    @Test
+    fun reply_in_thread_to_normal_timeline_message_should_create_a_thread() {
+        val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
+
+        val aliceSession = cryptoTestData.firstSession
+        val aliceRoomId = cryptoTestData.roomId
+
+        val aliceRoom = aliceSession.getRoom(aliceRoomId)!!
+
+        // Let's send a message in the normal timeline
+        val textMessage = "This is a normal timeline message"
+        val sentMessages = commonTestHelper.sendTextMessage(
+                room = aliceRoom,
+                message = textMessage,
+                nbOfMessages = 1)
+
+        val initMessage = sentMessages.first()
+
+        initMessage.root.isThread().shouldBeFalse()
+        initMessage.root.isTextMessage().shouldBeTrue()
+        initMessage.root.getRootThreadEventId().shouldBeNull()
+        initMessage.root.threadDetails?.isRootThread?.shouldBeFalse()
+
+        // Let's reply in timeline to that message
+        val repliesInThread = commonTestHelper.replyInThreadMessage(
+                room = aliceRoom,
+                message = "Reply In the above thread",
+                numberOfMessages = 1,
+                rootThreadEventId = initMessage.root.eventId.orEmpty())
+
+        val replyInThread = repliesInThread.first()
+        replyInThread.root.isThread().shouldBeTrue()
+        replyInThread.root.isTextMessage().shouldBeTrue()
+        replyInThread.root.getRootThreadEventId().shouldBeEqualTo(initMessage.root.eventId)
+
+        // The init normal message should now be a root thread event
+        val timeline = aliceRoom.createTimeline(null, TimelineSettings(30))
+        timeline.start()
+
+        aliceSession.startSync(true)
+        run {
+            val lock = CountDownLatch(1)
+            val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
+                val initMessageThreadDetails = snapshot.firstOrNull { it.root.eventId == initMessage.root.eventId }?.root?.threadDetails
+                initMessageThreadDetails?.isRootThread?.shouldBeTrue() ?: assert(false)
+                initMessageThreadDetails?.numberOfThreads?.shouldBe(1)
+                Timber.e("$logPrefix $initMessageThreadDetails")
+                true
+            }
+            timeline.addListener(eventsListener)
+            commonTestHelper.await(lock, 600_000)
+        }
+        aliceSession.stopSync()
+    }
+}
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
new file mode 100644
index 0000000000..099491655f
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/threads/RetryTestRule.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2022 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.session.room.threads
+
+import android.util.Log
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Retry test rule used to retry test that failed.
+ * Retry failed test 3 times
+ */
+class RetryTestRule(val retryCount: Int = 3) : TestRule {
+
+    private val TAG = RetryTestRule::class.java.simpleName
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return statement(base, description)
+    }
+
+    private fun statement(base: Statement, description: Description): Statement {
+        return object : Statement() {
+            @Throws(Throwable::class)
+            override fun evaluate() {
+                var caughtThrowable: Throwable? = null
+
+                // implement retry logic here
+                for (i in 0 until retryCount) {
+                    try {
+                        base.evaluate()
+                        return
+                    } catch (t: Throwable) {
+                        caughtThrowable = t
+                        Log.e(TAG, description.displayName + ": run " + (i + 1) + " failed")
+                    }
+                }
+
+                Log.e(TAG, description.displayName + ": giving up after " + retryCount + " failures")
+                throw caughtThrowable!!
+            }
+        }
+    }
+}