summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/build.gradle10
-rw-r--r--core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java15
-rw-r--r--core/src/test/assets/local-feed1/track1.mp3bin0 -> 43341 bytes
-rw-r--r--core/src/test/assets/local-feed2/folder.pngbin0 -> 1589 bytes
-rw-r--r--core/src/test/assets/local-feed2/track1.mp3bin0 -> 43341 bytes
-rw-r--r--core/src/test/assets/local-feed2/track2.mp3bin0 -> 43497 bytes
-rw-r--r--core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java138
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java208
-rw-r--r--core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java6
9 files changed, 373 insertions, 4 deletions
diff --git a/core/build.gradle b/core/build.gradle
index fe130063e..96f0f7f04 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -52,6 +52,12 @@ android {
dimension "market"
}
}
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
}
dependencies {
@@ -94,7 +100,9 @@ dependencies {
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
testImplementation 'junit:junit:4.13'
- testImplementation 'org.mockito:mockito-core:1.10.19'
+ testImplementation 'org.mockito:mockito-inline:3.5.13'
+ testImplementation 'org.robolectric:robolectric:4.3.1'
+ testImplementation 'javax.inject:javax.inject:1'
androidTestImplementation "com.jayway.android.robotium:robotium-solo:$robotiumSoloVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
androidTestImplementation "androidx.test:runner:$runnerVersion"
diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
index 142763d75..4c594783a 100644
--- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
+++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java
@@ -358,6 +358,21 @@ public class PodDBAdapter {
// do nothing
}
+ /**
+ * <p>Resets all database connections to ensure new database connections for
+ * the next test case. Call method only for unit tests.</p>
+ *
+ * <p>That's a workaround for a Robolectric issue in ShadowSQLiteConnection
+ * that leads to an error <tt>IllegalStateException: Illegal connection
+ * pointer</tt> if several threads try to use the same database connection.
+ * For more information see
+ * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p>
+ */
+ public static void tearDownTests() {
+ db = null;
+ SingletonHolder.dbHelper.close();
+ }
+
public static boolean deleteDatabase() {
PodDBAdapter adapter = getInstance();
adapter.open();
diff --git a/core/src/test/assets/local-feed1/track1.mp3 b/core/src/test/assets/local-feed1/track1.mp3
new file mode 100644
index 000000000..b1f993c3f
--- /dev/null
+++ b/core/src/test/assets/local-feed1/track1.mp3
Binary files differ
diff --git a/core/src/test/assets/local-feed2/folder.png b/core/src/test/assets/local-feed2/folder.png
new file mode 100644
index 000000000..9e522a986
--- /dev/null
+++ b/core/src/test/assets/local-feed2/folder.png
Binary files differ
diff --git a/core/src/test/assets/local-feed2/track1.mp3 b/core/src/test/assets/local-feed2/track1.mp3
new file mode 100644
index 000000000..b1f993c3f
--- /dev/null
+++ b/core/src/test/assets/local-feed2/track1.mp3
Binary files differ
diff --git a/core/src/test/assets/local-feed2/track2.mp3 b/core/src/test/assets/local-feed2/track2.mp3
new file mode 100644
index 000000000..310cddd6b
--- /dev/null
+++ b/core/src/test/assets/local-feed2/track2.mp3
Binary files differ
diff --git a/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java b/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java
new file mode 100644
index 000000000..8a8205c10
--- /dev/null
+++ b/core/src/test/java/androidx/documentfile/provider/AssetsDocumentFile.java
@@ -0,0 +1,138 @@
+package androidx.documentfile.provider;
+
+import android.content.res.AssetManager;
+import android.net.Uri;
+import android.webkit.MimeTypeMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.IOException;
+
+/**
+ * <p>Wraps an Android assets file or folder as a DocumentFile object.</p>
+ *
+ * <p>This is used to emulate access to the external storage.</p>
+ */
+public class AssetsDocumentFile extends DocumentFile {
+
+ /**
+ * Absolute file path in the assets folder.
+ */
+ @NonNull
+ private final String fileName;
+
+ @NonNull
+ private final AssetManager assetManager;
+
+ public AssetsDocumentFile(@NonNull String fileName, @NonNull AssetManager assetManager) {
+ super(null);
+ this.fileName = fileName;
+ this.assetManager = assetManager;
+ }
+
+ @Nullable
+ @Override
+ public DocumentFile createFile(@NonNull String mimeType, @NonNull String displayName) {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public DocumentFile createDirectory(@NonNull String displayName) {
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public Uri getUri() {
+ return Uri.parse(fileName);
+ }
+
+ @Nullable
+ @Override
+ public String getName() {
+ int pos = fileName.indexOf('/');
+ if (pos >= 0) {
+ return fileName.substring(pos + 1);
+ } else {
+ return fileName;
+ }
+ }
+
+ @Nullable
+ @Override
+ public String getType() {
+ String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
+ return MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ }
+
+ @Override
+ public boolean isDirectory() {
+ return false;
+ }
+
+ @Override
+ public boolean isFile() {
+ return true;
+ }
+
+ @Override
+ public boolean isVirtual() {
+ return false;
+ }
+
+ @Override
+ public long lastModified() {
+ return 0;
+ }
+
+ @Override
+ public long length() {
+ return 0;
+ }
+
+ @Override
+ public boolean canRead() {
+ return true;
+ }
+
+ @Override
+ public boolean canWrite() {
+ return false;
+ }
+
+ @Override
+ public boolean delete() {
+ return false;
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @NonNull
+ @Override
+ public DocumentFile[] listFiles() {
+ try {
+ String[] files = assetManager.list(fileName);
+ if (files == null) {
+ return new DocumentFile[0];
+ }
+ DocumentFile[] result = new DocumentFile[files.length];
+ for (int i = 0; i < files.length; i++) {
+ String subFileName = fileName + '/' + files[i];
+ result[i] = new AssetsDocumentFile(subFileName, assetManager);
+ }
+ return result;
+ } catch (IOException e) {
+ return new DocumentFile[0];
+ }
+ }
+
+ @Override
+ public boolean renameTo(@NonNull String displayName) {
+ return false;
+ }
+}
diff --git a/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
new file mode 100644
index 000000000..90bf59e92
--- /dev/null
+++ b/core/src/test/java/de/danoeh/antennapod/core/feed/LocalFeedUpdaterTest.java
@@ -0,0 +1,208 @@
+package de.danoeh.antennapod.core.feed;
+
+import android.app.Application;
+import android.content.Context;
+import android.media.MediaMetadataRetriever;
+import android.webkit.MimeTypeMap;
+
+import androidx.annotation.NonNull;
+import androidx.documentfile.provider.AssetsDocumentFile;
+import androidx.documentfile.provider.DocumentFile;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.shadows.ShadowMediaMetadataRetriever;
+
+import java.io.IOException;
+import java.util.List;
+
+import de.danoeh.antennapod.core.ApplicationCallbacks;
+import de.danoeh.antennapod.core.ClientConfig;
+import de.danoeh.antennapod.core.R;
+import de.danoeh.antennapod.core.preferences.UserPreferences;
+import de.danoeh.antennapod.core.storage.DBReader;
+import de.danoeh.antennapod.core.storage.PodDBAdapter;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.robolectric.Shadows.shadowOf;
+
+/**
+ * Test local feeds handling in class LocalFeedUpdater.
+ */
+@RunWith(RobolectricTestRunner.class)
+public class LocalFeedUpdaterTest {
+
+ /**
+ * URL to locate the local feed media files on the external storage (SD card).
+ * The exact URL doesn't matter here as access to external storage is mocked
+ * (seems not to be supported by Robolectric).
+ */
+ private static final String FEED_URL =
+ "content://com.android.externalstorage.documents/tree/primary%3ADownload%2Flocal-feed";
+ private static final String LOCAL_FEED_DIR1 = "local-feed1";
+ private static final String LOCAL_FEED_DIR2 = "local-feed2";
+
+ private Context context;
+
+ @Before
+ public void setUp() throws Exception {
+ // Initialize environment
+ context = InstrumentationRegistry.getInstrumentation().getContext();
+ UserPreferences.init(context);
+
+ Application app = (Application) context;
+ ClientConfig.applicationCallbacks = mock(ApplicationCallbacks.class);
+ when(ClientConfig.applicationCallbacks.getApplicationInstance()).thenReturn(app);
+
+ // Initialize database
+ PodDBAdapter.init(context);
+ PodDBAdapter.deleteDatabase();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ adapter.open();
+ adapter.close();
+
+ mapDummyMetadata(LOCAL_FEED_DIR1);
+ mapDummyMetadata(LOCAL_FEED_DIR2);
+ shadowOf(MimeTypeMap.getSingleton()).addExtensionMimeTypMapping("mp3", "audio/mp3");
+ }
+
+ @After
+ public void tearDown() {
+ PodDBAdapter.tearDownTests();
+ }
+
+ /**
+ * Test adding a new local feed.
+ */
+ @Test
+ public void testUpdateFeed_AddNewFeed() {
+ // check for empty database
+ List<Feed> feedListBefore = DBReader.getFeedList();
+ assertTrue(feedListBefore.isEmpty());
+
+ callUpdateFeed(LOCAL_FEED_DIR2);
+
+ // verify new feed in database
+ verifySingleFeedInDatabaseAndItemCount(2);
+ Feed feedAfter = verifySingleFeedInDatabase();
+ assertEquals(FEED_URL, feedAfter.getDownload_url());
+ }
+
+ /**
+ * Test adding further items to an existing local feed.
+ */
+ @Test
+ public void testUpdateFeed_AddMoreItems() {
+ // add local feed with 1 item (localFeedDir1)
+ callUpdateFeed(LOCAL_FEED_DIR1);
+
+ // now add another item (by changing to local feed folder localFeedDir2)
+ callUpdateFeed(LOCAL_FEED_DIR2);
+
+ verifySingleFeedInDatabaseAndItemCount(2);
+ }
+
+ /**
+ * Test removing items from an existing local feed without a corresponding media file.
+ */
+ @Test
+ public void testUpdateFeed_RemoveItems() {
+ // add local feed with 2 items (localFeedDir1)
+ callUpdateFeed(LOCAL_FEED_DIR2);
+
+ // now remove an item (by changing to local feed folder localFeedDir1)
+ callUpdateFeed(LOCAL_FEED_DIR1);
+
+ verifySingleFeedInDatabaseAndItemCount(1);
+ }
+
+ /**
+ * Test feed icon defined in the local feed media folder.
+ */
+ @Test
+ public void testUpdateFeed_FeedIconFromFolder() {
+ callUpdateFeed(LOCAL_FEED_DIR2);
+
+ Feed feedAfter = verifySingleFeedInDatabase();
+ assertTrue(feedAfter.getImageUrl().contains("local-feed2/folder.png"));
+ }
+
+ /**
+ * Test default feed icon if there is no matching file in the local feed media folder.
+ */
+ @Test
+ public void testUpdateFeed_FeedIconDefault() {
+ callUpdateFeed(LOCAL_FEED_DIR1);
+
+ Feed feedAfter = verifySingleFeedInDatabase();
+ String resourceEntryName = context.getResources().getResourceEntryName(R.raw.local_feed_default_icon);
+ assertTrue(feedAfter.getImageUrl().contains(resourceEntryName));
+ }
+
+ /**
+ * Fill ShadowMediaMetadataRetriever with dummy duration and title.
+ *
+ * @param localFeedDir assets local feed folder with media files
+ */
+ private void mapDummyMetadata(@NonNull String localFeedDir) throws IOException {
+ String[] fileNames = context.getAssets().list(localFeedDir);
+ for (String fileName : fileNames) {
+ String path = localFeedDir + '/' + fileName;
+ ShadowMediaMetadataRetriever.addMetadata(path,
+ MediaMetadataRetriever.METADATA_KEY_DURATION, "10");
+ ShadowMediaMetadataRetriever.addMetadata(path,
+ MediaMetadataRetriever.METADATA_KEY_TITLE, fileName);
+ }
+
+ }
+
+ /**
+ * Calls the method {@link LocalFeedUpdater#updateFeed(Feed, Context)} with
+ * the given local feed folder.
+ *
+ * @param localFeedDir assets local feed folder with media files
+ */
+ private void callUpdateFeed(@NonNull String localFeedDir) {
+ DocumentFile documentFile = new AssetsDocumentFile(localFeedDir, context.getAssets());
+ try (MockedStatic<DocumentFile> dfMock = Mockito.mockStatic(DocumentFile.class)) {
+ // mock external storage
+ dfMock.when(() -> DocumentFile.fromTreeUri(any(), any())).thenReturn(documentFile);
+
+ // call method to test
+ Feed feed = new Feed(FEED_URL, null);
+ LocalFeedUpdater.updateFeed(feed, context);
+ }
+ }
+
+ /**
+ * Verify that the database contains exactly one feed and return that feed.
+ */
+ @NonNull
+ private static Feed verifySingleFeedInDatabase() {
+ List<Feed> feedListAfter = DBReader.getFeedList();
+ assertEquals(1, feedListAfter.size());
+ return feedListAfter.get(0);
+ }
+
+ /**
+ * Verify that the database contains exactly one feed and the number of
+ * items in the feed.
+ *
+ * @param expectedItemCount expected number of items in the feed
+ */
+ private static void verifySingleFeedInDatabaseAndItemCount(int expectedItemCount) {
+ Feed feed = verifySingleFeedInDatabase();
+ List<FeedItem> feedItems = DBReader.getFeedItemList(feed);
+ assertEquals(expectedItemCount, feedItems.size());
+ }
+}
diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java
index ed7d2fa75..6c5a9daf1 100644
--- a/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java
+++ b/core/src/test/java/de/danoeh/antennapod/core/storage/ItemEnqueuePositionCalculatorTest.java
@@ -32,7 +32,7 @@ import static java.util.Collections.emptyList;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.stub;
+import static org.mockito.Mockito.when;
public class ItemEnqueuePositionCalculatorTest {
@@ -189,7 +189,7 @@ public class ItemEnqueuePositionCalculatorTest {
//
ItemEnqueuePositionCalculator calculator = new ItemEnqueuePositionCalculator(options);
DownloadStateProvider stubDownloadStateProvider = mock(DownloadStateProvider.class);
- stub(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).toReturn(false);
+ when(stubDownloadStateProvider.isDownloadingFile(any(FeedMedia.class))).thenReturn(false);
calculator.downloadStateProvider = stubDownloadStateProvider;
// Setup initial data
@@ -232,7 +232,7 @@ public class ItemEnqueuePositionCalculatorTest {
private static FeedItem setAsDownloading(FeedItem item, DownloadStateProvider stubDownloadStateProvider,
boolean isDownloading) {
- stub(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).toReturn(isDownloading);
+ when(stubDownloadStateProvider.isDownloadingFile(item.getMedia())).thenReturn(isDownloading);
return item;
}