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, "london
extracting
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!!