Merge pull request #3 from fknives/issue#2-update

Issue#2 update
This commit is contained in:
Gergely Hegedis 2022-04-21 14:22:13 +03:00 committed by GitHub
commit 3725da7e53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 350 additions and 245 deletions

View file

@ -6,13 +6,12 @@ plugins {
apply from: 'signing.config.gradle' apply from: 'signing.config.gradle'
android { android {
compileSdkVersion 30 compileSdk 31
buildToolsVersion "30.0.2"
defaultConfig { defaultConfig {
applicationId "org.fnives.tiktokdownloader" applicationId "org.fnives.tiktokdownloader"
minSdkVersion 23 minSdk 23
targetSdkVersion 30 targetSdk 31
versionCode 1 versionCode 1
versionName "1.0.0" versionName "1.0.0"
@ -51,41 +50,65 @@ android {
lintOptions { lintOptions {
abortOnError true abortOnError true
} }
testOptions.unitTests {
includeAndroidResources = true
all {
useJUnitPlatform()
testLogging {
events 'started', 'passed', 'skipped', 'failed'
exceptionFormat "full"
showStandardStreams true
}
}
}
}
tasks.configureEach { task ->
if (task.taskIdentity.type.toString() == "class org.jetbrains.kotlin.gradle.tasks.KotlinCompile") {
task.kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}
}
} }
dependencies { dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:1.3.2" implementation "androidx.core:core-ktx:1.7.0"
implementation "androidx.appcompat:appcompat:1.2.0" implementation "androidx.appcompat:appcompat:1.4.1"
implementation "androidx.activity:activity-ktx:1.2.0-beta01" implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.3.0-beta01" implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "com.google.android.material:material:1.2.1" implementation "com.google.android.material:material:1.5.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.constraintlayout:constraintlayout:2.1.3"
// Coroutines // Coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.0-M1" def coroutine_version = "1.6.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version"
implementation "androidx.fragment:fragment-ktx:1.2.5" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "com.github.bumptech.glide:glide:4.11.0" def glide_version = "4.11.0"
kapt "com.github.bumptech.glide:compiler:4.11.0" implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
def okhttp_version = "4.9.3"
implementation "com.squareup.retrofit2:retrofit:2.9.0" implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.7.2" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:10.0.5' implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:11.0.1'
testImplementation "org.junit.jupiter:junit-jupiter-engine:5.7.0" def junit_version = "5.7.0"
testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.0" testImplementation "org.junit.jupiter:junit-jupiter-engine:$junit_version"
testImplementation 'com.jraska.livedata:testing-ktx:1.1.2' testImplementation "org.junit.jupiter:junit-jupiter-params:$junit_version"
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation 'com.jraska.livedata:testing-ktx:1.2.0'
testImplementation "com.squareup.okhttp3:mockwebserver:4.2.1" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
testImplementation "com.squareup.okhttp3:mockwebserver:$okhttp_version"
testImplementation "commons-io:commons-io:2.8.0" testImplementation "commons-io:commons-io:2.8.0"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.0-M1" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutine_version"
testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "androidx.test.ext:junit:1.1.2" androidTestImplementation "androidx.test.ext:junit:1.1.3"
androidTestImplementation "androidx.test.espresso:espresso-core:3.3.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
} }

View file

@ -13,19 +13,23 @@
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:fullBackupContent="false"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.TikTokDownloader"> android:theme="@style/Theme.TikTokDownloader">
<activity android:name=".ui.main.MainActivity"> <activity
android:name=".ui.main.MainActivity"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".ui.service.DownloadIntentReceiverActivity" <activity
android:name=".ui.service.DownloadIntentReceiverActivity"
android:exported="true"
android:theme="@style/NoDisplayTheme"> android:theme="@style/NoDisplayTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@ -35,6 +39,7 @@
<data android:mimeType="message/*" /> <data android:mimeType="message/*" />
</intent-filter> </intent-filter>
</activity> </activity>
<service android:name=".ui.service.QueueService" /> <service android:name=".ui.service.QueueService" />
</application> </application>

View file

@ -0,0 +1,12 @@
package org.fnives.tiktokdownloader
object Logger {
private const val TAG = "TTDTag"
fun logMessage(message: String) {
if (BuildConfig.DEBUG) {
System.err.println("TTDTag $message")
}
}
}

View file

@ -39,10 +39,10 @@ class SharedPreferencesManagerImpl private constructor(private val sharedPrefere
override fun getValue(thisRef: SharedPreferencesManagerImpl, property: KProperty<*>): Flow<Set<String>> = override fun getValue(thisRef: SharedPreferencesManagerImpl, property: KProperty<*>): Flow<Set<String>> =
callbackFlow { callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ -> val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
offer(thisRef.getValues()) trySend(thisRef.getValues())
} }
thisRef.sharedPreferences.registerOnSharedPreferenceChangeListener(listener) thisRef.sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
offer(thisRef.getValues()) trySend(thisRef.getValues())
awaitClose { awaitClose {
thisRef.sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) thisRef.sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)

View file

@ -3,6 +3,7 @@ package org.fnives.tiktokdownloader.data.network
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.fnives.tiktokdownloader.Logger
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.fnives.tiktokdownloader.data.model.VideoInSavingIntoFile import org.fnives.tiktokdownloader.data.model.VideoInSavingIntoFile
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
@ -22,8 +23,10 @@ class TikTokDownloadRemoteSource(
wrapIntoProperException { wrapIntoProperException {
delay(delayBeforeRequest) // added just so captcha trigger may not happen delay(delayBeforeRequest) // added just so captcha trigger may not happen
val actualUrl = service.getContentActualUrlAndCookie(videoInPending.url) val actualUrl = service.getContentActualUrlAndCookie(videoInPending.url)
Logger.logMessage("actualUrl found = ${actualUrl.url}")
delay(delayBeforeRequest) // added just so captcha trigger may not happen delay(delayBeforeRequest) // added just so captcha trigger may not happen
val videoUrl = service.getVideoUrl(actualUrl.url) val videoUrl = service.getVideoUrl(actualUrl.url)
Logger.logMessage("videoFileUrl found = ${videoUrl.videoFileUrl}")
delay(delayBeforeRequest) // added just so captcha trigger may not happen delay(delayBeforeRequest) // added just so captcha trigger may not happen
val response = service.getVideo(videoUrl.videoFileUrl) val response = service.getVideo(videoUrl.videoFileUrl)

View file

@ -1,9 +1,9 @@
package org.fnives.tiktokdownloader.data.network.parsing.converter package org.fnives.tiktokdownloader.data.network.parsing.converter
import okhttp3.ResponseBody import okhttp3.ResponseBody
import org.fnives.tiktokdownloader.Logger
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl import org.fnives.tiktokdownloader.data.network.parsing.response.VideoFileUrl
import kotlin.jvm.Throws
class VideoFileUrlConverter( class VideoFileUrlConverter(
private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse private val throwIfIsCaptchaResponse: ThrowIfIsCaptchaResponse
@ -12,8 +12,8 @@ class VideoFileUrlConverter(
@Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class) @Throws(IllegalArgumentException::class, IndexOutOfBoundsException::class, CaptchaRequiredException::class)
override fun convertSafely(responseBody: ResponseBody): VideoFileUrl? { override fun convertSafely(responseBody: ResponseBody): VideoFileUrl? {
val html = responseBody.string().also(throwIfIsCaptchaResponse::invoke) val html = responseBody.string().also(throwIfIsCaptchaResponse::invoke)
val url = tryToParseDownloadLink(html) val url = tryToParseDownloadLink(html).also { Logger.logMessage("parsed download link = $it") }
?: tryToParseVideoSrc(html) ?: tryToParseVideoSrc(html).also { Logger.logMessage("parsed video src = $it") }
?: throw IllegalArgumentException("Couldn't parse url from HTML: $html") ?: throw IllegalArgumentException("Couldn't parse url from HTML: $html")
return VideoFileUrl(url) return VideoFileUrl(url)
@ -26,7 +26,7 @@ class VideoFileUrlConverter(
html.split("\"playAddr\"")[1] html.split("\"playAddr\"")[1]
.dropWhile { it != '\"' }.drop(1) .dropWhile { it != '\"' }.drop(1)
.takeWhile { it != '\"' } .takeWhile { it != '\"' }
.replace("\\u0026", "&") .urlCharacterReplacements()
} else { } else {
null null
} }
@ -39,10 +39,19 @@ class VideoFileUrlConverter(
.dropWhile { it != '=' } .dropWhile { it != '=' }
.dropWhile { it != '\"' }.drop(1) .dropWhile { it != '\"' }.drop(1)
.takeWhile { it != '\"' } .takeWhile { it != '\"' }
.replace("\\u0026", "&") .urlCharacterReplacements()
} else { } else {
null null
} }
private val replacements = mutableMapOf(
"\\u002F" to "/",
"\\u0026" to "&"
)
private fun String.urlCharacterReplacements(): String =
replacements.entries.fold(this) { result, toReplaceEntry ->
result.replace(toReplaceEntry.key, toReplaceEntry.value)
}
} }
} }

View file

@ -5,6 +5,6 @@ import org.fnives.tiktokdownloader.data.network.parsing.response.VideoResponse
class VideoResponseConverter : ParsingExceptionThrowingConverter<VideoResponse>() { class VideoResponseConverter : ParsingExceptionThrowingConverter<VideoResponse>() {
override fun convertSafely(value: ResponseBody): VideoResponse? = override fun convertSafely(responseBody: ResponseBody): VideoResponse? =
VideoResponse(value.contentType(), value.byteStream()) VideoResponse(responseBody.contentType(), responseBody.byteStream())
} }

View file

@ -1,7 +1,7 @@
package org.fnives.tiktokdownloader.data.usecase package org.fnives.tiktokdownloader.data.usecase
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@ -16,6 +16,7 @@ import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.fnives.tiktokdownloader.data.model.VideoInProgress import org.fnives.tiktokdownloader.data.model.VideoInProgress
import org.fnives.tiktokdownloader.data.model.VideoState import org.fnives.tiktokdownloader.data.model.VideoState
@OptIn(FlowPreview::class)
class StateOfVideosObservableUseCase( class StateOfVideosObservableUseCase(
videoInProgressLocalSource: VideoInProgressLocalSource, videoInProgressLocalSource: VideoInProgressLocalSource,
videoInPendingLocalSource: VideoInPendingLocalSource, videoInPendingLocalSource: VideoInPendingLocalSource,

View file

@ -2,6 +2,7 @@ package org.fnives.tiktokdownloader.data.usecase
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -30,6 +31,7 @@ import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredExcept
import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException
import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException
@OptIn(FlowPreview::class)
class VideoDownloadingProcessorUseCase( class VideoDownloadingProcessorUseCase(
private val tikTokDownloadRemoteSource: TikTokDownloadRemoteSource, private val tikTokDownloadRemoteSource: TikTokDownloadRemoteSource,
private val videoInProgressLocalSource: VideoInProgressLocalSource, private val videoInProgressLocalSource: VideoInProgressLocalSource,

View file

@ -15,6 +15,7 @@ class ViewModelFactory(
private val viewModelModule: ViewModelModule, private val viewModelModule: ViewModelModule,
) : AbstractSavedStateViewModelFactory(savedStateRegistryOwner, defaultArgs) { ) : AbstractSavedStateViewModelFactory(savedStateRegistryOwner, defaultArgs) {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T { override fun <T : ViewModel?> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
val viewModel = when (modelClass) { val viewModel = when (modelClass) {
MainViewModel::class.java -> viewModelModule.mainViewModel(handle) MainViewModel::class.java -> viewModelModule.mainViewModel(handle)

View file

@ -6,7 +6,6 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.OvershootInterpolator import android.view.animation.OvershootInterpolator
import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
@ -43,28 +42,28 @@ class MainActivity : AppCompatActivity() {
animateFabClicked(downloadFab) animateFabClicked(downloadFab)
viewModel.onFetchDownloadClicked() viewModel.onFetchDownloadClicked()
} }
viewModel.refreshActionVisibility.observe(this, { viewModel.refreshActionVisibility.observe(this) {
animateFabVisibility(downloadFab, it == true) animateFabVisibility(downloadFab, it == true)
}) }
viewModel.errorMessage.observe(this, { viewModel.errorMessage.observe(this) {
val stringRes = it?.item?.stringRes ?: return@observe val stringRes = it?.item?.stringRes ?: return@observe
Snackbar.make(snackBarAnchor, stringRes, Snackbar.LENGTH_SHORT).show() Snackbar.make(snackBarAnchor, stringRes, Snackbar.LENGTH_SHORT).show()
}) }
} }
private fun setupBottomNavigationView(bottomNavigationView: BottomNavigationView, savedInstanceState: Bundle?) { private fun setupBottomNavigationView(bottomNavigationView: BottomNavigationView, savedInstanceState: Bundle?) {
bottomNavigationView.setOnNavigationItemSelectedListener(BottomNavigationView.OnNavigationItemSelectedListener { item -> bottomNavigationView.setOnItemSelectedListener { item ->
val fragment = when (item.itemId) { val fragment = when (item.itemId) {
R.id.help_menu_item -> HelpFragment.newInstance() R.id.help_menu_item -> HelpFragment.newInstance()
R.id.queue_menu_item -> QueueFragment.newInstance() R.id.queue_menu_item -> QueueFragment.newInstance()
else -> return@OnNavigationItemSelectedListener false else -> return@setOnItemSelectedListener false
} }
item.toScreen()?.let(viewModel::onScreenSelected) item.toScreen()?.let(viewModel::onScreenSelected)
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.fragment_holder, fragment) .replace(R.id.fragment_holder, fragment)
.commit() .commit()
return@OnNavigationItemSelectedListener true return@setOnItemSelectedListener true
}) }
if (savedInstanceState == null) { if (savedInstanceState == null) {
bottomNavigationView.selectedItemId = R.id.queue_menu_item bottomNavigationView.selectedItemId = R.id.queue_menu_item
} }

View file

@ -75,7 +75,7 @@ class QueueService : Service() {
.setSmallIcon(R.drawable.ic_download) .setSmallIcon(R.drawable.ic_download)
.setContentIntent(buildMainPendingIntent(this)) .setContentIntent(buildMainPendingIntent(this))
.setAutoCancel(true) .setAutoCancel(true)
.setNotificationSilent() .setSilent(true)
.build() .build()
NotificationState.Finish -> { NotificationState.Finish -> {
stopSelf() stopSelf()

View file

@ -43,7 +43,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/default_padding" android:layout_marginTop="@dimen/default_padding"
app:autoPlay="false" app:autoPlay="false"
app:showFullScreenButton="false"
app:enableAutomaticInitialization="true" app:enableAutomaticInitialization="true"
app:handleNetworkEvents="true" app:handleNetworkEvents="true"
app:videoId="NXv3JpmwA8Y" /> app:videoId="NXv3JpmwA8Y" />
@ -82,7 +81,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/default_padding" android:layout_marginTop="@dimen/default_padding"
app:autoPlay="false" app:autoPlay="false"
app:showFullScreenButton="false"
app:enableAutomaticInitialization="true" app:enableAutomaticInitialization="true"
app:handleNetworkEvents="true" app:handleNetworkEvents="true"
app:videoId="jxaxffE8c4c" /> app:videoId="jxaxffE8c4c" />

View file

@ -1,20 +1,22 @@
package org.fnives.tiktokdownloader.data.local package org.fnives.tiktokdownloader.data.local
import com.nhaarman.mockitokotlin2.spy
import org.fnives.tiktokdownloader.data.local.persistent.SharedPreferencesManager import org.fnives.tiktokdownloader.data.local.persistent.SharedPreferencesManager
import org.fnives.tiktokdownloader.helper.mock.InMemorySharedPreferencesManager import org.fnives.tiktokdownloader.helper.mock.InMemorySharedPreferencesManager
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.mockito.kotlin.spy
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@Timeout(value = 2)
class CaptchaTimeoutLocalSourceTest { class CaptchaTimeoutLocalSourceTest {
private lateinit var mockSharedPreferencesManager: SharedPreferencesManager private lateinit var mockSharedPreferencesManager: SharedPreferencesManager
private lateinit var sut: CaptchaTimeoutLocalSource private lateinit var sut: CaptchaTimeoutLocalSource
@BeforeEach @BeforeEach
fun setup(){ fun setup() {
mockSharedPreferencesManager = spy(InMemorySharedPreferencesManager()) mockSharedPreferencesManager = spy(InMemorySharedPreferencesManager())
sut = CaptchaTimeoutLocalSource(mockSharedPreferencesManager, 60) sut = CaptchaTimeoutLocalSource(mockSharedPreferencesManager, 60)
} }

View file

@ -1,15 +1,9 @@
package org.fnives.tiktokdownloader.data.local package org.fnives.tiktokdownloader.data.local
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
@ -26,9 +20,16 @@ import org.fnives.tiktokdownloader.helper.mock.InMemorySharedPreferencesManager
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.whenever
import java.io.InputStream import java.io.InputStream
@Suppress("TestFunctionName")
@Timeout(value = 2)
class VideoDownloadedLocalSourceTest { class VideoDownloadedLocalSourceTest {
private lateinit var sut: VideoDownloadedLocalSource private lateinit var sut: VideoDownloadedLocalSource
@ -52,8 +53,8 @@ class VideoDownloadedLocalSourceTest {
@Test @Test
fun GIVEN_observing_saved_videos_WHEN_initialized_THEN_emptylist_is_emitted() = runBlocking<Unit> { fun GIVEN_observing_saved_videos_WHEN_initialized_THEN_emptylist_is_emitted() = runBlocking<Unit> {
Assertions.assertEquals(emptyList<VideoDownloaded>(), sut.savedVideos.first()) Assertions.assertEquals(emptyList<VideoDownloaded>(), sut.savedVideos.first())
verifyZeroInteractions(mockSaveVideoFile) verifyNoInteractions(mockSaveVideoFile)
verifyZeroInteractions(mockVerifyFileForUriExists) verifyNoInteractions(mockVerifyFileForUriExists)
} }
@Test @Test

View file

@ -15,8 +15,10 @@ import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
@Suppress("TestFunctionName")
@Timeout(value = 2)
class VideoInPendingLocalSourceTest { class VideoInPendingLocalSourceTest {
private lateinit var sut: VideoInPendingLocalSource private lateinit var sut: VideoInPendingLocalSource

View file

@ -13,8 +13,10 @@ import org.fnives.tiktokdownloader.data.model.VideoInProgress
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
@Suppress("TestFunctionName")
@Timeout(value = 2)
class VideoInProgressLocalSourceTest { class VideoInProgressLocalSourceTest {
private lateinit var sut: VideoInProgressLocalSource private lateinit var sut: VideoInProgressLocalSource

View file

@ -14,12 +14,14 @@ import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream import java.util.stream.Stream
@Suppress("TestFunctionName")
@Timeout(value = 2)
class TikTokDownloadRemoteSourceTest { class TikTokDownloadRemoteSourceTest {
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
@ -126,14 +128,19 @@ class TikTokDownloadRemoteSourceTest {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setHeader("Content-Type", "video/mp4").setBody("banan")) mockWebServer.enqueue(MockResponse().setResponseCode(200).setHeader("Content-Type", "video/mp4").setBody("banan"))
val videoInPending = VideoInPending("alma", TEST_URL) val videoInPending = VideoInPending("alma", TEST_URL)
val response = sut.getVideo(videoInPending) val response = sut.getVideo(videoInPending)
Assertions.assertEquals(expectedId, response.id) Assertions.assertEquals(expectedId, response.id)
Assertions.assertEquals(expectedUrl, response.url) Assertions.assertEquals(expectedUrl, response.url)
Assertions.assertEquals(expectedContentType, response.contentType) Assertions.assertEquals(expectedContentType, response.contentType)
Assertions.assertEquals("banan", response.byteStream.reader().readText()) Assertions.assertEquals("banan", response.byteStream.reader().readText())
mockWebServer.takeRequest()
mockWebServer.takeRequest()
val videoRequest = mockWebServer.takeRequest()
Assertions.assertEquals("http://localhost:8080/?a=a&b=b&c=c", videoRequest.requestUrl?.toUri()?.toString())
} }
@Test @Test
fun GIVEN_proper_responses_as_variant2_THEN_parsed_properly() = runBlocking<Unit> { fun GIVEN_proper_responses_as_variant2_THEN_parsed_properly() = runBlocking<Unit> {
val expectedId = "e-alma" val expectedId = "e-alma"
@ -152,6 +159,10 @@ class TikTokDownloadRemoteSourceTest {
Assertions.assertEquals(expectedUrl, response.url) Assertions.assertEquals(expectedUrl, response.url)
Assertions.assertEquals(expectedContentType, response.contentType) Assertions.assertEquals(expectedContentType, response.contentType)
Assertions.assertEquals("a-banan", response.byteStream.reader().readText()) Assertions.assertEquals("a-banan", response.byteStream.reader().readText())
mockWebServer.takeRequest()
mockWebServer.takeRequest()
val videoRequest = mockWebServer.takeRequest()
Assertions.assertEquals("http://localhost:8080/?a=a&b=b&c=c", videoRequest.requestUrl?.toUri()?.toString())
} }
@Test @Test
@ -257,31 +268,34 @@ class TikTokDownloadRemoteSourceTest {
private const val MAIN_PAGE_VARIANT_2_RESPONSE = "response/main_page_v1.html" private const val MAIN_PAGE_VARIANT_2_RESPONSE = "response/main_page_v1.html"
private const val PORT = 8080 private const val PORT = 8080
private const val TEST_URL = "http://127.0.0.1:$PORT" private const val TEST_URL = "http://127.0.0.1:$PORT"
private const val SHORTENED_TEST_URL = "http://127.0.0.1:$PORT"
private const val VIDEO_FILE_TEST_URL = "http:\\u002F/127.0.0.1:$PORT?a=a\\u0026b=b&c=c"
private const val CAPTCHA_TEST_URL = "http://127.0.0.1:$PORT"
private fun Any.readResourceFileShortenedUrlResponse() = private fun Any.readResourceFileShortenedUrlResponse() =
readResourceFile(SHORTENED_URL_RESPONSE) readResourceFile(SHORTENED_URL_RESPONSE)
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", TEST_URL) .replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", SHORTENED_TEST_URL)
private fun Any.readResourceFileMainPageVariant1Response() = private fun Any.readResourceFileMainPageVariant1Response() =
readResourceFile(MAIN_PAGE_VARIANT_1_RESPONSE) readResourceFile(MAIN_PAGE_VARIANT_1_RESPONSE)
.replace( .replace(
"https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=", "https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=",
TEST_URL VIDEO_FILE_TEST_URL
) )
private fun Any.readResourceFileMainPageVariant2Response() = private fun Any.readResourceFileMainPageVariant2Response() =
readResourceFile(MAIN_PAGE_VARIANT_2_RESPONSE) readResourceFile(MAIN_PAGE_VARIANT_2_RESPONSE)
.replace( .replace(
"https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=", "https://v16-web.tiktok.com/video/tos/alisg/tos-alisg-pve-0037c001/9ddfc12f43b04f6596f9953c9a9ca072/?a=1988\\u0026br=1534\\u0026bt=767\\u0026cr=0\\u0026cs=0\\u0026cv=1\\u0026dr=0\\u0026ds=3\\u0026er=\\u0026expire=1603682739\\u0026l=20201025212533010189074225590A080D\\u0026lr=tiktok_m\\u0026mime_type=video_mp4\\u0026policy=2\\u0026qs=0\\u0026rc=amlxbmV1O291eDMzMzczM0ApZDRoZDQ3Nzw1N2U5Nzs3O2dicW1vL2AxZV5fLS1iMTRzczA2Y2NgYTQ2LmE1Y2E0My46Yw%3D%3D\\u0026signature=cce079fd02e4dde94c1c93cfdbd1d100\\u0026tk=tt_webid_v2\\u0026vl=\\u0026vr=",
TEST_URL VIDEO_FILE_TEST_URL
) )
private fun Any.readCaptchaOneResponse() = private fun Any.readCaptchaOneResponse() =
readResourceFile(CAPTCHA_REQUIRED_RESPONSE_ONE) readResourceFile(CAPTCHA_REQUIRED_RESPONSE_ONE)
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", TEST_URL) .replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", CAPTCHA_TEST_URL)
private fun Any.readCaptchaTwoResponse() = private fun Any.readCaptchaTwoResponse() =
readResourceFile(CAPTCHA_REQUIRED_RESPONSE_TWO) readResourceFile(CAPTCHA_REQUIRED_RESPONSE_TWO)
.replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", TEST_URL) .replace("https://www.tiktok.com/@ieclauuu/video/6887614455967010049", CAPTCHA_TEST_URL)
@JvmStatic @JvmStatic
private fun captchaResponses() = Stream.of( private fun captchaResponses() = Stream.of(

View file

@ -9,6 +9,7 @@ import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import java.io.File import java.io.File
/** /**
@ -16,7 +17,8 @@ import java.io.File
* Since the website may change anytime without any notice, this test verifies with actual request going out. * Since the website may change anytime without any notice, this test verifies with actual request going out.
* However this makes the test shaky, because if the device has no proper connection it may fail. * However this makes the test shaky, because if the device has no proper connection it may fail.
*/ */
@Suppress("TestFunctionName")
@Timeout(value = 2)
class TikTokDownloadRemoteSourceUpToDateTest { class TikTokDownloadRemoteSourceUpToDateTest {
private lateinit var sut: TikTokDownloadRemoteSource private lateinit var sut: TikTokDownloadRemoteSource
@ -27,6 +29,7 @@ class TikTokDownloadRemoteSourceUpToDateTest {
} }
@Disabled("Can trigger captcha, so only run it separately") @Disabled("Can trigger captcha, so only run it separately")
@Timeout(value = 120)
@Test @Test
fun GIVEN_actualVideo_WHEN_downloading_THEN_the_file_matching_with_the_previously_loaded_video() { fun GIVEN_actualVideo_WHEN_downloading_THEN_the_file_matching_with_the_previously_loaded_video() {
val parameter = VideoInPending("123", SUBJECT_VIDEO_URL) val parameter = VideoInPending("123", SUBJECT_VIDEO_URL)

View file

@ -1,20 +1,22 @@
package org.fnives.tiktokdownloader.data.usecase package org.fnives.tiktokdownloader.data.usecase
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@Timeout(value = 2)
class AddVideoToQueueUseCaseTest { class AddVideoToQueueUseCaseTest {
private lateinit var sut: AddVideoToQueueUseCase private lateinit var sut: AddVideoToQueueUseCase
@ -30,8 +32,8 @@ class AddVideoToQueueUseCaseTest {
@Test @Test
fun GIVEN_no_action_THEN_the_local_source_and_verifier_is_not_touched() { fun GIVEN_no_action_THEN_the_local_source_and_verifier_is_not_touched() {
verifyZeroInteractions(mockUrlVerificationUseCase) verifyNoInteractions(mockUrlVerificationUseCase)
verifyZeroInteractions(mockVideoInPendingLocalSource) verifyNoInteractions(mockVideoInPendingLocalSource)
} }
@Test @Test
@ -83,6 +85,6 @@ class AddVideoToQueueUseCaseTest {
Assertions.assertFalse(actual, "Url is Saved while it should NOT be") Assertions.assertFalse(actual, "Url is Saved while it should NOT be")
verify(mockUrlVerificationUseCase, times(1)).invoke(expectedUrl) verify(mockUrlVerificationUseCase, times(1)).invoke(expectedUrl)
verifyNoMoreInteractions(mockUrlVerificationUseCase) verifyNoMoreInteractions(mockUrlVerificationUseCase)
verifyZeroInteractions(mockVideoInPendingLocalSource) verifyNoInteractions(mockVideoInPendingLocalSource)
} }
} }

View file

@ -1,19 +1,15 @@
package org.fnives.tiktokdownloader.data.usecase package org.fnives.tiktokdownloader.data.usecase
import com.nhaarman.mockitokotlin2.doReturn import kotlinx.coroutines.ExperimentalCoroutinesApi
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestDispatcher
import org.fnives.tiktokdownloader.data.local.VideoDownloadedLocalSource import org.fnives.tiktokdownloader.data.local.VideoDownloadedLocalSource
import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource
import org.fnives.tiktokdownloader.data.local.VideoInProgressLocalSource import org.fnives.tiktokdownloader.data.local.VideoInProgressLocalSource
@ -21,14 +17,26 @@ import org.fnives.tiktokdownloader.data.model.VideoDownloaded
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.fnives.tiktokdownloader.data.model.VideoInProgress import org.fnives.tiktokdownloader.data.model.VideoInProgress
import org.fnives.tiktokdownloader.data.model.VideoState import org.fnives.tiktokdownloader.data.model.VideoState
import org.fnives.tiktokdownloader.helper.advanceTimeBy
import org.fnives.tiktokdownloader.helper.advanceUntilIdle
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
@Timeout(value = 2)
class StateOfVideosObservableUseCaseTest { class StateOfVideosObservableUseCaseTest {
private lateinit var testDispatcher: TestCoroutineDispatcher private lateinit var testDispatcher: TestDispatcher
private lateinit var mockVideoInProgressLocalSource: VideoInProgressLocalSource private lateinit var mockVideoInProgressLocalSource: VideoInProgressLocalSource
private lateinit var mockVideoInPendingLocalSource: VideoInPendingLocalSource private lateinit var mockVideoInPendingLocalSource: VideoInPendingLocalSource
private lateinit var mockVideoDownloadedLocalSource: VideoDownloadedLocalSource private lateinit var mockVideoDownloadedLocalSource: VideoDownloadedLocalSource
@ -48,7 +56,7 @@ class StateOfVideosObservableUseCaseTest {
whenever(mockVideoInProgressLocalSource.videoInProcessFlow).doReturn(videoInProgressMutableFlow) whenever(mockVideoInProgressLocalSource.videoInProcessFlow).doReturn(videoInProgressMutableFlow)
whenever(mockVideoInPendingLocalSource.pendingVideos).doReturn(videoInPendingMutableFlow) whenever(mockVideoInPendingLocalSource.pendingVideos).doReturn(videoInPendingMutableFlow)
whenever(mockVideoDownloadedLocalSource.savedVideos).doReturn(videoDownloadedMutableFlow) whenever(mockVideoDownloadedLocalSource.savedVideos).doReturn(videoDownloadedMutableFlow)
testDispatcher = TestCoroutineDispatcher() testDispatcher = StandardTestDispatcher()
sut = StateOfVideosObservableUseCase( sut = StateOfVideosObservableUseCase(
videoInProgressLocalSource = mockVideoInProgressLocalSource, videoInProgressLocalSource = mockVideoInProgressLocalSource,
videoInPendingLocalSource = mockVideoInPendingLocalSource, videoInPendingLocalSource = mockVideoInPendingLocalSource,
@ -59,13 +67,13 @@ class StateOfVideosObservableUseCaseTest {
@Test @Test
fun WHEN_no_invoke_is_called_THEN_no_dependency_is_called() { fun WHEN_no_invoke_is_called_THEN_no_dependency_is_called() {
verifyZeroInteractions(mockVideoDownloadedLocalSource) verifyNoInteractions(mockVideoDownloadedLocalSource)
verifyZeroInteractions(mockVideoInPendingLocalSource) verifyNoInteractions(mockVideoInPendingLocalSource)
verifyZeroInteractions(mockVideoInProgressLocalSource) verifyNoInteractions(mockVideoInProgressLocalSource)
} }
@Test @Test
fun GIVEN_no_inProgress_AND_empty_pending_AND_empty_saved_THEN_emptyList_is_emitted() = runBlocking(testDispatcher) { fun GIVEN_no_inProgress_AND_empty_pending_AND_empty_saved_THEN_emptyList_is_emitted() = runBlocking {
videoInProgressMutableFlow.value = null videoInProgressMutableFlow.value = null
videoInPendingMutableFlow.value = emptyList() videoInPendingMutableFlow.value = emptyList()
videoDownloadedMutableFlow.value = emptyList() videoDownloadedMutableFlow.value = emptyList()
@ -83,7 +91,7 @@ class StateOfVideosObservableUseCaseTest {
} }
@Test @Test
fun GIVEN_inProgress_AND_empty_pending_AND_empty_saved_THEN_inProgress_is_emitted() = runBlocking(testDispatcher) { fun GIVEN_inProgress_AND_empty_pending_AND_empty_saved_THEN_inProgress_is_emitted() = runBlocking {
val videoInProgress = VideoInProgress("alma", "url") val videoInProgress = VideoInProgress("alma", "url")
val expected = listOf<VideoState>(VideoState.InProcess(videoInProgress)) val expected = listOf<VideoState>(VideoState.InProcess(videoInProgress))
val expectedList = listOf(emptyList(), expected) val expectedList = listOf(emptyList(), expected)
@ -109,7 +117,7 @@ class StateOfVideosObservableUseCaseTest {
} }
@Test @Test
fun GIVEN_inProgress_AND_pendingWithSameId_AND_empty_saved_THEN_inProgress_is_emitted() = runBlocking(testDispatcher) { fun GIVEN_inProgress_AND_pendingWithSameId_AND_empty_saved_THEN_inProgress_is_emitted() = runBlocking {
val videoInProgress = VideoInProgress("alma", "url") val videoInProgress = VideoInProgress("alma", "url")
val videoInPending = VideoInPending(id = videoInProgress.id, url = videoInProgress.url) val videoInPending = VideoInPending(id = videoInProgress.id, url = videoInProgress.url)
val expected = listOf<VideoState>(VideoState.InProcess(videoInProgress)) val expected = listOf<VideoState>(VideoState.InProcess(videoInProgress))
@ -130,7 +138,7 @@ class StateOfVideosObservableUseCaseTest {
} }
@Test @Test
fun GIVEN_in_pending_AND_nothing_inprogress_AND_empty_saved_THEN_inPending_is_emitted() = runBlocking(testDispatcher) { fun GIVEN_in_pending_AND_nothing_inprogress_AND_empty_saved_THEN_inPending_is_emitted() = runBlocking {
val videoInPending = VideoInPending(id = "alma", url = "url") val videoInPending = VideoInPending(id = "alma", url = "url")
val expected = listOf<VideoState>(VideoState.InPending(videoInPending)) val expected = listOf<VideoState>(VideoState.InPending(videoInPending))
val expectedList = listOf(emptyList(), expected) val expectedList = listOf(emptyList(), expected)
@ -151,7 +159,7 @@ class StateOfVideosObservableUseCaseTest {
@Test @Test
fun GIVEN_inProgress_AND_pendingWithSameId_AND_savedWithSameId_THEN_inProgress_And_saved_is_emitted() = fun GIVEN_inProgress_AND_pendingWithSameId_AND_savedWithSameId_THEN_inProgress_And_saved_is_emitted() =
runBlocking(testDispatcher) { runBlocking {
val videoInProgress = VideoInProgress("alma", "url") val videoInProgress = VideoInProgress("alma", "url")
val videoInPending = VideoInPending(id = videoInProgress.id, url = videoInProgress.url) val videoInPending = VideoInPending(id = videoInProgress.id, url = videoInProgress.url)
val videoDownloaded = VideoDownloaded(id = videoInProgress.id, url = videoInProgress.url, uri = "uri") val videoDownloaded = VideoDownloaded(id = videoInProgress.id, url = videoInProgress.url, uri = "uri")
@ -173,7 +181,7 @@ class StateOfVideosObservableUseCaseTest {
} }
@Test @Test
fun GIVEN_new_item_faster_than_debounce_THEN_only_the_last_items_are_emitted() = runBlocking(testDispatcher) { fun GIVEN_new_item_faster_than_debounce_THEN_only_the_last_items_are_emitted() = runBlocking {
val videoInProgress = VideoInProgress("alma", "url") val videoInProgress = VideoInProgress("alma", "url")
val expected = listOf(VideoState.InProcess(videoInProgress)) val expected = listOf(VideoState.InProcess(videoInProgress))
val expectedList = listOf(expected) val expectedList = listOf(expected)
@ -194,7 +202,7 @@ class StateOfVideosObservableUseCaseTest {
} }
@Test @Test
fun GIVEN_new_item_slower_than_debounce_THEN_both_list_is_emitted() = runBlocking(testDispatcher) { fun GIVEN_new_item_slower_than_debounce_THEN_both_list_is_emitted() = runBlocking {
val videoInProgress = VideoInProgress("alma", "url") val videoInProgress = VideoInProgress("alma", "url")
val expected = listOf(VideoState.InProcess(videoInProgress)) val expected = listOf(VideoState.InProcess(videoInProgress))
val expectedList = listOf(emptyList(), expected) val expectedList = listOf(emptyList(), expected)
@ -215,7 +223,7 @@ class StateOfVideosObservableUseCaseTest {
} }
@Test @Test
fun GIVEN_processing_LATER_pendingWithSameId_THEN_no_new_emition_happens() = runBlocking(testDispatcher) { fun GIVEN_processing_LATER_pendingWithSameId_THEN_no_new_emition_happens() = runBlocking {
val videoInProgress = VideoInProgress("alma", "url") val videoInProgress = VideoInProgress("alma", "url")
val videoInPendingSameAsProgress = VideoInPending(id = videoInProgress.id, url = videoInProgress.url) val videoInPendingSameAsProgress = VideoInPending(id = videoInProgress.id, url = videoInProgress.url)
val videoInPendingOther = VideoInPending(id = "alma2", url = "url2") val videoInPendingOther = VideoInPending(id = "alma2", url = "url2")

View file

@ -3,8 +3,10 @@ package org.fnives.tiktokdownloader.data.usecase
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
@Suppress("TestFunctionName")
@Timeout(value = 2)
class UrlVerificationUseCaseTest { class UrlVerificationUseCaseTest {
private lateinit var sut: UrlVerificationUseCase private lateinit var sut: UrlVerificationUseCase

View file

@ -1,21 +1,15 @@
package org.fnives.tiktokdownloader.data.usecase package org.fnives.tiktokdownloader.data.usecase
import com.nhaarman.mockitokotlin2.anyOrNull import kotlinx.coroutines.ExperimentalCoroutinesApi
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.take import kotlinx.coroutines.flow.take
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.runTest
import org.fnives.tiktokdownloader.data.local.CaptchaTimeoutLocalSource import org.fnives.tiktokdownloader.data.local.CaptchaTimeoutLocalSource
import org.fnives.tiktokdownloader.data.local.VideoDownloadedLocalSource import org.fnives.tiktokdownloader.data.local.VideoDownloadedLocalSource
import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource import org.fnives.tiktokdownloader.data.local.VideoInPendingLocalSource
@ -30,15 +24,27 @@ import org.fnives.tiktokdownloader.data.network.TikTokDownloadRemoteSource
import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException import org.fnives.tiktokdownloader.data.network.exceptions.CaptchaRequiredException
import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException import org.fnives.tiktokdownloader.data.network.exceptions.NetworkException
import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException import org.fnives.tiktokdownloader.data.network.exceptions.ParsingException
import org.fnives.tiktokdownloader.helper.advanceTimeBy
import org.fnives.tiktokdownloader.helper.advanceUntilIdle
import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import java.io.InputStream import java.io.InputStream
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@OptIn(ExperimentalCoroutinesApi::class)
//@Timeout(value = 2)
class VideoDownloadingProcessorUseCaseTest { class VideoDownloadingProcessorUseCaseTest {
private lateinit var testDispatcher: TestCoroutineDispatcher private lateinit var testDispatcher: TestDispatcher
private lateinit var mockVideoInProgressLocalSource: VideoInProgressLocalSource private lateinit var mockVideoInProgressLocalSource: VideoInProgressLocalSource
private lateinit var mockVideoInPendingLocalSource: VideoInPendingLocalSource private lateinit var mockVideoInPendingLocalSource: VideoInPendingLocalSource
private lateinit var mockVideoDownloadedLocalSource: VideoDownloadedLocalSource private lateinit var mockVideoDownloadedLocalSource: VideoDownloadedLocalSource
@ -62,7 +68,7 @@ class VideoDownloadingProcessorUseCaseTest {
whenever(mockVideoInProgressLocalSource.videoInProcessFlow).doReturn(videoInProgressMutableFlow) whenever(mockVideoInProgressLocalSource.videoInProcessFlow).doReturn(videoInProgressMutableFlow)
whenever(mockVideoInPendingLocalSource.pendingVideos).doReturn(videoInPendingMutableFlow) whenever(mockVideoInPendingLocalSource.pendingVideos).doReturn(videoInPendingMutableFlow)
whenever(mockVideoDownloadedLocalSource.savedVideos).doReturn(videoDownloadedMutableFlow) whenever(mockVideoDownloadedLocalSource.savedVideos).doReturn(videoDownloadedMutableFlow)
testDispatcher = TestCoroutineDispatcher() testDispatcher = StandardTestDispatcher()
sut = VideoDownloadingProcessorUseCase( sut = VideoDownloadingProcessorUseCase(
videoInProgressLocalSource = mockVideoInProgressLocalSource, videoInProgressLocalSource = mockVideoInProgressLocalSource,
videoInPendingLocalSource = mockVideoInPendingLocalSource, videoInPendingLocalSource = mockVideoInPendingLocalSource,
@ -75,24 +81,24 @@ class VideoDownloadingProcessorUseCaseTest {
@Test @Test
fun WHEN_no_method_invoked_THEN_no_interaction_with_dependencies() { fun WHEN_no_method_invoked_THEN_no_interaction_with_dependencies() {
verifyZeroInteractions(mockVideoInProgressLocalSource) verifyNoInteractions(mockVideoInProgressLocalSource)
verifyZeroInteractions(mockVideoInPendingLocalSource) verifyNoInteractions(mockVideoInPendingLocalSource)
verifyZeroInteractions(mockVideoDownloadedLocalSource) verifyNoInteractions(mockVideoDownloadedLocalSource)
verifyZeroInteractions(mockTikTokDownloadRemoteSource) verifyNoInteractions(mockTikTokDownloadRemoteSource)
} }
@Test @Test
fun GIVEN_not_observing_WHEN_fetching_THEN_nothing_happens() { fun GIVEN_not_observing_WHEN_fetching_THEN_nothing_happens() {
sut.fetchVideoInState() sut.fetchVideoInState()
verifyZeroInteractions(mockVideoInProgressLocalSource) verifyNoInteractions(mockVideoInProgressLocalSource)
verifyZeroInteractions(mockVideoInPendingLocalSource) verifyNoInteractions(mockVideoInPendingLocalSource)
verifyZeroInteractions(mockVideoDownloadedLocalSource) verifyNoInteractions(mockVideoDownloadedLocalSource)
verifyZeroInteractions(mockTikTokDownloadRemoteSource) verifyNoInteractions(mockTikTokDownloadRemoteSource)
} }
@Test @Test
fun GIVEN_empty_pendingVideos_WHEN_observing_THEN_error_is_emited() = runBlocking(testDispatcher) { fun GIVEN_empty_pendingVideos_WHEN_observing_THEN_error_is_emited() = runBlocking {
videoInPendingMutableFlow.value = emptyList() videoInPendingMutableFlow.value = emptyList()
val expected = ProcessState.Finished val expected = ProcessState.Finished
val expectedList = listOf(expected) val expectedList = listOf(expected)
@ -103,13 +109,12 @@ class VideoDownloadingProcessorUseCaseTest {
Assertions.assertEquals(expectedList, resultList.await()) Assertions.assertEquals(expectedList, resultList.await())
verify(mockVideoInPendingLocalSource, times(1)).pendingVideos verify(mockVideoInPendingLocalSource, times(1)).pendingVideos
verifyNoMoreInteractions(mockVideoInPendingLocalSource) verifyNoMoreInteractions(mockVideoInPendingLocalSource)
verifyZeroInteractions(mockVideoInPendingLocalSource) verifyNoInteractions(mockVideoDownloadedLocalSource)
verifyZeroInteractions(mockVideoDownloadedLocalSource) verifyNoInteractions(mockTikTokDownloadRemoteSource)
verifyZeroInteractions(mockTikTokDownloadRemoteSource)
} }
@Test @Test
fun GIVEN_one_pending_video_AND_network_error_WHEN_observing_THEN_error_is_emited() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_AND_network_error_WHEN_observing_THEN_error_is_emited() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() } whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() }
@ -126,11 +131,10 @@ class VideoDownloadingProcessorUseCaseTest {
verify(mockVideoDownloadedLocalSource, times(1)).savedVideos verify(mockVideoDownloadedLocalSource, times(1)).savedVideos
verifyNoMoreInteractions(mockTikTokDownloadRemoteSource) verifyNoMoreInteractions(mockTikTokDownloadRemoteSource)
verifyNoMoreInteractions(mockVideoDownloadedLocalSource) verifyNoMoreInteractions(mockVideoDownloadedLocalSource)
verifyZeroInteractions(mockVideoInPendingLocalSource)
} }
@Test @Test
fun GIVEN_one_pending_video_AND_parsing_error_WHEN_observing_THEN_parsingError_is_emited() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_AND_parsing_error_WHEN_observing_THEN_parsingError_is_emited() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw ParsingException() } whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw ParsingException() }
@ -144,7 +148,7 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_unexpected_error_WHEN_observing_THEN_unknown_error_is_emitted() = runBlockingTest(testDispatcher) { fun GIVEN_one_pending_video_AND_unexpected_error_WHEN_observing_THEN_unknown_error_is_emitted() = runTest(testDispatcher) {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw Throwable() } whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw Throwable() }
@ -158,7 +162,7 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_network_errors_WHILE_observing_WHEN_fetching_THEN_it_retries() = runBlocking<Unit>(testDispatcher) { fun GIVEN_one_pending_video_AND_network_errors_WHILE_observing_WHEN_fetching_THEN_it_retries() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
var specificException = true var specificException = true
@ -177,7 +181,7 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_parsing_errors_WHILE_observing_WHEN_fetching_THEN_it_retries() = runBlocking<Unit>(testDispatcher) { fun GIVEN_one_pending_video_AND_parsing_errors_WHILE_observing_WHEN_fetching_THEN_it_retries() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
var specificException = true var specificException = true
@ -196,7 +200,7 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_unknown_errors_WHILE_observing_WHEN_fetching_THEN_it_retries() = runBlocking<Unit>(testDispatcher) { fun GIVEN_one_pending_video_AND_unknown_errors_WHILE_observing_WHEN_fetching_THEN_it_retries() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
var specificException = true var specificException = true
@ -216,7 +220,7 @@ class VideoDownloadingProcessorUseCaseTest {
// verify that fetching even while request is running doesn't matter, only after error is emitted // verify that fetching even while request is running doesn't matter, only after error is emitted
@Test @Test
fun GIVEN_one_pending_video_AND_delaying_until_fetch_WHILE_observing_WHEN_fetching_THEN_emition_happens_only_once() = runBlocking<Unit>(testDispatcher) { fun GIVEN_one_pending_video_AND_delaying_until_fetch_WHILE_observing_WHEN_fetching_THEN_emition_happens_only_once() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
var specificException = true var specificException = true
@ -242,7 +246,7 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_failing_request_WHEN_observing_THEN_video_is_marked_processing_then_unprocessing() = runBlocking<Unit>(testDispatcher) { fun GIVEN_one_pending_video_AND_failing_request_WHEN_observing_THEN_video_is_marked_processing_then_unprocessing() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then {
@ -261,10 +265,10 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_successful_request_AND_storage_error_WHEN_observing_THEN_video_is_saved_called_and_error_is_propogated() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_AND_successful_request_AND_storage_error_WHEN_observing_THEN_video_is_saved_called_and_error_is_propogated() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
val videoInSavingIntoFile = VideoInSavingIntoFile("x","u",VideoInSavingIntoFile.ContentType("a","b"), FalseInputStream()) val videoInSavingIntoFile = VideoInSavingIntoFile("x", "u", VideoInSavingIntoFile.ContentType("a", "b"), FalseInputStream())
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile) whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile)
whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).then { whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).then {
throw StorageException() throw StorageException()
@ -282,10 +286,10 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_successful_request_AND_unexpected_error_WHEN_observing_THEN_video_is_saved_called_and_error_is_propogated() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_AND_successful_request_AND_unexpected_error_WHEN_observing_THEN_video_is_saved_called_and_error_is_propogated() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
val videoInSavingIntoFile = VideoInSavingIntoFile("x","u",VideoInSavingIntoFile.ContentType("a","b"), FalseInputStream()) val videoInSavingIntoFile = VideoInSavingIntoFile("x", "u", VideoInSavingIntoFile.ContentType("a", "b"), FalseInputStream())
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile) whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile)
whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).then { whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).then {
throw Throwable() throw Throwable()
@ -303,11 +307,11 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_successful_request_AND_successful_file_save_WHEN_observing_THEN_pending_is_removed() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_AND_successful_request_AND_successful_file_save_WHEN_observing_THEN_pending_is_removed() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
val videoDownloaded = VideoDownloaded("zz","yy","xx") val videoDownloaded = VideoDownloaded("zz", "yy", "xx")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
val videoInSavingIntoFile = VideoInSavingIntoFile("x","u",VideoInSavingIntoFile.ContentType("a","b"), FalseInputStream()) val videoInSavingIntoFile = VideoInSavingIntoFile("x", "u", VideoInSavingIntoFile.ContentType("a", "b"), FalseInputStream())
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile) whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile)
whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).doReturn(videoDownloaded) whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).doReturn(videoDownloaded)
val expectedList = listOf( val expectedList = listOf(
@ -325,7 +329,7 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_captcha_timeout_WHEN_observing_THEN_captcha_timeout_is_emited() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_AND_captcha_timeout_WHEN_observing_THEN_captcha_timeout_is_emited() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockCaptchaTimeoutLocalSource.isInCaptchaTimeout()).doReturn(true) whenever(mockCaptchaTimeoutLocalSource.isInCaptchaTimeout()).doReturn(true)
@ -344,33 +348,34 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_successful_request_AND_successful_file_save_WHEN_observing_with_2_THEN_pending_is_removed_AND_only_once_executed() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_AND_successful_request_AND_successful_file_save_WHEN_observing_with_2_THEN_pending_is_removed_AND_only_once_executed() =
val videoInPending = VideoInPending("alma", "banan") runBlocking {
val videoDownloaded = VideoDownloaded("zz","yy","xx") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) val videoDownloaded = VideoDownloaded("zz", "yy", "xx")
val videoInSavingIntoFile = VideoInSavingIntoFile("x","u",VideoInSavingIntoFile.ContentType("a","b"), FalseInputStream()) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile) val videoInSavingIntoFile = VideoInSavingIntoFile("x", "u", VideoInSavingIntoFile.ContentType("a", "b"), FalseInputStream())
whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).doReturn(videoDownloaded) whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).doReturn(videoInSavingIntoFile)
val expectedList = listOf( whenever(mockVideoDownloadedLocalSource.saveVideo(anyOrNull())).doReturn(videoDownloaded)
ProcessState.Processing(videoInPending), val expectedList = listOf(
ProcessState.Processed(videoDownloaded) ProcessState.Processing(videoInPending),
) ProcessState.Processed(videoDownloaded)
)
val resultList1 = async(testDispatcher) { sut.processState.take(2).toList() } val resultList1 = async(testDispatcher) { sut.processState.take(2).toList() }
val resultList2 = async(testDispatcher) { sut.processState.take(2).toList() } val resultList2 = async(testDispatcher) { sut.processState.take(2).toList() }
testDispatcher.advanceUntilIdle() testDispatcher.advanceUntilIdle()
Assertions.assertEquals(expectedList, resultList1.await()) Assertions.assertEquals(expectedList, resultList1.await())
Assertions.assertEquals(expectedList, resultList2.await()) Assertions.assertEquals(expectedList, resultList2.await())
verify(mockVideoInPendingLocalSource, times(1)).removeVideoFromQueue(videoInPending) verify(mockVideoInPendingLocalSource, times(1)).removeVideoFromQueue(videoInPending)
verify(mockVideoInPendingLocalSource, times(1)).pendingVideos verify(mockVideoInPendingLocalSource, times(1)).pendingVideos
verifyNoMoreInteractions(mockVideoInPendingLocalSource) verifyNoMoreInteractions(mockVideoInPendingLocalSource)
} }
@Test @Test
fun GIVEN_one_pending_video_BUT_already_downloaded_WHEN_observing_THEN_processed_is_emitted_but_no_request_call() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_BUT_already_downloaded_WHEN_observing_THEN_processed_is_emitted_but_no_request_call() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
val videoDownloaded = VideoDownloaded("alma","banan","xx") val videoDownloaded = VideoDownloaded("alma", "banan", "xx")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
videoDownloadedMutableFlow.value = listOf(videoDownloaded) videoDownloadedMutableFlow.value = listOf(videoDownloaded)
val expectedList = listOf( val expectedList = listOf(
@ -389,13 +394,13 @@ class VideoDownloadingProcessorUseCaseTest {
verifyNoMoreInteractions(mockVideoInPendingLocalSource) verifyNoMoreInteractions(mockVideoInPendingLocalSource)
verifyNoMoreInteractions(mockVideoDownloadedLocalSource) verifyNoMoreInteractions(mockVideoDownloadedLocalSource)
verifyNoMoreInteractions(mockVideoInProgressLocalSource) verifyNoMoreInteractions(mockVideoInProgressLocalSource)
verifyZeroInteractions(mockTikTokDownloadRemoteSource) verifyNoInteractions(mockTikTokDownloadRemoteSource)
} }
@Test @Test
fun GIVEN_one_pending_video_BUT_already_downloaded_AND_captcha_timeout_WHEN_observing_THEN_processed_is_emitted_but_no_request_call() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_BUT_already_downloaded_AND_captcha_timeout_WHEN_observing_THEN_processed_is_emitted_but_no_request_call() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
val videoDownloaded = VideoDownloaded("alma","banan","xx") val videoDownloaded = VideoDownloaded("alma", "banan", "xx")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
videoDownloadedMutableFlow.value = listOf(videoDownloaded) videoDownloadedMutableFlow.value = listOf(videoDownloaded)
whenever(mockCaptchaTimeoutLocalSource.isInCaptchaTimeout()).doReturn(true) whenever(mockCaptchaTimeoutLocalSource.isInCaptchaTimeout()).doReturn(true)
@ -415,11 +420,11 @@ class VideoDownloadingProcessorUseCaseTest {
verifyNoMoreInteractions(mockVideoInPendingLocalSource) verifyNoMoreInteractions(mockVideoInPendingLocalSource)
verifyNoMoreInteractions(mockVideoDownloadedLocalSource) verifyNoMoreInteractions(mockVideoDownloadedLocalSource)
verifyNoMoreInteractions(mockVideoInProgressLocalSource) verifyNoMoreInteractions(mockVideoInProgressLocalSource)
verifyZeroInteractions(mockTikTokDownloadRemoteSource) verifyNoInteractions(mockTikTokDownloadRemoteSource)
} }
@Test @Test
fun GIVEN_one_pending_video_BUT_CaptchaTimeoutException_WHEN_observing_THEN_its_saved_and_captchaError_emitted() = runBlocking(testDispatcher) { fun GIVEN_one_pending_video_BUT_CaptchaTimeoutException_WHEN_observing_THEN_its_saved_and_captchaError_emitted() = runBlocking {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
videoDownloadedMutableFlow.value = listOf() videoDownloadedMutableFlow.value = listOf()
@ -451,7 +456,7 @@ class VideoDownloadingProcessorUseCaseTest {
} }
@Test @Test
fun GIVEN_one_pending_video_AND_not_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking<Unit>(testDispatcher) { fun GIVEN_one_pending_video_AND_not_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() } whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() }
@ -459,21 +464,23 @@ class VideoDownloadingProcessorUseCaseTest {
val resultList = async(testDispatcher) { sut.processState.take(2).toList() } val resultList = async(testDispatcher) { sut.processState.take(2).toList() }
testDispatcher.advanceTimeBy(199) testDispatcher.advanceTimeBy(199)
verifyZeroInteractions(mockTikTokDownloadRemoteSource) verifyNoInteractions(mockTikTokDownloadRemoteSource)
testDispatcher.advanceUntilIdle()
resultList.cancelAndJoin() resultList.cancelAndJoin()
} }
@Test @Test
fun GIVEN_one_pending_video_AND_but_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking<Unit>(testDispatcher) { fun GIVEN_one_pending_video_AND_but_advancing_enough_WHILE_observing_WHEN_fetching_THEN_nothing_is_called() = runBlocking<Unit> {
val videoInPending = VideoInPending("alma", "banan") val videoInPending = VideoInPending("alma", "banan")
videoInPendingMutableFlow.value = listOf(videoInPending) videoInPendingMutableFlow.value = listOf(videoInPending)
whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() } whenever(mockTikTokDownloadRemoteSource.getVideo(videoInPending)).then { throw NetworkException() }
val resultList = async(testDispatcher) { sut.processState.take(2).toList() } val resultList = async(testDispatcher) { sut.processState.take(2).toList() }
testDispatcher.advanceTimeBy(200) testDispatcher.advanceTimeBy(201)
verify(mockTikTokDownloadRemoteSource, times(1)).getVideo(videoInPending) verify(mockTikTokDownloadRemoteSource, times(1)).getVideo(videoInPending)
verifyNoMoreInteractions(mockTikTokDownloadRemoteSource) verifyNoMoreInteractions(mockTikTokDownloadRemoteSource)
testDispatcher.advanceUntilIdle()
resultList.cancelAndJoin() resultList.cancelAndJoin()
} }

View file

@ -1,10 +1,6 @@
package org.fnives.tiktokdownloader.di package org.fnives.tiktokdownloader.di
import android.content.Context import android.content.Context
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain import kotlinx.coroutines.test.setMain
@ -14,10 +10,16 @@ import org.fnives.tiktokdownloader.ui.main.queue.QueueViewModel
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Timeout
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@Timeout(value = 2)
class ServiceLocatorTest { class ServiceLocatorTest {
private lateinit var mockContext : Context private lateinit var mockContext: Context
@BeforeEach @BeforeEach
fun setup() { fun setup() {
@ -30,7 +32,7 @@ class ServiceLocatorTest {
} }
@AfterEach @AfterEach
fun tearDown(){ fun tearDown() {
Dispatchers.resetMain() Dispatchers.resetMain()
} }

View file

@ -0,0 +1,10 @@
package org.fnives.tiktokdownloader.helper
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestDispatcher
@OptIn(ExperimentalCoroutinesApi::class)
fun TestDispatcher.advanceUntilIdle() = scheduler.advanceUntilIdle()
@OptIn(ExperimentalCoroutinesApi::class)
fun TestDispatcher.advanceTimeBy(delayTimeMillis: Long) = scheduler.advanceTimeBy(delayTimeMillis)

View file

@ -3,7 +3,7 @@ package org.fnives.tiktokdownloader.helper.mock
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.SavedStateRegistryOwner
import com.nhaarman.mockitokotlin2.mock import org.mockito.kotlin.mock
class MockSavedStateRegistryOwner( class MockSavedStateRegistryOwner(
private val lifecycle: Lifecycle = MockLifecycle(), private val lifecycle: Lifecycle = MockLifecycle(),

View file

@ -2,14 +2,7 @@ package org.fnives.tiktokdownloader.ui.main
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import com.jraska.livedata.test import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.doReturn import kotlinx.coroutines.flow.MutableSharedFlow
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.asFlow
import org.fnives.tiktokdownloader.data.model.ProcessState import org.fnives.tiktokdownloader.data.model.ProcessState
import org.fnives.tiktokdownloader.data.model.VideoDownloaded import org.fnives.tiktokdownloader.data.model.VideoDownloaded
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
@ -24,23 +17,29 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import java.util.stream.Stream import java.util.stream.Stream
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, MainDispatcherExtension::class) @ExtendWith(InstantExecutorExtension::class, MainDispatcherExtension::class)
class MainViewModelTest { class MainViewModelTest {
private lateinit var conflatedBroadcastChannel: ConflatedBroadcastChannel<ProcessState> private lateinit var processStateFlow: MutableSharedFlow<ProcessState>
private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase
private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase
private lateinit var sut: MainViewModel private lateinit var sut: MainViewModel
@BeforeEach @BeforeEach
fun setup() { fun setup() {
conflatedBroadcastChannel = ConflatedBroadcastChannel() processStateFlow = MutableSharedFlow(replay = 1)
mockVideoDownloadingProcessorUseCase = mock() mockVideoDownloadingProcessorUseCase = mock()
mockAddVideoToQueueUseCase = mock() mockAddVideoToQueueUseCase = mock()
whenever(mockVideoDownloadingProcessorUseCase.processState).doReturn(conflatedBroadcastChannel.asFlow()) whenever(mockVideoDownloadingProcessorUseCase.processState).doReturn(processStateFlow)
sut = MainViewModel(mockVideoDownloadingProcessorUseCase, mockAddVideoToQueueUseCase, SavedStateHandle()) sut = MainViewModel(mockVideoDownloadingProcessorUseCase, mockAddVideoToQueueUseCase, SavedStateHandle())
} }
@ -95,7 +94,7 @@ class MainViewModelTest {
val testObserver = sut.refreshActionVisibility.test() val testObserver = sut.refreshActionVisibility.test()
conflatedBroadcastChannel.offer(processState) processStateFlow.tryEmit(processState)
testObserver.assertHistorySize(2).assertValueHistory(false, expected) testObserver.assertHistorySize(2).assertValueHistory(false, expected)
verify(mockVideoDownloadingProcessorUseCase, times(1)).processState verify(mockVideoDownloadingProcessorUseCase, times(1)).processState
@ -114,7 +113,7 @@ class MainViewModelTest {
val testObserver = sut.errorMessage.test() val testObserver = sut.errorMessage.test()
conflatedBroadcastChannel.offer(processState) processStateFlow.tryEmit(processState)
testObserver.assertHistorySize(2).assertValueHistory(null, expected?.let(::Event)) testObserver.assertHistorySize(2).assertValueHistory(null, expected?.let(::Event))
verify(mockVideoDownloadingProcessorUseCase, times(1)).processState verify(mockVideoDownloadingProcessorUseCase, times(1)).processState

View file

@ -1,15 +1,7 @@
package org.fnives.tiktokdownloader.ui.main.queue package org.fnives.tiktokdownloader.ui.main.queue
import com.jraska.livedata.test import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.doReturn import kotlinx.coroutines.flow.MutableSharedFlow
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.asFlow
import org.fnives.tiktokdownloader.data.model.VideoInPending import org.fnives.tiktokdownloader.data.model.VideoInPending
import org.fnives.tiktokdownloader.data.model.VideoState import org.fnives.tiktokdownloader.data.model.VideoState
import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase import org.fnives.tiktokdownloader.data.usecase.AddVideoToQueueUseCase
@ -21,13 +13,19 @@ import org.fnives.tiktokdownloader.ui.shared.Event
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.ExtendWith
import kotlin.math.exp import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, MainDispatcherExtension::class) @ExtendWith(InstantExecutorExtension::class, MainDispatcherExtension::class)
class QueueViewModelTest { class QueueViewModelTest {
private lateinit var stateOfVideosConflatedBroadcastChannel: ConflatedBroadcastChannel<List<VideoState>> private lateinit var stateOfVideosFlow: MutableSharedFlow<List<VideoState>>
private lateinit var mockStateOfVideosObservableUseCase: StateOfVideosObservableUseCase private lateinit var mockStateOfVideosObservableUseCase: StateOfVideosObservableUseCase
private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase
private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase
@ -35,9 +33,9 @@ class QueueViewModelTest {
@BeforeEach @BeforeEach
fun setup() { fun setup() {
stateOfVideosConflatedBroadcastChannel = ConflatedBroadcastChannel() stateOfVideosFlow = MutableSharedFlow(replay = 1)
mockStateOfVideosObservableUseCase = mock() mockStateOfVideosObservableUseCase = mock()
whenever(mockStateOfVideosObservableUseCase.invoke()).doReturn(stateOfVideosConflatedBroadcastChannel.asFlow()) whenever(mockStateOfVideosObservableUseCase.invoke()).doReturn(stateOfVideosFlow)
mockAddVideoToQueueUseCase = mock() mockAddVideoToQueueUseCase = mock()
mockVideoDownloadingProcessorUseCase = mock() mockVideoDownloadingProcessorUseCase = mock()
sut = QueueViewModel(mockStateOfVideosObservableUseCase, mockAddVideoToQueueUseCase, mockVideoDownloadingProcessorUseCase) sut = QueueViewModel(mockStateOfVideosObservableUseCase, mockAddVideoToQueueUseCase, mockVideoDownloadingProcessorUseCase)
@ -48,37 +46,37 @@ class QueueViewModelTest {
sut.downloads.test().assertNoValue() sut.downloads.test().assertNoValue()
verify(mockStateOfVideosObservableUseCase, times(1)).invoke() verify(mockStateOfVideosObservableUseCase, times(1)).invoke()
verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoMoreInteractions(mockStateOfVideosObservableUseCase)
verifyZeroInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase)
verifyZeroInteractions(mockVideoDownloadingProcessorUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase)
} }
@Test @Test
fun GIVEN_initialized_AND_observing_WHEN_emitting_a_emptyList_THEN_it_is_sent_out() { fun GIVEN_initialized_AND_observing_WHEN_emitting_a_emptyList_THEN_it_is_sent_out() {
val expected = listOf<VideoState>() val expected = listOf<VideoState>()
stateOfVideosConflatedBroadcastChannel.offer(expected) stateOfVideosFlow.tryEmit(expected)
sut.downloads.test().assertValue(expected) sut.downloads.test().assertValue(expected)
verify(mockStateOfVideosObservableUseCase, times(1)).invoke() verify(mockStateOfVideosObservableUseCase, times(1)).invoke()
verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoMoreInteractions(mockStateOfVideosObservableUseCase)
verifyZeroInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase)
verifyZeroInteractions(mockVideoDownloadingProcessorUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase)
} }
@Test @Test
fun GIVEN_initialized_AND_observing_WHEN_emitting_two_list_THEN_both_are_sent_out_in_order() { fun GIVEN_initialized_AND_observing_WHEN_emitting_two_list_THEN_both_are_sent_out_in_order() {
val expected1 = listOf(VideoState.InPending(VideoInPending("a1","b1"))) val expected1 = listOf(VideoState.InPending(VideoInPending("a1", "b1")))
val expected2 = listOf(VideoState.InPending(VideoInPending("a2","b2"))) val expected2 = listOf(VideoState.InPending(VideoInPending("a2", "b2")))
val testObserver = sut.downloads.test() val testObserver = sut.downloads.test()
stateOfVideosConflatedBroadcastChannel.offer(expected1) stateOfVideosFlow.tryEmit(expected1)
stateOfVideosConflatedBroadcastChannel.offer(expected2) stateOfVideosFlow.tryEmit(expected2)
testObserver.assertHistorySize(2).assertHasValue() testObserver.assertHistorySize(2).assertHasValue()
.assertValueHistory(expected1, expected2) .assertValueHistory(expected1, expected2)
verify(mockStateOfVideosObservableUseCase, times(1)).invoke() verify(mockStateOfVideosObservableUseCase, times(1)).invoke()
verifyNoMoreInteractions(mockStateOfVideosObservableUseCase) verifyNoMoreInteractions(mockStateOfVideosObservableUseCase)
verifyZeroInteractions(mockAddVideoToQueueUseCase) verifyNoInteractions(mockAddVideoToQueueUseCase)
verifyZeroInteractions(mockVideoDownloadingProcessorUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase)
} }
@Test @Test

View file

@ -1,16 +1,7 @@
package org.fnives.tiktokdownloader.ui.service package org.fnives.tiktokdownloader.ui.service
import com.jraska.livedata.test import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.anyOrNull import kotlinx.coroutines.flow.MutableSharedFlow
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
import kotlinx.coroutines.flow.asFlow
import org.fnives.tiktokdownloader.R import org.fnives.tiktokdownloader.R
import org.fnives.tiktokdownloader.data.model.ProcessState import org.fnives.tiktokdownloader.data.model.ProcessState
import org.fnives.tiktokdownloader.data.model.VideoDownloaded import org.fnives.tiktokdownloader.data.model.VideoDownloaded
@ -25,22 +16,30 @@ import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoInteractions
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import java.util.stream.Stream import java.util.stream.Stream
@Suppress("TestFunctionName") @Suppress("TestFunctionName")
@ExtendWith(InstantExecutorExtension::class, MainDispatcherExtension::class) @ExtendWith(InstantExecutorExtension::class, MainDispatcherExtension::class)
class QueueServiceViewModelTest { class QueueServiceViewModelTest {
private lateinit var videoDownloadingProcessorChannel: ConflatedBroadcastChannel<ProcessState> private lateinit var videoDownloadingProcessorFlow: MutableSharedFlow<ProcessState>
private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase private lateinit var mockVideoDownloadingProcessorUseCase: VideoDownloadingProcessorUseCase
private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase private lateinit var mockAddVideoToQueueUseCase: AddVideoToQueueUseCase
private lateinit var sut: QueueServiceViewModel private lateinit var sut: QueueServiceViewModel
@BeforeEach @BeforeEach
fun setup() { fun setup() {
videoDownloadingProcessorChannel = ConflatedBroadcastChannel() videoDownloadingProcessorFlow = MutableSharedFlow(replay = 1)
mockVideoDownloadingProcessorUseCase = mock() mockVideoDownloadingProcessorUseCase = mock()
whenever(mockVideoDownloadingProcessorUseCase.processState).doReturn(videoDownloadingProcessorChannel.asFlow()) whenever(mockVideoDownloadingProcessorUseCase.processState).doReturn(videoDownloadingProcessorFlow)
mockAddVideoToQueueUseCase = mock() mockAddVideoToQueueUseCase = mock()
sut = QueueServiceViewModel(mockAddVideoToQueueUseCase, mockVideoDownloadingProcessorUseCase) sut = QueueServiceViewModel(mockAddVideoToQueueUseCase, mockVideoDownloadingProcessorUseCase)
} }
@ -64,7 +63,7 @@ class QueueServiceViewModelTest {
verify(mockAddVideoToQueueUseCase, times(1)).invoke("url.com") verify(mockAddVideoToQueueUseCase, times(1)).invoke("url.com")
verifyNoMoreInteractions(mockAddVideoToQueueUseCase) verifyNoMoreInteractions(mockAddVideoToQueueUseCase)
verifyZeroInteractions(mockVideoDownloadingProcessorUseCase) verifyNoInteractions(mockVideoDownloadingProcessorUseCase)
} }
@Test @Test
@ -89,7 +88,7 @@ class QueueServiceViewModelTest {
) { ) {
whenever(mockAddVideoToQueueUseCase.invoke(anyOrNull())).doReturn(true) whenever(mockAddVideoToQueueUseCase.invoke(anyOrNull())).doReturn(true)
sut.onUrlReceived("") sut.onUrlReceived("")
videoDownloadingProcessorChannel.offer(processState) videoDownloadingProcessorFlow.tryEmit(processState)
sut.notificationState.test() sut.notificationState.test()
.assertHistorySize(1) .assertHistorySize(1)
@ -101,7 +100,7 @@ class QueueServiceViewModelTest {
sut.onClear() sut.onClear()
sut.onUrlReceived("alma") sut.onUrlReceived("alma")
videoDownloadingProcessorChannel.offer(ProcessState.UnknownError) videoDownloadingProcessorFlow.tryEmit(ProcessState.UnknownError)
sut.notificationState.test().assertNoValue() sut.notificationState.test().assertNoValue()
} }

View file

@ -1,12 +1,13 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = "1.4.10" ext.kotlin_version = "1.6.20"
repositories { repositories {
mavenCentral()
google() google()
jcenter() maven { url "https://plugins.gradle.org/m2/" }
} }
dependencies { dependencies {
classpath "com.android.tools.build:gradle:4.1.0" classpath 'com.android.tools.build:gradle:7.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
@ -16,8 +17,8 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
mavenCentral()
google() google()
jcenter()
} }
} }

View file

@ -1,6 +1,6 @@
#Sat Oct 31 22:42:23 EET 2020 #Thu Jan 27 21:44:07 EET 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip zipStoreBase=GRADLE_USER_HOME