Skip to content

Commit 1d976fc

Browse files
committed
Add unit tests for changes
1 parent 1faaef6 commit 1d976fc

File tree

3 files changed

+779
-0
lines changed

3 files changed

+779
-0
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright (C) 2014-2024 Arpit Khurana <arpitkh96@gmail.com>, Vishal Nehra <vishalneham2@gmail.com>,
3+
* Emmanuel Messulam<emmanuelbendavid@gmail.com>, Raymond Lai <airwave209gt at gmail.com> and Contributors.
4+
*
5+
* This file is part of Amaze File Manager.
6+
*
7+
* Amaze File Manager is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.amaze.filemanager.filesystem
22+
23+
import android.content.ContentResolver
24+
import android.content.Context
25+
import android.database.Cursor
26+
import android.os.Build
27+
import android.os.Build.VERSION_CODES.LOLLIPOP
28+
import android.os.Build.VERSION_CODES.P
29+
import android.provider.MediaStore
30+
import androidx.test.ext.junit.runners.AndroidJUnit4
31+
import com.amaze.filemanager.filesystem.MediaStoreHackTest.Companion.FAKE_ROW_ID
32+
import com.amaze.filemanager.shadows.ShadowMultiDex
33+
import io.mockk.every
34+
import io.mockk.just
35+
import io.mockk.mockk
36+
import io.mockk.runs
37+
import io.mockk.unmockkAll
38+
import org.junit.After
39+
import org.junit.Assert.assertEquals
40+
import org.junit.Assert.assertNotNull
41+
import org.junit.Assert.assertNull
42+
import org.junit.Assert.assertTrue
43+
import org.junit.Test
44+
import org.junit.runner.RunWith
45+
import org.robolectric.annotation.Config
46+
47+
/**
48+
* Robolectric tests for [MediaStoreHack.getUriForMusicMediaFrom].
49+
*
50+
* The new method queries [MediaStore.Audio.Media.EXTERNAL_CONTENT_URI] by file-system path and
51+
* returns a content:// [android.net.Uri] when a matching row exists, or `null` when it does not.
52+
*
53+
* Robolectric is needed so that Android framework statics (e.g.
54+
* [MediaStore.Audio.Media.EXTERNAL_CONTENT_URI], [android.net.Uri]) are properly initialised.
55+
* The [ContentResolver] itself is mocked via MockK so tests are not affected by Robolectric 4.9's
56+
* limited in-process MediaStore ContentProvider support.
57+
*/
58+
@RunWith(AndroidJUnit4::class)
59+
@Config(
60+
sdk = [LOLLIPOP, P, Build.VERSION_CODES.R],
61+
shadows = [ShadowMultiDex::class],
62+
)
63+
class MediaStoreHackTest {
64+
companion object {
65+
private const val TEST_AUDIO_PATH = "/storage/emulated/0/Music/test_ringtone.mp3"
66+
private const val ABSENT_AUDIO_PATH = "/storage/emulated/0/Music/nonexistent.mp3"
67+
private const val FAKE_ROW_ID = 42
68+
}
69+
70+
/**
71+
* Builds a mock [Context] whose [ContentResolver] returns [cursor] for any query
72+
* against [MediaStore.Audio.Media.EXTERNAL_CONTENT_URI].
73+
*/
74+
private fun contextWithCursor(cursor: Cursor?): Context {
75+
val mockResolver = mockk<ContentResolver>()
76+
every {
77+
mockResolver.query(
78+
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
79+
any(),
80+
any(),
81+
any(),
82+
null,
83+
)
84+
} returns cursor
85+
86+
return mockk<Context>().also { ctx ->
87+
every { ctx.contentResolver } returns mockResolver
88+
}
89+
}
90+
91+
/**
92+
* Builds a mock [Cursor] that simulates a single-row result with [_ID][MediaStore.Audio.Media._ID]
93+
* equal to [FAKE_ROW_ID].
94+
*/
95+
private fun singleRowCursor(): Cursor {
96+
val idColumnIndex = 0
97+
return mockk<Cursor>(relaxed = true).also { cursor ->
98+
every { cursor.moveToFirst() } returns true
99+
every { cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) } returns idColumnIndex
100+
every { cursor.getInt(idColumnIndex) } returns FAKE_ROW_ID
101+
every { cursor.close() } just runs
102+
}
103+
}
104+
105+
/**
106+
* Builds a mock [Cursor] that simulates an empty result set (no matching rows).
107+
*/
108+
private fun emptyCursor(): Cursor =
109+
mockk<Cursor>(relaxed = true).also { cursor ->
110+
every { cursor.moveToFirst() } returns false
111+
every { cursor.close() } just runs
112+
}
113+
114+
/**
115+
* After test clean up.
116+
*/
117+
@After
118+
fun tearDown() {
119+
unmockkAll()
120+
}
121+
122+
// -------------------------------------------------------------------------
123+
// Tests
124+
// -------------------------------------------------------------------------
125+
126+
/**
127+
* When the ContentResolver returns a cursor with a matching row,
128+
* [MediaStoreHack.getUriForMusicMediaFrom] must return a non-null `content://` URI under
129+
* [MediaStore.Audio.Media.EXTERNAL_CONTENT_URI] whose last path segment is the row id.
130+
*/
131+
@Test
132+
fun testGetUriForMusicMediaFromReturnsUriWhenCursorHasMatchingRow() {
133+
val context = contextWithCursor(singleRowCursor())
134+
135+
val result = MediaStoreHack.getUriForMusicMediaFrom(TEST_AUDIO_PATH, context)
136+
137+
assertNotNull("Expected a non-null URI when the cursor has a matching row", result)
138+
assertEquals("content", result!!.scheme)
139+
assertTrue(
140+
"Returned URI should be under MediaStore.Audio.Media.EXTERNAL_CONTENT_URI",
141+
result.toString().startsWith(
142+
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString(),
143+
),
144+
)
145+
assertEquals(
146+
"Last path segment must equal the row id returned by the cursor",
147+
FAKE_ROW_ID.toString(),
148+
result.lastPathSegment,
149+
)
150+
}
151+
152+
/**
153+
* When the ContentResolver returns a cursor with NO matching rows,
154+
* [MediaStoreHack.getUriForMusicMediaFrom] must return `null`.
155+
*/
156+
@Test
157+
fun testGetUriForMusicMediaFromReturnsNullWhenCursorIsEmpty() {
158+
val context = contextWithCursor(emptyCursor())
159+
160+
val result = MediaStoreHack.getUriForMusicMediaFrom(ABSENT_AUDIO_PATH, context)
161+
162+
assertNull(
163+
"Expected null URI when the cursor is empty (no matching row)",
164+
result,
165+
)
166+
}
167+
168+
/**
169+
* When the ContentResolver returns a `null` cursor (provider error or unavailable),
170+
* [MediaStoreHack.getUriForMusicMediaFrom] must return `null` without throwing.
171+
*/
172+
@Test
173+
fun testGetUriForMusicMediaFromReturnsNullWhenCursorIsNull() {
174+
val context = contextWithCursor(null)
175+
176+
val result = MediaStoreHack.getUriForMusicMediaFrom(TEST_AUDIO_PATH, context)
177+
178+
assertNull(
179+
"Expected null URI when the ContentResolver returns a null cursor",
180+
result,
181+
)
182+
}
183+
184+
/**
185+
* The URI returned by [MediaStoreHack.getUriForMusicMediaFrom] must use only
186+
* the row [_ID][MediaStore.Audio.Media._ID] from the cursor — confirming that
187+
* different row ids produce distinct URIs (selection correctness guard).
188+
*/
189+
@Test
190+
fun testGetUriForMusicMediaFromBuildsUriFromCursorId() {
191+
val alternativeId = 99
192+
val altCursor =
193+
mockk<Cursor>(relaxed = true).also { cursor ->
194+
every { cursor.moveToFirst() } returns true
195+
every { cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) } returns 0
196+
every { cursor.getInt(0) } returns alternativeId
197+
every { cursor.close() } just runs
198+
}
199+
val context = contextWithCursor(altCursor)
200+
201+
val result = MediaStoreHack.getUriForMusicMediaFrom(TEST_AUDIO_PATH, context)
202+
203+
assertNotNull(result)
204+
assertEquals(
205+
"URI last path segment must equal the alternative row id",
206+
alternativeId.toString(),
207+
result!!.lastPathSegment,
208+
)
209+
// Verify it is distinct from a URI built with FAKE_ROW_ID
210+
val firstResult =
211+
MediaStoreHack.getUriForMusicMediaFrom(
212+
TEST_AUDIO_PATH,
213+
contextWithCursor(singleRowCursor()),
214+
)
215+
assertTrue(
216+
"URIs built from different row ids must differ",
217+
result.toString() != firstResult.toString(),
218+
)
219+
}
220+
}

0 commit comments

Comments
 (0)