Best Kotest code snippet using io.kotest.assertions.extracting.extracting
BrowserTreeStructureTest.kt
Source:BrowserTreeStructureTest.kt  
...34import fr.nihilus.music.service.MediaCategory35import fr.nihilus.music.service.MediaContent36import fr.nihilus.music.service.generateRandomTrackSequence37import io.kotest.assertions.assertSoftly38import io.kotest.assertions.extracting39import io.kotest.assertions.throwables.shouldNotThrowAny40import io.kotest.assertions.throwables.shouldThrow41import io.kotest.matchers.collections.*42import io.kotest.matchers.shouldBe43import io.kotest.matchers.types.shouldBeTypeOf44import kotlinx.coroutines.flow.first45import kotlinx.coroutines.test.runTest46import org.junit.Test47import org.junit.runner.RunWith48/**49 * Validate the structure of the [BrowserTree]:50 * - tree can be browsed from the root to the topmost leafs,51 * - children of those nodes are correctly fetched and mapped to [MediaContent]s.52 */53@RunWith(AndroidJUnit4::class)54internal class BrowserTreeStructureTest {55    private val context: Context56        get() = ApplicationProvider.getApplicationContext()57    @Test58    fun `When loading children of Root, then return all available types`() = runTest {59        val rootChildren = loadChildrenOf(MediaId(TYPE_ROOT))60        extracting(rootChildren) { id }.shouldContainExactlyInAnyOrder(61            MediaId(TYPE_TRACKS),62            MediaId(TYPE_ARTISTS),63            MediaId(TYPE_ALBUMS),64            MediaId(TYPE_PLAYLISTS),65            MediaId(TYPE_SMART)66        )67        assertThatAllAreBrowsableAmong(rootChildren)68        assertThatNoneArePlayableAmong(rootChildren)69    }70    @Test71    fun `When loading children of Track type, then return track categories`() = runTest {72        val trackTypeChildren = loadChildrenOf(MediaId(TYPE_TRACKS))73        extracting(trackTypeChildren) { id }.shouldContainExactlyInAnyOrder(74            MediaId(TYPE_TRACKS, CATEGORY_ALL),75            MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED),76            MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED),77            MediaId(TYPE_TRACKS, CATEGORY_POPULAR)78        )79        assertThatAllAreBrowsableAmong(trackTypeChildren)80        assertThatAllArePlayableAmong(trackTypeChildren)81    }82    @Test83    fun `When loading children of Album type, then return all albums from repository`() = runTest {84        val albumTypeChildren = loadChildrenOf(MediaId(TYPE_ALBUMS))85        extracting(albumTypeChildren) { id }.shouldContainExactly(86            MediaId(TYPE_ALBUMS, "40"),87            MediaId(TYPE_ALBUMS, "38"),88            MediaId(TYPE_ALBUMS, "102"),89            MediaId(TYPE_ALBUMS, "95"),90            MediaId(TYPE_ALBUMS, "7"),91            MediaId(TYPE_ALBUMS, "6"),92            MediaId(TYPE_ALBUMS, "65"),93            MediaId(TYPE_ALBUMS, "26")94        )95        assertThatAllAreBrowsableAmong(albumTypeChildren)96        assertThatAllArePlayableAmong(albumTypeChildren)97    }98    @Test99    fun `When loading children of album type, then return items from album metadata`() = runTest {100        val allAlbums = loadChildrenOf(MediaId(TYPE_ALBUMS))101        val anAlbum = allAlbums.requireItemWith(MediaId(TYPE_ALBUMS, "40"))102        anAlbum.shouldBeTypeOf<MediaCategory>()103        assertSoftly(anAlbum) {104            title shouldBe "The 2nd Law"105            subtitle shouldBe "Muse"106            count shouldBe 1107        }108    }109    @Test110    fun `When loading children of Artist type, then return all artists from repository`() =111        runTest {112            val allArtists = loadChildrenOf(MediaId(TYPE_ARTISTS))113            extracting(allArtists) { id }.shouldContainExactly(114                MediaId(TYPE_ARTISTS, "5"),115                MediaId(TYPE_ARTISTS, "26"),116                MediaId(TYPE_ARTISTS, "4"),117                MediaId(TYPE_ARTISTS, "13"),118                MediaId(TYPE_ARTISTS, "18")119            )120            assertThatAllAreBrowsableAmong(allArtists)121            assertThatNoneArePlayableAmong(allArtists)122        }123    @Test124    fun `When loading children of Artist type, then return items from artist metadata`() = runTest {125        val allArtists = loadChildrenOf(MediaId(TYPE_ARTISTS))126        val anArtist = allArtists.requireItemWith(MediaId(TYPE_ARTISTS, "5"))127        anArtist.shouldBeTypeOf<MediaCategory>()128        assertSoftly(anArtist) {129            title shouldBe "AC/DC"130            // TODO Use a plural string resource instead.131            subtitle shouldBe "1 albums, 2 tracks"132            count shouldBe 2133        }134    }135    @Test136    fun `When loading children of Playlist type, then return all playlists`() = runTest {137        val allPlaylists = loadChildrenOf(MediaId(TYPE_PLAYLISTS))138        extracting(allPlaylists) { id }.shouldContainExactly(139            MediaId(TYPE_PLAYLISTS, "1"),140            MediaId(TYPE_PLAYLISTS, "2"),141            MediaId(TYPE_PLAYLISTS, "3")142        )143        assertThatAllAreBrowsableAmong(allPlaylists)144        assertThatAllArePlayableAmong(allPlaylists)145    }146    @Test147    fun `When loading children of Playlist type, then return items from playlist metadata`() =148        runTest {149            val allPlaylists = loadChildrenOf(MediaId(TYPE_PLAYLISTS))150            val aPlaylist = allPlaylists.requireItemWith(MediaId(TYPE_PLAYLISTS, "1"))151            aPlaylist.title shouldBe "Zen"152        }153    @Test154    fun `Given any browsable parent, when loading its children then never throw`() = runTest {155        val browserTree = BrowserTreeImpl(156            context,157            TestMediaDao(),158            TestPlaylistDao(),159            TestUsageManager(),160            TestSpotifyManager()161        )162        browserTree.walk(MediaId(TYPE_ROOT)) { child, _ ->163            if (child is MediaCategory) {164                shouldNotThrowAny {165                    browserTree.getChildren(child.id).first()166                }167            }168        }169    }170    @Test171    fun `Given any non browsable item, when loading its children then throw NoSuchElementException`() =172        runTest {173            val browserTree = BrowserTreeImpl(174                context,175                TestMediaDao(),176                TestPlaylistDao(),177                TestUsageManager(),178                TestSpotifyManager()179            )180            browserTree.walk(MediaId(TYPE_ROOT)) { child, _ ->181                if (child !is MediaCategory) {182                    shouldThrow<NoSuchElementException> {183                        browserTree.getChildren(child.id).first()184                    }185                }186            }187        }188    @Test189    fun `When loading children of All Tracks, then return all tracks from repository`() = runTest {190        val allTracks = loadChildrenOf(MediaId(TYPE_TRACKS, CATEGORY_ALL))191        extracting(allTracks) { id }.shouldContainExactly(192            MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),193            MediaId(TYPE_TRACKS, CATEGORY_ALL, 309),194            MediaId(TYPE_TRACKS, CATEGORY_ALL, 481),195            MediaId(TYPE_TRACKS, CATEGORY_ALL, 48),196            MediaId(TYPE_TRACKS, CATEGORY_ALL, 125),197            MediaId(TYPE_TRACKS, CATEGORY_ALL, 294),198            MediaId(TYPE_TRACKS, CATEGORY_ALL, 219),199            MediaId(TYPE_TRACKS, CATEGORY_ALL, 75),200            MediaId(TYPE_TRACKS, CATEGORY_ALL, 464),201            MediaId(TYPE_TRACKS, CATEGORY_ALL, 477)202        )203        assertThatAllArePlayableAmong(allTracks)204        assertThatNoneAreBrowsableAmong(allTracks)205    }206    @Test207    fun `When loading children of All Tracks, then return items from track metadata`() = runTest {208        val allTracks = loadChildrenOf(MediaId(TYPE_TRACKS, CATEGORY_ALL))209        val aTrack = allTracks.requireItemWith(MediaId(TYPE_TRACKS, CATEGORY_ALL, 125L))210        aTrack.shouldBeTypeOf<AudioTrack>()211        assertSoftly(aTrack) {212            title shouldBe "Jailbreak"213            artist shouldBe "AC/DC"214            album shouldBe "Greatest Hits 30 Anniversary Edition"215            duration shouldBe 276668L216            disc shouldBe 2217            number shouldBe 14218        }219    }220    @Test221    fun `When loading children of Most Rated, then return most rated tracks from usage manager`() =222        runTest {223            val mostRatedTracks = loadChildrenOf(MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED))224            extracting(mostRatedTracks) { id }.shouldContainExactly(225                MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED, 75),226                MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED, 464),227                MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED, 48),228                MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED, 477),229                MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED, 294)230            )231            assertThatAllArePlayableAmong(mostRatedTracks)232            assertThatNoneAreBrowsableAmong(mostRatedTracks)233        }234    @Test235    fun `When loading children of Most Rated, then return items from track metadata`() = runTest {236        val mostRecentTracks = loadChildrenOf(MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED))237        val aTrack =238            mostRecentTracks.requireItemWith(MediaId(TYPE_TRACKS, CATEGORY_MOST_RATED, 75L))239        aTrack.shouldBeTypeOf<AudioTrack>()240        assertSoftly(aTrack) {241            title shouldBe "Nightmare"242            artist shouldBe "Avenged Sevenfold"243            album shouldBe "Nightmare"244            duration shouldBe 374648L245            disc shouldBe 1246            number shouldBe 1247        }248    }249    @Test250    fun `When loading children of Recently Added, then return tracks sorted by descending availability date`() =251        runTest {252            val mostRecentTracks = loadChildrenOf(MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED))253            extracting(mostRecentTracks) { id }.shouldContainExactly(254                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 481), // September 25th, 2019 (21:22)255                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 477), // September 25th, 2019 (21:22)256                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 161), // June 18th, 2016257                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 125), // February 12th, 2016 (21:49)258                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 48),  // February 12th, 2016 (21:48)259                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 309), // August 15th, 2015 (15:49)260                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 464), // August 15th, 2015 (15:49)261                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 75),  // August 14th, 2015262                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 294), // November 1st, 2014263                MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED, 219)  // February 12th, 2013264            )265        }266    @Test267    fun `When loading children of Recently Added, then return only playable items`() = runTest {268        val mostRecentTracks = loadChildrenOf(MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED))269        assertThatAllArePlayableAmong(mostRecentTracks)270        assertThatNoneAreBrowsableAmong(mostRecentTracks)271    }272    @Test273    fun `When loading children of Recently Added, then return no more than 25 tracks`() = runTest {274        val testTracks = generateRandomTrackSequence().take(50).toList()275        val media = TestMediaDao(tracks = testTracks)276        val browserTree =277            BrowserTreeImpl(context, media, TestPlaylistDao(), StubUsageManager, StubSpotifyManager)278        val mostRecentTracks = browserTree.getChildren(279            MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED)280        ).first()281        mostRecentTracks shouldHaveAtMostSize 25282    }283    @Test284    fun `When loading children of Recently Added, then return items from track metadata`() =285        runTest {286            val mostRecentTracks = loadChildrenOf(MediaId(TYPE_TRACKS, CATEGORY_RECENTLY_ADDED))287            val aTrack = mostRecentTracks.requireItemWith(288                MediaId(289                    TYPE_TRACKS,290                    CATEGORY_RECENTLY_ADDED,291                    481L292                )293            )294            aTrack.shouldBeTypeOf<AudioTrack>()295            assertSoftly(aTrack) {296                title shouldBe "Dirty Water"297                artist shouldBe "Foo Fighters"298                album shouldBe "Concrete and Gold"299                duration shouldBe 320914L300                disc shouldBe 1301                number shouldBe 6302            }303        }304    @Test305    fun `When loading children of an album, then return tracks from that album`() = runTest {306        assertAlbumHasTracksChildren(307            65L, listOf(308                MediaId(TYPE_ALBUMS, "65", 161)309            )310        )311        assertAlbumHasTracksChildren(312            102L, listOf(313                MediaId(TYPE_ALBUMS, "102", 477),314                MediaId(TYPE_ALBUMS, "102", 481)315            )316        )317        assertAlbumHasTracksChildren(318            7L, listOf(319                MediaId(TYPE_ALBUMS, "7", 48),320                MediaId(TYPE_ALBUMS, "7", 125)321            )322        )323    }324    private suspend fun assertAlbumHasTracksChildren(325        albumId: Long,326        expectedMediaIds: List<MediaId>327    ) {328        val children = loadChildrenOf(MediaId(TYPE_ALBUMS, albumId.toString()))329        assertThatAllArePlayableAmong(children)330        assertThatNoneAreBrowsableAmong(children)331        extracting(children) { id }.shouldContainExactly(expectedMediaIds)332    }333    @Test334    fun `When loading children of an artist, then return its albums followed by its tracks`() =335        runTest {336            val artistChildren = loadChildrenOf(MediaId(TYPE_ARTISTS, "18"))337            val indexOfFirstTrack = artistChildren.indexOfFirst { it.id.track != null }338            val childrenAfterAlbums = artistChildren.subList(indexOfFirstTrack, artistChildren.size)339            val nonTracksAfterAlbums = childrenAfterAlbums.filter { it.id.track == null }340            nonTracksAfterAlbums.shouldBeEmpty()341        }342    @Test343    fun `When loading children of an artist, then return albums from that artist sorted by desc release date`() =344        runTest {345            assertArtistHasAlbumsChildren(26L, listOf(MediaId(TYPE_ALBUMS, "65")))346            assertArtistHasAlbumsChildren(347                18L,348                listOf(MediaId(TYPE_ALBUMS, "40"), MediaId(TYPE_ALBUMS, "38"))349            )350            assertArtistHasAlbumsChildren(351                13L,352                listOf(353                    MediaId(TYPE_ALBUMS, "102"),354                    MediaId(TYPE_ALBUMS, "26"),355                    MediaId(TYPE_ALBUMS, "95")356                )357            )358        }359    private suspend fun assertArtistHasAlbumsChildren(360        artistId: Long,361        expectedAlbumIds: List<MediaId>362    ) {363        val artistChildren = loadChildrenOf(MediaId(TYPE_ARTISTS, artistId.toString()))364        val artistAlbums = artistChildren.filter { it.id.track == null }365        assertThatAllAreBrowsableAmong(artistAlbums)366        assertThatAllArePlayableAmong(artistAlbums)367        extracting(artistAlbums) { id }.shouldContainExactly(expectedAlbumIds)368    }369    @Test370    fun `When loading children of an artist, then return tracks from that artist sorted alphabetically`() =371        runTest {372            assertArtistHasTracksChildren(373                26L, listOf(374                    MediaId(TYPE_ARTISTS, "26", 161)375                )376            )377            assertArtistHasTracksChildren(378                18L, listOf(379                    MediaId(TYPE_ARTISTS, "18", 309),380                    MediaId(TYPE_ARTISTS, "18", 294)381                )382            )383            assertArtistHasTracksChildren(384                13L, listOf(385                    MediaId(TYPE_ARTISTS, "13", 481),386                    MediaId(TYPE_ARTISTS, "13", 219),387                    MediaId(TYPE_ARTISTS, "13", 464),388                    MediaId(TYPE_ARTISTS, "13", 477)389                )390            )391        }392    @Test393    fun `When loading children of an artist, then return artist albums from metadata`() = runTest {394        val artistChildren = loadChildrenOf(MediaId(TYPE_ARTISTS, "26"))395        val anAlbum = artistChildren.requireItemWith(MediaId(TYPE_ALBUMS, "65"))396        anAlbum.shouldBeTypeOf<MediaCategory>()397        assertSoftly(anAlbum) {398            title shouldBe "Sunset on the Golden Age"399            count shouldBe 1400        }401    }402    @Test403    fun `When loading children of an artist, then return artist tracks from metadata`() = runTest {404        val artistChildren = loadChildrenOf(MediaId(TYPE_ARTISTS, "26"))405        val aTrack = artistChildren.requireItemWith(MediaId(TYPE_ARTISTS, "26", 161L))406        aTrack.shouldBeTypeOf<AudioTrack>()407        assertSoftly(aTrack) {408            title shouldBe "1741 (The Battle of Cartagena)"409            duration shouldBe 437603L410        }411    }412    @Test413    fun `When loading children of a playlist, then return tracks from that playlist`() = runTest {414        assertPlaylistHasTracks(415            1L, listOf(416                MediaId(TYPE_PLAYLISTS, "1", 309)417            )418        )419        assertPlaylistHasTracks(420            2L, listOf(421                MediaId(TYPE_PLAYLISTS, "2", 477),422                MediaId(TYPE_PLAYLISTS, "2", 48),423                MediaId(TYPE_PLAYLISTS, "2", 125)424            )425        )426    }427    @Test428    fun `When loading children of a playlist, then return items from track metadata`() = runTest {429        val playlistChildren = loadChildrenOf(MediaId(TYPE_PLAYLISTS, "1"))430        val aPlaylistTrack = playlistChildren.requireItemWith(MediaId(TYPE_PLAYLISTS, "1", 309L))431        aPlaylistTrack.shouldBeTypeOf<AudioTrack>()432        assertSoftly(aPlaylistTrack) {433            title shouldBe "The 2nd Law: Isolated System"434            duration shouldBe 300042L435        }436    }437    @Test438    fun `Given an unknown category, when loading its children then return null`() = runTest {439        assertHasNoChildren(MediaId("unknown"))440        assertHasNoChildren(MediaId(TYPE_TRACKS, "undefined"))441        assertHasNoChildren(MediaId(TYPE_ALBUMS, "1234"))442        assertHasNoChildren(MediaId(TYPE_ARTISTS, "1234"))443        assertHasNoChildren(MediaId(TYPE_PLAYLISTS, "1234"))444    }445    @Test446    fun `When requesting any item, then return an item with the same id as requested`() = runTest {447        assertLoadedItemHasSameMediaId(MediaId(TYPE_ROOT))448        assertLoadedItemHasSameMediaId(MediaId(TYPE_TRACKS, CATEGORY_ALL))449        assertLoadedItemHasSameMediaId(MediaId(TYPE_TRACKS, CATEGORY_ALL, 477L))450        assertLoadedItemHasSameMediaId(MediaId(TYPE_ALBUMS, "102"))451        assertLoadedItemHasSameMediaId(MediaId(TYPE_TRACKS))452        assertLoadedItemHasSameMediaId(MediaId(TYPE_ALBUMS, "102", 477L))453        assertLoadedItemHasSameMediaId(MediaId(TYPE_ARTISTS, "13"))454        assertLoadedItemHasSameMediaId(MediaId(TYPE_ARTISTS, "13", 477L))455        assertLoadedItemHasSameMediaId(MediaId(TYPE_PLAYLISTS, "2"))456        assertLoadedItemHasSameMediaId(MediaId(TYPE_PLAYLISTS, "2", 477L))457    }458    private suspend fun assertLoadedItemHasSameMediaId(itemId: MediaId) {459        val browserTree = BrowserTreeImpl(460            context,461            TestMediaDao(),462            TestPlaylistDao(),463            StubUsageManager,464            StubSpotifyManager465        )466        val requestedItem = browserTree.getItem(itemId)467            ?: failAssumption("Expected an item with id $itemId")468        requestedItem.id shouldBe itemId469    }470    @Test471    fun `When requesting any item, then that item should be in its parents children`() = runTest {472        assertItemIsPartOfItsParentsChildren(MediaId(TYPE_ROOT), MediaId(TYPE_TRACKS))473        assertItemIsPartOfItsParentsChildren(474            MediaId(TYPE_TRACKS),475            MediaId(TYPE_TRACKS, CATEGORY_ALL)476        )477        assertItemIsPartOfItsParentsChildren(478            MediaId(TYPE_TRACKS, CATEGORY_ALL),479            MediaId(TYPE_TRACKS, CATEGORY_ALL, 477L)480        )481        assertItemIsPartOfItsParentsChildren(MediaId(TYPE_ALBUMS), MediaId(TYPE_ALBUMS, "102"))482        assertItemIsPartOfItsParentsChildren(483            MediaId(TYPE_ALBUMS, "102"),484            MediaId(TYPE_ALBUMS, "102", 477L)485        )486        assertItemIsPartOfItsParentsChildren(MediaId(TYPE_ARTISTS), MediaId(TYPE_ARTISTS, "13"))487        assertItemIsPartOfItsParentsChildren(488            MediaId(TYPE_ARTISTS, "13"),489            MediaId(TYPE_ARTISTS, "13", 477L)490        )491        assertItemIsPartOfItsParentsChildren(MediaId(TYPE_PLAYLISTS), MediaId(TYPE_PLAYLISTS, "2"))492        assertItemIsPartOfItsParentsChildren(493            MediaId(TYPE_PLAYLISTS, "2"),494            MediaId(TYPE_PLAYLISTS, "2", 477L)495        )496    }497    private suspend fun assertItemIsPartOfItsParentsChildren(parentId: MediaId, itemId: MediaId) {498        val browserTree = BrowserTreeImpl(499            context,500            TestMediaDao(),501            TestPlaylistDao(),502            StubUsageManager,503            StubSpotifyManager504        )505        val item = browserTree.getItem(itemId)506            ?: failAssumption("Expected $itemId to be an existing item")507        val parentChildren = browserTree.getChildren(parentId).first()508        parentChildren.shouldContain(item)509    }510    private suspend fun assertHasNoChildren(parentId: MediaId) {511        val browserTree = BrowserTreeImpl(512            context,513            TestMediaDao(),514            TestPlaylistDao(),515            StubUsageManager,516            StubSpotifyManager517        )518        shouldThrow<NoSuchElementException> {519            browserTree.getChildren(parentId).first()520        }521    }522    private suspend fun assertPlaylistHasTracks(playlistId: Long, expectedTrackIds: List<MediaId>) {523        val playlistChildren = loadChildrenOf(MediaId(TYPE_PLAYLISTS, playlistId.toString()))524        assertThatAllArePlayableAmong(playlistChildren)525        assertThatNoneAreBrowsableAmong(playlistChildren)526        extracting(playlistChildren) { id }.shouldContainExactly(expectedTrackIds)527    }528    /**529     * Assume that the given collection of media items contains a media with the specified [media id][itemId],530     * and if it does, return it ; otherwise the test execution is stopped due to assumption failure.531     */532    private fun List<MediaContent>.requireItemWith(itemId: MediaId): MediaContent {533        return find { it.id == itemId }534            ?: failAssumption(buildString {535                append("Missing an item with id = $itemId in ")536                joinTo(this, ", ", "[", "]", 10) { it.id.encoded }537            })538    }539    private suspend fun assertArtistHasTracksChildren(540        artistId: Long,541        expectedTrackIds: List<MediaId>542    ) {543        val artistChildren = loadChildrenOf(MediaId(TYPE_ARTISTS, artistId.toString()))544        val artistTracks = artistChildren.filter { it.id.track != null }545        assertThatAllArePlayableAmong(artistTracks)546        assertThatNoneAreBrowsableAmong(artistTracks)547        extracting(artistTracks) { id }.shouldContainExactly(expectedTrackIds)548    }549    private suspend fun loadChildrenOf(parentId: MediaId): List<MediaContent> {550        val browserTree = BrowserTreeImpl(551            context,552            TestMediaDao(),553            TestPlaylistDao(),554            TestUsageManager(),555            StubSpotifyManager556        )557        return browserTree.getChildren(parentId).first()558    }559    private fun assertThatAllAreBrowsableAmong(children: List<MediaContent>) {560        val nonBrowsableItems = children.filterNot { it.browsable }561        if (nonBrowsableItems.isNotEmpty()) {...SubscriptionManagerTest.kt
Source:SubscriptionManagerTest.kt  
...26import fr.nihilus.music.core.os.PermissionDeniedException27import fr.nihilus.music.core.test.coroutines.CoroutineTestRule28import fr.nihilus.music.core.test.failAssumption29import fr.nihilus.music.service.browser.PaginationOptions30import io.kotest.assertions.extracting31import io.kotest.assertions.throwables.shouldNotThrow32import io.kotest.assertions.throwables.shouldThrow33import io.kotest.matchers.collections.*34import io.kotest.matchers.nulls.shouldNotBeNull35import io.kotest.matchers.shouldBe36import io.kotest.matchers.types.shouldBeSameInstanceAs37import io.kotest.matchers.types.shouldNotBeSameInstanceAs38import kotlinx.coroutines.CoroutineScope39import kotlinx.coroutines.delay40import kotlinx.coroutines.yield41import org.junit.Rule42import org.junit.runner.RunWith43import kotlin.test.BeforeTest44import kotlin.test.Test45@RunWith(AndroidJUnit4::class)46class SubscriptionManagerTest {47    @get:Rule48    val test = CoroutineTestRule()49    private lateinit var dispatchers: AppDispatchers50    @BeforeTest51    fun initDispatchers() {52        dispatchers = AppDispatchers(test.dispatcher)53    }54    @Test55    fun `When loading children, then subscribe to their parent in the tree`() =56        test.runWithin { scope ->57            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)58            val children = manager.loadChildren(MediaId(TYPE_TRACKS, CATEGORY_ALL), null)59            extracting(children, MediaContent::id).shouldContainExactly(60                MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),61                MediaId(TYPE_TRACKS, CATEGORY_ALL, 309),62                MediaId(TYPE_TRACKS, CATEGORY_ALL, 481),63                MediaId(TYPE_TRACKS, CATEGORY_ALL, 48),64                MediaId(TYPE_TRACKS, CATEGORY_ALL, 125),65                MediaId(TYPE_TRACKS, CATEGORY_ALL, 294),66                MediaId(TYPE_TRACKS, CATEGORY_ALL, 219),67                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75),68                MediaId(TYPE_TRACKS, CATEGORY_ALL, 464),69                MediaId(TYPE_TRACKS, CATEGORY_ALL, 477)70            )71        }72    @Test73    fun `Given active subscription, when reloading then return cached children`() =74        test.runWithin { scope ->75            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)76            val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)77            // Trigger initial subscription78            val children = manager.loadChildren(parentId, null)79            // Reload children, and check that those are the same80            val reloadedChildren = manager.loadChildren(parentId, null)81            reloadedChildren shouldBeSameInstanceAs children82        }83    @Test84    fun `When loading children of invalid parent, then fail with NoSuchElementException`() =85        test.runWithin { scope ->86            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)87            shouldThrow<NoSuchElementException> {88                val invalidMediaId = MediaId(TYPE_PLAYLISTS, "unknown")89                manager.loadChildren(invalidMediaId, null)90            }91        }92    @Test93    fun `Given denied permission, when loading children then fail with PermissionDeniedException`() =94        test.runWithin { scope ->95            val deniedTree = PermissionBrowserTree(granted = false)96            val manager = CachingSubscriptionManager(scope, deniedTree, dispatchers)97            val permissionFailure = shouldThrow<PermissionDeniedException> {98                val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)99                manager.loadChildren(parentId, null)100            }101            permissionFailure.permission shouldBe Manifest.permission.READ_EXTERNAL_STORAGE102        }103    @Test104    fun `After permission grant, when loading children then proceed without error`() =105        test.runWithin { scope ->106            val permissionTree = PermissionBrowserTree(granted = false)107            val manager = CachingSubscriptionManager(scope, permissionTree, dispatchers)108            val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)109            shouldThrow<PermissionDeniedException> {110                manager.loadChildren(parentId, null)111            }112            permissionTree.granted = true113            shouldNotThrow<PermissionDeniedException> {114                manager.loadChildren(parentId, null)115            }116        }117    @Test118    fun `After permission denial, when loading children then recover`() = test.runWithin { scope ->119        val permissionTree = PermissionBrowserTree(granted = true)120        val manager = CachingSubscriptionManager(scope, permissionTree, dispatchers)121        // Start initial subscription.122        val parentId = MediaId(TYPE_TRACKS, CATEGORY_ALL)123        shouldNotThrow<PermissionDeniedException> {124            manager.loadChildren(parentId, null)125        }126        // Then the permission is denied. When trying to update children, it should fail.127        permissionTree.granted = false128        delay(1001)129        shouldThrow<PermissionDeniedException> {130            manager.loadChildren(parentId, null)131        }132        // It should create a new subscription and succeed.133        permissionTree.granted = true134        shouldNotThrow<PermissionDeniedException> {135            manager.loadChildren(parentId, null)136        }137    }138    @Test139    fun `Given max subscriptions, when loading children then dispose oldest subscriptions`() =140        test.runWithin { scope ->141            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)142            // Trigger subscription of albums 0 to MAX included.143            val childrenPerAlbumId = (0..MAX_ACTIVE_SUBSCRIPTIONS).map { albumId ->144                val parentId = MediaId(TYPE_ALBUMS, albumId.toString())145                manager.loadChildren(parentId, null)146            }147            // Subscription to album 0 should have been disposed when subscribing to album MAX.148            // Its children should not be loaded from cache.149            val albumZeroChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "0"), null)150            albumZeroChildren shouldNotBeSameInstanceAs childrenPerAlbumId[0]151            // Previous re-subscription to album 0 should clear subscription to album 1,152            // and therefore it should not load its children from cache.153            val albumOneChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "1"), null)154            albumOneChildren shouldNotBeSameInstanceAs childrenPerAlbumId[1]155            // Subscription to album MAX should still be active,156            // hence children are loaded from cache.157            val lastAlbumId = MediaId(TYPE_ALBUMS, MAX_ACTIVE_SUBSCRIPTIONS.toString())158            val albumMaxChildren = manager.loadChildren(lastAlbumId, null)159            albumMaxChildren shouldBeSameInstanceAs childrenPerAlbumId[MAX_ACTIVE_SUBSCRIPTIONS]160        }161    @Test162    fun `Given max subscriptions, when reloading children then keep its subscription longer`() =163        test.runWithin { scope ->164            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)165            // Trigger subscriptions to reach the max allowed count.166            val childrenPerAlbumId = (0 until MAX_ACTIVE_SUBSCRIPTIONS).map { albumId ->167                val parentId = MediaId(TYPE_ALBUMS, albumId.toString())168                manager.loadChildren(parentId, null)169            }170            // Reload children of album 0, then create a new subscription.171            manager.loadChildren(MediaId(TYPE_ALBUMS, "0"), null)172            manager.loadChildren(MediaId(TYPE_ALBUMS, MAX_ACTIVE_SUBSCRIPTIONS.toString()), null)173            // If album 0 had not been reloaded, its subscription should have been disposed.174            // The oldest subscription now being that of album 1, it has been disposed instead.175            val albumZeroChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "0"), null)176            val albumOneChildren = manager.loadChildren(MediaId(TYPE_ALBUMS, "1"), null)177            albumZeroChildren shouldBeSameInstanceAs childrenPerAlbumId[0]178            albumOneChildren shouldNotBeSameInstanceAs childrenPerAlbumId[1]179        }180    @Test181    fun `Given pages of size N, when loading children then return the N first items`() =182        test.runWithin { scope ->183            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)184            val paginatedChildren = manager.loadChildren(185                MediaId(TYPE_TRACKS, CATEGORY_ALL),186                PaginationOptions(0, 3)187            )188            extracting(paginatedChildren, MediaContent::id).shouldContainExactly(189                MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),190                MediaId(TYPE_TRACKS, CATEGORY_ALL, 309),191                MediaId(TYPE_TRACKS, CATEGORY_ALL, 481)192            )193        }194    @Test195    fun `Given the page X of size N, when loading children then return N items from position NX`() =196        test.runWithin { scope ->197            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)198            val paginatedChildren = manager.loadChildren(199                MediaId(TYPE_TRACKS, CATEGORY_ALL),200                PaginationOptions(3, 2)201            )202            extracting(paginatedChildren, MediaContent::id).shouldContainExactly(203                MediaId(TYPE_TRACKS, CATEGORY_ALL, 219),204                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)205            )206        }207    @Test208    fun `Given a page after the last page, when loading children then return no children`() =209        test.runWithin { scope ->210            val manager = CachingSubscriptionManager(scope, TestBrowserTree, dispatchers)211            val pagePastChildren = manager.loadChildren(212                MediaId(TYPE_TRACKS, CATEGORY_ALL),213                PaginationOptions(2, 5)214            )215            pagePastChildren.shouldBeEmpty()216        }...BrowserTreeSearchTest.kt
Source:BrowserTreeSearchTest.kt  
...26import fr.nihilus.music.media.provider.MediaDao27import fr.nihilus.music.media.provider.Track28import fr.nihilus.music.media.usage.UsageManager29import fr.nihilus.music.service.MediaContent30import io.kotest.assertions.extracting31import io.kotest.matchers.collections.shouldBeEmpty32import io.kotest.matchers.collections.shouldContainAll33import io.kotest.matchers.collections.shouldContainExactly34import kotlinx.coroutines.test.runTest35import org.junit.runner.RunWith36import kotlin.test.Test37@RunWith(AndroidJUnit4::class)38internal class BrowserTreeSearchTest {39    private val context: Context40        get() = ApplicationProvider.getApplicationContext()41    @Test42    fun `When searching with an empty query then return no results`() = runTest {43        val tree = givenRealisticBrowserTree()44        val results = tree.search(SearchQuery.Empty)45        results.shouldBeEmpty()46    }47    @Test48    fun `Given artist focus, when searching an artist then return that artist`() = runTest {49        val browserTree = givenRealisticBrowserTree()50        val results = browserTree.search(SearchQuery.Artist("Foo Fighters"))51        extracting(results, MediaContent::id).shouldContainExactly(52            MediaId(TYPE_ARTISTS, "13")53        )54    }55    @Test56    fun `Given album focus, when searching an album then return that album`() = runTest {57        val tree = givenRealisticBrowserTree()58        val results = tree.search(SearchQuery.Album("Foo Fighters", "Wasting Light"))59        extracting(results, MediaContent::id).shouldContainExactly(60            MediaId(TYPE_ALBUMS, "26")61        )62    }63    @Test64    fun `Given track focused query, when searching songs then return that song`() = runTest {65        val tree = givenRealisticBrowserTree()66        val results = tree.search(67            SearchQuery.Song(68                artist = "Foo Fighters",69                album = "Concrete and Gold",70                title = "Dirty Water"71            )72        )73        extracting(results, MediaContent::id).shouldContainExactly(74            MediaId(TYPE_TRACKS, CATEGORY_ALL, 481)75        )76    }77    @Test78    fun `Given exact artist name, when searching unfocused then return that artist`() = runTest {79        val tree = givenRealisticBrowserTree()80        val results = tree.search(SearchQuery.Unspecified("foo fighters"))81        extracting(results, MediaContent::id).shouldContainExactly(82            MediaId(TYPE_ARTISTS, "13")83        )84    }85    @Test86    fun `Given exact album title, when searching unfocused then return that album`() = runTest {87        val tree = givenRealisticBrowserTree()88        val results = tree.search(SearchQuery.Unspecified("concrete and gold"))89        extracting(results, MediaContent::id).shouldContainExactly(90            MediaId(TYPE_ALBUMS, "102")91        )92    }93    @Test94    fun `Given exact song title, when searching unfocused then return that song`() = runTest {95        val tree = givenRealisticBrowserTree()96        val results = tree.search(SearchQuery.Unspecified("dirty water"))97        extracting(results, MediaContent::id).shouldContainExactly(98            MediaId(TYPE_TRACKS, CATEGORY_ALL, 481)99        )100    }101    @Test102    fun `Given query matching both album and song, when searching albums then return only that album`() =103        runTest {104            val tree = givenRealisticBrowserTree()105            val results = tree.search(SearchQuery.Album("Avenged Sevenfold", "Nightmare"))106            extracting(results, MediaContent::id).shouldContainExactly(107                MediaId(TYPE_ALBUMS, "6")108            )109        }110    @Test111    fun `Given query matching both album and song, when searching unfocused then return both`() =112        runTest {113            val tree = givenRealisticBrowserTree()114            // Both the album "Nightmare" and its eponymous track are listed in search results.115            // Note that the album should be listed first.116            val results = tree.search(SearchQuery.Unspecified("nightmare"))117            extracting(results, MediaContent::id).shouldContainExactly(118                MediaId(TYPE_ALBUMS, "6"),119                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)120            )121        }122    @Test123    fun `Given uppercase query, when searching unfocused then return results`() = runTest {124        val tree = givenRealisticBrowserTree()125        val results = tree.search(SearchQuery.Unspecified("Nightmare"))126        extracting(results, MediaContent::id).shouldContainAll(127            MediaId(TYPE_ALBUMS, "6"),128            MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)129        )130    }131    @Test132    fun `Given pattern query, when searching then return items containing that pattern`() =133        runTest {134            val tracks = listOf(135                Track(136                    23,137                    "Another Brick In The Wall",138                    "Pink Floyd",139                    "The Wall",140                    0,141                    1,142                    5,143                    "",144                    null,145                    0,146                    2,147                    2,148                    0149                ),150                Track(151                    34,152                    "Another One Bites the Dust",153                    "Queen",154                    "The Game",155                    0,156                    1,157                    3,158                    "",159                    null,160                    0,161                    3,162                    3,163                    0164                ),165                Track(166                    56,167                    "Nothing Else Matters",168                    "Metallica",169                    "Metallica",170                    0L,171                    1,172                    8,173                    "",174                    null,175                    0,176                    4,177                    4,178                    0179                ),180                Track(181                    12,182                    "Otherside",183                    "Red Hot Chili Peppers",184                    "Californication",185                    0,186                    1,187                    6,188                    "",189                    null,190                    0,191                    1,192                    1,193                    0194                ),195                Track(196                    98,197                    "You've Got Another Thing Comin",198                    "Judas Priest",199                    "Screaming for Vengeance",200                    0,201                    1,202                    8,203                    "",204                    null,205                    0,206                    7,207                    7,208                    0209                )210            )211            val tree = BrowserTree(TestMediaDao(emptyList(), emptyList(), tracks))212            // "OTHERside" is listed first (it starts with the pattern),213            // then "AnOTHER Brick In the Wall" (same pattern at same position),214            // then "AnOTHER One Bites the Dust" (one word contains the pattern but slightly longer),215            // then "You've Got AnOTHER Thing Comin" (pattern matches farther)216            val results = tree.search(SearchQuery.Unspecified("other"))217            extracting(results, MediaContent::id).shouldContainExactly(218                MediaId(TYPE_TRACKS, CATEGORY_ALL, 12),219                MediaId(TYPE_TRACKS, CATEGORY_ALL, 23),220                MediaId(TYPE_TRACKS, CATEGORY_ALL, 34),221                MediaId(TYPE_TRACKS, CATEGORY_ALL, 98)222            )223    }224    @Test225    fun `Given pattern query that matches multiple items equally, when searching then return shortest first`() =226        runTest {227            val tracks = listOf(228                Track(229                    10,230                    "Are You Ready",231                    "AC/DC",232                    "The Razor's Edge",233                    0,234                    1,235                    7,236                    "",237                    null,238                    0,239                    32,240                    18,241                    0242                ),243                Track(244                    42,245                    "Are You Gonna Be My Girl",246                    "Jet",247                    "Get Born",248                    0,249                    1,250                    2,251                    "",252                    null,253                    0,254                    78,255                    90,256                    0257                ),258                Track(259                    63,260                    "Are You Gonna Go My Way",261                    "Lenny Kravitz",262                    "Are You Gonna Go My Way",263                    0,264                    1,265                    1,266                    "",267                    null,268                    0,269                    57,270                    23,271                    0272                )273            )274            val tree = BrowserTree(TestMediaDao(emptyList(), emptyList(), tracks))275            // When the pattern matches multiple items equally,276            // shorter items should be displayed first.277            val results = tree.search(SearchQuery.Unspecified("are"))278            extracting(results, MediaContent::id).shouldContainExactly(279                MediaId(TYPE_TRACKS, CATEGORY_ALL, 10),280                MediaId(TYPE_TRACKS, CATEGORY_ALL, 63),281                MediaId(TYPE_TRACKS, CATEGORY_ALL, 42)282            )283    }284    @Test285    fun `When search pattern matches multiple items, then first return results that matches the start of a word`() =286        runTest {287            val tracks = listOf(288                Track(90, "Avalanche", "Ghost", "Prequelle", 0, 1, 12, "", null, 0, 56, 97, 0),289                Track(290                    91,291                    "No Grave But The Sea",292                    "Alestorm",293                    "No Grave But The Sea",294                    0,295                    1,296                    1,297                    "",298                    null,299                    0,300                    456,301                    856,302                    0303                ),304                Track(305                    356,306                    "Gravity",307                    "Bullet For My Valentine",308                    "Gravity",309                    0,310                    1,311                    8,312                    "",313                    null,314                    0,315                    45,316                    99,317                    0318                )319            )320            val artist = listOf(321                Artist(65, "Avatar", 0, 0, null),322                Artist(98, "Avenged Sevenfold", 0, 0, null)323            )324            val tree = BrowserTree(TestMediaDao(artist, emptyList(), tracks))325            val results = tree.search(SearchQuery.Unspecified("av"))326            extracting(results, MediaContent::id).shouldContainExactly(327                MediaId(TYPE_ARTISTS, "65"), // AVatar328                MediaId(TYPE_TRACKS, CATEGORY_ALL, 90), // AValanche329                MediaId(TYPE_ARTISTS, "98"), // AVenged Sevenfold330                MediaId(TYPE_TRACKS, CATEGORY_ALL, 356), // GrAVity331                MediaId(TYPE_TRACKS, CATEGORY_ALL, 91) // No GrAVe But the Sea332            )333    }334    private fun BrowserTree(335        mediaDao: MediaDao,336        usageManager: UsageManager = StubUsageManager337    ): BrowserTree = BrowserTreeImpl(context, mediaDao, StubPlaylistDao, usageManager, StubSpotifyManager)338    private fun givenRealisticBrowserTree(): BrowserTreeImpl =339        BrowserTreeImpl(context, TestMediaDao(), TestPlaylistDao(), TestUsageManager(), StubSpotifyManager)340}...ManagePlaylistActionTest.kt
Source:ManagePlaylistActionTest.kt  
...28import fr.nihilus.music.core.media.MediaId.Builder.TYPE_TRACKS29import fr.nihilus.music.core.test.coroutines.CoroutineTestRule30import fr.nihilus.music.core.test.os.TestClock31import io.kotest.assertions.assertSoftly32import io.kotest.assertions.extracting33import io.kotest.assertions.throwables.shouldThrow34import io.kotest.inspectors.forAll35import io.kotest.inspectors.forNone36import io.kotest.matchers.collections.shouldBeEmpty37import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder38import io.kotest.matchers.collections.shouldHaveSize39import io.kotest.matchers.collections.shouldNotContain40import io.kotest.matchers.file.shouldBeAFile41import io.kotest.matchers.file.shouldContainFile42import io.kotest.matchers.file.shouldNotBeEmpty43import io.kotest.matchers.file.shouldNotExist44import io.kotest.matchers.shouldBe45import kotlinx.coroutines.test.runTest46import org.junit.Rule47import org.junit.rules.RuleChain48import org.junit.rules.TemporaryFolder49import org.junit.runner.RunWith50import java.io.File51import kotlin.test.Test52private const val TEST_TIME = 1585662510L53private const val NEW_PLAYLIST_NAME = "My favorites"54private const val BASE_ICON_URI = "content://fr.nihilus.music.test.provider/icons"55private val SAMPLE_PLAYLIST = Playlist(56    id = 1L,57    title = "Zen",58    created = 0L,59    iconUri = "content://fr.nihilus.music.test.provider/icons/zen.png".toUri()60)61@RunWith(AndroidJUnit4::class)62internal class ManagePlaylistActionTest {63    private val test = CoroutineTestRule()64    private val iconDir = TemporaryFolder()65    @get:Rule66    val rules: RuleChain = RuleChain67        .outerRule(test)68        .around(iconDir)69    private val clock = TestClock(TEST_TIME)70    private val dispatchers = AppDispatchers(test.dispatcher)71    @Test72    fun `When creating a playlist without tracks then record it to PlaylistDao`() = test {73        val dao = InMemoryPlaylistDao()74        val action = ManagePlaylistAction(dao)75        action.createPlaylist(NEW_PLAYLIST_NAME, emptyList())76        val playlists = dao.savedPlaylists77        playlists shouldHaveSize 178        assertSoftly(playlists[0]) {79            title shouldBe NEW_PLAYLIST_NAME80            created shouldBe TEST_TIME81            iconUri shouldBe "content://fr.nihilus.music.test.provider/icons/My_favorites.png".toUri()82        }83        dao.savedTracks.shouldBeEmpty()84    }85    @Test86    fun `When creating a playlist then generate and save its icon`() = test {87        val action = ManagePlaylistAction(InMemoryPlaylistDao())88        action.createPlaylist(NEW_PLAYLIST_NAME, emptyList())89        iconDir.root shouldContainFile "My_favorites.png"90        val iconFile = File(iconDir.root, "My_favorites.png")91        iconFile.shouldBeAFile()92        iconFile.shouldNotBeEmpty()93    }94    @Test95    fun `Given blank name, when creating a playlist then fail with IAE`() = test {96        val action = ManagePlaylistAction(InMemoryPlaylistDao())97        shouldThrow<IllegalArgumentException> {98            action.createPlaylist("", emptyList())99        }100        shouldThrow<IllegalArgumentException> {101            action.createPlaylist("  \t\n\r", emptyList())102        }103    }104    @Test105    fun `When creating a playlist with tracks then record them to PlaylistDao`() = test {106        val dao = InMemoryPlaylistDao()107        val action = ManagePlaylistAction(dao)108        action.createPlaylist(109            name = NEW_PLAYLIST_NAME,110            members = listOf(111                MediaId(TYPE_TRACKS, CATEGORY_ALL, 16L),112                MediaId(TYPE_TRACKS, CATEGORY_ALL, 42L)113            )114        )115        val playlists = dao.savedPlaylists116        playlists shouldHaveSize 1117        val newPlaylist = playlists[0]118        newPlaylist.title shouldBe NEW_PLAYLIST_NAME119        newPlaylist.created shouldBe TEST_TIME120        val tracks = dao.savedTracks121        tracks shouldHaveSize 2122        tracks.forAll { it.playlistId shouldBe newPlaylist.id }123        extracting(tracks) { trackId }.shouldContainExactlyInAnyOrder(16L, 42L)124    }125    @Test126    fun `When creating a playlist with non-track members them fail with IAE`() = test {127        val action = ManagePlaylistAction(InMemoryPlaylistDao())128        for (mediaId in invalidTrackIds()) {129            shouldThrow<IllegalArgumentException> {130                action.createPlaylist(131                    name = NEW_PLAYLIST_NAME,132                    members = listOf(mediaId)133                )134            }135        }136    }137    @Test138    fun `When appending members then add tracks to that playlist`() = test {139        val dao = InMemoryPlaylistDao(initialPlaylists = listOf(SAMPLE_PLAYLIST))140        val action = ManagePlaylistAction(dao)141        action.appendMembers(142            targetPlaylist = MediaId(TYPE_PLAYLISTS, SAMPLE_PLAYLIST.id.toString()),143            members = listOf(144                MediaId(TYPE_TRACKS, CATEGORY_ALL, 16L),145                MediaId(TYPE_TRACKS, CATEGORY_ALL, 42L)146            )147        )148        val tracks = dao.savedTracks149        tracks shouldHaveSize 2150        tracks.forAll { it.playlistId shouldBe SAMPLE_PLAYLIST.id }151        extracting(tracks) { trackId }.shouldContainExactlyInAnyOrder(16L, 42L)152    }153    @Test154    fun `Given invalid target media id, when appending members then fail with IAE`() = test {155        val dao = InMemoryPlaylistDao(initialPlaylists = listOf(SAMPLE_PLAYLIST))156        val action = ManagePlaylistAction(dao)157        val newMemberIds = listOf(158            MediaId(TYPE_TRACKS, CATEGORY_ALL, 16L),159            MediaId(TYPE_TRACKS, CATEGORY_ALL, 42L)160        )161        for (mediaId in invalidPlaylistIds()) {162            shouldThrow<IllegalArgumentException> {163                action.appendMembers(164                    targetPlaylist = mediaId,165                    members = newMemberIds...DeleteTracksActionTest.kt
Source:DeleteTracksActionTest.kt  
...22import fr.nihilus.music.core.media.MediaId.Builder.TYPE_PLAYLISTS23import fr.nihilus.music.core.media.MediaId.Builder.TYPE_TRACKS24import fr.nihilus.music.media.provider.DeleteTracksResult25import fr.nihilus.music.media.provider.Track26import io.kotest.assertions.extracting27import io.kotest.assertions.throwables.shouldThrow28import io.kotest.matchers.collections.shouldNotContain29import io.kotest.matchers.shouldBe30import io.kotest.matchers.types.shouldBeInstanceOf31import kotlinx.coroutines.flow.first32import kotlinx.coroutines.test.runTest33import kotlin.test.Test34private val SAMPLE_TRACKS = listOf(35    Track(36        161,37        "1741 (The Battle of Cartagena)",38        "Alestorm",39        "Sunset on the Golden Age",40        437603,41        1,42        4,43        "",44        null,45        1466283480,46        26,47        65,48        17_506_48149    ),50    Track(51        309,52        "The 2nd Law: Isolated System",53        "Muse",54        "The 2nd Law",55        300042,56        1,57        13,58        "",59        null,60        1439653800,61        18,62        40,63        12_075_96764    ),65    Track(66        481,67        "Dirty Water",68        "Foo Fighters",69        "Concrete and Gold",70        320914,71        1,72        6,73        "",74        null,75        1506374520,76        13,77        102,78        12_912_28279    ),80    Track(81        48,82        "Give It Up",83        "AC/DC",84        "Greatest Hits 30 Anniversary Edition",85        233592,86        1,87        19,88        "",89        null,90        1455310080,91        5,92        7,93        5_716_57894    ),95    Track(96        125,97        "Jailbreak",98        "AC/DC",99        "Greatest Hits 30 Anniversary Edition",100        276668,101        2,102        14,103        "",104        null,105        1455310140,106        5,107        7,108        6_750_404109    ),110    Track(111        294,112        "Knights of Cydonia",113        "Muse",114        "Black Holes and Revelations",115        366946,116        1,117        11,118        "",119        null,120        1414880700,121        18,122        38,123        11_746_572124    ),125    Track(126        219,127        "A Matter of Time",128        "Foo Fighters",129        "Wasting Light",130        276140,131        1,132        8,133        "",134        null,135        1360677660,136        13,137        26,138        11_149_678139    ),140    Track(141        75,142        "Nightmare",143        "Avenged Sevenfold",144        "Nightmare",145        374648,146        1,147        1,148        "",149        null,150        1439590380,151        4,152        6,153        10_828_662154    ),155    Track(156        464,157        "The Pretenders",158        "Foo Fighters",159        "Echoes, Silence, Patience & Grace",160        266509,161        1,162        1,163        "",164        null,165        1439653740,166        13,167        95,168        4_296_041169    ),170    Track(171        477,172        "Run",173        "Foo Fighters",174        "Concrete and Gold",175        323424,176        1,177        2,178        "",179        null,180        1506374520,181        13,182        102,183        13_012_576184    )185)186/**187 * Verify behavior of [DeleteTracksAction].188 */189internal class DeleteTracksActionTest {190    @Test191    fun `Given invalid track media ids, when deleting then fail with IAE`() = runTest {192        val dao = InMemoryTrackDao()193        val action = DeleteTracksAction(dao)194        val invalidTrackIds = listOf(195            MediaId(TYPE_TRACKS, CATEGORY_ALL),196            MediaId(TYPE_ALBUMS, "13"),197            MediaId(TYPE_ARTISTS, "78"),198            MediaId(TYPE_PLAYLISTS, "9")199        )200        for (mediaId in invalidTrackIds) {201            shouldThrow<IllegalArgumentException> {202                action.delete(listOf(mediaId))203            }204        }205    }206    @Test207    fun `When deleting tracks then remove records from dao`() = runTest {208        val dao = InMemoryTrackDao(initial = SAMPLE_TRACKS)209        val action = DeleteTracksAction(dao)210        val deleteResult = action.delete(211            mediaIds = listOf(212                MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),213                MediaId(TYPE_TRACKS, CATEGORY_ALL, 48),214                MediaId(TYPE_TRACKS, CATEGORY_ALL, 75)215            )216        )217        deleteResult.shouldBeInstanceOf<DeleteTracksResult.Deleted>()218        deleteResult.count shouldBe 3219        val savedTracks = dao.tracks.first()220        savedTracks.size shouldBe 7221        extracting(savedTracks) { id }.also {222            it shouldNotContain 161223            it shouldNotContain 48224            it shouldNotContain 75225        }226    }227    @Test228    fun `Given denied permission, when deleting tracks then return RequiresPermission`() = runTest {229        val deniedDao = InMemoryTrackDao(permissionGranted = false)230        val action = DeleteTracksAction(deniedDao)231        val targetTrackIds = listOf(232            MediaId(TYPE_TRACKS, CATEGORY_ALL, 161),233            MediaId(TYPE_TRACKS, CATEGORY_ALL, 464)234        )235        val result = action.delete(targetTrackIds)...UserHashedAuthenticationPasswordSpec.kt
Source:UserHashedAuthenticationPasswordSpec.kt  
...6import stasis.client_android.lib.encryption.secrets.UserHashedAuthenticationPassword7import java.util.UUID8class UserHashedAuthenticationPasswordSpec : WordSpec({9    "A UserHashedAuthenticationPassword" should {10        "allow extracting the hashed password" {11            val originalPassword = "test-password"12            val expectedPassword = "dGVzdC1wYXNzd29yZA"13            val actualPassword = UserHashedAuthenticationPassword(14                user = UUID.randomUUID(),15                hashedPassword = originalPassword.encodeUtf8()16            )17            actualPassword.extract() shouldBe (expectedPassword)18        }19        "fail if the password is extracted more than once" {20            val originalPassword = "test-password"21            val expectedPassword = "dGVzdC1wYXNzd29yZA"22            val actualPassword = UserHashedAuthenticationPassword(23                user = UUID.randomUUID(),24                hashedPassword = originalPassword.encodeUtf8()...ExtractTest.kt
Source:ExtractTest.kt  
1package com.sksamuel.kotest2import io.kotest.assertions.extracting3import io.kotest.assertions.throwables.shouldThrowAny4import io.kotest.core.spec.style.WordSpec5import io.kotest.matchers.collections.shouldContainAll6import io.kotest.matchers.shouldBe7class ExtractTest : WordSpec() {8  init {9    data class Person(val name: String, val age: Int, val friends: List<Person>)10    val p1 = Person("John Doe", 20, emptyList())11    val p2 = Person("Samantha Rose", 19, listOf(p1))12    val persons = listOf(p1, p2)13    "extracting" should {14      "extract simple properties"{15        extracting(persons) { name }16            .shouldContainAll("John Doe", "Samantha Rose")17      }18      "extract complex properties"{19        extracting(persons) { Pair(name, age) }20            .shouldContainAll(21                Pair("John Doe", 20),22                Pair("Samantha Rose", 19)23            )24      }25      "fail if the matcher fails"{26        shouldThrowAny {27          extracting(persons) { name }28              .shouldContainAll("<Some name that is wrong>")29        }.message shouldBe """Collection should contain all of ["<Some name that is wrong>"] but was missing ["<Some name that is wrong>"]"""30      }31    }32  }33}extracting.kt
Source:extracting.kt  
1package io.kotest.assertions2/**3 * `extracting` pulls property values out of a list of objects for _typed_ bulk assertions on properties.4 *5 * The **simple example** shows how `extracting` helps with disjunct collection assertions:6 * ```7 * extracting(persons){ name }8 *   .shouldContainAll("John Doe", "Samantha Roes")9 * ```10 *11 * This is similar to using multiple [forOne] however allows for a more concise notation.12 * ```13 * forOne(persons){ it.name shouldBe "John Doe" }14 * forOne(persons){ it.name shouldBe "Samantha Rose" }15 * ```16 *17 * `extracting` also allows to define complex return types shown in this **elaborate example**:18 * ```19 * extracting(persons){ Pair(name, age) }20 *   .shouldContainAll(21 *     Pair("John Doe", 20),22 *     Pair("Samantha Roes", 19)23 *   )24 * ```25 * @param col the collection of objects from which to extract the properties26 * @param extractor the extractor that defines _which_ properties are returned27 * @author Hannes Thaller28 */29fun <K, T> extracting(col: Collection<K>, extractor: K.() -> T): List<T> {30  return col.map(extractor)31}...extracting
Using AI Code Generation
1@Test fun `extracting should work with one property`() {2val result = extracting(Person::name).from(listOf(Person("sam"), Person("joe")))3result.shouldBe(listOf("sam", "joe"))4}5@Test fun `extracting should work with two properties`() {6val result = extracting(Person::name, Person::age).from(listOf(Person("sam", 30), Person("joe", 40)))7result.shouldBe(listOf(listOf("sam", 30), listOf("joe", 40)))8}9@Test fun `extracting should work with three properties`() {10val result = extracting(Person::name, Person::age, Person::city).from(listOf(Person("sam", 30, "london"), Person("joe", 40, "london")))11result.shouldBe(listOf(listOf("sam", 30, "london"), listOf("joe", 40, "london")))12}13@Test fun `extracting should work with four properties`() {14val result = extracting(Person::name, Person::age, Person::city, Person::country).from(listOf(Person("sam", 30, "london", "uk"), Person("joe", 40, "london", "uk")))15result.shouldBe(listOf(listOf("sam", 30, "london", "uk"), listOf("joe", 40, "london", "uk")))16}17@Test fun `extracting should work with five properties`() {18val result = extracting(Person::name, Person::age, Person::city, Person::country, Person::continent).from(listOf(Person("sam", 30, "london", "uk", "europe"), Person("joe", 40, "london", "uk", "europe")))19result.shouldBe(listOf(listOf("sam", 30, "london", "uk", "europe"), listOf("joe", 40, "london", "uk", "europe")))20}21@Test fun `extracting should work with six properties`() {22val result = extracting(Person::name, Person::age, Person::city, Person::country, Person::continent, Person::planet).from(listOf(Person("sam", 30, "london", "uk", "europe", "earth"), Person("joe", 40, "londonextracting
Using AI Code Generation
1@get:Rule val listener = TestListener(testCaseOrder = TestCaseOrder.Sequential)2@Test fun `extracting should support multiple values`() {3val result = extracting(1, "foo", 3.0) { a, b, c -> a + b + c }4}5@Test fun `extracting should support single value`() {6val result = extracting(1) { a -> a }7}8@Test fun `extracting should support no values`() {9val result = extracting { "foo" }10}11@Test fun `extracting should support multiple values with name`() {12val result = extracting(1, "foo", 3.0, name = "my name") { a, b, c -> a + b + c }13}14@Test fun `extracting should support single value with name`() {15val result = extracting(1, name = "my name") { a -> a }16}17@Test fun `extracting should support no values with name`() {18val result = extracting(name = "my name") { "foo" }19}20@Test fun `extracting should support multiple values with name and description`() {21val result = extracting(1, "foo", 3.0, name = "my name", desc = "my desc") { a, b, c -> a + b + c }22}23@Test fun `extracting should support single value with name and description`() {24val result = extracting(1, name = "my name", desc = "my desc") { a -> a }25}26@Test fun `extracting should support no values with name and description`() {27val result = extracting(name = "my name", desc = "my desc") { "foo" }28}29}30@get:Rule val listener = TestListener(testCaseOrder = TestCaseOrder.Sequential)31@Test fun `extracting should support multiple values`() {32val result = extracting(1, "foo", 3.0) { a, b, c -> a +Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!
