Merge pull request #1878 from vector-im/feature/bma/moreTests2

Add more unit tests
This commit is contained in:
Benoit Marty 2023-11-24 17:17:52 +01:00 committed by GitHub
commit a2c1476793
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 615 additions and 226 deletions

View file

@ -22,27 +22,19 @@ interface BugReporter {
/**
* Send a bug report.
*
* @param reportType The report type (bug, suggestion, feedback)
* @param withDevicesLogs true to include the device log
* @param withCrashLogs true to include the crash logs
* @param withKeyRequestHistory true to include the crash logs
* @param withScreenshot true to include the screenshot
* @param theBugDescription the bug description
* @param serverVersion version of the server
* @param canContact true if the user opt in to be contacted directly
* @param customFields fields which will be sent with the report
* @param listener the listener
*/
suspend fun sendBugReport(
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean = false,
customFields: Map<String, String>? = null,
listener: BugReporterListener?
)

View file

@ -42,5 +42,5 @@ interface BugReporterListener {
/**
* The bug report upload succeeded.
*/
fun onUploadSucceed(reportUrl: String?)
fun onUploadSucceed()
}

View file

@ -23,6 +23,12 @@ plugins {
android {
namespace = "io.element.android.features.rageshake.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
}
anvil {
@ -50,12 +56,16 @@ dependencies {
ksp(libs.showkase.processor)
testImplementation(libs.test.junit)
testImplementation(libs.test.robolectric)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.mockk)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.sessionStorage.implMemory)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.services.toolbox.test)
testImplementation(libs.network.mockwebserver)
}

View file

@ -25,12 +25,11 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import io.element.android.features.rageshake.api.crash.CrashDataStore
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.features.rageshake.api.crash.CrashDataStore
import io.element.android.features.rageshake.impl.logs.VectorFileLogger
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
import io.element.android.features.rageshake.impl.logs.VectorFileLogger
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import kotlinx.coroutines.CoroutineScope
@ -64,7 +63,7 @@ class BugReportPresenter @Inject constructor(
sendingAction.value = Async.Loading()
}
override fun onUploadSucceed(reportUrl: String?) {
override fun onUploadSucceed() {
sendingProgress.floatValue = 0f
sendingAction.value = Async.Success(Unit)
}
@ -135,15 +134,11 @@ class BugReportPresenter @Inject constructor(
listener: BugReporterListener,
) = launch {
bugReporter.sendBugReport(
reportType = ReportType.BUG_REPORT,
withDevicesLogs = formState.sendLogs,
withCrashLogs = hasCrashLogs && formState.sendLogs,
withKeyRequestHistory = false,
withScreenshot = formState.sendScreenshot,
theBugDescription = formState.description,
serverVersion = "",
canContact = formState.canContact,
customFields = emptyMap(),
listener = listener
)
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 New Vector Ltd
* Copyright (c) 2023 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.
@ -14,13 +14,10 @@
* limitations under the License.
*/
package io.element.android.features.rageshake.api.reporter
package io.element.android.features.rageshake.impl.reporter
enum class ReportType {
BUG_REPORT,
SUGGESTION,
SPACE_BETA_FEEDBACK,
THREADS_BETA_FEEDBACK,
AUTO_UISI,
AUTO_UISI_SENDER,
import okhttp3.HttpUrl
fun interface BugReporterUrlProvider {
fun provide(): HttpUrl
}

View file

@ -25,14 +25,12 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.rageshake.api.crash.CrashDataStore
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.features.rageshake.api.screenshot.ScreenshotHolder
import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.androidutils.file.compressFile
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.extensions.toOnOff
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.di.AppScope
@ -77,26 +75,12 @@ class DefaultBugReporter @Inject constructor(
private val userAgentProvider: UserAgentProvider,
private val sessionStore: SessionStore,
private val buildMeta: BuildMeta,
/*
private val versionProvider: VersionProvider,
private val vectorPreferences: VectorPreferences,
private val vectorFileLogger: VectorFileLogger,
private val systemLocaleProvider: SystemLocaleProvider,
private val matrix: Matrix,
private val processInfo: ProcessInfo,
private val sdkIntProvider: BuildVersionSdkIntProvider,
private val vectorLocale: VectorLocaleProvider,
*/
private val bugReporterUrlProvider: BugReporterUrlProvider,
) : BugReporter {
var inMultiWindowMode = false
companion object {
// filenames
private const val LOG_CAT_ERROR_FILENAME = "logcatError.log"
private const val LOG_CAT_FILENAME = "logcat.log"
private const val LOG_DIRECTORY_NAME = "logs"
// private const val KEY_REQUESTS_FILENAME = "keyRequests.log"
private const val BUFFER_SIZE = 1024 * 1024 * 50
}
@ -105,58 +89,20 @@ class DefaultBugReporter @Inject constructor(
// boolean to cancel the bug report
private val isCancelled = false
/*
val adapter = MatrixJsonParser.getMoshi()
.adapter<JsonDict>(Types.newParameterizedType(Map::class.java, String::class.java, Any::class.java))
*/
private val logcatCommandError = arrayOf(
"logcat", // /< Run 'logcat' command
"-d", // /< Dump the log rather than continue outputting it
"-v", // formatting
"threadtime", // include timestamps
"AndroidRuntime:E " + // /< Pick all AndroidRuntime errors (such as uncaught exceptions)"communicatorjni:V " + ///< All communicatorjni logging
"libcommunicator:V " + // /< All libcommunicator logging
"DEBUG:V " + // /< All DEBUG logging - which includes native land crashes (seg faults, etc)
"*:S" // /< Everything else silent, so don't pick it..
)
private val logcatCommandDebug = arrayOf("logcat", "-d", "-v", "threadtime", "*:*")
/**
* Send a bug report.
*
* @param reportType The report type (bug, suggestion, feedback)
* @param withDevicesLogs true to include the device log
* @param withCrashLogs true to include the crash logs
* @param withKeyRequestHistory true to include the crash logs
* @param withScreenshot true to include the screenshot
* @param theBugDescription the bug description
* @param serverVersion version of the server
* @param canContact true if the user opt in to be contacted directly
* @param customFields fields which will be sent with the report
* @param listener the listener
*/
override suspend fun sendBugReport(
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean,
customFields: Map<String, String>?,
listener: BugReporterListener?
) {
// enumerate files to delete
val bugReportFiles: MutableList<File> = ArrayList()
try {
var serverError: String? = null
var reportURL: String? = null
withContext(coroutineDispatchers.io) {
var bugDescription = theBugDescription
val crashCallStack = crashDataStore.crashInfo().first()
@ -181,7 +127,7 @@ class DefaultBugReporter @Inject constructor(
}
if (!isCancelled && (withCrashLogs || withDevicesLogs)) {
val gzippedLogcat = saveLogCat(false)
val gzippedLogcat = saveLogCat()
if (null != gzippedLogcat) {
if (gzippedFiles.size == 0) {
@ -192,68 +138,21 @@ class DefaultBugReporter @Inject constructor(
}
}
/*
activeSessionHolder.getSafeActiveSession()
?.takeIf { !mIsCancelled && withKeyRequestHistory }
?.cryptoService()
?.getGossipingEvents()
?.let { GossipingEventsSerializer().serialize(it) }
?.toByteArray()
?.let { rawByteArray ->
File(context.cacheDir.absolutePath, KEY_REQUESTS_FILENAME)
.also {
it.outputStream()
.use { os -> os.write(rawByteArray) }
}
}
?.let { compressFile(it) }
?.let { gzippedFiles.add(it) }
*/
val sessionData = sessionStore.getLatestSession()
val deviceId = sessionData?.deviceId ?: "undefined"
val userId = sessionData?.userId ?: "undefined"
var olmVersion = "undefined"
if (!isCancelled) {
val text = when (reportType) {
ReportType.BUG_REPORT -> bugDescription
ReportType.SUGGESTION -> "[Suggestion] $bugDescription"
ReportType.SPACE_BETA_FEEDBACK -> "[spaces-feedback] $bugDescription"
ReportType.THREADS_BETA_FEEDBACK -> "[threads-feedback] $bugDescription"
ReportType.AUTO_UISI_SENDER,
ReportType.AUTO_UISI -> bugDescription
}
// build the multi part request
val builder = BugReporterMultipartBody.Builder()
.addFormDataPart("text", text)
.addFormDataPart("app", rageShakeAppNameForReport(reportType))
.addFormDataPart("text", bugDescription)
.addFormDataPart("app", context.getString(R.string.bug_report_app_name))
.addFormDataPart("user_agent", userAgentProvider.provide())
.addFormDataPart("user_id", userId)
.addFormDataPart("can_contact", canContact.toString())
.addFormDataPart("device_id", deviceId)
// .addFormDataPart("version", versionProvider.getVersion(longFormat = true))
// .addFormDataPart("branch_name", buildMeta.gitBranchName)
// .addFormDataPart("matrix_sdk_version", Matrix.getSdkVersion())
.addFormDataPart("olm_version", olmVersion)
.addFormDataPart("device", Build.MODEL.trim())
// .addFormDataPart("verbose_log", vectorPreferences.labAllowedExtendedLogging().toOnOff())
.addFormDataPart("multi_window", inMultiWindowMode.toOnOff())
// .addFormDataPart(
// "os", Build.VERSION.RELEASE + " (API " + sdkIntProvider.get() + ") " +
// Build.VERSION.INCREMENTAL + "-" + Build.VERSION.CODENAME
// )
.addFormDataPart("locale", Locale.getDefault().toString())
// .addFormDataPart("app_language", vectorLocale.applicationLocale.toString())
// .addFormDataPart("default_app_language", systemLocaleProvider.getSystemLocale().toString())
// .addFormDataPart("theme", ThemeUtils.getApplicationTheme(context))
.addFormDataPart("server_version", serverVersion)
.apply {
customFields?.forEach { (name, value) ->
addFormDataPart(name, value)
}
}
// add the gzipped files, don't cancel the whole upload if only some file failed to upload
var uploadedSomeLogs = false
@ -293,31 +192,6 @@ class DefaultBugReporter @Inject constructor(
// add some github labels
builder.addFormDataPart("label", buildMeta.versionName)
// builder.addFormDataPart("label", buildMeta.flavorDescription)
// builder.addFormDataPart("label", buildMeta.gitBranchName)
// Possible values for BuildConfig.BUILD_TYPE: "debug", "nightly", "release".
// builder.addFormDataPart("label", BuildConfig.BUILD_TYPE)
when (reportType) {
ReportType.BUG_REPORT -> {
/* nop */
}
ReportType.SUGGESTION -> builder.addFormDataPart("label", "[Suggestion]")
ReportType.SPACE_BETA_FEEDBACK -> builder.addFormDataPart("label", "spaces-feedback")
ReportType.THREADS_BETA_FEEDBACK -> builder.addFormDataPart("label", "threads-feedback")
ReportType.AUTO_UISI -> {
builder.addFormDataPart("label", "Z-UISI")
builder.addFormDataPart("label", "android")
builder.addFormDataPart("label", "uisi-recipient")
}
ReportType.AUTO_UISI_SENDER -> {
builder.addFormDataPart("label", "Z-UISI")
builder.addFormDataPart("label", "android")
builder.addFormDataPart("label", "uisi-sender")
}
}
if (crashCallStack.isNotEmpty() && withCrashLogs) {
builder.addFormDataPart("label", "crash")
}
@ -350,7 +224,7 @@ class DefaultBugReporter @Inject constructor(
// build the request
val request = Request.Builder()
.url(context.getString(R.string.bug_report_url))
.url(bugReporterUrlProvider.provide())
.post(requestBody)
.build()
@ -379,7 +253,6 @@ class DefaultBugReporter @Inject constructor(
} else {
try {
val inputStream = response.body!!.byteStream()
serverError = inputStream.use {
buildString {
var ch = it.read()
@ -389,7 +262,6 @@ class DefaultBugReporter @Inject constructor(
}
}
}
// check if the error message
serverError?.let {
try {
@ -401,7 +273,6 @@ class DefaultBugReporter @Inject constructor(
Timber.e(e, "doInBackground ; Json conversion failed")
}
}
// should never happen
if (null == serverError) {
serverError = "Failed with error $responseCode"
@ -412,25 +283,17 @@ class DefaultBugReporter @Inject constructor(
Timber.e(e, "## sendBugReport() : failed to parse error")
}
}
} else {
/*
reportURL = response?.body?.string()?.let { stringBody ->
adapter.fromJson(stringBody)?.get("report_url")?.toString()
}
*/
}
}
}
withContext(coroutineDispatchers.main) {
bugReportCall = null
if (null != listener) {
try {
if (isCancelled) {
listener.onUploadCancelled()
} else if (null == serverError) {
listener.onUploadSucceed(reportURL)
listener.onUploadSucceed()
} else {
listener.onUploadFailed(serverError)
}
@ -449,47 +312,6 @@ class DefaultBugReporter @Inject constructor(
}
}
/**
* Send a bug report either with email or with Vector.
*/
/* TODO Remove
fun openBugReportScreen(activity: FragmentActivity, reportType: ReportType = ReportType.BUG_REPORT) {
screenshot = takeScreenshot(activity)
logDbInfo()
logProcessInfo()
logOtherInfo()
activity.startActivity(BugReportActivity.intent(activity, reportType))
}
*/
// private fun logOtherInfo() {
// Timber.i("SyncThread state: " + activeSessionHolder.getSafeActiveSession()?.syncService()?.getSyncState())
// }
// private fun logDbInfo() {
// val dbInfo = matrix.debugService().getDbUsageInfo()
// Timber.i(dbInfo)
// }
// private fun logProcessInfo() {
// val pInfo = processInfo.getInfo()
// Timber.i(pInfo)
// }
private fun rageShakeAppNameForReport(reportType: ReportType): String {
// As per https://github.com/matrix-org/rageshake
// app: Identifier for the application (eg 'riot-web').
// Should correspond to a mapping configured in the configuration file for github issue reporting to work.
// (see R.string.bug_report_url for configured RS server)
return context.getString(
when (reportType) {
ReportType.AUTO_UISI_SENDER,
ReportType.AUTO_UISI -> R.string.bug_report_auto_uisi_app_name
else -> R.string.bug_report_app_name
}
)
}
override fun logDirectory(): File {
return File(context.cacheDir, LOG_DIRECTORY_NAME)
}
@ -545,11 +367,10 @@ class DefaultBugReporter @Inject constructor(
/**
* Save the logcat.
*
* @param isErrorLogcat true to save the error logcat
* @return the file if the operation succeeds
*/
private fun saveLogCat(isErrorLogcat: Boolean): File? {
val logCatErrFile = File(context.cacheDir.absolutePath, if (isErrorLogcat) LOG_CAT_ERROR_FILENAME else LOG_CAT_FILENAME)
private fun saveLogCat(): File? {
val logCatErrFile = File(context.cacheDir.absolutePath, LOG_CAT_FILENAME)
if (logCatErrFile.exists()) {
logCatErrFile.safeDelete()
@ -557,7 +378,7 @@ class DefaultBugReporter @Inject constructor(
try {
logCatErrFile.writer().use {
getLogCatError(it, isErrorLogcat)
getLogCatError(it)
}
return compressFile(logCatErrFile)
@ -578,13 +399,12 @@ class DefaultBugReporter @Inject constructor(
* Retrieves the logs.
*
* @param streamWriter the stream writer
* @param isErrorLogCat true to save the error logs
*/
private fun getLogCatError(streamWriter: OutputStreamWriter, isErrorLogCat: Boolean) {
private fun getLogCatError(streamWriter: OutputStreamWriter) {
val logcatProc: Process
try {
logcatProc = Runtime.getRuntime().exec(if (isErrorLogCat) logcatCommandError else logcatCommandDebug)
logcatProc = Runtime.getRuntime().exec(logcatCommandDebug)
} catch (e1: IOException) {
return
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 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 io.element.android.features.rageshake.impl.reporter
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.rageshake.impl.R
import io.element.android.libraries.di.AppScope
import io.element.android.services.toolbox.api.strings.StringProvider
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultBugReporterUrlProvider @Inject constructor(
private val stringProvider: StringProvider
) : BugReporterUrlProvider {
override fun provide(): HttpUrl {
return stringProvider.getString(R.string.bug_report_url).toHttpUrl()
}
}

View file

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) 2022 New Vector Ltd
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@ -19,7 +18,12 @@
<!-- Rageshake configuration -->
<string name="bug_report_url" translatable="false">https://riot.im/bugreports/submit</string>
<!--
As per https://github.com/matrix-org/rageshake
bug_report_app_name: Identifier for the application (eg 'riot-web').
Should correspond to a mapping configured in the configuration file for github issue reporting to work.
-->
<string name="bug_report_app_name" translatable="false">element-x-android</string>
<string name="bug_report_auto_uisi_app_name" translatable="false">element-auto-uisi</string>
</resources>

View file

@ -18,22 +18,17 @@ package io.element.android.features.rageshake.impl.bugreport
import io.element.android.features.rageshake.api.reporter.BugReporter
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.api.reporter.ReportType
import io.element.android.libraries.matrix.test.A_FAILURE_REASON
import kotlinx.coroutines.delay
import java.io.File
class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Success) : BugReporter {
override suspend fun sendBugReport(
reportType: ReportType,
withDevicesLogs: Boolean,
withCrashLogs: Boolean,
withKeyRequestHistory: Boolean,
withScreenshot: Boolean,
theBugDescription: String,
serverVersion: String,
canContact: Boolean,
customFields: Map<String, String>?,
listener: BugReporterListener?,
) {
delay(100)
@ -54,7 +49,7 @@ class FakeBugReporter(val mode: FakeBugReporterMode = FakeBugReporterMode.Succes
}
listener?.onProgress(100)
delay(100)
listener?.onUploadSucceed(null)
listener?.onUploadSucceed()
}
override fun cleanLogDirectoryIfNeeded() {

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2023 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 io.element.android.features.rageshake.impl.crash
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.test.AN_EXCEPTION
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class VectorUncaughtExceptionHandlerTest {
@Test
fun `activate should change the default handler`() {
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
sut.activate()
assertThat(Thread.getDefaultUncaughtExceptionHandler()).isInstanceOf(VectorUncaughtExceptionHandler::class.java)
}
@Test
fun `uncaught exception`() = runTest {
val crashDataStore = PreferencesCrashDataStore(RuntimeEnvironment.getApplication())
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
assertThat(crashDataStore.crashInfo().first()).isEmpty()
val sut = VectorUncaughtExceptionHandler(RuntimeEnvironment.getApplication())
sut.uncaughtException(Thread(), AN_EXCEPTION)
assertThat(crashDataStore.appHasCrashed().first()).isTrue()
val crashInfo = crashDataStore.crashInfo().first()
assertThat(crashInfo).isNotEmpty()
assertThat(crashInfo).contains("Memory statuses")
crashDataStore.resetAppHasCrashed()
assertThat(crashDataStore.appHasCrashed().first()).isFalse()
}
}

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2023 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 io.element.android.features.rageshake.impl.reporter
import com.google.common.truth.Truth.assertThat
import io.element.android.features.rageshake.api.reporter.BugReporterListener
import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.network.useragent.DefaultUserAgentProvider
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
@RunWith(RobolectricTestRunner::class)
class DefaultBugReporterTest {
@Test
fun `test sendBugReport success`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(200)
)
server.start()
val sut = createDefaultBugReporter(server)
var onUploadCancelledCalled = false
var onUploadFailedCalled = false
var progressValues = mutableListOf<Int>()
var onUploadSucceedCalled = false
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
theBugDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
override fun onUploadCancelled() {
onUploadCancelledCalled = true
}
override fun onUploadFailed(reason: String?) {
onUploadFailedCalled = true
}
override fun onProgress(progress: Int) {
progressValues.add(progress)
}
override fun onUploadSucceed() {
onUploadSucceedCalled = true
}
},
)
val request = server.takeRequest()
assertThat(request.path).isEqualTo("/")
assertThat(request.method).isEqualTo("POST")
server.shutdown()
assertThat(onUploadCancelledCalled).isFalse()
assertThat(onUploadFailedCalled).isFalse()
assertThat(progressValues.size).isEqualTo(10)
assertThat(onUploadSucceedCalled).isTrue()
}
@Test
fun `test sendBugReport error`() = runTest {
val server = MockWebServer()
server.enqueue(
MockResponse()
.setResponseCode(400)
.setBody("""{"error": "An error body"}""")
)
server.start()
val sut = createDefaultBugReporter(server)
var onUploadCancelledCalled = false
var onUploadFailedCalled = false
var onUploadFailedReason: String? = null
var progressValues = mutableListOf<Int>()
var onUploadSucceedCalled = false
sut.sendBugReport(
withDevicesLogs = true,
withCrashLogs = true,
withScreenshot = true,
theBugDescription = "a bug occurred",
canContact = true,
listener = object : BugReporterListener {
override fun onUploadCancelled() {
onUploadCancelledCalled = true
}
override fun onUploadFailed(reason: String?) {
onUploadFailedCalled = true
onUploadFailedReason = reason
}
override fun onProgress(progress: Int) {
progressValues.add(progress)
}
override fun onUploadSucceed() {
onUploadSucceedCalled = true
}
},
)
val request = server.takeRequest()
assertThat(request.path).isEqualTo("/")
assertThat(request.method).isEqualTo("POST")
server.shutdown()
assertThat(onUploadCancelledCalled).isFalse()
assertThat(onUploadFailedCalled).isTrue()
assertThat(onUploadFailedReason).isEqualTo("An error body")
assertThat(progressValues.size).isEqualTo(10)
assertThat(onUploadSucceedCalled).isFalse()
}
private fun TestScope.createDefaultBugReporter(
server: MockWebServer
): DefaultBugReporter {
val buildMeta = aBuildMeta()
return DefaultBugReporter(
context = RuntimeEnvironment.getApplication(),
screenshotHolder = FakeScreenshotHolder(),
crashDataStore = FakeCrashDataStore(),
coroutineScope = this,
systemClock = FakeSystemClock(),
coroutineDispatchers = testCoroutineDispatchers(),
okHttpClient = { OkHttpClient.Builder().build() },
userAgentProvider = DefaultUserAgentProvider(buildMeta),
sessionStore = InMemorySessionStore(),
buildMeta = buildMeta,
bugReporterUrlProvider = { server.url("/") }
)
}
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2023 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 io.element.android.features.rageshake.impl.reporter
import com.google.common.truth.Truth.assertThat
import io.element.android.services.toolbox.test.strings.FakeStringProvider
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Test
class DefaultBugReporterUrlProviderTest {
@Test
fun `test DefaultBugReporterUrlProvider`() {
val sut = DefaultBugReporterUrlProvider(FakeStringProvider("https://example.org"))
val result = sut.provide()
assertThat(result).isEqualTo("https://example.org".toHttpUrl())
}
}