mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
cover lms and nextcloud music in the integration suite
Run the integration harness from its own devshell so the server packages stay out of the everyday shell. Add lms and a Nextcloud Music instance to the harness and extend the suite with auth-fallback, media, error, write round-trip, multi-source and prune tests. The auth test caught that lms answers OpenSubsonic error 42 (not 41) when it refuses token auth, so the client now falls back on both codes.
This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
||||
package com.subtracks.data.source.subsonic
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class SubsonicAuthIntegrationTest(
|
||||
private val server: TestServer,
|
||||
) {
|
||||
@Test
|
||||
fun tokenAuthFallsBackToPlaintextWhenTheServerRejectsIt() =
|
||||
runBlocking {
|
||||
var fallbacks = 0
|
||||
val source = SubsonicSource(1, TestServers.client(server, useTokenAuth = true) { fallbacks++ })
|
||||
|
||||
source.ping()
|
||||
|
||||
assertEquals("$server: fallback fired once", !server.supportsTokenAuth, fallbacks == 1)
|
||||
|
||||
source.ping()
|
||||
|
||||
assertEquals("$server: fallback is not retried", if (server.supportsTokenAuth) 0 else 1, fallbacks)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun plaintextAuthPings() =
|
||||
runBlocking {
|
||||
TestServers.source(server, useTokenAuth = false).ping()
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun servers(): List<Array<Any>> = TestServers.all.map { arrayOf<Any>(it) }
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.subtracks.data.source.subsonic
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class SubsonicErrorIntegrationTest(
|
||||
private val server: TestServer,
|
||||
) {
|
||||
@Test
|
||||
fun wrongPasswordThrowsCode40() =
|
||||
runBlocking {
|
||||
val bad = server.copy(password = "${server.password}-wrong")
|
||||
val source = SubsonicSource(1, TestServers.client(bad))
|
||||
|
||||
val failure = runCatching { source.ping() }
|
||||
|
||||
val error = failure.exceptionOrNull()
|
||||
assertTrue("$server: $failure", error is SubsonicException)
|
||||
assertEquals("$server", 40, (error as SubsonicException).code)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun unknownMethodThrows() =
|
||||
runBlocking {
|
||||
val client = TestServers.client(server)
|
||||
|
||||
val failure = runCatching { client.check("definitelyNotASubsonicMethod") }
|
||||
|
||||
assertTrue("$server: $failure", failure.exceptionOrNull() is SubsonicException)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun servers(): List<Array<Any>> = TestServers.all.map { arrayOf<Any>(it) }
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.subtracks.data.source.subsonic
|
||||
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class SubsonicMediaIntegrationTest(
|
||||
private val server: TestServer,
|
||||
) {
|
||||
private val http = OkHttpClient()
|
||||
|
||||
@Test
|
||||
fun streamsOriginalAudio() =
|
||||
runBlocking {
|
||||
val source = TestServers.source(server)
|
||||
val song =
|
||||
source
|
||||
.songs()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first()
|
||||
|
||||
get(source.streamUri(song.id).toString()).use { response ->
|
||||
assertTrue("$server: HTTP ${response.code}", response.isSuccessful)
|
||||
val bytes = response.body.bytes()
|
||||
assertTrue("$server: ${bytes.size} bytes", bytes.size > 1000)
|
||||
assertFalse("$server: stream returned a text body", bytes.startsWithText())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadsOriginalAudio() =
|
||||
runBlocking {
|
||||
val source = TestServers.source(server)
|
||||
val song =
|
||||
source
|
||||
.songs()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first()
|
||||
|
||||
get(source.downloadUri(song.id).toString()).use { response ->
|
||||
assertTrue("$server: HTTP ${response.code}", response.isSuccessful)
|
||||
val bytes = response.body.bytes()
|
||||
assertTrue("$server: ${bytes.size} bytes", bytes.size > 1000)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fetchesCoverArtImage() =
|
||||
runBlocking {
|
||||
val source = TestServers.source(server)
|
||||
val album =
|
||||
source
|
||||
.albums()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.coverArt != null }
|
||||
|
||||
get(source.coverArtUri(album.coverArt)!!.toString()).use { response ->
|
||||
assertTrue("$server: HTTP ${response.code}", response.isSuccessful)
|
||||
val bytes = response.body.bytes()
|
||||
assertTrue("$server: ${bytes.size} bytes", bytes.size > 100)
|
||||
assertTrue("$server: not a JPEG or PNG", bytes.isJpegOrPng())
|
||||
}
|
||||
}
|
||||
|
||||
private fun get(url: String): Response = http.newCall(Request.Builder().url(url).build()).execute()
|
||||
|
||||
private fun ByteArray.startsWithText(): Boolean = decodeToString(0, minOf(8, size)).trimStart().startsWith("<")
|
||||
|
||||
private fun ByteArray.isJpegOrPng(): Boolean {
|
||||
val jpeg = size >= 2 && this[0] == 0xFF.toByte() && this[1] == 0xD8.toByte()
|
||||
val png =
|
||||
size >= 4 &&
|
||||
this[0] == 0x89.toByte() &&
|
||||
this[1] == 0x50.toByte() &&
|
||||
this[2] == 0x4E.toByte() &&
|
||||
this[3] == 0x47.toByte()
|
||||
return jpeg || png
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun servers(): List<Array<Any>> = TestServers.all.map { arrayOf<Any>(it) }
|
||||
}
|
||||
}
|
||||
+3
-24
@@ -3,8 +3,6 @@ package com.subtracks.data.source.subsonic
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
@@ -16,21 +14,13 @@ import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class SubsonicSourceIntegrationTest(
|
||||
private val server: Server,
|
||||
private val server: TestServer,
|
||||
) {
|
||||
private lateinit var source: SubsonicSource
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val client =
|
||||
SubsonicClient(
|
||||
baseUrl = server.baseUrl.toHttpUrl(),
|
||||
username = server.username,
|
||||
password = server.password,
|
||||
useTokenAuth = false,
|
||||
http = OkHttpClient(),
|
||||
)
|
||||
source = SubsonicSource(1, client)
|
||||
source = TestServers.source(server)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,20 +97,9 @@ class SubsonicSourceIntegrationTest(
|
||||
|
||||
private suspend fun <T> Flow<List<T>>.collectAll(): List<T> = toList().flatten()
|
||||
|
||||
data class Server(
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun servers(): List<Array<Any>> =
|
||||
listOf(
|
||||
arrayOf(Server("navidrome", "http://localhost:4533/", "admin", "password")),
|
||||
arrayOf(Server("gonic", "http://localhost:4747/", "admin", "admin")),
|
||||
)
|
||||
fun servers(): List<Array<Any>> = TestServers.all.map { arrayOf<Any>(it) }
|
||||
}
|
||||
}
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package com.subtracks.data.source.subsonic
|
||||
|
||||
import com.subtracks.data.source.StarType
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class SubsonicWriteIntegrationTest(
|
||||
private val server: TestServer,
|
||||
) {
|
||||
@Test
|
||||
fun starAndUnstarAlbumRoundTrips() =
|
||||
runBlocking {
|
||||
val source = TestServers.source(server)
|
||||
val album =
|
||||
source
|
||||
.albums()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.name == "Retroconnaissance EP" }
|
||||
|
||||
source.setStar(StarType.Album, album.id, true)
|
||||
try {
|
||||
assertNotNull(
|
||||
"$server",
|
||||
source
|
||||
.albums()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.id == album.id }
|
||||
.starred,
|
||||
)
|
||||
} finally {
|
||||
source.setStar(StarType.Album, album.id, false)
|
||||
}
|
||||
assertNull(
|
||||
"$server",
|
||||
source
|
||||
.albums()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.id == album.id }
|
||||
.starred,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun starAndUnstarArtistRoundTrips() =
|
||||
runBlocking {
|
||||
val source = TestServers.source(server)
|
||||
val artist =
|
||||
source
|
||||
.artists()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.name == "Brad Sucks" }
|
||||
|
||||
source.setStar(StarType.Artist, artist.id, true)
|
||||
try {
|
||||
assertNotNull(
|
||||
"$server",
|
||||
source
|
||||
.artists()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.id == artist.id }
|
||||
.starred,
|
||||
)
|
||||
} finally {
|
||||
source.setStar(StarType.Artist, artist.id, false)
|
||||
}
|
||||
assertNull(
|
||||
"$server",
|
||||
source
|
||||
.artists()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.id == artist.id }
|
||||
.starred,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun starAndUnstarSongRoundTrips() =
|
||||
runBlocking {
|
||||
val source = TestServers.source(server)
|
||||
val song =
|
||||
source
|
||||
.songs()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.album == "Retroconnaissance EP" }
|
||||
|
||||
source.setStar(StarType.Song, song.id, true)
|
||||
try {
|
||||
assertNotNull(
|
||||
"$server",
|
||||
source
|
||||
.songs()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.id == song.id }
|
||||
.starred,
|
||||
)
|
||||
} finally {
|
||||
source.setStar(StarType.Song, song.id, false)
|
||||
}
|
||||
assertNull(
|
||||
"$server",
|
||||
source
|
||||
.songs()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first { it.id == song.id }
|
||||
.starred,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun scrobbleAcceptsNowPlayingAndSubmission() =
|
||||
runBlocking {
|
||||
val source = TestServers.source(server)
|
||||
val song =
|
||||
source
|
||||
.songs()
|
||||
.toList()
|
||||
.flatten()
|
||||
.first()
|
||||
|
||||
source.scrobble(song.id, submission = false)
|
||||
source.scrobble(song.id, submission = true, time = System.currentTimeMillis())
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun servers(): List<Array<Any>> = TestServers.all.map { arrayOf<Any>(it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.subtracks.data.source.subsonic
|
||||
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
|
||||
data class TestServer(
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val supportsTokenAuth: Boolean,
|
||||
) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
object TestServers {
|
||||
val all =
|
||||
listOf(
|
||||
TestServer("navidrome", "http://localhost:4533/", "admin", "password", supportsTokenAuth = true),
|
||||
TestServer("gonic", "http://localhost:4747/", "admin", "admin", supportsTokenAuth = true),
|
||||
TestServer("lms", "http://localhost:5082/", "admin", "subtracks-lms", supportsTokenAuth = false),
|
||||
TestServer(
|
||||
"nextcloud",
|
||||
"http://localhost:8090/index.php/apps/music/subsonic/",
|
||||
"admin",
|
||||
"subtracks-nextcloud",
|
||||
supportsTokenAuth = false,
|
||||
),
|
||||
)
|
||||
|
||||
fun client(
|
||||
server: TestServer,
|
||||
useTokenAuth: Boolean = false,
|
||||
onTokenAuthUnsupported: () -> Unit = {},
|
||||
): SubsonicClient =
|
||||
SubsonicClient(
|
||||
baseUrl = server.baseUrl.toHttpUrl(),
|
||||
username = server.username,
|
||||
password = server.password,
|
||||
useTokenAuth = useTokenAuth,
|
||||
http = OkHttpClient(),
|
||||
onTokenAuthUnsupported = onTokenAuthUnsupported,
|
||||
)
|
||||
|
||||
fun source(
|
||||
server: TestServer,
|
||||
id: Long = 1,
|
||||
useTokenAuth: Boolean = false,
|
||||
): SubsonicSource = SubsonicSource(id, client(server, useTokenAuth))
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.subtracks.data.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.source.subsonic.TestServers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class MultiSourceSyncIntegrationTest {
|
||||
@Test
|
||||
fun syncsEveryServerIntoOneLibraryWithoutInterference() =
|
||||
runBlocking {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val db =
|
||||
Room
|
||||
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
try {
|
||||
TestServers.all.forEachIndexed { index, server ->
|
||||
val id = index + 1L
|
||||
db.sourcesDao().upsertSource(
|
||||
Source(id = id, name = server.name, address = server.baseUrl, isActive = id == 1L, createdAt = 0),
|
||||
)
|
||||
SyncService(db, TestServers.source(server, id = id)).sync()
|
||||
}
|
||||
|
||||
val library = db.libraryDao()
|
||||
TestServers.all.forEachIndexed { index, server ->
|
||||
val id = index + 1L
|
||||
assertEquals(server.name, 2, library.artistIds(id).size)
|
||||
assertEquals(server.name, 3, library.albumIds(id).size)
|
||||
assertEquals(server.name, 20, library.songIdsAfter(id, "", 1000).size)
|
||||
assertEquals(server.name, 1, library.playlistIds(id).size)
|
||||
}
|
||||
|
||||
SyncService(db, TestServers.source(TestServers.all.first(), id = 1)).sync()
|
||||
|
||||
TestServers.all.drop(1).forEachIndexed { index, server ->
|
||||
assertEquals("$server survived a resync of another source", 3, library.albumIds(index + 2L).size)
|
||||
}
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.subtracks.data.sync
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room3.Room
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.source.subsonic.SubsonicClient
|
||||
import com.subtracks.data.source.subsonic.SubsonicSource
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.io.File
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class PruneSyncIntegrationTest {
|
||||
@Test
|
||||
fun resyncPrunesAlbumsAndSongsRemovedFromTheServer() =
|
||||
runBlocking {
|
||||
val baseUrl = requireProperty("prune.baseUrl")
|
||||
val musicDir = requireProperty("prune.musicDir")
|
||||
val removed = File(musicDir, REMOVED_ALBUM_DIR)
|
||||
check(removed.isDirectory) { "$removed is missing; the harness must prepare the prune library" }
|
||||
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
val db =
|
||||
Room
|
||||
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
|
||||
.setDriver(BundledSQLiteDriver())
|
||||
.build()
|
||||
try {
|
||||
db.sourcesDao().upsertSource(
|
||||
Source(id = 1, name = "prune", address = baseUrl, isActive = true, createdAt = 0),
|
||||
)
|
||||
val source =
|
||||
SubsonicSource(
|
||||
1,
|
||||
SubsonicClient(
|
||||
baseUrl = baseUrl.toHttpUrl(),
|
||||
username = "admin",
|
||||
password = "password",
|
||||
useTokenAuth = false,
|
||||
http = OkHttpClient(),
|
||||
),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
val library = db.libraryDao()
|
||||
assertEquals(1, library.artistIds(1).size)
|
||||
assertEquals(2, library.albumIds(1).size)
|
||||
assertEquals(8, library.songIdsAfter(1, "", 1000).size)
|
||||
|
||||
removed.deleteRecursively()
|
||||
rescan(baseUrl)
|
||||
SyncService(db, source).sync()
|
||||
|
||||
assertEquals(1, library.artistIds(1).size)
|
||||
assertEquals(1, library.albumIds(1).size)
|
||||
assertEquals(4, library.songIdsAfter(1, "", 1000).size)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
private fun rescan(baseUrl: String) {
|
||||
val http = OkHttpClient()
|
||||
val root = baseUrl.trimEnd('/')
|
||||
|
||||
fun call(
|
||||
method: String,
|
||||
extra: String = "",
|
||||
): String {
|
||||
val url = "$root/rest/$method.view?u=admin&p=password&v=1.16.1&c=subtracks-test&f=json$extra"
|
||||
return http.newCall(Request.Builder().url(url).build()).execute().use { it.body.string() }
|
||||
}
|
||||
|
||||
call("startScan")
|
||||
repeat(120) {
|
||||
if (!call("getScanStatus").contains("\"scanning\":true")) return
|
||||
Thread.sleep(500)
|
||||
}
|
||||
error("the prune server did not finish rescanning")
|
||||
}
|
||||
|
||||
private fun requireProperty(name: String): String =
|
||||
System.getProperty(name)?.takeIf { it.isNotBlank() }
|
||||
?: error("$name is not set; run the integration harness (tools/integration-test.nu)")
|
||||
|
||||
private companion object {
|
||||
const val REMOVED_ALBUM_DIR = "197"
|
||||
}
|
||||
}
|
||||
+3
-30
@@ -7,11 +7,8 @@ import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Source
|
||||
import com.subtracks.data.source.subsonic.SubsonicClient
|
||||
import com.subtracks.data.source.subsonic.SubsonicSource
|
||||
import com.subtracks.data.source.subsonic.TestServers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.OkHttpClient
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -32,20 +29,7 @@ class SyncServiceIntegrationTest {
|
||||
db.sourcesDao().upsertSource(
|
||||
Source(id = 1, name = server.name, address = server.baseUrl, isActive = true, createdAt = 0),
|
||||
)
|
||||
val source =
|
||||
SubsonicSource(
|
||||
id = 1,
|
||||
client =
|
||||
SubsonicClient(
|
||||
baseUrl = server.baseUrl.toHttpUrl(),
|
||||
username = server.username,
|
||||
password = server.password,
|
||||
useTokenAuth = false,
|
||||
http = OkHttpClient(),
|
||||
),
|
||||
)
|
||||
|
||||
SyncService(db, source).sync()
|
||||
SyncService(db, TestServers.source(server)).sync()
|
||||
|
||||
val library = db.libraryDao()
|
||||
assertEquals(server.name, 2, library.artistIds(1).size)
|
||||
@@ -58,18 +42,7 @@ class SyncServiceIntegrationTest {
|
||||
}
|
||||
}
|
||||
|
||||
private data class Server(
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
val servers =
|
||||
listOf(
|
||||
Server("navidrome", "http://localhost:4533/", "admin", "password"),
|
||||
Server("gonic", "http://localhost:4747/", "admin", "admin"),
|
||||
)
|
||||
val servers = TestServers.all
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class SubsonicClient(
|
||||
return try {
|
||||
execute(method, params, body)
|
||||
} catch (failure: SubsonicException) {
|
||||
if (failure.code != TOKEN_AUTH_UNSUPPORTED || !usedTokenAuth) throw failure
|
||||
if (failure.code !in TOKEN_AUTH_UNSUPPORTED_CODES || !usedTokenAuth) throw failure
|
||||
disableTokenAuth()
|
||||
execute(method, params, body)
|
||||
}
|
||||
@@ -95,7 +95,10 @@ class SubsonicClient(
|
||||
// stays at the highest minor they accept. OpenSubsonic fields do not depend on it.
|
||||
const val API_VERSION = "1.15.0"
|
||||
const val CLIENT = "subtracks"
|
||||
const val TOKEN_AUTH_UNSUPPORTED = 41
|
||||
|
||||
// 41 is the classic Subsonic code for "token authentication not supported"; OpenSubsonic
|
||||
// servers (lms) answer 42 instead, so both must disable token auth and retry.
|
||||
val TOKEN_AUTH_UNSUPPORTED_CODES = setOf(41, 42)
|
||||
|
||||
private fun randomSalt(): String = (1..4).map { ('a'..'z').random() }.joinToString("")
|
||||
|
||||
|
||||
@@ -148,6 +148,21 @@ class SubsonicClientTest {
|
||||
assertEquals(1, notified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun openSubsonicAuthUnsupportedCodeAlsoRetriesWithPassword() {
|
||||
server.enqueue(MockResponse().setBody(failed(42, "Provided authentication mechanism not supported.")))
|
||||
server.enqueue(MockResponse().setBody("<subsonic-response status=\"ok\" version=\"1.16.1\"/>"))
|
||||
var notified = 0
|
||||
|
||||
client(tokenAuth = true, onTokenAuthUnsupported = { notified++ }).check("ping")
|
||||
|
||||
val first = server.takeRequest().requestUrl!!
|
||||
assertNotNull(first.queryParameter("t"))
|
||||
val second = server.takeRequest().requestUrl!!
|
||||
assertEquals("secret", second.queryParameter("p"))
|
||||
assertEquals(1, notified)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun tokenAuthUnsupportedIsNotifiedOnceAndNotRetriedAgain() {
|
||||
server.enqueue(MockResponse().setBody(failed(41, "Token authentication not supported for LDAP users.")))
|
||||
|
||||
Reference in New Issue
Block a user