#7: Add unit tests to OAuthDatasource
This commit is contained in:
parent
6d086c021f
commit
e60d93a05a
|
@ -1,51 +1,82 @@
|
|||
image: jangrewe/gitlab-ci-android
|
||||
|
||||
variables:
|
||||
JACOCO_CSV_LOCATION: '$CI_PROJECT_DIR/app/build/reports/jacoco/testDebugUnitTestCoverage/testDebugUnitTestCoverage.csv'
|
||||
|
||||
stages:
|
||||
- build
|
||||
- deploy
|
||||
- test
|
||||
- visualize
|
||||
- build
|
||||
- deploy
|
||||
|
||||
|
||||
.build:
|
||||
.gradle-default:
|
||||
stage: build
|
||||
before_script:
|
||||
- export GRADLE_USER_HOME=$(pwd)/.gradle
|
||||
- chmod +x ./gradlew
|
||||
- mkdir -p .android && touch .android/repositories.cfg
|
||||
- export GRADLE_USER_HOME=$(pwd)/.gradle
|
||||
- chmod +x ./gradlew
|
||||
- mkdir -p .android && touch .android/repositories.cfg
|
||||
script:
|
||||
- echo "Overwrite me"
|
||||
- echo "Overwrite me"
|
||||
|
||||
cache:
|
||||
key: ${CI_PROJECT_ID}
|
||||
paths:
|
||||
- .gradle/
|
||||
- .gradle/
|
||||
|
||||
.build:
|
||||
extends: .gradle-default
|
||||
artifacts:
|
||||
paths:
|
||||
- app/build/outputs/apk/debug/app-debug.apk
|
||||
- app/build/outputs/apk/debug/app-debug.apk
|
||||
|
||||
test:
|
||||
extends: .gradle-default
|
||||
stage: test
|
||||
script:
|
||||
- ./gradlew test testDebugUnitTestCoverage
|
||||
- awk -F"," '{ instructions += $4 + $5; covered += $5 } END { print covered, "/", instructions, " instructions covered"; print 100*covered/instructions, "% covered" }' $JACOCO_CSV_LOCATION
|
||||
artifacts:
|
||||
reports:
|
||||
junit: app/build/test-results/test**/TEST-*.xml
|
||||
paths:
|
||||
- app/build/reports/jacoco/testDebugUnitTestCoverage/testDebugUnitTestCoverage.xml
|
||||
|
||||
coverage:
|
||||
stage: visualize
|
||||
image: gjrtimmer/jacoco2cobertura:1.0.8
|
||||
script:
|
||||
# convert report from jacoco to cobertura, use relative project path
|
||||
- 'python /opt/cover2cover.py app/build/reports/jacoco/testDebugUnitTestCoverage/testDebugUnitTestCoverage.xml $CI_PROJECT_DIR/app/src/main/java > app/build/reports/cobertura.xml'
|
||||
needs: [ "test" ]
|
||||
dependencies:
|
||||
- test
|
||||
artifacts:
|
||||
reports:
|
||||
cobertura: app/build/reports/cobertura.xml
|
||||
|
||||
build-develop:
|
||||
extends: .build
|
||||
script:
|
||||
- echo -n $SIGNING_KEY_STORE | base64 -d > app/android.keystore
|
||||
- ./gradlew assembleDebug -Psigning.store=android.keystore -Psigning.store_passphrase=$SIGNING_KEY_PASS -Psigning.key_passphrase=$SIGNING_KEY_PASS
|
||||
- echo -n $SIGNING_KEY_STORE | base64 -d > app/android.keystore
|
||||
- ./gradlew assembleDebug -Psigning.store=android.keystore -Psigning.store_passphrase=$SIGNING_KEY_PASS -Psigning.key_passphrase=$SIGNING_KEY_PASS
|
||||
only:
|
||||
- develop
|
||||
- develop
|
||||
|
||||
build-bleeding-edge:
|
||||
extends: .build
|
||||
script:
|
||||
- ./gradlew assembleDebug
|
||||
- ./gradlew assembleDebug
|
||||
except:
|
||||
- develop
|
||||
- develop
|
||||
|
||||
deploy-develop:
|
||||
stage: deploy
|
||||
only:
|
||||
- develop
|
||||
only:
|
||||
- develop
|
||||
script:
|
||||
- eval `ssh-agent -s`
|
||||
- ssh-add <(echo "$SSH_PRIVATE_KEY")
|
||||
- scp -o StrictHostKeyChecking=no app/build/outputs/apk/debug/app-debug.apk fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/repo/audio.funkwhale.ffa.dev-$CI_COMMIT_SHORT_SHA.apk
|
||||
- ssh -o StrictHostKeyChecking=no fdroid@apps.funkwhale.audio 'docker run --rm -u $(id -u):$(id -g) -v /srv/fdroid/fdroid/develop:/repo registry.gitlab.com/fdroid/docker-executable-fdroidserver:master update'
|
||||
- eval `ssh-agent -s`
|
||||
- ssh-add <(echo "$SSH_PRIVATE_KEY")
|
||||
- scp -o StrictHostKeyChecking=no app/build/outputs/apk/debug/app-debug.apk fdroid@apps.funkwhale.audio:/srv/fdroid/fdroid/develop/repo/audio.funkwhale.ffa.dev-$CI_COMMIT_SHORT_SHA.apk
|
||||
- ssh -o StrictHostKeyChecking=no fdroid@apps.funkwhale.audio 'docker run --rm -u $(id -u):$(id -g) -v /srv/fdroid/fdroid/develop:/repo registry.gitlab.com/fdroid/docker-executable-fdroidserver:master update'
|
||||
tags:
|
||||
- shell
|
||||
|
|
|
@ -9,6 +9,7 @@ plugins {
|
|||
id("org.jlleitschuh.gradle.ktlint") version "8.1.0"
|
||||
id("com.gladed.androidgitversion") version "0.4.14"
|
||||
id("com.github.triplet.play") version "2.4.2"
|
||||
jacoco
|
||||
}
|
||||
|
||||
val props = Properties().apply {
|
||||
|
@ -18,6 +19,10 @@ val props = Properties().apply {
|
|||
}
|
||||
}
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.7"
|
||||
}
|
||||
|
||||
androidGitVersion {
|
||||
codeFormat = "MMNNPPBBB"
|
||||
format = "%tag%%-count%%-commit%%-branch%"
|
||||
|
@ -75,6 +80,10 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
isDebuggable = true
|
||||
|
@ -84,6 +93,8 @@ android {
|
|||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
isTestCoverageEnabled = true
|
||||
|
||||
resValue("string", "debug.hostname", props.getProperty("debug.hostname", ""))
|
||||
resValue("string", "debug.username", props.getProperty("debug.username", ""))
|
||||
resValue("string", "debug.password", props.getProperty("debug.password", ""))
|
||||
|
@ -160,6 +171,60 @@ dependencies {
|
|||
implementation("com.google.code.gson:gson:2.8.7")
|
||||
implementation("com.squareup.picasso:picasso:2.71828")
|
||||
implementation("jp.wasabeef:picasso-transformations:2.4.0")
|
||||
|
||||
implementation("net.openid:appauth:0.9.1")
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.12.0")
|
||||
androidTestImplementation("io.mockk:mockk-android:1.12.0")
|
||||
testImplementation("androidx.test:core:1.4.0")
|
||||
testImplementation("io.strikt:strikt-core:0.31.0")
|
||||
}
|
||||
|
||||
project.afterEvaluate {
|
||||
android.applicationVariants.forEach { variant ->
|
||||
val testTaskName = "test${variant.name.capitalize()}UnitTest"
|
||||
tasks.create<JacocoReport>(name = "${testTaskName}Coverage") {
|
||||
|
||||
dependsOn(testTaskName)
|
||||
|
||||
group = "Reporting"
|
||||
description = "Generate Jacoco coverage reports after running tests."
|
||||
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
csv.required.set(true)
|
||||
html.required.set(true)
|
||||
}
|
||||
|
||||
val excludes = listOf(
|
||||
"**/R.class",
|
||||
"**/R$*.class",
|
||||
"**/BuildConfig.*",
|
||||
"**/Manifest*.*",
|
||||
"**/*Test*.*",
|
||||
"android/**/*.*"
|
||||
)
|
||||
|
||||
val javaClasses =
|
||||
fileTree(baseDir = variant.javaCompileProvider.get().destinationDirectory).matching {
|
||||
setExcludes(excludes)
|
||||
}
|
||||
val kotlinClasses =
|
||||
fileTree(baseDir = "$buildDir/tmp/kotlin-classes/${variant.name}").matching {
|
||||
setExcludes(excludes)
|
||||
}
|
||||
classDirectories.setFrom(files(listOf(javaClasses, kotlinClasses)))
|
||||
|
||||
val sourceDirectories = files(
|
||||
listOf(
|
||||
"$project.projectDir/src/main/java",
|
||||
"$project.projectDir/src/${variant.name}/java",
|
||||
"$project.projectDir/src/main/kotlin",
|
||||
"$project.projectDir/src/${variant.name}/kotlin"
|
||||
)
|
||||
)
|
||||
|
||||
sourceDirectories.setFrom(files(sourceDirectories))
|
||||
executionData.setFrom(files("${project.buildDir}/jacoco/$testTaskName.exec"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import audio.funkwhale.ffa.databinding.ActivityLoginBinding
|
|||
import audio.funkwhale.ffa.fragments.LoginDialog
|
||||
import audio.funkwhale.ffa.utils.AppContext
|
||||
import audio.funkwhale.ffa.utils.OAuth
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.Userinfo
|
||||
import audio.funkwhale.ffa.utils.log
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
|
@ -28,13 +29,13 @@ data class FwCredentials(val token: String, val non_field_errors: List<String>?)
|
|||
class LoginActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityLoginBinding
|
||||
|
||||
private lateinit var oAuth: OAuth
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityLoginBinding.inflate(layoutInflater)
|
||||
|
||||
oAuth = OAuthFactory.instance()
|
||||
setContentView(binding.root)
|
||||
limitContainerWidth()
|
||||
}
|
||||
|
@ -45,7 +46,7 @@ class LoginActivity : AppCompatActivity() {
|
|||
data?.let {
|
||||
when (requestCode) {
|
||||
0 -> {
|
||||
OAuth.exchange(this, data,
|
||||
oAuth.exchange(this, data,
|
||||
{
|
||||
PowerPreference
|
||||
.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
|
@ -114,11 +115,9 @@ class LoginActivity : AppCompatActivity() {
|
|||
|
||||
private fun authedLogin(hostname: String) {
|
||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).setString("hostname", hostname)
|
||||
|
||||
OAuth.init(hostname)
|
||||
|
||||
OAuth.register {
|
||||
OAuth.authorize(this)
|
||||
oAuth.init(hostname)
|
||||
oAuth.register {
|
||||
oAuth.authorize(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,16 +7,21 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import audio.funkwhale.ffa.FFA
|
||||
import audio.funkwhale.ffa.utils.AppContext
|
||||
import audio.funkwhale.ffa.utils.OAuth
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.Settings
|
||||
|
||||
class SplashActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var oAuth: OAuth
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
oAuth = OAuthFactory.instance()
|
||||
|
||||
getSharedPreferences(AppContext.PREFS_CREDENTIALS, Context.MODE_PRIVATE)
|
||||
.apply {
|
||||
when (OAuth.isAuthorized(this@SplashActivity) || Settings.isAnonymous()) {
|
||||
when (oAuth.isAuthorized(this@SplashActivity) || Settings.isAnonymous()) {
|
||||
true -> Intent(this@SplashActivity, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||
startActivity(this)
|
||||
|
@ -30,5 +35,4 @@ class SplashActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ import com.google.android.exoplayer2.upstream.TransferListener
|
|||
|
||||
class OAuthDatasource(
|
||||
private val context: Context,
|
||||
private val http: HttpDataSource
|
||||
private val http: HttpDataSource,
|
||||
private val oauth: OAuth
|
||||
) : DataSource {
|
||||
|
||||
override fun addTransferListener(transferListener: TransferListener?) {
|
||||
|
@ -19,7 +20,10 @@ class OAuthDatasource(
|
|||
}
|
||||
|
||||
override fun open(dataSpec: DataSpec?): Long {
|
||||
OAuth.tryRefreshAccessToken(context)
|
||||
oauth.tryRefreshAccessToken(context)
|
||||
http.apply {
|
||||
setRequestProperty("Authorization", "Bearer ${oauth.state().accessToken}")
|
||||
}
|
||||
return http.open(dataSpec)
|
||||
}
|
||||
|
||||
|
@ -34,15 +38,15 @@ class OAuthDatasource(
|
|||
override fun close() {
|
||||
http.close()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class OAuth2DatasourceFactory(
|
||||
private val context: Context,
|
||||
private val http: DefaultHttpDataSourceFactory
|
||||
private val http: DefaultHttpDataSourceFactory,
|
||||
private val oauth: OAuth
|
||||
) : DataSource.Factory {
|
||||
|
||||
override fun createDataSource(): DataSource {
|
||||
return OAuthDatasource(context, http.createDataSource())
|
||||
return OAuthDatasource(context, http.createDataSource(), oauth)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import audio.funkwhale.ffa.utils.Command
|
|||
import audio.funkwhale.ffa.utils.CommandBus
|
||||
import audio.funkwhale.ffa.utils.Event
|
||||
import audio.funkwhale.ffa.utils.EventBus
|
||||
import audio.funkwhale.ffa.utils.OAuth
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.QueueCache
|
||||
import audio.funkwhale.ffa.utils.Settings
|
||||
import audio.funkwhale.ffa.utils.Track
|
||||
|
@ -17,6 +17,7 @@ import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
|||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.upstream.DataSource
|
||||
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory
|
||||
import com.google.android.exoplayer2.upstream.FileDataSource
|
||||
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
|
||||
|
@ -32,19 +33,9 @@ class QueueManager(val context: Context) {
|
|||
companion object {
|
||||
|
||||
fun factory(context: Context): CacheDataSourceFactory {
|
||||
val http = DefaultHttpDataSourceFactory(
|
||||
Util.getUserAgent(context, context.getString(R.string.app_name))
|
||||
)
|
||||
.apply {
|
||||
defaultRequestProperties.apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
set("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val playbackCache =
|
||||
CacheDataSourceFactory(FFA.get().exoCache, OAuth2DatasourceFactory(context, http))
|
||||
CacheDataSourceFactory(FFA.get().exoCache, createDatasourceFactory(context))
|
||||
|
||||
return CacheDataSourceFactory(
|
||||
FFA.get().exoDownloadCache,
|
||||
|
@ -55,6 +46,17 @@ class QueueManager(val context: Context) {
|
|||
null
|
||||
)
|
||||
}
|
||||
|
||||
private fun createDatasourceFactory(context: Context): DataSource.Factory {
|
||||
val http = DefaultHttpDataSourceFactory(
|
||||
Util.getUserAgent(context, context.getString(R.string.app_name))
|
||||
)
|
||||
return if (!Settings.isAnonymous()) {
|
||||
OAuth2DatasourceFactory(context, http, OAuthFactory.instance())
|
||||
} else {
|
||||
http
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import audio.funkwhale.ffa.utils.Album
|
||||
import audio.funkwhale.ffa.utils.AlbumsCache
|
||||
import audio.funkwhale.ffa.utils.AlbumsResponse
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
|
@ -11,6 +12,8 @@ import java.io.BufferedReader
|
|||
class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
|
||||
Repository<Album, AlbumsCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId: String by lazy {
|
||||
if (artistId == null) "albums"
|
||||
else "albums-artist-$artistId"
|
||||
|
@ -25,7 +28,8 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
|
|||
context!!,
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
url,
|
||||
object : TypeToken<AlbumsResponse>() {}.type
|
||||
object : TypeToken<AlbumsResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package audio.funkwhale.ffa.repositories
|
||||
|
||||
import android.content.Context
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.OtterResponse
|
||||
import audio.funkwhale.ffa.utils.Track
|
||||
import audio.funkwhale.ffa.utils.TracksCache
|
||||
|
@ -12,13 +13,16 @@ import java.io.BufferedReader
|
|||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) :
|
||||
Repository<Track, TracksCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "tracks-artist-$artistId"
|
||||
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.AtOnce,
|
||||
"/api/v1/tracks/?playable=true&artist=$artistId",
|
||||
object : TypeToken<TracksResponse>() {}.type
|
||||
object : TypeToken<TracksResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import audio.funkwhale.ffa.utils.Artist
|
||||
import audio.funkwhale.ffa.utils.ArtistsCache
|
||||
import audio.funkwhale.ffa.utils.ArtistsResponse
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.OtterResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
|
@ -11,13 +12,16 @@ import java.io.BufferedReader
|
|||
|
||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "artists"
|
||||
|
||||
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
"/api/v1/artists/?playable=true&ordering=name",
|
||||
object : TypeToken<ArtistsResponse>() {}.type
|
||||
object : TypeToken<ArtistsResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
|
|
|
@ -5,7 +5,7 @@ import audio.funkwhale.ffa.FFA
|
|||
import audio.funkwhale.ffa.utils.Cache
|
||||
import audio.funkwhale.ffa.utils.FavoritedCache
|
||||
import audio.funkwhale.ffa.utils.FavoritedResponse
|
||||
import audio.funkwhale.ffa.utils.OAuth
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.OtterResponse
|
||||
import audio.funkwhale.ffa.utils.Settings
|
||||
import audio.funkwhale.ffa.utils.Track
|
||||
|
@ -28,13 +28,16 @@ import java.io.BufferedReader
|
|||
|
||||
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
||||
|
||||
private var oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "favorites.v2"
|
||||
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(
|
||||
context!!,
|
||||
HttpUpstream.Behavior.AtOnce,
|
||||
"/api/v1/tracks/?favorites=true&playable=true&ordering=title",
|
||||
object : TypeToken<TracksResponse>() {}.type
|
||||
object : TypeToken<TracksResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
|
@ -67,7 +70,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +92,7 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
val request = Fuel.post(mustNormalizeUrl("/api/v1/favorites/tracks/remove/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
request.header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
request.header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,12 +109,16 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
}
|
||||
|
||||
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "favorited"
|
||||
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.Single,
|
||||
"/api/v1/favorites/tracks/all/?playable=true",
|
||||
object : TypeToken<FavoritedResponse>() {}.type
|
||||
object : TypeToken<FavoritedResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Int>) = FavoritedCache(data)
|
||||
|
|
|
@ -23,7 +23,8 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
|
|||
val context: Context?,
|
||||
val behavior: Behavior,
|
||||
private val url: String,
|
||||
private val type: Type
|
||||
private val type: Type,
|
||||
private val oAuth: OAuth
|
||||
) : Upstream<D> {
|
||||
|
||||
enum class Behavior {
|
||||
|
@ -110,7 +111,7 @@ class HttpUpstream<D : Any, R : OtterResponse<D>>(
|
|||
return if (http.refresh()) {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package audio.funkwhale.ffa.repositories
|
||||
|
||||
import android.content.Context
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.OtterResponse
|
||||
import audio.funkwhale.ffa.utils.PlaylistTrack
|
||||
import audio.funkwhale.ffa.utils.PlaylistTracksCache
|
||||
|
@ -15,13 +16,16 @@ import java.io.BufferedReader
|
|||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) :
|
||||
Repository<PlaylistTrack, PlaylistTracksCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "tracks-playlist-$playlistId"
|
||||
|
||||
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.Single,
|
||||
"/api/v1/playlists/$playlistId/tracks/?playable=true",
|
||||
object : TypeToken<PlaylistTracksResponse>() {}.type
|
||||
object : TypeToken<PlaylistTracksResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package audio.funkwhale.ffa.repositories
|
||||
|
||||
import android.content.Context
|
||||
import audio.funkwhale.ffa.utils.OAuth
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.OtterResponse
|
||||
import audio.funkwhale.ffa.utils.Playlist
|
||||
import audio.funkwhale.ffa.utils.PlaylistsCache
|
||||
|
@ -30,7 +30,8 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist,
|
|||
context!!,
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
"/api/v1/playlists/?playable=true&ordering=name",
|
||||
object : TypeToken<PlaylistsResponse>() {}.type
|
||||
object : TypeToken<PlaylistsResponse>() {}.type,
|
||||
OAuthFactory.instance()
|
||||
)
|
||||
|
||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||
|
@ -41,13 +42,16 @@ class PlaylistsRepository(override val context: Context?) : Repository<Playlist,
|
|||
class ManagementPlaylistsRepository(override val context: Context?) :
|
||||
Repository<Playlist, PlaylistsCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "tracks-playlists-management"
|
||||
|
||||
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.AtOnce,
|
||||
"/api/v1/playlists/?scope=me&ordering=name",
|
||||
object : TypeToken<PlaylistsResponse>() {}.type
|
||||
object : TypeToken<PlaylistsResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||
|
@ -62,7 +66,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
|||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +89,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
|||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/add/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -106,7 +110,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
|||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/remove/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +129,7 @@ class ManagementPlaylistsRepository(override val context: Context?) :
|
|||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/$id/move/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${oAuth.state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package audio.funkwhale.ffa.repositories
|
||||
|
||||
import android.content.Context
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.OtterResponse
|
||||
import audio.funkwhale.ffa.utils.Radio
|
||||
import audio.funkwhale.ffa.utils.RadiosCache
|
||||
|
@ -11,13 +12,16 @@ import java.io.BufferedReader
|
|||
|
||||
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "radios"
|
||||
|
||||
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
"/api/v1/radios/radios/?ordering=name",
|
||||
object : TypeToken<RadiosResponse>() {}.type
|
||||
object : TypeToken<RadiosResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Radio>) = RadiosCache(data)
|
||||
|
|
|
@ -8,6 +8,7 @@ import audio.funkwhale.ffa.utils.AlbumsResponse
|
|||
import audio.funkwhale.ffa.utils.Artist
|
||||
import audio.funkwhale.ffa.utils.ArtistsCache
|
||||
import audio.funkwhale.ffa.utils.ArtistsResponse
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.Track
|
||||
import audio.funkwhale.ffa.utils.TracksCache
|
||||
import audio.funkwhale.ffa.utils.TracksResponse
|
||||
|
@ -22,6 +23,8 @@ import java.io.BufferedReader
|
|||
class TracksSearchRepository(override val context: Context?, var query: String) :
|
||||
Repository<Track, TracksCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId: String? = null
|
||||
|
||||
override val upstream: Upstream<Track>
|
||||
|
@ -29,7 +32,8 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
|||
context,
|
||||
HttpUpstream.Behavior.AtOnce,
|
||||
"/api/v1/tracks/?playable=true&q=$query",
|
||||
object : TypeToken<TracksResponse>() {}.type
|
||||
object : TypeToken<TracksResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
|
@ -61,13 +65,17 @@ class TracksSearchRepository(override val context: Context?, var query: String)
|
|||
|
||||
class ArtistsSearchRepository(override val context: Context?, var query: String) :
|
||||
Repository<Artist, ArtistsCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Artist>
|
||||
get() = HttpUpstream(
|
||||
context,
|
||||
HttpUpstream.Behavior.AtOnce,
|
||||
"/api/v1/artists/?playable=true&q=$query",
|
||||
object : TypeToken<ArtistsResponse>() {}.type
|
||||
object : TypeToken<ArtistsResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
|
@ -77,13 +85,17 @@ class ArtistsSearchRepository(override val context: Context?, var query: String)
|
|||
|
||||
class AlbumsSearchRepository(override val context: Context?, var query: String) :
|
||||
Repository<Album, AlbumsCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId: String? = null
|
||||
override val upstream: Upstream<Album>
|
||||
get() = HttpUpstream(
|
||||
context,
|
||||
HttpUpstream.Behavior.AtOnce,
|
||||
"/api/v1/albums/?playable=true&q=$query",
|
||||
object : TypeToken<AlbumsResponse>() {}.type
|
||||
object : TypeToken<AlbumsResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Album>) = AlbumsCache(data)
|
||||
|
|
|
@ -2,6 +2,7 @@ package audio.funkwhale.ffa.repositories
|
|||
|
||||
import android.content.Context
|
||||
import audio.funkwhale.ffa.FFA
|
||||
import audio.funkwhale.ffa.utils.OAuthFactory
|
||||
import audio.funkwhale.ffa.utils.OtterResponse
|
||||
import audio.funkwhale.ffa.utils.Track
|
||||
import audio.funkwhale.ffa.utils.TracksCache
|
||||
|
@ -19,13 +20,16 @@ import java.io.BufferedReader
|
|||
class TracksRepository(override val context: Context?, albumId: Int) :
|
||||
Repository<Track, TracksCache>() {
|
||||
|
||||
private val oAuth = OAuthFactory.instance()
|
||||
|
||||
override val cacheId = "tracks-album-$albumId"
|
||||
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(
|
||||
context,
|
||||
HttpUpstream.Behavior.AtOnce,
|
||||
"/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position",
|
||||
object : TypeToken<TracksResponse>() {}.type
|
||||
object : TypeToken<TracksResponse>() {}.type,
|
||||
oAuth
|
||||
)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
|
|
|
@ -19,25 +19,33 @@ object RefreshError : Throwable()
|
|||
class HTTP(val context: Context?) {
|
||||
|
||||
suspend fun refresh(): Boolean {
|
||||
val body = mapOf(
|
||||
"username" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.getString("username"),
|
||||
"password" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.getString("password")
|
||||
).toList()
|
||||
context?.let {
|
||||
val body = mapOf(
|
||||
"username" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.getString("username"),
|
||||
"password" to PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.getString("password")
|
||||
).toList()
|
||||
|
||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/token"), body)
|
||||
.awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||
val result = Fuel.post(mustNormalizeUrl("/api/v1/token"), body).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(it)
|
||||
header("Authorization", "Bearer ${OAuthFactory.instance().state().accessToken}")
|
||||
}
|
||||
}
|
||||
.awaitObjectResult(gsonDeserializerOf(FwCredentials::class.java))
|
||||
|
||||
return result.fold(
|
||||
{ data ->
|
||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.setString("access_token", data.token)
|
||||
return result.fold(
|
||||
{ data ->
|
||||
PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.setString("access_token", data.token)
|
||||
|
||||
true
|
||||
},
|
||||
{ false }
|
||||
)
|
||||
true
|
||||
},
|
||||
{ false }
|
||||
)
|
||||
}
|
||||
throw IllegalStateException("Illegal state: context is null")
|
||||
}
|
||||
|
||||
suspend inline fun <reified T : Any> get(url: String): Result<T, FuelError> {
|
||||
|
@ -46,7 +54,7 @@ class HTTP(val context: Context?) {
|
|||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(it)
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${OAuthFactory.instance().state().accessToken}")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,18 +73,13 @@ class HTTP(val context: Context?) {
|
|||
url: String
|
||||
): Result<T, FuelError> {
|
||||
context?.let {
|
||||
return if (refresh()) {
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
}
|
||||
val request = Fuel.get(mustNormalizeUrl(url)).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
authorize(context)
|
||||
header("Authorization", "Bearer ${OAuthFactory.instance().state().accessToken}")
|
||||
}
|
||||
|
||||
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
|
||||
} else {
|
||||
Result.Failure(FuelError.wrap(RefreshError))
|
||||
}
|
||||
request.awaitObjectResult(gsonDeserializerOf(T::class.java))
|
||||
}
|
||||
throw IllegalStateException("Illegal state: context is null")
|
||||
}
|
||||
|
|
|
@ -80,16 +80,17 @@ fun Request.authorize(context: Context): Request {
|
|||
return runBlocking {
|
||||
this@authorize.apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
OAuth.state().let { state ->
|
||||
val oauth = OAuthFactory.instance()
|
||||
oauth.state().let { state ->
|
||||
val old = state.accessToken
|
||||
val auth = ClientSecretPost(OAuth.state().clientSecret)
|
||||
val auth = ClientSecretPost(oauth.state().clientSecret)
|
||||
val done = CompletableDeferred<Boolean>()
|
||||
|
||||
state.performActionWithFreshTokens(OAuth.service(context), auth) { token, _, _ ->
|
||||
state.performActionWithFreshTokens(oauth.service(context), auth) { token, _, _ ->
|
||||
if (token != old && token != null) {
|
||||
state.save()
|
||||
}
|
||||
header("Authorization", "Bearer ${OAuth.state().accessToken}")
|
||||
header("Authorization", "Bearer ${oauth.state().accessToken}")
|
||||
done.complete(true)
|
||||
}
|
||||
done.await()
|
||||
|
|
|
@ -29,13 +29,64 @@ fun AuthState.save() {
|
|||
}
|
||||
}
|
||||
|
||||
object OAuth {
|
||||
interface OAuth {
|
||||
|
||||
fun exchange(context: Activity, authorization: Intent, success: () -> Unit, error: () -> Unit)
|
||||
|
||||
fun init(hostname: String)
|
||||
|
||||
fun register(callback: () -> Unit)
|
||||
|
||||
fun authorize(context: Activity)
|
||||
|
||||
fun isAuthorized(context: Context): Boolean
|
||||
|
||||
fun tryRefreshAccessToken(context: Context, overrideNeedsTokenRefresh: Boolean = false): Boolean
|
||||
|
||||
fun tryState(): AuthState?
|
||||
|
||||
fun state(): AuthState
|
||||
|
||||
fun service(context: Context): AuthorizationService
|
||||
}
|
||||
|
||||
object OAuthFactory {
|
||||
|
||||
private val oAuth: OAuth
|
||||
|
||||
init {
|
||||
oAuth = DefaultOAuth()
|
||||
}
|
||||
|
||||
fun instance() = oAuth
|
||||
}
|
||||
|
||||
class DefaultOAuth : OAuth {
|
||||
|
||||
companion object {
|
||||
|
||||
private val REDIRECT_URI =
|
||||
Uri.parse("urn:/audio.funkwhale.funkwhale-android/oauth/callback")
|
||||
}
|
||||
|
||||
data class App(val client_id: String, val client_secret: String)
|
||||
|
||||
private val REDIRECT_URI =
|
||||
Uri.parse("urn:/audio.funkwhale.funkwhale-android/oauth/callback")
|
||||
override fun tryState(): AuthState? {
|
||||
|
||||
fun isAuthorized(context: Context): Boolean {
|
||||
val savedState = PowerPreference
|
||||
.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.getString("state")
|
||||
|
||||
return if (savedState != null && savedState.isNotEmpty()) {
|
||||
return AuthState.jsonDeserialize(savedState)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun state(): AuthState = tryState()!!
|
||||
|
||||
override fun isAuthorized(context: Context): Boolean {
|
||||
val state = tryState()
|
||||
return if (state != null) {
|
||||
state.isAuthorized || tryRefreshAccessToken(context)
|
||||
|
@ -46,9 +97,10 @@ object OAuth {
|
|||
}
|
||||
}
|
||||
|
||||
fun state(): AuthState = tryState()!!
|
||||
|
||||
fun tryRefreshAccessToken(context: Context, overrideNeedsTokenRefresh: Boolean = false): Boolean {
|
||||
override fun tryRefreshAccessToken(
|
||||
context: Context,
|
||||
overrideNeedsTokenRefresh: Boolean
|
||||
): Boolean {
|
||||
tryState()?.let { state ->
|
||||
val shouldRefreshAccessToken = overrideNeedsTokenRefresh || state.needsTokenRefresh
|
||||
if (shouldRefreshAccessToken && state.refreshToken != null) {
|
||||
|
@ -71,26 +123,13 @@ object OAuth {
|
|||
}
|
||||
}
|
||||
|
||||
fun tryState(): AuthState? {
|
||||
|
||||
val savedState = PowerPreference
|
||||
.getFileByName(AppContext.PREFS_CREDENTIALS)
|
||||
.getString("state")
|
||||
|
||||
return if (savedState != null && savedState.isNotEmpty()) {
|
||||
return AuthState.jsonDeserialize(savedState)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun init(hostname: String) {
|
||||
override fun init(hostname: String) {
|
||||
AuthState(config(hostname)).save()
|
||||
}
|
||||
|
||||
fun service(context: Context) = AuthorizationService(context)
|
||||
override fun service(context: Context): AuthorizationService = AuthorizationService(context)
|
||||
|
||||
fun register(callback: () -> Unit) {
|
||||
override fun register(callback: () -> Unit) {
|
||||
state().authorizationServiceConfiguration?.let { config ->
|
||||
|
||||
runBlocking {
|
||||
|
@ -133,7 +172,7 @@ object OAuth {
|
|||
)
|
||||
}
|
||||
|
||||
fun authorize(context: Activity) {
|
||||
override fun authorize(context: Activity) {
|
||||
val intent = service(context).run {
|
||||
authorizationRequest()?.let {
|
||||
getAuthorizationRequestIntent(it)
|
||||
|
@ -143,7 +182,12 @@ object OAuth {
|
|||
context.startActivityForResult(intent, 0)
|
||||
}
|
||||
|
||||
fun exchange(context: Activity, authorization: Intent, success: () -> Unit, error: () -> Unit) {
|
||||
override fun exchange(
|
||||
context: Activity,
|
||||
authorization: Intent,
|
||||
success: () -> Unit,
|
||||
error: () -> Unit
|
||||
) {
|
||||
state().let { state ->
|
||||
state.apply {
|
||||
update(
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
package audio.funkwhale.ffa.playback
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import audio.funkwhale.ffa.utils.OAuth
|
||||
import audio.funkwhale.util.MockKJUnitRunner
|
||||
import com.google.android.exoplayer2.upstream.DataSpec
|
||||
import com.google.android.exoplayer2.upstream.HttpDataSource
|
||||
import com.google.android.exoplayer2.upstream.TransferListener
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.InjectMockKs
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import strikt.api.expectThat
|
||||
import strikt.assertions.isEqualTo
|
||||
|
||||
@RunWith(MockKJUnitRunner::class)
|
||||
class OAuthDatasourceTest {
|
||||
|
||||
@InjectMockKs
|
||||
private lateinit var datasource: OAuthDatasource
|
||||
|
||||
@MockK
|
||||
private lateinit var context: Context
|
||||
|
||||
@MockK
|
||||
private lateinit var http: HttpDataSource
|
||||
|
||||
@MockK
|
||||
private lateinit var oAuth: OAuth
|
||||
|
||||
private var dataSpec: DataSpec = DataSpec(Uri.EMPTY)
|
||||
|
||||
@Test
|
||||
fun `open() should set accessToken and delegate to http dataSource`() {
|
||||
every { http.open(any()) } returns 0
|
||||
every { oAuth.tryRefreshAccessToken(any(), any()) } returns true
|
||||
every { oAuth.state().accessToken } returns "accessToken"
|
||||
|
||||
datasource.open(dataSpec)
|
||||
verify { http.open(dataSpec) }
|
||||
verify { http.setRequestProperty("Authorization", "Bearer accessToken") }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close() should delegate to http dataSource`() {
|
||||
datasource.close()
|
||||
verify { http.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `addTransferListener() should delegate to http dataSource`() {
|
||||
val transferListener = mockk<TransferListener>()
|
||||
datasource.addTransferListener(transferListener)
|
||||
verify { http.addTransferListener(transferListener) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read() should delegate to http dataSource`() {
|
||||
every { http.read(any(), any(), any()) } returns 0
|
||||
datasource.read("123".encodeToByteArray(), 1, 2)
|
||||
verify { http.read("123".encodeToByteArray(), 1, 2) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUri() should delegate to http dataSource`() {
|
||||
every { http.uri } returns Uri.EMPTY
|
||||
val result = datasource.uri
|
||||
verify { http.uri }
|
||||
expectThat(result).isEqualTo(Uri.EMPTY)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package audio.funkwhale.util
|
||||
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.clearAllMocks
|
||||
import org.junit.Test
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runner.Runner
|
||||
import org.junit.runner.notification.Failure
|
||||
import org.junit.runner.notification.RunNotifier
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class MockKJUnitRunner(private val testClass: Class<*>) : Runner() {
|
||||
|
||||
private val methodDescriptions: MutableMap<Method, Description> = mutableMapOf()
|
||||
|
||||
init {
|
||||
// Build method/descriptions map
|
||||
testClass.methods
|
||||
.map { method ->
|
||||
val annotation: Annotation? = method.getAnnotation(Test::class.java)
|
||||
method to annotation
|
||||
}
|
||||
.filter { (_, annotation) ->
|
||||
annotation != null
|
||||
}
|
||||
.map { (method, annotation) ->
|
||||
val desc = Description.createTestDescription(testClass, method.name, annotation)
|
||||
method to desc
|
||||
}
|
||||
.forEach { (method, desc) -> methodDescriptions[method] = desc }
|
||||
}
|
||||
|
||||
override fun getDescription(): Description {
|
||||
val description = Description.createSuiteDescription(
|
||||
testClass.name, *testClass.annotations
|
||||
)
|
||||
methodDescriptions.values.forEach { description.addChild(it) }
|
||||
return description
|
||||
}
|
||||
|
||||
override fun run(notifier: RunNotifier?) {
|
||||
val testObject = testClass.newInstance()
|
||||
MockKAnnotations.init(testObject, relaxUnitFun = true)
|
||||
|
||||
methodDescriptions
|
||||
.onEach { (_, _) -> clearAllMocks() }
|
||||
.onEach { (_, desc) -> notifier!!.fireTestStarted(desc) }
|
||||
.forEach { (method, desc) ->
|
||||
try {
|
||||
method.invoke(testObject)
|
||||
} catch (e: Throwable) {
|
||||
notifier!!.fireTestFailure(Failure(desc, e.cause))
|
||||
} finally {
|
||||
notifier!!.fireTestFinished(desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue