Clean up app gradle modules

This commit is contained in:
Torsten Grote
2025-12-29 15:07:21 -03:00
parent 83f8f80332
commit d3164687cd
876 changed files with 966 additions and 980 deletions

View File

@@ -1,290 +0,0 @@
package org.fdroid.fdroid
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.verify
import io.mockk.verifyOrder
import org.fdroid.database.AppManifest
import org.fdroid.database.AppVersion
import org.fdroid.database.DbUpdateChecker
import org.fdroid.database.Repository
import org.fdroid.database.UpdatableApp
import org.fdroid.download.Downloader
import org.fdroid.fdroid.data.App
import org.fdroid.fdroid.installer.InstallManagerService
import org.fdroid.fdroid.installer.Installer
import org.fdroid.fdroid.installer.InstallerFactory
import org.fdroid.index.RepoManager
import org.fdroid.index.v2.FileV1
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.File
import java.io.IOException
class AppUpdateManagerTest {
private val context: Context = mockk()
private val repoManager: RepoManager = mockk()
private val updateChecker: DbUpdateChecker = mockk()
private val preferences: Preferences = mockk()
private val downloaderFactory: org.fdroid.download.DownloaderFactory = mockk()
private val statusManager: AppUpdateStatusManager = mockk()
private val appUpdateManager =
AppUpdateManager(context, repoManager, updateChecker, downloaderFactory, statusManager)
private val downloader: Downloader = mockk()
private val installer: Installer = mockk()
private val packageManager: PackageManager = mockk()
private val installManagerService: InstallManagerService = mockk(relaxed = true)
private val repoUri: Uri = mockk(relaxed = true)
private val app1 = App().apply {
packageName = "org.example.1"
name = "One"
repoId = 42
}
private val app2 = App().apply {
packageName = "org.example.2"
name = "Two"
repoId = 42
}
private val repo: Repository = mockk(relaxed = true) {
every { repoId } returns app1.repoId
every { address } returns "https://example.org/repo"
}
private val file1 = FileV1("one", "one")
private val file2 = FileV1("two", "one")
private val version1: AppVersion = mockk<AppVersion>(relaxed = true) {
every { repoId } returns app1.repoId
every { packageName } returns app1.packageName
every { manifest } returns AppManifest("1", 1)
every { file } returns file1
}
private val version2: AppVersion = mockk<AppVersion>(relaxed = true) {
every { repoId } returns app2.repoId
every { packageName } returns app2.packageName
every { manifest } returns AppManifest("2", 2)
every { file } returns file2
}
// need to mock apps as their constructor is internal
private val updatableApp1: UpdatableApp = mockk(relaxed = true) {
every { packageName } returns app1.packageName
every { name } returns app1.name
every { summary } returns app1.summary
every { repoId } returns app1.repoId
every { update } returns version1
every { update.file } returns file1
every { update.manifest } returns AppManifest("1", 1)
every { update.repoId } returns app1.repoId
every { update.packageName } returns app1.packageName
every { installedVersionCode } returns app1.installedVersionCode
}
private val updatableApp2: UpdatableApp = mockk(relaxed = true) {
every { packageName } returns app2.packageName
every { name } returns app2.name
every { summary } returns app2.summary
every { repoId } returns app2.repoId
every { update } returns version2
every { update.repoId } returns app2.repoId
every { update.packageName } returns app2.packageName
every { update.file } returns file2
every { update.manifest } returns AppManifest("2", 2)
every { installedVersionCode } returns app2.installedVersionCode
}
init {
mockkStatic(Preferences::get)
every { Preferences.get() } returns preferences
mockkStatic(InstallManagerService::getInstance)
every { InstallManagerService.getInstance(any()) } returns installManagerService
mockkStatic(Uri::parse)
every { Uri.parse(any()) } returns repoUri
mockkStatic(InstallerFactory::create)
every { InstallerFactory.create(any(), any(), any()) } returns installer
every { context.packageManager } returns packageManager
}
@Test
fun testNoUpdates() {
val updates = emptyList<UpdatableApp>()
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { statusManager.addUpdatableApps(updates, false) } just Runs
assertTrue(appUpdateManager.updateApps())
}
@Test
fun testSomeUpdates() {
val updates = listOf(updatableApp1, updatableApp2)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns null
every { repoManager.getRepository(app1.repoId) } returns repo
every { repoManager.getRepository(app2.repoId) } returns repo
every { statusManager.addUpdatableApps(updates, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(any<String>(), any<Int>())
} returns getPackageInfo(0)
every { context.cacheDir } returns File("/tmp/fdroid-app-update-test")
every { downloaderFactory.create(repo, any(), file1, any()) } returns downloader
every { downloaderFactory.create(repo, any(), file2, any()) } returns downloader
every { downloader.setListener(any()) } just Runs
every { downloader.download() } just Runs
every { installer.installPackage(any(), any()) } just Runs
assertTrue(appUpdateManager.updateApps())
verify(exactly = 2) {
installManagerService.onDownloadComplete(any())
installer.installPackage(any(), any())
}
}
@Test
fun testVersionAlreadyInstalled() {
val updates = listOf(updatableApp1, updatableApp2)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns null
every { repoManager.getRepository(app1.repoId) } returns repo
every { repoManager.getRepository(app2.repoId) } returns repo
every { statusManager.addUpdatableApps(updates, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(app1.packageName, any<Int>())
} returns getPackageInfo(1)
every {
packageManager.getPackageInfo(app2.packageName, any<Int>())
} returns getPackageInfo(2)
assertTrue(appUpdateManager.updateApps())
verify(exactly = 0) {
installManagerService.onDownloadComplete(any())
installer.installPackage(any(), any())
}
}
@Test
fun testOurOwnAppIsUpdatedLast() {
val installer1: Installer = mockk()
val installer2: Installer = mockk()
val updates = listOf(updatableApp1, updatableApp2)
val updatesSorted = listOf(updatableApp2, updatableApp1)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns app1.packageName // we are app1
every { repoManager.getRepository(app1.repoId) } returns repo
every { repoManager.getRepository(app2.repoId) } returns repo
every { statusManager.addUpdatableApps(updatesSorted, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(any<String>(), any<Int>())
} returns getPackageInfo(0)
every { context.cacheDir } returns File("/tmp/fdroid-app-update-test")
every { downloaderFactory.create(repo, any(), file1, any()) } returns downloader
every { downloaderFactory.create(repo, any(), file2, any()) } returns downloader
every { downloader.setListener(any()) } just Runs
every { downloader.download() } just Runs
every {
InstallerFactory.create(any(), match { it.packageName == app1.packageName }, any())
} returns installer1
every {
InstallerFactory.create(any(), match { it.packageName == app2.packageName }, any())
} returns installer2
every { installer1.installPackage(any(), any()) } just Runs
every { installer2.installPackage(any(), any()) } just Runs
assertTrue(appUpdateManager.updateApps())
verifyOrder {
// app1 gets installed last, because it is us and updating us kills us
installer2.installPackage(any(), any())
installer1.installPackage(any(), any())
}
}
@Test
fun testFailedDownloadSkipsUpdate() {
val updates = listOf(updatableApp1)
every { preferences.backendReleaseChannels } returns null
every {
updateChecker.getUpdatableApps(
releaseChannels = null,
onlyFromPreferredRepo = true,
includeKnownVulnerabilities = false,
)
} returns updates
every { context.packageName } returns null
every { repoManager.getRepository(app1.repoId) } returns repo
every { statusManager.addUpdatableApps(updates, false) } just Runs
every { statusManager.addApk(any(), any(), any(), any()) } just Runs
every {
packageManager.getPackageInfo(any<String>(), any<Int>())
} returns getPackageInfo(0)
every { context.cacheDir } returns File("/tmp/fdroid-app-update-test")
every { downloaderFactory.create(repo, any(), file1, any()) } returns downloader
every { downloader.setListener(any()) } just Runs
every { downloader.download() } throws IOException("foo bar")
assertFalse(appUpdateManager.updateApps())
verify(exactly = 0) {
installer.installPackage(any(), any())
}
verify {
installManagerService.onDownloadFailed(any(), any())
}
}
private fun getPackageInfo(versionCode: Int) = PackageInfo().apply {
@Suppress("DEPRECATION") // longVersionCode doesn't work
this.versionCode = versionCode
}
}

View File

@@ -1,174 +0,0 @@
/*
* Copyright (C) 2018 Senecto Limited
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.fdroid.fdroid;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import androidx.preference.PreferenceManager;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
import java.util.HashMap;
import java.util.Map;
@RunWith(RobolectricTestRunner.class)
public class PreferencesTest {
private static final String TAG = "PreferencesTest";
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
private SharedPreferences defaults;
/**
* Manually read the {@code preferences.xml} defaults to a separate
* instance. Clear the preference state before each test so that each
* test starts as if it was a first time install.
*/
@Before
public void setup() {
ShadowLog.stream = System.out;
defaults = getSharedPreferences(CONTEXT);
assertTrue(defaults.getAll().size() > 0);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(CONTEXT);
Log.d(TAG, "Clearing DefaultSharedPreferences containing: " + sharedPreferences.getAll().size());
sharedPreferences.edit().clear().commit();
assertEquals(0, sharedPreferences.getAll().size());
SharedPreferences defaultValueSp = CONTEXT.getSharedPreferences(PreferenceManager.KEY_HAS_SET_DEFAULT_VALUES,
Context.MODE_PRIVATE);
defaultValueSp.edit().remove(PreferenceManager.KEY_HAS_SET_DEFAULT_VALUES).commit();
}
public static SharedPreferences getSharedPreferences(Context context) {
String sharedPreferencesName = context.getPackageName() + "_preferences_defaults";
PreferenceManager pm = new PreferenceManager(context);
pm.setSharedPreferencesName(sharedPreferencesName);
pm.setSharedPreferencesMode(Context.MODE_PRIVATE);
pm.inflateFromResource(context, R.xml.preferences, null);
return pm.getSharedPreferences();
}
/**
* Check that the defaults are being set when using
* {@link PreferenceManager#getDefaultSharedPreferences(Context)}, and that
* the values match. {@link Preferences#Preferences(Context)} sets the
* values of {@link Preferences#PREF_LOCAL_REPO_NAME} and
* {@link Preferences#PREF_AUTO_DOWNLOAD_INSTALL_UPDATES} dynamically, so
* there are two more preferences.
*/
@Test
public void testSetDefaultValues() {
Preferences.setupForTests(CONTEXT);
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(CONTEXT);
assertEquals(defaults.getAll().size() + 2, sharedPreferences.getAll().size());
assertTrue(sharedPreferences.contains(Preferences.PREF_LOCAL_REPO_NAME));
assertTrue(sharedPreferences.contains(Preferences.PREF_AUTO_DOWNLOAD_INSTALL_UPDATES));
Map<String, ?> entries = sharedPreferences.getAll();
for (Map.Entry<String, ?> entry : defaults.getAll().entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
assertTrue(sharedPreferences.contains(entry.getKey()));
assertEquals(entries.get(key), value);
}
String key = Preferences.PREF_EXPERT;
boolean defaultValue = defaults.getBoolean(key, false);
sharedPreferences.edit().putBoolean(key, !defaultValue).commit();
assertNotEquals(defaultValue, sharedPreferences.getBoolean(key, false));
}
@Test
public void testMethodsUseDefaults() {
Preferences.setupForTests(CONTEXT);
Preferences preferences = Preferences.get();
assertEquals(defaults.getBoolean(Preferences.PREF_EXPERT, false),
preferences.expertMode());
assertEquals(defaults.getBoolean(Preferences.PREF_FORCE_TOUCH_APPS, false),
preferences.isForceOldIndexEnabled());
assertEquals(defaults.getBoolean(Preferences.PREF_FORCE_OLD_INDEX, false),
preferences.isForceOldIndexEnabled());
assertEquals(defaults.getBoolean(Preferences.PREF_PREVENT_SCREENSHOTS, false),
preferences.preventScreenshots());
assertEquals(defaults.getStringSet(Preferences.PREF_SHOW_ANTI_FEATURES, null),
preferences.showAppsWithAntiFeatures());
assertEquals(defaults.getBoolean(Preferences.PREF_SHOW_INCOMPAT_VERSIONS, false),
preferences.showIncompatibleVersions());
assertEquals(defaults.getBoolean(Preferences.PREF_UPDATE_NOTIFICATION_ENABLED, false),
preferences.isUpdateNotificationEnabled());
assertEquals(Long.parseLong(defaults.getString(Preferences.PREF_KEEP_CACHE_TIME, null)),
preferences.getKeepCacheTime());
assertEquals(Preferences.Theme.valueOf(defaults.getString(Preferences.PREF_THEME, null)),
preferences.getTheme());
// now test setting the prefs
boolean defaultValue = defaults.getBoolean(Preferences.PREF_EXPERT, false);
preferences.setExpertMode(!defaultValue);
assertNotEquals(defaultValue, preferences.expertMode());
}
/**
* When {@link Preferences#Preferences(Context)} calls
* {@link PreferenceManager#setDefaultValues(Context, int, boolean)}, any
* existing preference values should not be overridden.
*/
@Test
public void testMigrationWithSetDefaultValues() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(CONTEXT);
long testValue = (long) (Math.random() * Long.MAX_VALUE);
String testValueString = String.valueOf(testValue);
assertNotEquals(testValueString, defaults.getString(Preferences.PREF_KEEP_CACHE_TIME, "not a long"));
sharedPreferences.edit().putString(Preferences.PREF_KEEP_CACHE_TIME, testValueString).commit();
Preferences.setupForTests(CONTEXT);
assertEquals(testValue, Preferences.get().getKeepCacheTime());
assertNotEquals(Long.parseLong(defaults.getString(Preferences.PREF_KEEP_CACHE_TIME, null)),
Preferences.get().getKeepCacheTime());
}
@Test
public void testMirrorErrorMethods() {
Preferences.setupForTests(CONTEXT);
Preferences preferences = Preferences.get();
// serialize an empty map
preferences.setMirrorErrorData(new HashMap<>(0));
Map<String, Integer> result = preferences.getMirrorErrorData();
// deserializing should return an empty map without throwing any exceptions
assertNotNull(result);
assertEquals(result.size(), 0);
}
}

View File

@@ -1,212 +0,0 @@
package org.fdroid.fdroid
import android.content.Context
import android.content.pm.PackageManager
import android.os.Looper
import app.cash.turbine.test
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.verify
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import kotlinx.coroutines.runBlocking
import org.fdroid.CompatibilityChecker
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.Repository
import org.fdroid.database.RepositoryDao
import org.fdroid.fdroid.work.RepoUpdateWorker
import org.fdroid.index.IndexUpdateResult
import org.fdroid.index.RepoManager
import org.fdroid.index.RepoUpdater
import org.fdroid.index.v1.IndexV1Updater
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.IOException
class RepoUpdateManagerTest {
private val context: Context = mockk()
private val db: FDroidDatabase = mockk()
private val repoManager: RepoManager = mockk()
private val notificationManager: NotificationManager = mockk()
private val compatibilityChecker: CompatibilityChecker = mockk()
private val repoUpdater: RepoUpdater = mockk()
private val indexV1Updater: IndexV1Updater = mockk()
private val packageManager: PackageManager = mockk()
private val preferences: Preferences = mockk()
private val repositoryDao: RepositoryDao = mockk()
init {
// needed for Flow#asLiveData()
mockkStatic(Looper::class)
every { Looper.getMainLooper() } returns mockk<Looper> {
every { thread } returns Thread.currentThread()
}
// avoids having to deal with WorkManager here
mockkObject(RepoUpdateWorker)
every { RepoUpdateWorker.getAutoUpdateWorkInfo(any()) } returns mockk()
mockkStatic(Preferences::get)
every { Preferences.get() } returns preferences
every { context.packageManager } returns packageManager
every { context.getString(any(), any()) } returns "foo bar"
every { db.getRepositoryDao() } returns repositoryDao
}
private val repoUpdateManager = RepoUpdateManager(
context = context,
db = db,
repoManager = repoManager,
notificationManager = notificationManager,
compatibilityChecker = compatibilityChecker,
repoUpdater = repoUpdater,
indexV1Updater = null,
)
@Test
fun testUpdateSingleRepo() {
val repo: Repository = mockk(relaxed = true)
every { repoManager.getRepository(1L) } returns repo
every { preferences.isUpdateNotificationEnabled } returns true
every { notificationManager.showUpdateRepoNotification(any(), false, null) } just Runs
every { repoUpdater.update(repo) } returns IndexUpdateResult.Processed
every { notificationManager.cancelUpdateRepoNotification() } just Runs
every { repositoryDao.walCheckpoint() } just Runs
runBlocking {
repoUpdateManager.isUpdating.test {
assertFalse(awaitItem()) // not updating
// do the update now
assertEquals(IndexUpdateResult.Processed, repoUpdateManager.updateRepo(1L))
assertTrue(awaitItem()) // now updating
assertFalse(awaitItem()) // at the end again not updating
}
}
verify {
notificationManager.cancelUpdateRepoNotification()
repositoryDao.walCheckpoint()
}
}
@Test
fun testUpdateSingleForcedV1Repo() {
val repoUpdateManager = RepoUpdateManager(
context = context,
db = db,
repoManager = repoManager,
notificationManager = notificationManager,
compatibilityChecker = compatibilityChecker,
repoUpdater = repoUpdater,
indexV1Updater = indexV1Updater,
)
val repo: Repository = mockk(relaxed = true)
every { repoManager.getRepository(1L) } returns repo
every { preferences.isUpdateNotificationEnabled } returns true
every { notificationManager.showUpdateRepoNotification(any(), false, null) } just Runs
every { indexV1Updater.update(repo) } returns IndexUpdateResult.Unchanged
every { notificationManager.cancelUpdateRepoNotification() } just Runs
every { repositoryDao.walCheckpoint() } just Runs
runBlocking {
repoUpdateManager.isUpdating.test {
assertFalse(awaitItem()) // not updating
// do the update now and expect unchanged result
assertEquals(IndexUpdateResult.Unchanged, repoUpdateManager.updateRepo(1L))
assertTrue(awaitItem()) // now updating
assertFalse(awaitItem()) // at the end again not updating
}
}
verify {
notificationManager.cancelUpdateRepoNotification()
repositoryDao.walCheckpoint()
}
}
@Test
fun testUpdateSingleRepoCleansUpException() {
val repo: Repository = mockk(relaxed = true)
every { repoManager.getRepository(1L) } returns repo
every { preferences.isUpdateNotificationEnabled } returns true
every { notificationManager.showUpdateRepoNotification(any(), false, null) } just Runs
every { repoUpdater.update(repo) } throws IOException()
every { notificationManager.cancelUpdateRepoNotification() } just Runs
every { repositoryDao.walCheckpoint() } just Runs
runBlocking {
repoUpdateManager.isUpdating.test {
assertFalse(awaitItem()) // not updating
// do the update now
assertThrows(IOException::class.java) {
repoUpdateManager.updateRepo(1L)
}
assertTrue(awaitItem()) // now updating
assertFalse(awaitItem()) // at the end again not updating
}
}
verify {
notificationManager.cancelUpdateRepoNotification()
repositoryDao.walCheckpoint()
}
}
@Test
fun testUpdateReposDoesntDoQuickRecheck() {
// we did a check just now
every { preferences.lastUpdateCheck } returns System.currentTimeMillis() - 500
repoUpdateManager.updateRepos()
}
@Test
fun testUpdateThreeReposOneDisabled() {
val repo1: Repository = mockk(relaxed = true) {
every { enabled } returns true
}
val repo2: Repository = mockk(relaxed = true) {
every { enabled } returns false
}
val repo3: Repository = mockk(relaxed = true) {
every { enabled } returns true
}
every { preferences.lastUpdateCheck } returns 1337
every { repositoryDao.getRepositories() } returns listOf(repo1, repo2)
every { preferences.isUpdateNotificationEnabled } returns true
every { notificationManager.showUpdateRepoNotification(any(), false, null) } just Runs
every { repoUpdater.update(repo1) } returns IndexUpdateResult.Unchanged
every { repoUpdater.update(repo3) } returns IndexUpdateResult.Processed
every { notificationManager.cancelUpdateRepoNotification() } just Runs
every { repositoryDao.walCheckpoint() } just Runs
every { preferences.lastUpdateCheck = any() } just Runs
runBlocking {
repoUpdateManager.isUpdating.test {
assertFalse(awaitItem()) // not updating
repoUpdateManager.updateRepos()
assertTrue(awaitItem()) // now updating
assertFalse(awaitItem()) // at the end again not updating
}
}
verify(exactly = 1) {
notificationManager.cancelUpdateRepoNotification()
repositoryDao.walCheckpoint()
}
// repo2 is disabled and should not get updated
verify(exactly = 0) { repoUpdater.update(repo2) }
}
}

View File

@@ -1,145 +0,0 @@
/*
* Copyright (C) 2021 Angus Gratton
* Copyright (C) 2018 Senecto Limited
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.fdroid.fdroid;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.index.v1.IndexV1UpdaterKt;
import org.fdroid.index.v2.FileV1;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class RepoUrlsTest {
private final Context context = ApplicationProvider.getApplicationContext();
/**
* Private class describing a repository URL we're going to test, and
* the file pattern for any files within that URL.
*/
private static class TestRepo {
// Repo URL for the test case
final String repoUrl;
// String format pattern for generating file URLs, should contain a single %s for the filename
final String fileUrlPattern;
TestRepo(String repoUrl, String fileUrlPattern) {
this.repoUrl = repoUrl;
this.fileUrlPattern = fileUrlPattern;
}
}
private static final String APK_NAME = "test-v1.apk";
private static final TestRepo[] REPOS = {
new TestRepo(
"https://microg.org/fdroid/repo",
"https://microg.org/fdroid/repo/%s"),
new TestRepo(
"http://bdf2wcxujkg6qqff.onion/fdroid/repo",
"http://bdf2wcxujkg6qqff.onion/fdroid/repo/%s"),
new TestRepo(
"http://lysator7eknrfl47rlyxvgeamrv7ucefgrrlhk7rouv3sna25asetwid.onion/pub/fdroid/repo",
"http://lysator7eknrfl47rlyxvgeamrv7ucefgrrlhk7rouv3sna25asetwid.onion/pub/fdroid/repo/%s"),
new TestRepo(
"https://mirrors.nju.edu.cn/fdroid/repo?fingerprint=43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB",
"https://mirrors.nju.edu.cn/fdroid/repo/%s?fingerprint=43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB"),
new TestRepo(
"https://raw.githubusercontent.com/guardianproject/fdroid-repo/master/fdroid/repo",
"https://raw.githubusercontent.com/guardianproject/fdroid-repo/master/fdroid/repo/%s"),
new TestRepo(
"content://com.android.externalstorage.documents/tree/1AFB-2402%3A/document/1AFB-2402%3Atesty.at.or.at%2Ffdroid%2Frepo",
// note: to have a URL-escaped path in a format string pattern, we need to
// %-escape all URL %
"content://com.android.externalstorage.documents/tree/1AFB-2402%%3A/document/1AFB-2402%%3Atesty.at.or.at%%2Ffdroid%%2Frepo%%2F%s"),
new TestRepo(
"content://authority/tree/313E-1F1C%3A/document/313E-1F1C%3Aguardianproject.info%2Ffdroid%2Frepo",
// note: to have a URL-escaped path in a format string pattern, we need to
// %-escape all URL %
"content://authority/tree/313E-1F1C%%3A/document/313E-1F1C%%3Aguardianproject.info%%2Ffdroid%%2Frepo%%2F%s"),
new TestRepo(
"http://10.20.31.244:8888/fdroid/repo?FINGERPRINT=35521D88285A9D06FBE33D35FB8B4BB872D753666CF981728E2249FEE6D2D0F2&SWAP=1&BSSID=FE:EE:DA:45:2D:4E",
"http://10.20.31.244:8888/fdroid/repo/%s?FINGERPRINT=35521D88285A9D06FBE33D35FB8B4BB872D753666CF981728E2249FEE6D2D0F2&SWAP=1&BSSID=FE:EE:DA:45:2D:4E"),
new TestRepo(
"fdroidrepos://briarproject.org/fdroid/repo?fingerprint=1FB874BEE7276D28ECB2C9B06E8A122EC4BCB4008161436CE474C257CBF49BD6",
"fdroidrepos://briarproject.org/fdroid/repo/%s?fingerprint=1FB874BEE7276D28ECB2C9B06E8A122EC4BCB4008161436CE474C257CBF49BD6"),
};
@Before
public void setup() {
Preferences.setupForTests(context);
}
interface GetFileFromRepo {
String get(TestRepo tr);
}
/**
* Utility test function - go through the list of test repos,
* using the useOfRepo interface to instantiate a repo from the URL
* and return a file of some kind (Apk, index, etc.) and check that
* it matches the test repo's expected URL format.
*
* @param fileName File that 'useOfRepo' will return in the repo, when called
* @param useOfRepo Instance of the function that uses the repo to build a file URL
*/
private void testReposWithFile(String fileName, GetFileFromRepo useOfRepo) {
for (TestRepo tr : REPOS) {
String expectedUrl = String.format(tr.fileUrlPattern, fileName);
System.out.println("Testing URL " + expectedUrl);
String actualUrl = useOfRepo.get(tr);
assertEquals(expectedUrl, actualUrl);
}
}
@Test
public void testIndexUrls() {
testReposWithFile("index.jar", tr ->
Utils.getUri(tr.repoUrl, "index.jar").toString()
);
}
@Test
public void testIndexV1Urls() {
testReposWithFile(IndexV1UpdaterKt.SIGNED_FILE_NAME, tr ->
Utils.getUri(tr.repoUrl, IndexV1UpdaterKt.SIGNED_FILE_NAME).toString()
);
}
@Test
public void testApkUrls() {
testReposWithFile(APK_NAME, tr -> {
Apk apk = new Apk();
apk.apkFile = new FileV1(APK_NAME, "hash", null, null);
apk.versionCode = 1;
apk.repoAddress = tr.repoUrl;
apk.canonicalRepoAddress = tr.repoUrl;
return apk.getCanonicalUrl();
});
}
}

View File

@@ -1,14 +0,0 @@
package org.fdroid.fdroid;
import android.app.Application;
/**
* Due to there being so much static initialization in the main FDroidApp, it becomes hard to reset
* that state between Robolectric test runs. Therefore, robolectric tests will default to this
* {@link Application} instead of {@link FDroidApp}. It intentionally doesn't extends {@link FDroidApp}
* so that the static initialization in {@link FDroidApp#onCreate()} is not executed.
*/
@SuppressWarnings("unused")
public class TestFDroidApp extends Application {
}

View File

@@ -1,82 +0,0 @@
package org.fdroid.fdroid;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.index.v2.FileV1;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
public class TestUtils {
@SuppressWarnings("unused")
private static final String TAG = "TestUtils"; // NOPMD
/**
* These signers are used to sign the Bitcoin Wallet in f-droid.org/repo.
*/
public static final String FDROID_SIGNER = "bcb4b93636bb10c7ddaf61aa9604ff795dfdb05fad4d9412b335682f0b612e32";
public static final String UPSTREAM_SIGNER = "58dcd8a0edf2a590683ba022d22a8dca5659aabf4728741a5c07af738d53db38";
public static Apk getApk(int versionCode) {
return getApk(versionCode, "signature", null);
}
public static Apk getApk(int versionCode, String signer, String releaseChannel) {
Apk apk = new Apk();
apk.repoAddress = "http://www.example.com/fdroid/repo";
apk.canonicalRepoAddress = "http://www.example.com/fdroid/repo";
apk.versionCode = versionCode;
apk.repoId = 1;
apk.versionName = "The good one";
apk.apkFile = new FileV1("Test Apk", "hash", null, null);
apk.size = 10000;
apk.compatible = true;
apk.signer = signer;
apk.releaseChannels = releaseChannel == null ?
null : Collections.singletonList(releaseChannel);
return apk;
}
public static App getApp() {
App app = new App();
app.packageName = "com.example.app";
app.name = "Test App";
app.repoId = 1;
app.summary = "test summary";
app.description = "test description";
app.license = "GPL?";
app.compatible = true;
return app;
}
public static File copyResourceToTempFile(String resourceName) {
File tempFile = null;
InputStream input = null;
OutputStream output = null;
try {
tempFile = File.createTempFile(resourceName + "-", ".testasset");
input = TestUtils.class.getClassLoader().getResourceAsStream(resourceName);
output = new FileOutputStream(tempFile);
Utils.copy(input, output);
} catch (IOException e) {
e.printStackTrace();
if (tempFile != null && tempFile.exists()) {
assertTrue(tempFile.delete());
}
fail();
return null;
} finally {
Utils.closeQuietly(output);
Utils.closeQuietly(input);
}
return tempFile;
}
}

View File

@@ -1,222 +0,0 @@
package org.fdroid.fdroid;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static vendored.org.apache.commons.codec.digest.MessageDigestAlgorithms.SHA_256;
import android.content.Context;
import android.content.res.Resources;
import android.text.format.DateUtils;
import androidx.test.core.app.ApplicationProvider;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.encode.Contents;
import com.google.zxing.encode.QRCodeEncoder;
import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.File;
import java.io.IOException;
/**
* @see <a href="https://gitlab.com/fdroid/fdroidclient/-/merge_requests/1089#note_822501322">forced to vendor Apache Commons Codec</a>
*/
@RunWith(RobolectricTestRunner.class)
@SuppressWarnings("LineLength")
public class UtilsTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setUp() {
Preferences.setupForTests(context);
}
@Test
public void trailingNewLines() {
CharSequence threeParagraphs = AppDetailsRecyclerViewAdapter.trimTrailingNewlines("Paragraph One\n\nParagraph Two\n\nParagraph Three\n\n");
assertEquals("Paragraph One\n\nParagraph Two\n\nParagraph Three", threeParagraphs);
CharSequence leadingAndExtraTrailing = AppDetailsRecyclerViewAdapter.trimTrailingNewlines("\n\n\nA\n\n\n");
assertEquals("\n\n\nA", leadingAndExtraTrailing);
}
@Test
public void testFormatFingerprint() {
Context context = ApplicationProvider.getApplicationContext();
String badResult = Utils.formatFingerprint(context, "");
// real fingerprints
String formatted;
String fdroidFingerprint =
"43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB";
formatted = Utils.formatFingerprint(context, fdroidFingerprint);
assertNotEquals(formatted, badResult);
assertTrue(formatted.matches("[A-Z0-9][A-Z0-9] [A-Z0-9 ]+"));
String gpRepoFingerprint =
"59050C8155DCA377F23D5A15B77D3713400CDBD8B42FBFBE0E3F38096E68CECE";
formatted = Utils.formatFingerprint(context, gpRepoFingerprint);
assertNotEquals(formatted, badResult);
assertTrue(formatted.matches("[A-Z0-9][A-Z0-9] [A-Z0-9 ]+"));
String gpTest1Fingerprint =
"C63AED1AC79D37C7B0474472AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C50";
formatted = Utils.formatFingerprint(context, gpTest1Fingerprint);
assertNotEquals(formatted, badResult);
assertTrue(formatted.matches("[A-Z0-9][A-Z0-9] [A-Z0-9 ]+"));
// random garbage
assertEquals(
badResult,
Utils.formatFingerprint(context, "234k2lk3jljwlk4j2lk3jlkmqwekljrlkj34lk2jlk2j34lkjl2k3j4lk2j34lja"));
assertEquals(
badResult,
Utils.formatFingerprint(context, "g000000000000000000000000000000000000000000000000000000000000000"));
assertEquals(
badResult,
Utils.formatFingerprint(context, "98273498723948728934789237489273p1928731982731982739182739817238"));
// too short
assertEquals(
badResult,
Utils.formatFingerprint(context, "C63AED1AC79D37C7B0474472AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C5"));
assertEquals(
badResult,
Utils.formatFingerprint(context, "C63AED1"));
assertEquals(
badResult,
Utils.formatFingerprint(context, "f"));
assertEquals(
badResult,
Utils.formatFingerprint(context, ""));
assertEquals(
badResult,
Utils.formatFingerprint(context, null));
// real digits but too long
assertEquals(
badResult,
Utils.formatFingerprint(context, "43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB43238D512C1E5EB2D6569F4A3AFBF5523418B82E0A3ED1552770ABB9A9C9CCAB"));
assertEquals(
badResult,
Utils.formatFingerprint(context, "C63AED1AC79D37C7B0474472AC6EFA6C3AB2B11A767A4F42CF360FA5496E3C50F"));
assertEquals(
badResult,
Utils.formatFingerprint(context, "3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b73301e170d3130303732333137313032345a170d3337313230383137313032345a3071310b300906035504061302554b3110300e06035504081307556e6b6e6f776e3111300f0603550407130857657468657262793110300e060355040a1307556e6b6e6f776e3110300e060355040b1307556e6b6e6f776e311930170603550403131043696172616e2047756c746e69656b7330820122300d06092a864886f70d01010105000382010f003082010a028201010096d075e47c014e7822c89fd67f795d23203e2a8843f53ba4e6b1bf5f2fd0e225938267cfcae7fbf4fe596346afbaf4070fdb91f66fbcdf2348a3d92430502824f80517b156fab00809bdc8e631bfa9afd42d9045ab5fd6d28d9e140afc1300917b19b7c6c4df4a494cf1f7cb4a63c80d734265d735af9e4f09455f427aa65a53563f87b336ca2c19d244fcbba617ba0b19e56ed34afe0b253ab91e2fdb1271f1b9e3c3232027ed8862a112f0706e234cf236914b939bcf959821ecb2a6c18057e070de3428046d94b175e1d89bd795e535499a091f5bc65a79d539a8d43891ec504058acb28c08393b5718b57600a211e803f4a634e5c57f25b9b8c4422c6fd90203010001300d06092a864886f70d0101050500038201010008e4ef699e9807677ff56753da73efb2390d5ae2c17e4db691d5df7a7b60fc071ae509c5414be7d5da74df2811e83d3668c4a0b1abc84b9fa7d96b4cdf30bba68517ad2a93e233b042972ac0553a4801c9ebe07bf57ebe9a3b3d6d663965260e50f3b8f46db0531761e60340a2bddc3426098397fda54044a17e5244549f9869b460ca5e6e216b6f6a2db0580b480ca2afe6ec6b46eedacfa4aa45038809ece0c5978653d6c85f678e7f5a2156d1bedd8117751e64a4b0dcd140f3040b021821a8d93aed8d01ba36db6c82372211fed714d9a32607038cdfd565bd529ffc637212aaa2c224ef22b603eccefb5bf1e085c191d4b24fe742b17ab3f55d4e6f05ef"));
}
@Test
public void testIsFileMatchingHash() {
Utils.isFileMatchingHash(null, null, null);
Utils.isFileMatchingHash(new File("/"), "", null);
assertFalse(Utils.isFileMatchingHash(null, null, ""));
assertFalse(Utils.isFileMatchingHash(null, null, SHA_256));
assertFalse(Utils.isFileMatchingHash(new File("/"), null, SHA_256));
assertFalse(Utils.isFileMatchingHash(new File("/"), "", SHA_256));
assertTrue(Utils.isFileMatchingHash(TestUtils.copyResourceToTempFile("Norway_bouvet_europe_2.obf.zip"),
"6e8a584e004c6cd26d3822a04b0591e355dc5d07b5a3d0f8e309443f47ad1208", SHA_256));
assertTrue(Utils.isFileMatchingHash(TestUtils.copyResourceToTempFile("install_history_all"),
"4ad118d4a600dcc104834635d248a89e337fc91b173163d646996b9c54d77372", SHA_256));
File f = TestUtils.copyResourceToTempFile("additional_repos.xml");
assertTrue(Utils.isFileMatchingHash(f,
"47ad2284d3042373e6280012cc10e9b82f75352db6d6d9bab1e06934b7b1dab7", SHA_256));
assertFalse("uppercase fails",
Utils.isFileMatchingHash(f,
"47AD2284D3042373E6280012CC10E9B82F75352DB6D6D9BAB1E06934B7B1DAB7", SHA_256));
assertFalse("one uppercase digit fails",
Utils.isFileMatchingHash(f,
"47Ad2284d3042373e6280012cc10e9b82f75352db6d6d9bab1e06934b7b1dab7", SHA_256));
assertFalse("missing digit fails",
Utils.isFileMatchingHash(f,
"47ad2284d3042373e6280012cc10e9b82f75352db6d6d9bab1e06934b7b1dab", SHA_256));
assertFalse("extra digit fails",
Utils.isFileMatchingHash(f,
"47ad2284d3042373e6280012cc10e9b82f75352db6d6d9bab1e06934b7b1dab71", SHA_256));
assertFalse("all zeros fails",
Utils.isFileMatchingHash(f,
"0000000000000000000000000000000000000000000000000000000000000000", SHA_256));
assertFalse("null fails",
Utils.isFileMatchingHash(f, null, SHA_256));
assertFalse("empty string fails",
Utils.isFileMatchingHash(f, "", SHA_256));
}
@Test
public void testGetFileHexDigest() throws IOException {
// zero size file should have a stable hex digest file
assertEquals("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
Utils.getFileHexDigest(File.createTempFile("asdf", "asdf"), SHA_256));
assertNull(Utils.getFileHexDigest(new File("/kasdfkjasdhflkjasd"), SHA_256));
}
@Test(expected = IllegalArgumentException.class)
public void testGetFileHexDigestBadAlgo() {
File f = TestUtils.copyResourceToTempFile("additional_repos.xml");
assertNull(Utils.getFileHexDigest(f, "FAKE"));
}
@Test(expected = NullPointerException.class)
public void testGetFileHexDigestNullFile() {
assertNull(Utils.getFileHexDigest(null, SHA_256));
}
// TODO write tests that work with a Certificate
@Test
public void testFormatLastUpdated() {
Resources res = context.getResources();
long now = System.currentTimeMillis();
long offset = DateUtils.MINUTE_IN_MILLIS * 10;
assertEquals(
"Updated today",
Utils.formatLastUpdated(res, now - offset)
);
assertEquals(
"Updated today",
Utils.formatLastUpdated(res, now - DateUtils.DAY_IN_MILLIS / 2 + offset)
);
assertEquals(
"Updated 1 day ago",
Utils.formatLastUpdated(res, now - DateUtils.DAY_IN_MILLIS / 2 - offset)
);
assertEquals(
"Updated 1 day ago",
Utils.formatLastUpdated(res, now - DateUtils.DAY_IN_MILLIS - offset)
);
assertEquals(
"Updated 3 days ago",
Utils.formatLastUpdated(res, now - 234834870L)
);
assertEquals(
"Updated 13 days ago",
Utils.formatLastUpdated(res, now - DateUtils.DAY_IN_MILLIS * 13 - offset)
);
assertEquals(
"Updated 7 months ago",
Utils.formatLastUpdated(res, now - DateUtils.DAY_IN_MILLIS * 30 * 7 + offset)
);
}
@Test
public void testQrCodeEncoding() throws Exception {
String text1 = "http://192.168.3.159:8888/fdroid/repo?FINGERPRINT=" +
"BA29D02E303B2604D00C91189600E868B26FA0B248DC39D75C5C0F4349CA5FA9" +
"&SWAP=1&BSSID=44:FE:3B:7F:7F:EE";
int bytesNum1 = new QRCodeEncoder(text1, null, Contents.Type.TEXT,
BarcodeFormat.QR_CODE.toString(), 500).encodeAsBitmap().getByteCount();
String text2 = "http://192.168.3.159:8888/fdroid/repo?fingerprint=" +
"ba29d02e303b2604d00c91189600e868b26fa0b248dc39d75c5c0f4349ca5fa9" +
"&swap=1&bssid=44:fe:3b:7f:7f:ee";
int bytesNum2 = new QRCodeEncoder(text2, null, Contents.Type.TEXT,
BarcodeFormat.QR_CODE.toString(), 500).encodeAsBitmap().getByteCount();
assertEquals(bytesNum1, bytesNum2);
}
}

View File

@@ -1,93 +0,0 @@
package org.fdroid.fdroid.data;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.ContextWrapper;
import android.os.Environment;
import android.webkit.MimeTypeMap;
import androidx.test.core.app.ApplicationProvider;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.installer.ApkCache;
import org.fdroid.fdroid.nearby.PublicSourceDirProvider;
import org.fdroid.index.v2.FileV1;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowLog;
import org.robolectric.shadows.ShadowMimeTypeMap;
import java.io.File;
import java.io.IOException;
import java.net.URL;
@RunWith(RobolectricTestRunner.class)
public class ApkTest {
private final ContextWrapper context = ApplicationProvider.getApplicationContext();
@Before
public final void setUp() {
ShadowMimeTypeMap mimeTypeMap = Shadows.shadowOf(MimeTypeMap.getSingleton());
mimeTypeMap.addExtensionMimeTypeMapping("apk", "application/vnd.android.package-archive");
mimeTypeMap.addExtensionMimeTypeMapping("obf", "application/octet-stream");
mimeTypeMap.addExtensionMimeTypeMapping("zip", PublicSourceDirProvider.SHARE_APK_MIME_TYPE);
ShadowLog.stream = System.out;
}
@Test(expected = IllegalStateException.class)
public void testGetMediaInstallPathWithApk() {
Apk apk = new Apk();
apk.apkFile = new FileV1("test.apk", "hash", null, null);
apk.repoAddress = "https://example.com/fdroid/repo";
apk.canonicalRepoAddress = "https://example.com/fdroid/repo";
assertTrue(apk.isApk());
apk.getMediaInstallPath(context);
}
@Test
public void testGetMediaInstallPathWithOta() throws IOException {
Apk apk = new Apk();
apk.apkFile = new FileV1("org.fdroid.fdroid.privileged.ota_2110.zip", "hash", null, null);
apk.repoAddress = "https://example.com/fdroid/repo";
apk.canonicalRepoAddress = "https://example.com/fdroid/repo";
assertFalse(apk.isApk());
copyResourceFileToCache(apk);
File path = apk.getMediaInstallPath(context);
assertEquals(new File(context.getApplicationInfo().dataDir + "/ota"), path);
}
@Test
public void testGetMediaInstallPathWithObf() {
Apk apk = new Apk();
apk.apkFile = new FileV1("Norway_bouvet_europe_2.obf", "hash", null, null);
apk.repoAddress = "https://example.com/fdroid/repo";
apk.canonicalRepoAddress = "https://example.com/fdroid/repo";
assertFalse(apk.isApk());
File path = apk.getMediaInstallPath(context);
assertEquals(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), path);
}
@Test
public void testGetMediaInstallPathWithObfZip() throws IOException {
Apk apk = new Apk();
apk.apkFile = new FileV1("Norway_bouvet_europe_2.obf.zip", "hash", null, null);
apk.repoAddress = "https://example.com/fdroid/repo";
apk.canonicalRepoAddress = "https://example.com/fdroid/repo";
assertFalse(apk.isApk());
copyResourceFileToCache(apk);
File path = apk.getMediaInstallPath(context);
assertEquals(context.getCacheDir(), path);
}
private void copyResourceFileToCache(Apk apk) throws IOException {
URL res = getClass().getClassLoader().getResource(apk.apkFile.getName());
FileUtils.copyInputStreamToFile(res.openStream(),
ApkCache.getApkDownloadPath(context, apk.getCanonicalUrl()));
}
}

View File

@@ -1,338 +0,0 @@
package org.fdroid.fdroid.data;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import org.apache.commons.io.IOUtils;
import org.fdroid.database.FDroidDatabase;
import org.fdroid.database.InitialRepository;
import org.fdroid.database.RepositoryDao;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.TestUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.robolectric.RobolectricTestRunner;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
public class DBHelperTest {
private static final String TAG = "DBHelperTest";
private final Context context = ApplicationProvider.getApplicationContext();
private List<String> getReposFromXml(String xml) throws IOException, XmlPullParserException {
File additionalReposXml = File.createTempFile("." + context.getPackageName() + "-DBHelperTest_",
"_additional_repos.xml");
Log.i(TAG, "additionalReposXml: " + additionalReposXml);
FileOutputStream outputStream = new FileOutputStream(additionalReposXml);
outputStream.write(xml.getBytes());
outputStream.close();
// Now parse that xml file
return DBHelper.parseAdditionalReposXml(additionalReposXml);
}
@Test
public void testDefaultReposAddedToDb() {
FDroidDatabase db = mock(FDroidDatabase.class);
RepositoryDao repoDao = mock(RepositoryDao.class);
when(db.getRepositoryDao()).thenReturn(repoDao);
// pre-populate the DB
DBHelper.prePopulateDb(context, db);
// verify that all default repos were added to DB
int numRepos = getDefaultRepoCount();
verify(repoDao, times(numRepos)).insert(ArgumentMatchers.any(InitialRepository.class));
}
/**
* Returns the number of repos in app/src/main/res/default_repo.xml
*/
private int getDefaultRepoCount() {
int itemCount = context.getResources().getStringArray(R.array.default_repos).length;
return itemCount / DBHelper.REPO_XML_ITEM_COUNT;
}
@Test
public void parseAdditionalReposXmlAllOneLineTest() throws IOException, XmlPullParserException {
String oneRepoXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +
"<resources>" +
"<string-array name=\"default_repos\">\n" +
"<!-- name -->" +
"<item>F-Droid</item>" +
"<!-- address -->" +
"<item>https://f-droid.org/repo</item>" +
"<!-- description -->" +
"<item>The official F-Droid repository. Applications in this repository are mostly built" +
"directory from the source code. Some are official binaries built by the original" +
"application developers - these will be replaced by source-built versions over time." +
"</item>" +
"<!-- version -->" +
"<item>13</item>" +
"<!-- enabled -->" +
"<item>1</item>" +
"<!-- push requests -->" +
"<item>ignore</item>" +
"<!-- pubkey -->" +
"<item>" +
"3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b30090603550406130255" +
"</item>" +
"</string-array>" +
"</resources>";
List<String> repos = getReposFromXml(oneRepoXml);
assertEquals("Should contain one repo's worth of items", DBHelper.REPO_XML_ITEM_COUNT, repos.size());
}
@Test
public void parseAdditionalReposXmlIncludedPriorityTest() throws IOException, XmlPullParserException {
String wrongXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<resources>" +
"<string-array name=\"default_repos\">\n" +
"<!-- name -->" +
"<item>F-Droid</item>" +
"<!-- address -->" +
"<item>https://f-droid.org/repo</item>" +
"<!-- description -->" +
"<item>The official F-Droid repository. Applications in this repository are mostly built" +
"directory from the source code. Some are official binaries built by the original" +
"application developers - these will be replaced by source-built versions over time." +
"</item>" +
"<!-- version -->" +
"<item>13</item>" +
"<!-- enabled -->" +
"<item>1</item>" +
"<!-- priority -->" +
"<item>1</item>" +
"<!-- push requests -->" +
"<item>ignore</item>" +
"<!-- pubkey -->" +
"<item>" +
"3082035e30820246a00302010202044c49cd00300d06092a864886f70d01010505003071310b3009060355040613025" +
"</item>" +
"</string-array>" +
"</resources>";
getReposFromXml(wrongXml);
List<String> repos = getReposFromXml(wrongXml);
assertEquals("Should be empty", 0, repos.size());
}
@Test(expected = XmlPullParserException.class)
public void parseAdditionalReposXmlDoubleTagTest() throws IOException, XmlPullParserException {
String wrongXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<resources>"
+ "<string-array name=\"additional_repos\">"
+ "<!-- address -->"
+ "<item><item>https://www.oem0.com/yeah/repo</item>"
+ "<!-- description -->"
+ "<item>I'm the first oem repo.</item>"
+ "<!-- version -->"
+ "<item>22</item>"
+ "<!-- enabled -->"
+ "<item>1</item>"
+ "<!-- push requests -->"
+ "<item>ignore</item>"
+ "<!-- pubkey -->"
+ "<item>fffff2313aaaaabcccc111</item>"
+ "</string-array>"
+ "</resources>";
getReposFromXml(wrongXml);
fail("Invalid xml read successfully --> Wrong");
}
@Test(expected = XmlPullParserException.class)
public void parseAdditionalReposXmlMissingStartTagTest() throws IOException, XmlPullParserException {
String wrongXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<item>https://www.oem0.com/yeah/repo</item>"
+ "<!-- description -->"
+ "<item>I'm the first oem repo.</item>"
+ "<!-- version -->"
+ "<item>22</item>"
+ "<!-- enabled -->"
+ "<item>1</item>"
+ "<!-- push requests -->"
+ "<item>ignore</item>"
+ "<!-- pubkey -->"
+ "<item>fffff2313aaaaabcccc111</item>"
+ "</string-array>"
+ "</resources>";
getReposFromXml(wrongXml);
fail("Invalid xml read successfully --> Wrong");
}
@Test
public void parseAdditionalReposXmlWrongCountTest() throws IOException, XmlPullParserException {
String wrongXml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<resources>"
+ "<string-array name=\"default_repos\"><item>foo</item></string-array>"
+ "</resources>";
List<String> repos = getReposFromXml(wrongXml);
assertEquals("Should be empty", 0, repos.size());
}
/**
* Parse valid xml but make sure that only <item> tags are parsed
*/
@Test
public void parseAdditionalReposXmlSloppyTest() throws IOException, XmlPullParserException {
InputStream input = TestUtils.class.getClassLoader().getResourceAsStream("ugly_additional_repos.xml");
String validXml = IOUtils.toString(input, StandardCharsets.UTF_8);
List<String> repos = getReposFromXml(validXml);
assertEquals(2 * DBHelper.REPO_XML_ITEM_COUNT, repos.size());
assertEquals("Repo Name", repos.get(7));
assertEquals("https://www.oem0.com/yeah/repo", repos.get(8));
}
@Test
public void parseAdditionalReposXmlPositiveTest() throws IOException {
InputStream input = TestUtils.class.getClassLoader().getResourceAsStream("additional_repos.xml");
String reposXmlContent = IOUtils.toString(input, StandardCharsets.UTF_8);
List<String> additionalRepos;
try {
additionalRepos = getReposFromXml(reposXmlContent);
} catch (IOException io) {
fail("IOException. Failed parsing xml string into repos.");
return;
} catch (XmlPullParserException xppe) {
fail("XmlPullParserException. Failed parsing xml string into repos.");
return;
}
// We should have loaded these repos
List<String> oem0 = Arrays.asList(
"oem0Name",
"https://www.oem0.com/yeah/repo",
"I'm the first oem repo.",
"22",
"1",
"ignore",
"fffff2313aaaaabcccc111");
List<String> oem1 = Arrays.asList(
"oem1MyNameIs",
"https://www.mynameis.com/rapper/repo",
"Who is the first repo?",
"22",
"0",
"ignore",
"ddddddd2313aaaaabcccc111");
List<String> shouldBeRepos = new LinkedList<>();
shouldBeRepos.addAll(oem0);
shouldBeRepos.addAll(oem1);
assertEquals(additionalRepos.size(), shouldBeRepos.size());
for (int i = 0; i < additionalRepos.size(); i++) {
assertEquals(shouldBeRepos.get(i), additionalRepos.get(i));
}
}
@SuppressWarnings("LineLength")
@Test
public void canAddAdditionalRepos() throws IOException {
File oemEtcDir = new File("/oem/etc");
File oemEtcPackageDir = new File(oemEtcDir, context.getPackageName());
if (!oemEtcPackageDir.canWrite() || !oemEtcDir.canWrite()) {
if (oemEtcDir.canWrite() || new File("/").canWrite()) {
oemEtcPackageDir.mkdirs();
}
assumeTrue(oemEtcPackageDir.isDirectory());
if (TextUtils.isEmpty(System.getenv("CI")) && !oemEtcPackageDir.isDirectory()) {
Log.e(TAG, "Cannot create " + oemEtcDir + ", skipping test!");
return;
}
}
File additionalReposXmlFile = new File(oemEtcPackageDir, "additional_repos.xml");
FileOutputStream outputStream = new FileOutputStream(additionalReposXmlFile);
outputStream.write(("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
+ "<resources>"
+ "<string-array name=\"default_repos\">"
+ "<!-- name -->"
+ "<item>oem0Name</item>"
+ "<!-- address -->"
+ "<item>https://www.oem0.com/yeah/repo</item>"
+ "<!-- description -->"
+ "<item>I'm the first oem repo.</item>"
+ "<!-- version -->"
+ "<item>22</item>"
+ "<!-- enabled -->"
+ "<item>1</item>"
+ "<!-- push requests -->"
+ "<item>ignore</item>"
+ "<!-- pubkey -->"
+ "<item>fffff2313aaaaabcccc111</item>"
+ "<!-- name -->"
+ "<item>oem1MyNameIs</item>"
+ "<!-- address -->"
+ "<item>https://www.mynameis.com/rapper/repo</item>"
+ "<!-- description -->"
+ "<item>Who is the first repo?</item>"
+ "<!-- version -->"
+ "<item>22</item>"
+ "<!-- enabled -->"
+ "<item>0</item>"
+ "<!-- push requests -->"
+ "<item>ignore</item>"
+ "<!-- pubkey -->"
+ "<item>ddddddd2313aaaaabcccc111</item>"
+ "</string-array>"
+ "</resources>").getBytes());
outputStream.close();
try {
List<String> initialRepos = DBHelper.loadInitialRepos(context);
// Construct the repos that we should have loaded
List<String> oem0 = Arrays.asList("oem0Name", "https://www.oem0.com/yeah/repo", "I'm the first oem repo.",
"22", "1", "ignore", "fffff2313aaaaabcccc111");
List<String> oem1 = Arrays.asList("oem1MyNameIs", "https://www.mynameis.com/rapper/repo", "Who is the first repo?",
"22", "0", "ignore", "ddddddd2313aaaaabcccc111");
String[] defaultRepos = context.getResources().getStringArray(R.array.default_repos);
List<String> shouldBeRepos = new LinkedList<>();
shouldBeRepos.addAll(Arrays.asList(defaultRepos));
shouldBeRepos.addAll(oem0);
shouldBeRepos.addAll(oem1);
// Normalize whitespace in descriptions, just like DBHelper does.
final int descriptionIndex = 2;
for (int i = descriptionIndex; i < shouldBeRepos.size(); i += DBHelper.REPO_XML_ITEM_COUNT) {
String description = shouldBeRepos.get(i);
shouldBeRepos.set(i, description.replaceAll("\\s+", " "));
}
for (int i = 0; i < initialRepos.size(); i++) {
assertEquals(shouldBeRepos.get(i), initialRepos.get(i));
}
} finally {
//noinspection ResultOfMethodCallIgnored
additionalReposXmlFile.delete();
}
}
}

View File

@@ -1,57 +0,0 @@
package org.fdroid.fdroid.data;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assume.assumeTrue;
import org.junit.Test;
import java.io.File;
public class SanitizedFileTest {
@Test
public void testSanitizedFile() {
assumeTrue("/".equals(System.getProperty("file.separator")));
File directory = new File("/tmp/blah");
String safeFile = "safe";
String nonEvilFile = "$%^safe-and_bleh.boo*@~";
String evilFile = ";rm /etc/shadow;";
File safeNotSanitized = new File(directory, safeFile);
File nonEvilNotSanitized = new File(directory, nonEvilFile);
File evilNotSanitized = new File(directory, evilFile);
assertEquals("/tmp/blah/safe", safeNotSanitized.getAbsolutePath());
assertEquals("/tmp/blah/$%^safe-and_bleh.boo*@~", nonEvilNotSanitized.getAbsolutePath());
assertEquals("/tmp/blah/;rm /etc/shadow;", evilNotSanitized.getAbsolutePath());
assertEquals("safe", safeNotSanitized.getName());
assertEquals("$%^safe-and_bleh.boo*@~", nonEvilNotSanitized.getName());
assertEquals("shadow;", evilNotSanitized.getName());
SanitizedFile safeSanitized = new SanitizedFile(directory, safeFile);
SanitizedFile nonEvilSanitized = new SanitizedFile(directory, nonEvilFile);
SanitizedFile evilSanitized = new SanitizedFile(directory, evilFile);
assertEquals("/tmp/blah/safe", safeSanitized.getAbsolutePath());
assertEquals("/tmp/blah/safe-and_bleh.boo", nonEvilSanitized.getAbsolutePath());
assertEquals("/tmp/blah/rm etcshadow", evilSanitized.getAbsolutePath());
assertEquals("safe", safeSanitized.getName());
assertEquals("safe-and_bleh.boo", nonEvilSanitized.getName());
assertEquals("rm etcshadow", evilSanitized.getName());
}
@Test
public void testSanitizeFileName() {
for (String valid : new String[]{"An.stop", "a.0", "packageName", "com.this-and-that", "A_.o"}) {
assertEquals(valid, SanitizedFile.sanitizeFileName(valid));
}
for (String invalid : new String[]{"'--;DROP", "a.0)", "packageName\n"}) {
assertNotEquals(invalid, SanitizedFile.sanitizeFileName(invalid));
}
}
}

View File

@@ -1,141 +0,0 @@
package org.fdroid.fdroid.data;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.app.Application;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.database.AppPrefs;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.TestUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Config(application = Application.class)
@RunWith(RobolectricTestRunner.class)
public class SuggestedVersionTest {
private final Context context = ApplicationProvider.getApplicationContext();
@Before
public void setup() {
Preferences.setupForTests(context);
}
@Test
public void singleRepoSingleSig() {
App singleApp = TestUtils.getApp();
singleApp.installedVersionCode = 1;
singleApp.installedSigner = TestUtils.FDROID_SIGNER;
Apk apk1 = TestUtils.getApk(1, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk2 = TestUtils.getApk(2, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk3 = TestUtils.getApk(3, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_BETA);
List<Apk> apks = new ArrayList<>();
apks.add(apk3);
apks.add(apk2);
apks.add(apk1);
assertSuggested(singleApp, apks, 2, Apk.RELEASE_CHANNEL_STABLE);
// By enabling the beta channel we should suggest the latest version (3).
Preferences.get().setUnstableUpdates(true);
assertSuggested(singleApp, apks, 3, Apk.RELEASE_CHANNEL_BETA);
}
@Test
public void singleRepoMultiSigner() {
App singleApp = TestUtils.getApp();
singleApp.installedVersionCode = 0;
Apk apk1 = TestUtils.getApk(1, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk2 = TestUtils.getApk(2, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk3 = TestUtils.getApk(3, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk4 = TestUtils.getApk(4, TestUtils.UPSTREAM_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk5 = TestUtils.getApk(5, TestUtils.UPSTREAM_SIGNER, Apk.RELEASE_CHANNEL_BETA);
List<Apk> apks = new ArrayList<>();
apks.add(apk5);
apks.add(apk4);
apks.add(apk3);
apks.add(apk2);
apks.add(apk1);
// Given we aren't installed yet, we don't care which signature.
// Just get as close to suggestedVersionCode as possible.
assertSuggested(singleApp, apks, 4, Apk.RELEASE_CHANNEL_STABLE, false);
// Now install v1 with the f-droid signature. In response, we should only suggest
// apps with that sig in the future. That is, version 4 from upstream is not considered.
singleApp.installedSigner = TestUtils.FDROID_SIGNER;
singleApp.installedVersionCode = 1;
assertSuggested(singleApp, apks, 3, Apk.RELEASE_CHANNEL_STABLE);
// This adds the "suggestedVersionCode" version of the app, but signed by f-droid.
Apk apk4f = TestUtils.getApk(4, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk5f = TestUtils.getApk(5, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_BETA);
apks.clear();
apks.add(apk5);
apks.add(apk5f);
apks.add(apk4);
apks.add(apk4f);
apks.add(apk3);
apks.add(apk2);
apks.add(apk1);
assertSuggested(singleApp, apks, 4, Apk.RELEASE_CHANNEL_STABLE);
// Version 5 from F-Droid is not the "suggestedVersionCode", but with beta updates it should
// still become the suggested version now.
assertSuggested(singleApp, apks, 5, Apk.RELEASE_CHANNEL_BETA);
}
public void assertSuggested(App app, List<Apk> apks, int suggestedVersion,
String releaseChannel) {
assertSuggested(app, apks, suggestedVersion, releaseChannel, true);
}
@Test
public void testIncompatibleWithBeta() {
App singleApp = TestUtils.getApp();
singleApp.installedVersionCode = 1;
singleApp.installedSigner = TestUtils.FDROID_SIGNER;
Apk apk1 = TestUtils.getApk(1, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk2 = TestUtils.getApk(2, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
Apk apk3 = TestUtils.getApk(3, TestUtils.FDROID_SIGNER, Apk.RELEASE_CHANNEL_STABLE);
apk3.compatible = false;
List<Apk> apks = new ArrayList<>();
apks.add(apk3);
apks.add(apk2);
apks.add(apk1);
assertSuggested(singleApp, apks, 2, Apk.RELEASE_CHANNEL_BETA);
}
/**
* Checks that the app exists, that its suggested version code is correct, and that the apk which is "suggested"
* has the correct signature.
* <p>
* If {@param installedSigner} is null then {@param installedVersion} is
* ignored and the signature of the suggested APK is not checked.
*/
public void assertSuggested(App app, List<Apk> apks, int suggestedVersion,
String releaseChannel, boolean hasUpdates) {
Apk suggestedApk = app.findSuggestedApk(apks, releaseChannel);
assertNotNull(suggestedApk);
assertEquals("Suggested version on App", suggestedVersion, suggestedApk.versionCode);
if (app.installedSigner != null) {
assertEquals("Installed signature on Apk", app.installedSigner, suggestedApk.signer);
}
assertTrue(app.canAndWantToUpdate(suggestedApk));
AppPrefs appPrefs = new AppPrefs(app.packageName, 0, null, Collections.singletonList(releaseChannel));
assertEquals(hasUpdates, app.hasUpdates(apks, appPrefs));
}
}

View File

@@ -1,64 +0,0 @@
package org.fdroid.fdroid.installer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.content.ContextWrapper;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
import java.io.File;
@RunWith(RobolectricTestRunner.class)
public class ApkCacheTest {
private static final String TAG = "ApkCacheTest";
private ContextWrapper context;
private File cacheDir;
@Before
public final void setUp() {
context = ApplicationProvider.getApplicationContext();
cacheDir = ApkCache.getApkCacheDir(context);
ShadowLog.stream = System.out;
}
@Test
public void testGetApkCacheDir() {
Log.i(TAG, "path: " + cacheDir);
assertTrue("Must be full path", cacheDir.isAbsolute());
assertTrue("Must be a directory", cacheDir.isDirectory());
assertTrue("Must be writable", cacheDir.canWrite());
}
@Test
public void testGetApkDownloadPath() {
assertEquals("Should be in folder based on repo hostname",
new File(cacheDir, "f-droid.org--1/org.fdroid.fdroid_1008000.apk"),
ApkCache.getApkDownloadPath(context,
"https://f-droid.org/repo/org.fdroid.fdroid_1008000.apk"));
assertEquals("Should be in folder based on repo hostname with port number",
new File(cacheDir, "192.168.234.12-8888/sun.bob.leela_2.apk"),
ApkCache.getApkDownloadPath(context,
"http://192.168.234.12:8888/fdroid/repo/sun.bob.leela_2.apk"));
assertEquals("Should work for OTA files also",
new File(cacheDir, "f-droid.org--1/org.fdroid.fdroid.privileged.ota_2110.zip"),
ApkCache.getApkDownloadPath(context,
"http://f-droid.org/fdroid/repo/org.fdroid.fdroid.privileged.ota_2110.zip"));
assertEquals("Should work for ZIP files also",
new File(cacheDir, "example.com--1/Norway_bouvet_europe_2.obf.zip"),
ApkCache.getApkDownloadPath(context,
"https://example.com/fdroid/repo/Norway_bouvet_europe_2.obf.zip"));
assertEquals("Should work for OBF files also",
new File(cacheDir, "example.com--1/Norway_bouvet_europe_2.obf"),
ApkCache.getApkDownloadPath(context,
"https://example.com/fdroid/repo/Norway_bouvet_europe_2.obf"));
}
}

View File

@@ -1,46 +0,0 @@
package org.fdroid.fdroid.installer;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import android.content.ContextWrapper;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.index.v2.FileV1;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
@RunWith(RobolectricTestRunner.class)
public class FileInstallerTest {
private ContextWrapper context;
@Before
public final void setUp() {
context = ApplicationProvider.getApplicationContext();
Preferences.setupForTests(context);
ShadowLog.stream = System.out;
}
@Test
public void testInstallOtaZip() {
App app = new App();
app.packageName = "org.fdroid.fdroid.privileged.ota";
Apk apk = new Apk();
apk.apkFile = new FileV1("org.fdroid.fdroid.privileged.ota_2010.zip", "hash", null, null);
apk.packageName = "org.fdroid.fdroid.privileged.ota";
apk.versionCode = 2010;
assertFalse(apk.isApk());
Installer installer = InstallerFactory.create(context, app, apk);
assertEquals("should be a FileInstaller",
FileInstaller.class,
installer.getClass());
}
}

View File

@@ -1,58 +0,0 @@
package org.fdroid.fdroid.installer;
import static org.junit.Assert.assertEquals;
import android.content.ContextWrapper;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.index.v2.FileV1;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class InstallerFactoryTest {
private ContextWrapper context;
@Before
public final void setUp() {
context = ApplicationProvider.getApplicationContext();
Preferences.setupForTests(context);
}
@Test
public void testApkInstallerInstance() {
for (String filename : new String[]{"test.apk", "A.APK", "b.ApK"}) {
App app = new App();
app.packageName = "test";
Apk apk = new Apk();
apk.apkFile = new FileV1(filename, "hash", null, null);
apk.packageName = "test";
Installer installer = InstallerFactory.create(context, app, apk);
assertEquals(filename + " should use a DefaultInstaller",
DefaultInstaller.class,
installer.getClass());
}
}
@Test
public void testFileInstallerInstance() {
for (String filename : new String[]{"org.fdroid.fdroid.privileged.ota_2110.zip", "test.ZIP"}) {
App app = new App();
app.packageName = "cafe0088";
Apk apk = new Apk();
apk.apkFile = new FileV1(filename, "hash", null, null);
apk.packageName = "cafe0088";
Installer installer = InstallerFactory.create(context, app, apk);
assertEquals("should be a FileInstaller",
FileInstaller.class,
installer.getClass());
}
}
}

View File

@@ -1,152 +0,0 @@
package org.fdroid.fdroid.views;
import static org.junit.Assert.assertEquals;
import android.app.Application;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.database.AppPrefs;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App;
import org.fdroid.index.v2.FileV2;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Config(application = Application.class)
@RunWith(RobolectricTestRunner.class)
public class AppDetailsAdapterTest {
private final AppPrefs appPrefs = new AppPrefs("com.example.app", 0, null, null);
private Context context;
@Before
public void setup() {
// Must manually set the theme again here other than in AndroidManifest,xml
// https://github.com/mozilla-mobile/fenix/pull/15646#issuecomment-707345798
ApplicationProvider.getApplicationContext().setTheme(R.style.Theme_App);
context = new ContextThemeWrapper(ApplicationProvider.getApplicationContext(), R.style.Theme_App);
Preferences.setupForTests(context);
}
@Test
public void appWithNoVersionsOrScreenshots() {
App app = TestUtils.getApp();
AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks);
adapter.updateItems(TestUtils.getApp(), Collections.emptyList(), appPrefs);
populateViewHolders(adapter);
assertEquals(3, adapter.getItemCount());
}
@Test
public void appWithScreenshots() {
App app = TestUtils.getApp();
app.phoneScreenshots = new ArrayList<>(2);
app.phoneScreenshots.add(FileV2.fromPath("screenshot1.png"));
app.phoneScreenshots.add(FileV2.fromPath("screenshot2.png"));
AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks);
adapter.updateItems(app, Collections.emptyList(), appPrefs);
populateViewHolders(adapter);
assertEquals(4, adapter.getItemCount());
}
@Test
public void appWithVersions() {
App app = TestUtils.getApp();
app.preferredSigner = "eaa1d713b9c2a0475234a86d6539f910";
List<Apk> apks = new ArrayList<>();
apks.add(TestUtils.getApk(1));
apks.add(TestUtils.getApk(2));
apks.add(TestUtils.getApk(3));
app.installedApk = apks.get(0);
AppDetailsRecyclerViewAdapter adapter = new AppDetailsRecyclerViewAdapter(context, app, dummyCallbacks);
adapter.updateItems(app, apks, appPrefs);
populateViewHolders(adapter);
// Starts collapsed, not showing versions at all. (also showing permissions)
assertEquals(4, adapter.getItemCount());
adapter.setShowVersions(true);
assertEquals(7, adapter.getItemCount());
adapter.setShowVersions(false);
assertEquals(4, adapter.getItemCount());
}
/**
* Ensures that every single item in the adapter gets its view holder created and bound.
* Doesn't care about what type of holder it should be, the adapter is able to figure all that
* out for us .
*/
private void populateViewHolders(RecyclerView.Adapter<RecyclerView.ViewHolder> adapter) {
ViewGroup parent = (ViewGroup) LayoutInflater.from(context).inflate(R.layout.app_details2_links, null);
for (int i = 0; i < adapter.getItemCount(); i++) {
RecyclerView.ViewHolder viewHolder = adapter.createViewHolder(parent, adapter.getItemViewType(i));
adapter.bindViewHolder(viewHolder, i);
}
}
private final AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks dummyCallbacks
= new AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks() {
@Override
public boolean isAppDownloading() {
return false;
}
@Override
public void openUrl(String url) {
}
@Override
public void installApk(Apk apk) {
}
@Override
public void uninstallApk() {
}
@Override
public void installCancel() {
}
@Override
public void launchApk() {
}
@Override
public void onRepoChanged(long repoId) {
}
@Override
public void onPreferredRepoChanged(long repoId) {
}
};
}

View File

@@ -1,24 +0,0 @@
package org.fdroid.fdroid.views.main;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import org.junit.Test;
public class MainActivityTest {
@Test
public void testSanitizeSearchTerms() {
for (String valid : new String[]{"private browser", "πÇÇ", "现代 通用字", "български", "عربي"}) {
assertEquals(valid, MainActivity.sanitizeSearchTerms(valid));
}
for (String invalid : new String[]{
"Robert'); DROP TABLE Students; --",
"xxx') OR 1 = 1 -- ]",
"105 OR 1=1;",
"\" OR \"=\"",
}) {
assertNotEquals(invalid, MainActivity.sanitizeSearchTerms(invalid));
}
}
}

View File

@@ -1,95 +0,0 @@
package org.fdroid.fdroid.work;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.nearby.LocalRepoManager;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.shadows.ShadowLog;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
/**
* Test non-time-based cache deletion methods. Robolectric is lacking full
* support for getting time from files, so methods that rely on time must be
* tested in {@code CleanCacheWorkerTest} in {@code androidTest}.
*/
@RunWith(RobolectricTestRunner.class)
public class CleanCacheWorkerTest {
public static final String TAG = "CleanCacheWorkerTest";
private static final Context CONTEXT = ApplicationProvider.getApplicationContext();
private static final File FILES_DIR = CONTEXT.getFilesDir();
@Before
public void setUp() {
ShadowLog.stream = System.out;
Preferences.setupForTests(CONTEXT);
}
@Test
public void testDeleteOldInstallerFiles() throws IOException {
Assume.assumeTrue(BuildConfig.FLAVOR.startsWith("full"));
ArrayList<File> webRootAssetFiles = new ArrayList<>();
File indexHtml = new File(FILES_DIR, "index.html");
assertTrue(indexHtml.createNewFile());
webRootAssetFiles.add(indexHtml);
for (String name : LocalRepoManager.WEB_ROOT_ASSET_FILES) {
File f = new File(FILES_DIR, name);
assertTrue(f.createNewFile());
webRootAssetFiles.add(f);
}
File apk = new File(FILES_DIR, "fake.apk");
assertTrue(apk.createNewFile());
File giantblob = new File(FILES_DIR, "giantblob");
assertTrue(giantblob.createNewFile());
File obf = new File(FILES_DIR, "fake.obf");
assertTrue(obf.createNewFile());
File zip = new File(FILES_DIR, "fake.zip");
assertTrue(zip.createNewFile());
CleanCacheWorker.deleteOldInstallerFiles(CONTEXT);
assertFalse(apk.exists());
assertFalse(giantblob.exists());
assertFalse(obf.exists());
assertFalse(zip.exists());
for (File f : webRootAssetFiles) {
assertTrue(f.exists());
}
}
/**
* Pure smoke check, Robolectric does not support file times fully.
*/
@Test
public void testDeleteExpiredApksFromCache() {
CleanCacheWorker.deleteExpiredApksFromCache(CONTEXT);
}
/**
* Pure smoke check, Robolectric does not support file times fully.
*/
@Test
public void testDeleteStrayIndexFiles() {
CleanCacheWorker.deleteStrayIndexFiles(CONTEXT);
}
/**
* Pure smoke check, Robolectric does not support file times fully.
*/
@Test
public void testDeleteOldIcons() {
CleanCacheWorker.deleteOldIcons(CONTEXT);
}
}

View File

@@ -1,98 +0,0 @@
package org.fdroid.fdroid.work;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import android.app.Application;
import android.content.ContextWrapper;
import android.text.format.DateUtils;
import androidx.test.core.app.ApplicationProvider;
import org.apache.commons.io.FileUtils;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.installer.InstallHistoryService;
import org.fdroid.fdroid.work.FDroidMetricsWorker.MatomoEvent;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.Collection;
@Config(application = Application.class)
@RunWith(RobolectricTestRunner.class)
public class FDroidMetricsWorkerTest {
protected ContextWrapper context;
@Before
public final void setUp() {
context = ApplicationProvider.getApplicationContext();
Preferences.setupForTests(context);
}
@Test
public void testNormalizeTimestampToWeek() {
long startTime = 1610038865743L;
long endTime = 1610037631519L;
long normalizedStart = FDroidMetricsWorker.toCleanInsightsTimestamp(startTime);
long normalizedEnd = FDroidMetricsWorker.toCleanInsightsTimestamp(endTime);
assertEquals(normalizedStart, normalizedEnd);
long normalizedRelativeEnd = FDroidMetricsWorker.toCleanInsightsTimestamp(startTime, endTime);
assertEquals(1609976365L, normalizedRelativeEnd);
}
@Test
public void testGenerateReport() throws IOException {
String json = FDroidMetricsWorker.generateReport(context);
System.out.println(json);
File downloads = new File(System.getenv("HOME"), "Downloads");
if (downloads.exists()) {
File output = new File(downloads, getClass().getName() + ".testGenerateReport.json");
FileUtils.writeStringToFile(output, json);
}
// TODO validate against the schema
}
@Test
public void testParseInstallHistory() throws IOException {
FileUtils.copyFile(TestUtils.copyResourceToTempFile("install_history_all"),
InstallHistoryService.getInstallHistoryFile(context));
long weekStart = FDroidMetricsWorker.getReportingWeekStart(1611268892206L + DateUtils.WEEK_IN_MILLIS);
Collection<? extends MatomoEvent> events = FDroidMetricsWorker.parseInstallHistoryCsv(context,
weekStart);
assertEquals(3, events.size());
for (MatomoEvent event : events) {
assertEquals(event.name, "com.termux");
}
Collection<? extends MatomoEvent> oneWeekAgo = FDroidMetricsWorker.parseInstallHistoryCsv(context,
weekStart - DateUtils.WEEK_IN_MILLIS);
assertEquals(11, oneWeekAgo.size());
Collection<? extends MatomoEvent> twoWeeksAgo = FDroidMetricsWorker.parseInstallHistoryCsv(context,
weekStart - (2 * DateUtils.WEEK_IN_MILLIS));
assertEquals(0, twoWeeksAgo.size());
Collection<? extends MatomoEvent> threeWeeksAgo = FDroidMetricsWorker.parseInstallHistoryCsv(context,
weekStart - (3 * DateUtils.WEEK_IN_MILLIS));
assertEquals(9, threeWeeksAgo.size());
assertNotEquals(oneWeekAgo, threeWeeksAgo);
}
@Test
public void testGetReportingWeekStart() throws ParseException {
long now = System.currentTimeMillis();
long start = FDroidMetricsWorker.getReportingWeekStart(now);
assertTrue((now - DateUtils.WEEK_IN_MILLIS) > start);
assertTrue((now - DateUtils.WEEK_IN_MILLIS) < (start + DateUtils.WEEK_IN_MILLIS));
}
}

View File

@@ -0,0 +1,69 @@
package org.fdroid.ui.details
import org.junit.Test
import kotlin.test.assertEquals
/**
* Tests modifications to app details descriptions done in [getHtmlDescription].
*/
class HtmlDescriptionTest {
@Test
fun testLinks() {
val description = """
2. If you have experience with Java and the Android SDK, then we look forward to your contributions! More info: https://mediawiki.org/wiki/Wikimedia_Apps/Team/Android/App_hacking
3. Explanation of permissions needed by the app: https://mediawiki.org/wiki/Wikimedia_Apps/Android_FAQ#Security_and_Permissions
"""
val expectedDescription = """<br>
2. If you have experience with Java and the Android SDK, then we look forward to your contributions! More info: <a href="https://mediawiki.org/wiki/Wikimedia_Apps/Team/Android/App_hacking">https://mediawiki.org/wiki/Wikimedia_Apps/Team/Android/App_hacking</a><br>
<br>
3. Explanation of permissions needed by the app: <a href="https://mediawiki.org/wiki/Wikimedia_Apps/Android_FAQ#Security_and_Permissions">https://mediawiki.org/wiki/Wikimedia_Apps/Android_FAQ#Security_and_Permissions</a><br>
"""
assertEquals(expectedDescription, getHtmlDescription(description))
}
@Test
fun testLinkAtTheVeryEnd() {
val description = """
Project page: https://github.com/lukaspieper/Gcam-Services-Provider"""
val expectedDescription = """<br>
Project page: <a href="https://github.com/lukaspieper/Gcam-Services-Provider">https://github.com/lukaspieper/Gcam-Services-Provider</a>"""
assertEquals(expectedDescription, getHtmlDescription(description))
}
@Test
fun testLinkWithDotAtTheEnd() {
val description = """please visit our website: https://wikimediafoundation.org/."""
@Suppress("ktlint:standard:max-line-length")
val expectedDescription = """please visit our website: <a href="https://wikimediafoundation.org/">https://wikimediafoundation.org/</a>."""
assertEquals(expectedDescription, getHtmlDescription(description))
}
@Test
fun testLinkInRoundBrackets() {
val description = """our link (https://wikimediafoundation.org/)."""
@Suppress("ktlint:standard:max-line-length")
val expectedDescription = """our link (<a href="https://wikimediafoundation.org/">https://wikimediafoundation.org/</a>)."""
assertEquals(expectedDescription, getHtmlDescription(description))
}
@Test
fun testHeadlineRemoval() {
val description = """<h1>SimpleX - the first messaging platform that has no user identifiers, not even random numbers!</h1>
<p><a href="https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html" target="_blank">Security assessment</a> was done by Trail of Bits in November 2022.</p>
<p>SimpleX Chat features:</p>
<ul>
<li>end-to-end encrypted messages, with editing, replies and deletion of messages.</li>
<li>sending end-to-end encrypted images and files.</li>"""
val expectedDescription = """SimpleX - the first messaging platform that has no user identifiers, not even random numbers!<br>
<p><a href="https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html" target="_blank">Security assessment</a> was done by Trail of Bits in November 2022.</p>
<p>SimpleX Chat features:</p>
<ul>
<li>end-to-end encrypted messages, with editing, replies and deletion of messages.</li>
<li>sending end-to-end encrypted images and files.</li>"""
assertEquals(expectedDescription, getHtmlDescription(description))
}
}

View File

@@ -0,0 +1,109 @@
package org.fdroid.ui.navigation
import android.content.Intent
import android.content.Intent.ACTION_SHOW_APP_INFO
import android.content.Intent.ACTION_VIEW
import android.content.Intent.CATEGORY_BROWSABLE
import android.content.Intent.EXTRA_PACKAGE_NAME
import androidx.compose.runtime.mutableStateOf
import androidx.core.net.toUri
import androidx.navigation3.runtime.NavBackStack
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals
@Config(sdk = [24]) // needed for ACTION_SHOW_APP_INFO
@RunWith(RobolectricTestRunner::class)
class IntentRouterTest {
val navigationState = NavigationState(
startRoute = NavigationKey.Discover,
topLevelRoute = mutableStateOf(NavigationKey.Discover),
backStacks = topLevelRoutes.associateWith { key ->
NavBackStack(key)
}
)
val navigator = Navigator(navigationState)
private val intentRouter = IntentRouter(navigator)
@Test
fun testBrowserUris() {
val packageName = "org.fdroid.fdroid"
listOf(
"https://f-droid.org/packages/$packageName/",
"https://f-droid.org/de/packages/$packageName/",
"https://f-droid.org/en-US/packages/$packageName",
"https://cloudflare.f-droid.org/zh_Hans/packages/$packageName",
"market://details?id=$packageName",
).forEach { url ->
val i = Intent().apply {
action = ACTION_VIEW
addCategory(CATEGORY_BROWSABLE)
data = url.toUri()
}
intentRouter.accept(i)
assertEquals(NavigationKey.AppDetails(packageName), navigator.last)
}
}
@Test
fun testMalformedBrowserUris() {
val packageName = "fdroid') DROP TABLE apps; --"
listOf(
"https://f-droid.org/packages/$packageName/",
"https://f-droid.org/de/packages/$packageName/",
"https://f-droid.org/en-US/packages/$packageName",
"https://cloudflare.f-droid.org/zh_Hans/packages/$packageName",
"market://details?id=$packageName",
).forEach { url ->
val i = Intent().apply {
action = ACTION_VIEW
addCategory(CATEGORY_BROWSABLE)
data = url.toUri()
}
intentRouter.accept(i)
assertEquals(NavigationKey.Discover, navigator.last)
}
}
@Test
fun testShowAppInfo() {
val packageName = "org.fdroid.fdroid"
val i = Intent().apply {
action = ACTION_SHOW_APP_INFO
putExtra(EXTRA_PACKAGE_NAME, packageName)
}
intentRouter.accept(i)
assertEquals(NavigationKey.AppDetails(packageName), navigator.last)
}
@Test
fun testShowAppInfoMalformed() {
val packageName = "fdroid') DROP TABLE apps; --"
val i = Intent().apply {
action = ACTION_SHOW_APP_INFO
putExtra(EXTRA_PACKAGE_NAME, packageName)
}
intentRouter.accept(i)
assertEquals(NavigationKey.Discover, navigator.last)
}
@Test
fun testRepoUris() {
listOf(
"fdroidrepos://example.org/repo",
"FDROIDREPOS://example.org/repo",
"https://fdroid.link/#repo=https://f-droid.org/repo",
"https://fdroid.link/#foo/bar",
).forEach { uri ->
val i = Intent(ACTION_VIEW).apply {
data = uri.toUri()
}
intentRouter.accept(i)
assertEquals(NavigationKey.AddRepo(uri), navigator.last)
}
}
}