Skip to content

Commit 8e192ac

Browse files
committed
Add test zips and extensive tests for ImportExportManager
Now all possible combinations of files in the zip (present or not) are checked at the same time
1 parent d842349 commit 8e192ac

18 files changed

Lines changed: 211 additions & 13 deletions

app/src/main/java/org/schabi/newpipe/settings/export/ImportExportManager.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.grack.nanojson.JsonWriter
99
import org.schabi.newpipe.streams.io.SharpOutputStream
1010
import org.schabi.newpipe.streams.io.StoredFileHelper
1111
import org.schabi.newpipe.util.ZipHelper
12+
import java.io.FileNotFoundException
1213
import java.io.IOException
1314
import java.io.ObjectOutputStream
1415
import java.util.zip.ZipOutputStream
@@ -110,10 +111,12 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
110111
fun loadSerializedPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
111112
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_SERIALIZED_PREFS) {
112113
PreferencesObjectInputStream(it).use { input ->
113-
val editor = preferences.edit()
114-
editor.clear()
115114
@Suppress("UNCHECKED_CAST")
116115
val entries = input.readObject() as Map<String, *>
116+
117+
val editor = preferences.edit()
118+
editor.clear()
119+
117120
for ((key, value) in entries) {
118121
when (value) {
119122
is Boolean -> editor.putBoolean(key, value)
@@ -133,19 +136,24 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
133136
Log.e(TAG, "Unable to loadSerializedPrefs")
134137
}
135138
}
139+
}.let { fileExists ->
140+
if (!fileExists) {
141+
throw FileNotFoundException(BackupFileLocator.FILE_NAME_SERIALIZED_PREFS)
142+
}
136143
}
137144
}
138145

139146
/**
140147
* Remove all shared preferences from the app and load the preferences supplied to the manager.
141148
*/
142-
@Throws(JsonParserException::class)
149+
@Throws(IOException::class, JsonParserException::class)
143150
fun loadJsonPrefs(zipFile: StoredFileHelper, preferences: SharedPreferences) {
144151
ZipHelper.extractFileFromZip(zipFile, BackupFileLocator.FILE_NAME_JSON_PREFS) {
152+
val jsonObject = JsonParser.`object`().from(it)
153+
145154
val editor = preferences.edit()
146155
editor.clear()
147156

148-
val jsonObject = JsonParser.`object`().from(it)
149157
for ((key, value) in jsonObject) {
150158
when (value) {
151159
is Boolean -> editor.putBoolean(key, value)
@@ -162,6 +170,10 @@ class ImportExportManager(private val fileLocator: BackupFileLocator) {
162170
if (!editor.commit()) {
163171
Log.e(TAG, "Unable to loadJsonPrefs")
164172
}
173+
}.let { fileExists ->
174+
if (!fileExists) {
175+
throw FileNotFoundException(BackupFileLocator.FILE_NAME_JSON_PREFS)
176+
}
165177
}
166178
}
167179
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package org.schabi.newpipe.settings
2+
3+
import android.content.SharedPreferences
4+
import org.junit.Assert
5+
import org.junit.Test
6+
import org.mockito.Mockito
7+
import org.schabi.newpipe.settings.export.BackupFileLocator
8+
import org.schabi.newpipe.settings.export.ImportExportManager
9+
import org.schabi.newpipe.streams.io.StoredFileHelper
10+
import us.shandian.giga.io.FileStream
11+
import java.io.File
12+
import java.io.IOException
13+
import java.nio.file.Files
14+
15+
class ImportAllCombinationsTest {
16+
17+
companion object {
18+
private val classloader = ImportExportManager::class.java.classLoader!!
19+
}
20+
21+
private enum class Ser(val id: String) {
22+
YES("ser"),
23+
VULNERABLE("vulnser"),
24+
NO("noser");
25+
}
26+
27+
private data class FailData(
28+
val containsDb: Boolean,
29+
val containsSer: Ser,
30+
val containsJson: Boolean,
31+
val filename: String,
32+
val throwable: Throwable,
33+
)
34+
35+
private fun testZipCombination(
36+
containsDb: Boolean,
37+
containsSer: Ser,
38+
containsJson: Boolean,
39+
filename: String,
40+
runTest: (test: () -> Unit) -> Unit,
41+
) {
42+
val zipFile = File(classloader.getResource(filename)?.file!!)
43+
val zip = Mockito.mock(StoredFileHelper::class.java, Mockito.withSettings().stubOnly())
44+
Mockito.`when`(zip.stream).then { FileStream(zipFile) }
45+
46+
val fileLocator = Mockito.mock(
47+
BackupFileLocator::class.java,
48+
Mockito.withSettings().stubOnly()
49+
)
50+
val db = File.createTempFile("newpipe_", "")
51+
val dbJournal = File.createTempFile("newpipe_", "")
52+
val dbWal = File.createTempFile("newpipe_", "")
53+
val dbShm = File.createTempFile("newpipe_", "")
54+
Mockito.`when`(fileLocator.db).thenReturn(db)
55+
Mockito.`when`(fileLocator.dbJournal).thenReturn(dbJournal)
56+
Mockito.`when`(fileLocator.dbShm).thenReturn(dbShm)
57+
Mockito.`when`(fileLocator.dbWal).thenReturn(dbWal)
58+
59+
if (containsDb) {
60+
runTest {
61+
Assert.assertTrue(ImportExportManager(fileLocator).extractDb(zip))
62+
Assert.assertFalse(dbJournal.exists())
63+
Assert.assertFalse(dbWal.exists())
64+
Assert.assertFalse(dbShm.exists())
65+
Assert.assertTrue("database file size is zero", Files.size(db.toPath()) > 0)
66+
}
67+
} else {
68+
runTest {
69+
Assert.assertFalse(ImportExportManager(fileLocator).extractDb(zip))
70+
Assert.assertTrue(dbJournal.exists())
71+
Assert.assertTrue(dbWal.exists())
72+
Assert.assertTrue(dbShm.exists())
73+
Assert.assertEquals(0, Files.size(db.toPath()))
74+
}
75+
}
76+
77+
val preferences = Mockito.mock(SharedPreferences::class.java, Mockito.withSettings().stubOnly())
78+
var editor = Mockito.mock(SharedPreferences.Editor::class.java)
79+
Mockito.`when`(preferences.edit()).thenReturn(editor)
80+
Mockito.`when`(editor.commit()).thenReturn(true)
81+
82+
when (containsSer) {
83+
Ser.YES -> runTest {
84+
Assert.assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip))
85+
ImportExportManager(fileLocator).loadSerializedPrefs(zip, preferences)
86+
87+
Mockito.verify(editor, Mockito.times(1)).clear()
88+
Mockito.verify(editor, Mockito.times(1)).commit()
89+
Mockito.verify(editor, Mockito.atLeastOnce())
90+
.putBoolean(Mockito.anyString(), Mockito.anyBoolean())
91+
Mockito.verify(editor, Mockito.atLeastOnce())
92+
.putString(Mockito.anyString(), Mockito.anyString())
93+
Mockito.verify(editor, Mockito.atLeastOnce())
94+
.putInt(Mockito.anyString(), Mockito.anyInt())
95+
}
96+
Ser.VULNERABLE -> runTest {
97+
Assert.assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip))
98+
Assert.assertThrows(ClassNotFoundException::class.java) {
99+
ImportExportManager(fileLocator).loadSerializedPrefs(zip, preferences)
100+
}
101+
102+
Mockito.verify(editor, Mockito.never()).clear()
103+
Mockito.verify(editor, Mockito.never()).commit()
104+
}
105+
Ser.NO -> runTest {
106+
Assert.assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(zip))
107+
Assert.assertThrows(IOException::class.java) {
108+
ImportExportManager(fileLocator).loadSerializedPrefs(zip, preferences)
109+
}
110+
111+
Mockito.verify(editor, Mockito.never()).clear()
112+
Mockito.verify(editor, Mockito.never()).commit()
113+
}
114+
}
115+
116+
// recreate editor mock so verify() behaves correctly
117+
editor = Mockito.mock(SharedPreferences.Editor::class.java)
118+
Mockito.`when`(preferences.edit()).thenReturn(editor)
119+
Mockito.`when`(editor.commit()).thenReturn(true)
120+
121+
if (containsJson) {
122+
runTest {
123+
Assert.assertTrue(ImportExportManager(fileLocator).exportHasJsonPrefs(zip))
124+
ImportExportManager(fileLocator).loadJsonPrefs(zip, preferences)
125+
126+
Mockito.verify(editor, Mockito.times(1)).clear()
127+
Mockito.verify(editor, Mockito.times(1)).commit()
128+
Mockito.verify(editor, Mockito.atLeastOnce())
129+
.putBoolean(Mockito.anyString(), Mockito.anyBoolean())
130+
Mockito.verify(editor, Mockito.atLeastOnce())
131+
.putString(Mockito.anyString(), Mockito.anyString())
132+
Mockito.verify(editor, Mockito.atLeastOnce())
133+
.putInt(Mockito.anyString(), Mockito.anyInt())
134+
}
135+
} else {
136+
runTest {
137+
Assert.assertFalse(ImportExportManager(fileLocator).exportHasJsonPrefs(zip))
138+
Assert.assertThrows(IOException::class.java) {
139+
ImportExportManager(fileLocator).loadJsonPrefs(zip, preferences)
140+
}
141+
142+
Mockito.verify(editor, Mockito.never()).clear()
143+
Mockito.verify(editor, Mockito.never()).commit()
144+
}
145+
}
146+
}
147+
148+
@Test
149+
fun `Importing all possible combinations of zip files`() {
150+
val failedAssertions = mutableListOf<FailData>()
151+
for (containsDb in listOf(true, false)) {
152+
for (containsSer in Ser.entries) {
153+
for (containsJson in listOf(true, false)) {
154+
val filename = "settings/${if (containsDb) "db" else "nodb"}_${
155+
containsSer.id}_${if (containsJson) "json" else "nojson"}.zip"
156+
testZipCombination(containsDb, containsSer, containsJson, filename) { test ->
157+
try {
158+
test()
159+
} catch (e: Throwable) {
160+
failedAssertions.add(
161+
FailData(
162+
containsDb, containsSer, containsJson,
163+
filename, e
164+
)
165+
)
166+
}
167+
}
168+
}
169+
}
170+
}
171+
172+
if (failedAssertions.isNotEmpty()) {
173+
for (a in failedAssertions) {
174+
println(
175+
"Assertion failed with containsDb=${a.containsDb}, containsSer=${
176+
a.containsSer}, containsJson=${a.containsJson}, filename=${a.filename}:"
177+
)
178+
a.throwable.printStackTrace()
179+
println()
180+
}
181+
Assert.fail("${failedAssertions.size} assertions failed")
182+
}
183+
}
184+
}

app/src/test/java/org/schabi/newpipe/settings/ImportExportManagerTest.kt

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class ImportExportManagerTest {
109109
`when`(fileLocator.dbShm).thenReturn(dbShm)
110110
`when`(fileLocator.dbWal).thenReturn(dbWal)
111111

112-
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
112+
val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!)
113113
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
114114
val success = ImportExportManager(fileLocator).extractDb(storedFileHelper)
115115

@@ -128,7 +128,7 @@ class ImportExportManagerTest {
128128
val dbShm = File.createTempFile("newpipe_", "")
129129
`when`(fileLocator.db).thenReturn(db)
130130

131-
val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!)
131+
val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!)
132132
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
133133
val success = ImportExportManager(fileLocator).extractDb(storedFileHelper)
134134

@@ -141,21 +141,21 @@ class ImportExportManagerTest {
141141

142142
@Test
143143
fun `Contains setting must return true if a settings file exists in the zip`() {
144-
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
144+
val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!)
145145
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
146146
assertTrue(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper))
147147
}
148148

149149
@Test
150-
fun `Contains setting must return false if a no settings file exists in the zip`() {
151-
val emptyZip = File(classloader.getResource("settings/empty.zip")?.file!!)
150+
fun `Contains setting must return false if no settings file exists in the zip`() {
151+
val emptyZip = File(classloader.getResource("settings/nodb_noser_nojson.zip")?.file!!)
152152
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
153153
assertFalse(ImportExportManager(fileLocator).exportHasSerializedPrefs(storedFileHelper))
154154
}
155155

156156
@Test
157157
fun `Preferences must be set from the settings file`() {
158-
val zip = File(classloader.getResource("settings/newpipe.zip")?.file!!)
158+
val zip = File(classloader.getResource("settings/db_ser_json.zip")?.file!!)
159159
`when`(storedFileHelper.stream).thenReturn(FileStream(zip))
160160

161161
val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly())
@@ -172,12 +172,10 @@ class ImportExportManagerTest {
172172

173173
@Test
174174
fun `Importing preferences with a serialization injected class should fail`() {
175-
val emptyZip = File(classloader.getResource("settings/vulnerable_serialization.zip")?.file!!)
175+
val emptyZip = File(classloader.getResource("settings/db_vulnser_json.zip")?.file!!)
176176
`when`(storedFileHelper.stream).thenReturn(FileStream(emptyZip))
177177

178178
val preferences = Mockito.mock(SharedPreferences::class.java, withSettings().stubOnly())
179-
val editor = Mockito.mock(SharedPreferences.Editor::class.java)
180-
`when`(preferences.edit()).thenReturn(editor)
181179

182180
assertThrows(ClassNotFoundException::class.java) {
183181
ImportExportManager(fileLocator).loadSerializedPrefs(storedFileHelper, preferences)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
`*.zip` files in this folder are NewPipe database exports, in all possible configurations:
2+
- `db` / `nodb` indicates if there is a `newpipe.db` database included or not
3+
- `ser` / `vulnser` / `noser` indicates if there is a `newpipe.settings` Java-serialized preferences file included, if it is included and contains an injection attack, of if it is not included
4+
- `json` / `nojson` indicates if there is a `preferences.json` JSON preferences file included or not
5.3 KB
Binary file not shown.
3.95 KB
Binary file not shown.
7.07 KB
Binary file not shown.
5.67 KB
Binary file not shown.
6.59 KB
Binary file not shown.
5.24 KB
Binary file not shown.

0 commit comments

Comments
 (0)