[db] Add plumping for fetching and adding a new repo

This commit is contained in:
Torsten Grote
2023-06-23 18:14:30 -03:00
parent 937fe99062
commit f3e8a0a45b
18 changed files with 1640 additions and 1 deletions

View File

@@ -0,0 +1,32 @@
package org.fdroid.download
import android.net.Uri
import org.fdroid.IndexFile
import org.fdroid.database.Repository
import java.io.File
internal class TestDownloadFactory(private val httpManager: HttpManager) : DownloaderFactory() {
override fun create(
repo: Repository,
uri: Uri,
indexFile: IndexFile,
destFile: File,
): Downloader = HttpDownloaderV2(
httpManager = httpManager,
request = DownloadRequest(indexFile, repo.getMirrors()),
destFile = destFile
)
override fun create(
repo: Repository,
mirrors: List<Mirror>,
uri: Uri,
indexFile: IndexFile,
destFile: File,
tryFirst: Mirror?,
): Downloader = HttpDownloaderV2(
httpManager = httpManager,
request = DownloadRequest(indexFile, repo.getMirrors()),
destFile = destFile
)
}

View File

@@ -0,0 +1,135 @@
package org.fdroid.repo
import androidx.core.os.LocaleListCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import app.cash.turbine.test
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.NewRepository
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.download.HttpManager
import org.fdroid.download.TestDownloadFactory
import org.fdroid.index.TempFileProvider
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class)
internal class RepoAdderIntegrationTest {
@get:Rule
var folder: TemporaryFolder = TemporaryFolder()
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val db = mockk<FDroidDatabase>()
private val repoDao = mockk<RepositoryDaoInt>()
private val tempFileProvider = TempFileProvider { folder.newFile() }
private val httpManager = HttpManager("test")
private val downloaderFactory = TestDownloadFactory(httpManager)
private val repoAdder: RepoAdder
init {
every { db.getRepositoryDao() } returns repoDao
repoAdder = RepoAdder(context, db, tempFileProvider, downloaderFactory, httpManager)
}
@Before
fun optIn() {
assumeTrue(false) // don't run integration tests with real repos all the time
}
@Test
fun testFedilabV1() = runTest {
// repo not in DB
every { repoDao.getRepository(any<String>()) } returns null
repoAdder.fetchRepository(
url = "https://fdroid.fedilab.app/repo/" +
"?fingerprint=11F0A69910A4280E2CD3CCC3146337D006BE539B18E1A9FEACE15FF757A94FEB",
username = null,
password = null,
proxy = null
)
repoAdder.addRepoState.test {
assertEquals(None, awaitItem())
val firstFetching = awaitItem()
assertTrue(firstFetching is Fetching)
assertNull(firstFetching.repo)
assertTrue(firstFetching.apps.isEmpty())
val secondFetching = awaitItem()
assertTrue(secondFetching is Fetching, "$secondFetching")
val repo = secondFetching.repo
assertNotNull(repo)
assertEquals("https://fdroid.fedilab.app/repo", repo.address)
println(repo.getName(LocaleListCompat.getDefault()) ?: "null")
println(repo.certificate)
assertEquals(1, (awaitItem() as Fetching).apps.size)
assertEquals(2, (awaitItem() as Fetching).apps.size)
assertEquals(3, (awaitItem() as Fetching).apps.size)
assertTrue(awaitItem() is Fetching)
}
val state = repoAdder.addRepoState.value
assertTrue(state is Fetching, state.toString())
assertTrue(state.apps.isNotEmpty())
state.apps.forEach { app ->
println(" ${app.packageName} ${app.summary}")
}
val newRepo: Repository = mockk()
every { repoDao.insert(any<NewRepository>()) } returns 42L
every { repoDao.getRepository(42L) } returns newRepo
repoAdder.addFetchedRepository()
repoAdder.addRepoState.test {
val addedState = awaitItem()
assertTrue(addedState is Added, addedState.toString())
assertEquals(newRepo, addedState.repo)
}
}
@Test
fun testIzzy() = runBlocking {
// repo not in DB
every { repoDao.getRepository(any<String>()) } returns null
repoAdder.fetchRepositoryInt(
url = "https://apt.izzysoft.de/fdroid/repo" +
"?fingerprint=3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A"
)
val state = repoAdder.addRepoState.value
assertTrue(state is Fetching, state.toString())
assertTrue(state.apps.isNotEmpty())
println(state.repo?.getName(LocaleListCompat.getDefault()) ?: "null")
println(state.repo?.certificate)
state.apps.forEach { app ->
println(" ${app.packageName} ${app.summary}")
}
}
@Test
fun testIzzyWrongFingerprint() = runBlocking {
repoAdder.fetchRepositoryInt("https://apt.izzysoft.de/fdroid/repo?fingerprint=fooBar")
val state = repoAdder.addRepoState.value
assertTrue(state is AddRepoError, state.toString())
assertEquals(state.errorType, INVALID_FINGERPRINT, state.errorType.name)
}
}

View File

@@ -0,0 +1,561 @@
package org.fdroid.repo
import android.content.Context
import android.content.res.AssetManager
import android.net.Uri
import android.os.UserManager
import android.os.UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES
import androidx.core.os.LocaleListCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import app.cash.turbine.test
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import kotlinx.coroutines.test.runTest
import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.Mirror
import org.fdroid.database.NewRepository
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDaoInt
import org.fdroid.database.RepositoryPreferences
import org.fdroid.database.toCoreRepository
import org.fdroid.download.Downloader
import org.fdroid.download.DownloaderFactory
import org.fdroid.download.HttpManager
import org.fdroid.download.NotFoundException
import org.fdroid.download.getDigestInputStream
import org.fdroid.fdroid.DigestInputStream
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.SigningException
import org.fdroid.index.TempFileProvider
import org.fdroid.repo.AddRepoError.ErrorType.INVALID_FINGERPRINT
import org.fdroid.repo.AddRepoError.ErrorType.IO_ERROR
import org.fdroid.repo.AddRepoError.ErrorType.UNKNOWN_SOURCES_DISALLOWED
import org.fdroid.test.TestDataMinV2
import org.fdroid.test.TestUtils.decodeHex
import org.fdroid.test.VerifierConstants
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.runner.RunWith
import java.io.ByteArrayInputStream
import java.io.IOException
import java.security.MessageDigest
import java.util.concurrent.Callable
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
@RunWith(AndroidJUnit4::class)
internal class RepoAdderTest {
@get:Rule
var folder: TemporaryFolder = TemporaryFolder()
private val context = InstrumentationRegistry.getInstrumentation().targetContext
private val db = mockk<FDroidDatabase>()
private val repoDao = mockk<RepositoryDaoInt>()
private val tempFileProvider = mockk<TempFileProvider>()
private val httpManager = mockk<HttpManager>()
private val downloaderFactory = mockk<DownloaderFactory>()
private val downloader = mockk<Downloader>()
private val digest = mockk<MessageDigest>()
private val repoAdder: RepoAdder
private val assets: AssetManager = context.resources.assets
private val localeList = LocaleListCompat.getDefault()
init {
every { db.getRepositoryDao() } returns repoDao
every { digest.update(any(), any(), any()) } just Runs
every { digest.update(any<Byte>()) } just Runs
mockkStatic("org.fdroid.download.HttpManagerKt")
repoAdder = RepoAdder(context, db, tempFileProvider, downloaderFactory, httpManager)
}
@Test
fun testDisallowInstallUnknownSources() = runTest {
val context = mockk<Context>()
val userManager = mockk<UserManager>()
val repoAdder = RepoAdder(context, db, tempFileProvider, downloaderFactory, httpManager)
every { context.getSystemService("user") } returns userManager
every {
userManager.hasUserRestriction(DISALLOW_INSTALL_UNKNOWN_SOURCES)
} returns true
repoAdder.fetchRepositoryInt("https://example.org/repo/")
repoAdder.addRepoState.test {
val state1 = awaitItem()
assertTrue(state1 is AddRepoError)
assertEquals(UNKNOWN_SOURCES_DISALLOWED, state1.errorType)
}
}
@Test
fun testAddingMinRepo() = runTest {
val url = "https://example.org/repo/"
val repoName = TestDataMinV2.repo.name.getBestLocale(localeList)
expectDownloadOfMinRepo(url)
// repo not in DB
every { repoDao.getRepository(any<String>()) } returns null
expectMinRepoPreview(repoName, FetchResult.IsNewRepository) {
repoAdder.fetchRepository(
url = url,
username = null,
password = null,
proxy = null,
)
}
val newRepo: Repository = mockk()
every {
repoDao.insert(match<NewRepository> {
// Note that we are not using the url the user used to add the repo,
// but what the repo tells us to use
it.address == TestDataMinV2.repo.address &&
it.formatVersion == IndexFormatVersion.TWO &&
it.name.getBestLocale(localeList) == repoName
})
} returns 42L
every { repoDao.getRepository(42L) } returns newRepo
repoAdder.addRepoState.test {
assertIs<Fetching>(awaitItem()) // still Fetching from last call
repoAdder.addFetchedRepository()
assertIs<Adding>(awaitItem()) // now moved to Adding
val addedState = awaitItem()
assertIs<Added>(addedState)
assertEquals(newRepo, addedState.repo)
}
}
@Test
fun testAddingMirrorForMinRepo() = runTest {
val url = "https://example.com/repo/"
val repoName = TestDataMinV2.repo.name.getBestLocale(localeList)
expectDownloadOfMinRepo(url)
// repo is already in the DB
val existingRepo = Repository(
repository = TestDataMinV2.repo.toCoreRepository(
repoId = 42L,
version = 1337L,
formatVersion = IndexFormatVersion.TWO,
certificate = "cert",
),
mirrors = emptyList(),
antiFeatures = emptyList(),
categories = emptyList(),
releaseChannels = emptyList(),
preferences = RepositoryPreferences(42L, 23),
)
every { repoDao.getRepository(any<String>()) } returns existingRepo
expectMinRepoPreview(repoName, FetchResult.IsNewMirror(42L, url.trimEnd('/'))) {
repoAdder.fetchRepository(
url = url,
username = null,
password = null,
proxy = null
)
}
val transactionSlot = slot<Callable<Repository>>()
every {
db.runInTransaction(capture(transactionSlot))
} answers { transactionSlot.captured.call() }
every { repoDao.getRepository(42L) } returns existingRepo
every { repoDao.updateUserMirrors(42L, listOf(url.trimEnd('/'))) } just Runs
repoAdder.addRepoState.test {
assertIs<Fetching>(awaitItem()) // still Fetching from last call
repoAdder.addFetchedRepository()
assertIs<Adding>(awaitItem()) // now moved to Adding
val addedState = awaitItem()
assertTrue(addedState is Added, addedState.toString())
assertEquals(existingRepo, addedState.repo)
}
}
@Test
fun testRepoAlreadyExists() = runTest {
val url = "https://min-v1.org/repo/"
// repo is already in the DB
val existingRepo = Repository(
repository = TestDataMinV2.repo.toCoreRepository(
repoId = REPO_ID,
version = 1337L,
formatVersion = IndexFormatVersion.TWO,
certificate = "cert",
).copy(address = url), // change address, because TestDataMinV2 misses /repo
mirrors = emptyList(),
antiFeatures = emptyList(),
categories = emptyList(),
releaseChannels = emptyList(),
preferences = RepositoryPreferences(REPO_ID, 23),
)
testRepoAlreadyExists(url, existingRepo)
}
@Test
fun testRepoAlreadyExistsWithMirror() = runTest {
val url = "https://example.org/repo/"
// repo is already in the DB
val existingRepo = Repository(
repository = TestDataMinV2.repo.toCoreRepository(
repoId = REPO_ID,
version = 1337L,
formatVersion = IndexFormatVersion.TWO,
certificate = "cert",
),
mirrors = listOf(Mirror(REPO_ID, url), Mirror(REPO_ID, "http://example.org")),
antiFeatures = emptyList(),
categories = emptyList(),
releaseChannels = emptyList(),
preferences = RepositoryPreferences(REPO_ID, 23),
)
testRepoAlreadyExists(url, existingRepo)
}
@Test
fun testRepoAlreadyExistsWithFingerprint() = runTest {
val url = "https://example.org/repo?fingerprint=${VerifierConstants.FINGERPRINT}"
// repo is already in the DB
val existingRepo = Repository(
repository = TestDataMinV2.repo.toCoreRepository(
repoId = REPO_ID,
version = 1337L,
formatVersion = IndexFormatVersion.TWO,
certificate = VerifierConstants.CERTIFICATE,
),
mirrors = listOf(Mirror(REPO_ID, "https://example.org/repo/")),
antiFeatures = emptyList(),
categories = emptyList(),
releaseChannels = emptyList(),
preferences = RepositoryPreferences(REPO_ID, 23),
)
testRepoAlreadyExists(url, existingRepo, "https://example.org/repo")
}
@Test
fun testRepoAlreadyExistsWithFingerprintTrailingSlash() = runTest {
val url = "https://example.org/repo/?fingerprint=${VerifierConstants.FINGERPRINT}"
// repo is already in the DB
val existingRepo = Repository(
repository = TestDataMinV2.repo.toCoreRepository(
repoId = REPO_ID,
version = 1337L,
formatVersion = IndexFormatVersion.TWO,
certificate = VerifierConstants.CERTIFICATE,
),
mirrors = listOf(
Mirror(REPO_ID, "https://example.org/repo"),
Mirror(REPO_ID, "http://example.org"),
),
antiFeatures = emptyList(),
categories = emptyList(),
releaseChannels = emptyList(),
preferences = RepositoryPreferences(REPO_ID, 23),
)
testRepoAlreadyExists(url, existingRepo, "https://example.org/repo")
}
@Test
fun testRepoAlreadyExistsUserMirror() = runTest {
val url = "https://example.net/repo/"
// repo is already in the DB
val existingRepo = Repository(
repository = TestDataMinV2.repo.toCoreRepository(
repoId = REPO_ID,
version = 1337L,
formatVersion = IndexFormatVersion.TWO,
certificate = "cert",
),
mirrors = emptyList(),
antiFeatures = emptyList(),
categories = emptyList(),
releaseChannels = emptyList(),
preferences = RepositoryPreferences(
repoId = REPO_ID,
weight = 23,
userMirrors = listOf(url, "http://example.org"),
),
)
testRepoAlreadyExists(url, existingRepo)
}
private suspend fun testRepoAlreadyExists(
url: String,
existingRepo: Repository,
downloadUrl: String = url,
) {
val repoName = TestDataMinV2.repo.name.getBestLocale(localeList)
expectDownloadOfMinRepo(downloadUrl)
// repo is already in the DB
every { repoDao.getRepository(any<String>()) } returns existingRepo
expectMinRepoPreview(repoName, FetchResult.IsExistingRepository, canAdd = false) {
repoAdder.fetchRepository(
url = url,
username = null,
password = null,
proxy = null
)
}
assertFailsWith<IllegalStateException> {
repoAdder.addFetchedRepository()
}
}
@Test
fun testDownloadEntryThrowsIoException() = runTest {
val url = "https://example.org/repo"
val jarFile = folder.newFile()
every { tempFileProvider.createTempFile() } returns jarFile
every {
downloaderFactory.create(
repo = match { it.address == url && it.formatVersion == IndexFormatVersion.TWO },
uri = Uri.parse("$url/entry.jar"),
indexFile = any(),
destFile = jarFile,
)
} returns downloader
every { downloader.download() } throws IOException()
repoAdder.addRepoState.test {
assertIs<None>(awaitItem())
repoAdder.fetchRepository(
url = url,
username = null,
password = null,
proxy = null
)
val state1 = awaitItem()
assertIs<Fetching>(state1)
assertNull(state1.repo)
assertTrue(state1.apps.isEmpty())
assertFalse(state1.canAdd)
val state2 = awaitItem()
assertTrue(state2 is AddRepoError, "$state2")
assertEquals(IO_ERROR, state2.errorType)
}
}
@Test
fun testWrongFingerprint() = runTest {
val url = "https://example.org/repo/"
val jarFile = folder.newFile()
every { tempFileProvider.createTempFile() } returns jarFile
every {
downloaderFactory.create(
repo = match {
it.address == url.trimEnd('/') && it.formatVersion == IndexFormatVersion.TWO
},
uri = Uri.parse(url + "entry.jar"),
indexFile = any(),
destFile = jarFile,
)
} returns downloader
// not actually thrown by the downloader, but mocking verifier is harder
every { downloader.download() } throws SigningException("boom!")
repoAdder.addRepoState.test {
assertIs<None>(awaitItem())
repoAdder.fetchRepository(
url = url,
username = null,
password = null,
proxy = null
)
val state1 = awaitItem()
assertIs<Fetching>(state1)
assertNull(state1.repo)
assertTrue(state1.apps.isEmpty())
assertFalse(state1.canAdd)
val state2 = awaitItem()
assertTrue(state2 is AddRepoError, "$state2")
assertEquals(INVALID_FINGERPRINT, state2.errorType)
}
}
@Test
fun testFallbackToV1() = runTest {
val url = "https://example.org/repo/"
val urlTrimmed = "https://example.org/repo"
val jarFile = folder.newFile()
every { tempFileProvider.createTempFile() } returns jarFile
every {
downloaderFactory.create(
repo = match {
it.address == urlTrimmed && it.formatVersion == IndexFormatVersion.TWO
},
uri = Uri.parse("$urlTrimmed/entry.jar"),
indexFile = any(),
destFile = jarFile,
)
} returns downloader
every { downloader.download() } throws NotFoundException()
val downloaderV1 = mockk<Downloader>()
every {
downloaderFactory.create(
repo = match {
it.address == urlTrimmed && it.formatVersion == IndexFormatVersion.ONE
},
uri = Uri.parse("$urlTrimmed/index-v1.jar"),
indexFile = any(),
destFile = jarFile,
)
} returns downloaderV1
every { downloaderV1.download() } answers {
jarFile.outputStream().use { outputStream ->
assets.open("testy.at.or.at_index-v1.jar").use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
every { repoDao.getRepository(any<String>()) } returns null
repoAdder.addRepoState.test {
assertIs<None>(awaitItem())
repoAdder.fetchRepository(
url = url,
username = null,
password = null,
proxy = null
)
val state1 = awaitItem()
assertIs<Fetching>(state1)
assertNull(state1.repo)
assertTrue(state1.apps.isEmpty())
assertFalse(state1.canAdd)
for (i in 0..64) assertIs<Fetching>(awaitItem())
}
val addRepoState = repoAdder.addRepoState.value
assertIs<Fetching>(addRepoState)
assertTrue(addRepoState.canAdd)
assertEquals(63, addRepoState.apps.size)
}
private fun expectDownloadOfMinRepo(url: String) {
val urlTrimmed = url.trimEnd('/')
val jarFile = folder.newFile()
val indexFile = assets.open("index-min-v2.json")
val index = indexFile.use { it.readBytes() }
val indexStream = DigestInputStream(ByteArrayInputStream(index), digest)
every { tempFileProvider.createTempFile() } returns jarFile
every {
downloaderFactory.create(
repo = match {
it.address == urlTrimmed && it.formatVersion == IndexFormatVersion.TWO
},
uri = Uri.parse("$urlTrimmed/entry.jar"),
indexFile = any(),
destFile = jarFile,
)
} returns downloader
every { downloader.download() } answers {
jarFile.outputStream().use { outputStream ->
assets.open("diff-empty-min/entry.jar").use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
coEvery {
httpManager.getDigestInputStream(match {
it.indexFile.name == "../index-min-v2.json" &&
it.mirrors.size == 1 && it.mirrors[0].baseUrl == urlTrimmed
})
} returns indexStream
every {
digest.digest() // sha256 from entry.json
} returns "851ecda085ed53adab25f761a9dbf4c09d59e5bff9c9d5530814d56445ae30f2".decodeHex()
}
private suspend fun expectMinRepoPreview(
repoName: String?,
fetchResult: FetchResult,
canAdd: Boolean = true,
block: suspend () -> Unit = {},
) {
repoAdder.addRepoState.test {
assertIs<None>(awaitItem())
block()
// FIXME executing this block may emit items too fast, so we might miss one
// causing flaky tests.
val state1 = awaitItem()
assertIs<Fetching>(state1)
assertNull(state1.repo)
assertEquals(emptyList(), state1.apps)
assertFalse(state1.canAdd)
assertFalse(state1.done)
val state2 = awaitItem()
assertIs<Fetching>(state2)
val repo = state2.repo ?: fail()
assertEquals(TestDataMinV2.repo.address, repo.address)
assertEquals(repoName, repo.getName(localeList))
val result = state2.fetchResult ?: fail()
assertEquals(fetchResult, result)
assertTrue(state2.apps.isEmpty())
assertEquals(canAdd, state2.canAdd)
assertFalse(state2.done)
val state3 = awaitItem()
assertIs<Fetching>(state3)
assertEquals(TestDataMinV2.packages.size, state3.apps.size)
assertEquals(TestDataMinV2.packageName, state3.apps[0].packageName)
assertEquals(canAdd, state3.canAdd)
assertFalse(state3.done)
val state4 = awaitItem()
assertIs<Fetching>(state4)
assertEquals(canAdd, state4.canAdd)
assertTrue(state4.done)
}
}
}

View File

@@ -0,0 +1,59 @@
package org.fdroid.repo
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.test.assertEquals
import kotlin.test.assertNull
@RunWith(AndroidJUnit4::class)
internal class RepoUriGetterTest {
@Test
fun testTrailingSlash() {
val uri = RepoUriGetter.getUri("http://example.org/fdroid/repo/")
assertEquals("http://example.org/fdroid/repo", uri.uri.toString())
assertNull(uri.fingerprint)
}
@Test
fun testWithoutTrailingSlash() {
val uri = RepoUriGetter.getUri("http://example.org/fdroid/repo")
assertEquals("http://example.org/fdroid/repo", uri.uri.toString())
assertNull(uri.fingerprint)
}
@Test
fun testFingerprint() {
val uri = RepoUriGetter.getUri("https://example.org/repo?fingerprint=foobar&test=42")
assertEquals("https://example.org/repo", uri.uri.toString())
assertEquals("foobar", uri.fingerprint)
}
@Test
fun testHash() {
val uri = RepoUriGetter.getUri("https://example.org/repo?fingerprint=foobar&test=42#hash")
assertEquals("https://example.org/repo", uri.uri.toString())
assertEquals("foobar", uri.fingerprint)
}
@Test
fun testAddFdroidSlashRepo() {
val uri = RepoUriGetter.getUri("https://example.org")
assertEquals("https://example.org/fdroid/repo", uri.uri.toString())
assertNull(uri.fingerprint)
}
@Test
fun testLeaveSingleRepo() {
val uri = RepoUriGetter.getUri("https://example.org/repo")
assertEquals("https://example.org/repo", uri.uri.toString())
assertNull(uri.fingerprint)
}
@Test
fun testAddsMissingRepo() {
val uri = RepoUriGetter.getUri("https://example.org/fdroid/")
assertEquals("https://example.org/fdroid/repo", uri.uri.toString())
assertNull(uri.fingerprint)
}
}