From 4ffd7ed9bffa5139277ffb91de5a69f2b714222c Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Fri, 17 Apr 2020 00:24:13 +0530 Subject: [PATCH] Enable emulator tests (#708) * github: Enable instrumentation testing in PRs Signed-off-by: Harsh Shandilya * Upgrade to Gradle 6.3 Signed-off-by: Harsh Shandilya * Remove outdated and broken tests, redo PasswordEntryTest in Kotlin Signed-off-by: Harsh Shandilya * Remove now unused test assets Signed-off-by: Harsh Shandilya * github: Disable debug APK uploads in PR testing Signed-off-by: Harsh Shandilya * Update .github/workflows/pull_request.yml * Update .github/workflows/pull_request.yml Switching to Ubuntu fails due to the SDK 29 missing. Co-authored-by: Fabian Henneke --- .github/workflows/pull_request.yml | 16 +- .../assets/clear-store/category/sub | 3 - app/src/androidTest/assets/clear-store/pass | 3 - .../assets/encrypted-store/category/sub.gpg | Bin 362 -> 0 bytes .../assets/encrypted-store/pass.gpg | Bin 361 -> 0 bytes app/src/androidTest/assets/private_key | Bin 2575 -> 0 bytes .../java/com/zeapo/pwdstore/DecryptTest.kt | 182 ------------------ .../java/com/zeapo/pwdstore/EncryptTest.kt | 100 ---------- .../java/com/zeapo/pwdstore/OtpTest.java | 15 -- .../com/zeapo/pwdstore/PasswordEntryTest.java | 97 ---------- .../com/zeapo/pwdstore/PasswordEntryTest.kt | 97 ++++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 58695 -> 58694 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- 13 files changed, 109 insertions(+), 406 deletions(-) delete mode 100644 app/src/androidTest/assets/clear-store/category/sub delete mode 100644 app/src/androidTest/assets/clear-store/pass delete mode 100644 app/src/androidTest/assets/encrypted-store/category/sub.gpg delete mode 100644 app/src/androidTest/assets/encrypted-store/pass.gpg delete mode 100644 app/src/androidTest/assets/private_key delete mode 100644 app/src/androidTest/java/com/zeapo/pwdstore/DecryptTest.kt delete mode 100644 app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt delete mode 100644 app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java delete mode 100644 app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java create mode 100644 app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.kt diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9d4d1f076..2e1fae0a4 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -3,7 +3,10 @@ on: pull_request name: Check pull request jobs: test-pr: - runs-on: ubuntu-latest + runs-on: macos-latest + strategy: + matrix: + api-level: [23, 25, 27, 29] steps: - uses: actions/checkout@master @@ -16,9 +19,12 @@ jobs: path: ~/.gradle/caches key: gradle-${{ runner.os }}-${{ hashFiles('**/build.gradle') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/dependencies.gradle') }} - - run: ./gradlew spotlessCheck assembleDebug testDebug lintDebug -Dpre-dex=false + - name: Run unit tests + run: ./gradlew spotlessCheck testDebug lintDebug -Dpre-dex=false - - uses: actions/upload-artifact@master + - name: Run instrumentation tests + uses: reactivecircus/android-emulator-runner@v2 with: - name: Debug APK - path: app/build/outputs/apk/debug/app-debug.apk + api-level: ${{ matrix.api-level }} + target: default + script: ./gradlew connectedCheck diff --git a/app/src/androidTest/assets/clear-store/category/sub b/app/src/androidTest/assets/clear-store/category/sub deleted file mode 100644 index eb9106c15..000000000 --- a/app/src/androidTest/assets/clear-store/category/sub +++ /dev/null @@ -1,3 +0,0 @@ -sub_pass -login: user -sub_extra diff --git a/app/src/androidTest/assets/clear-store/pass b/app/src/androidTest/assets/clear-store/pass deleted file mode 100644 index 8415d1ab3..000000000 --- a/app/src/androidTest/assets/clear-store/pass +++ /dev/null @@ -1,3 +0,0 @@ -password -username: user -extra diff --git a/app/src/androidTest/assets/encrypted-store/category/sub.gpg b/app/src/androidTest/assets/encrypted-store/category/sub.gpg deleted file mode 100644 index dded01322b92d423490cce7eb767b23db4f1dbc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 362 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 = ActivityTestRule(PgpActivity::class.java, true, false) - - private fun init() { - targetContext = InstrumentationRegistry.getInstrumentation().targetContext - testContext = InstrumentationRegistry.getInstrumentation().context - copyAssets("encrypted-store", File(targetContext.filesDir, "test-store").absolutePath) - repoPath = File(targetContext.filesDir, "test-store").absolutePath - path = "$repoPath/$parentPath/$name.gpg".replace("//", "/") - - val intent = Intent(targetContext, PgpActivity::class.java) - intent.putExtra("OPERATION", "DECRYPT") - intent.putExtra("FILE_PATH", path) - intent.putExtra("REPO_PATH", repoPath) - - activity = mActivityRule.launchActivity(intent) - } - - @Test - fun pathShouldDecompose() { - val pathOne = "/fake/path/cat1/n1.gpg" - val pathTwo = "/fake/path/n2.gpg" - - assertEquals("/cat1/n1.gpg", PgpActivity.getRelativePath(pathOne, "/fake/path")) - assertEquals("/cat1/", PgpActivity.getParentPath(pathOne, "/fake/path")) - assertEquals("n1", PgpActivity.getName("$pathOne/fake/path")) - // test that even if we append a `/` it still works - assertEquals("n1", PgpActivity.getName("$pathOne/fake/path/")) - - assertEquals("/n2.gpg", PgpActivity.getRelativePath(pathTwo, "/fake/path")) - assertEquals("/", PgpActivity.getParentPath(pathTwo, "/fake/path")) - assertEquals("n2", PgpActivity.getName("$pathTwo/fake/path")) - assertEquals("n2", PgpActivity.getName("$pathTwo/fake/path/")) - } - - @Test - fun activityShouldShowName() { - init() - - val categoryView = activity.crypto_password_category_decrypt - assertNotNull(categoryView) - assertEquals(parentPath, categoryView.text) - - val nameView = activity.crypto_password_file - assertNotNull(nameView) - assertEquals(name, nameView.text) - } - - @SuppressLint("ApplySharedPref") // we need the preferences right away - @Test - fun shouldDecrypt() { - init() - val clearPass = IOUtils.toString(testContext.assets.open("clear-store/category/sub"), StandardCharsets.UTF_8) - val passEntry = PasswordEntry(clearPass) - - // Setup the timer to 1 second - // first remember the previous timer to set it back later - val showTime = try { - Integer.parseInt(activity.settings.getString("general_show_time", "45") ?: "45") - } catch (e: NumberFormatException) { - 45 - } - // second set the new timer - activity.settings.edit().putString("general_show_time", "2").commit() - - activity.onBound(object : IOpenPgpService2 { - override fun createOutputPipe(p0: Int): ParcelFileDescriptor { - TODO("Not yet implemented") - } - - override fun asBinder(): IBinder { - TODO("Not yet implemented") - } - - override fun execute(p0: Intent?, p1: ParcelFileDescriptor?, p2: Int): Intent { - TODO("Not yet implemented") - } - }) - - // have we decrypted things correctly? - assertEquals(passEntry.password, activity.crypto_password_show.text) - assertEquals(passEntry.username, activity.crypto_username_show.text.toString()) - assertEquals(passEntry.extraContent, activity.crypto_extra_show.text.toString()) - - // did we copy the password? - val clipboard: ClipboardManager = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - assertEquals(passEntry.password, clipboard.primaryClip!!.getItemAt(0).text) - - // wait until the clipboard is cleared - SystemClock.sleep(4000) - - // The clipboard should be cleared!! - for (i in 0..clipboard.primaryClip!!.itemCount) { - assertEquals("", clipboard.primaryClip!!.getItemAt(i).text) - } - - // set back the timer - activity.settings.edit().putString("general_show_time", showTime.toString()).commit() - } - - companion object { - fun copyAssets(source: String, destination: String) { - FileUtils.forceMkdir(File(destination)) - FileUtils.cleanDirectory(File(destination)) - - val testContext = InstrumentationRegistry.getInstrumentation().context - val assetManager = testContext.assets - val files: Array? = assetManager.list(source) - - files?.map { filename -> - val destPath = "$destination/$filename" - val sourcePath = "$source/$filename" - - if (assetManager.list(sourcePath)!!.isNotEmpty()) { - FileUtils.forceMkdir(File(destination, filename)) - copyAssets("$source/$filename", destPath) - } else { - try { - val input = assetManager.open(sourcePath) - val outFile = File(destination, filename) - val output = FileOutputStream(outFile) - IOUtils.copy(input, output) - input.close() - output.flush() - output.close() - } catch (e: IOException) { - Log.e("tag", "Failed to copy asset file: $filename", e) - } - } - } - } - } -} diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt deleted file mode 100644 index ba63ce1ab..000000000 --- a/app/src/androidTest/java/com/zeapo/pwdstore/EncryptTest.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.os.IBinder -import android.os.ParcelFileDescriptor -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.scrollTo -import androidx.test.espresso.action.ViewActions.typeText -import androidx.test.espresso.assertion.ViewAssertions -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.LargeTest -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.rule.ActivityTestRule -import com.zeapo.pwdstore.crypto.PgpActivity -import java.io.File -import org.apache.commons.io.FileUtils -import org.apache.commons.io.IOUtils -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.openintents.openpgp.IOpenPgpService2 - -@RunWith(AndroidJUnit4::class) -@LargeTest -class EncryptTest { - private lateinit var targetContext: Context - private lateinit var testContext: Context - private lateinit var activity: PgpActivity - - private val name = "sub" - private val parentPath = "/category/" - private lateinit var path: String - private lateinit var repoPath: String - - @Rule @JvmField - var mActivityRule: ActivityTestRule = ActivityTestRule(PgpActivity::class.java, true, false) - - private fun init() { - targetContext = InstrumentationRegistry.getInstrumentation().targetContext - testContext = InstrumentationRegistry.getInstrumentation().context - - // have an empty store - FileUtils.forceMkdir(File(targetContext.filesDir, "test-store")) - FileUtils.cleanDirectory(File(targetContext.filesDir, "test-store")) - - repoPath = File(targetContext.filesDir, "test-store").absolutePath - - path = "$repoPath/$parentPath/".replace("//", "/") - - val intent = Intent(targetContext, PgpActivity::class.java) - intent.putExtra("OPERATION", "ENCRYPT") - intent.putExtra("FILE_PATH", path) - intent.putExtra("REPO_PATH", repoPath) - - activity = mActivityRule.launchActivity(intent) - } - - @SuppressLint("ApplySharedPref", "SetTextI18n") - @Test - fun shouldEncrypt() { - init() - - onView(withId(R.id.crypto_password_category)).check(ViewAssertions.matches(withText(parentPath))) - activity.onBound(object : IOpenPgpService2 { - override fun createOutputPipe(p0: Int): ParcelFileDescriptor { - TODO("Not yet implemented") - } - - override fun asBinder(): IBinder { - TODO("Not yet implemented") - } - - override fun execute(p0: Intent?, p1: ParcelFileDescriptor?, p2: Int): Intent { - TODO("Not yet implemented") - } - }) - val clearPass = IOUtils.toString(testContext.assets.open("clear-store/category/sub"), Charsets.UTF_8.name()) - val passEntry = PasswordEntry(clearPass) - - onView(withId(R.id.crypto_password_file_edit)).perform(typeText("sub")) - onView(withId(R.id.crypto_password_edit)).perform(typeText(passEntry.password)) - onView(withId(R.id.crypto_extra_edit)).perform(scrollTo(), click()) - onView(withId(R.id.crypto_extra_edit)).perform(typeText(passEntry.extraContent)) - - // we should return to the home screen once we confirm - onView(withId(R.id.crypto_confirm_add)).perform(click()) - - // The resulting file should exist - assert(File("$path/$name.gpg").exists()) - } -} diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java b/app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java deleted file mode 100644 index 76be43a12..000000000 --- a/app/src/androidTest/java/com/zeapo/pwdstore/OtpTest.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore; - -import com.zeapo.pwdstore.utils.Otp; -import junit.framework.TestCase; - -public class OtpTest extends TestCase { - public void testOtp() { - String code = Otp.calculateCode("JBSWY3DPEHPK3PXP", 0L, "sha1", "s"); - assertEquals("282760", code); - } -} diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java b/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java deleted file mode 100644 index dd55fbda5..000000000 --- a/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -package com.zeapo.pwdstore; - -import junit.framework.TestCase; - -public class PasswordEntryTest extends TestCase { - - public void testGetPassword() { - assertEquals("fooooo", new PasswordEntry("fooooo\nbla\n").getPassword()); - assertEquals("fooooo", new PasswordEntry("fooooo\nbla").getPassword()); - assertEquals("fooooo", new PasswordEntry("fooooo\n").getPassword()); - assertEquals("fooooo", new PasswordEntry("fooooo").getPassword()); - assertEquals("", new PasswordEntry("\nblubb\n").getPassword()); - assertEquals("", new PasswordEntry("\nblubb").getPassword()); - assertEquals("", new PasswordEntry("\n").getPassword()); - assertEquals("", new PasswordEntry("").getPassword()); - } - - public void testGetExtraContent() { - assertEquals("bla\n", new PasswordEntry("fooooo\nbla\n").getExtraContent()); - assertEquals("bla", new PasswordEntry("fooooo\nbla").getExtraContent()); - assertEquals("", new PasswordEntry("fooooo\n").getExtraContent()); - assertEquals("", new PasswordEntry("fooooo").getExtraContent()); - assertEquals("blubb\n", new PasswordEntry("\nblubb\n").getExtraContent()); - assertEquals("blubb", new PasswordEntry("\nblubb").getExtraContent()); - assertEquals("", new PasswordEntry("\n").getExtraContent()); - assertEquals("", new PasswordEntry("").getExtraContent()); - } - - public void testGetUsername() { - assertEquals( - "username", - new PasswordEntry("secret\nextra\nlogin: username\ncontent\n").getUsername()); - assertEquals( - "username", - new PasswordEntry("\nextra\nusername: username\ncontent\n").getUsername()); - assertEquals( - "username", new PasswordEntry("\nUSERNaMe: username\ncontent\n").getUsername()); - assertEquals("username", new PasswordEntry("\nLOGiN:username").getUsername()); - assertNull(new PasswordEntry("secret\nextra\ncontent\n").getUsername()); - } - - public void testHasUsername() { - assertTrue(new PasswordEntry("secret\nextra\nlogin: username\ncontent\n").hasUsername()); - assertFalse(new PasswordEntry("secret\nextra\ncontent\n").hasUsername()); - assertFalse(new PasswordEntry("secret\nlogin failed\n").hasUsername()); - assertFalse(new PasswordEntry("\n").hasUsername()); - assertFalse(new PasswordEntry("").hasUsername()); - } - - public void testNoTotpUriPresent() { - PasswordEntry entry = new PasswordEntry("secret\nextra\nlogin: username\ncontent"); - assertFalse(entry.hasTotp()); - assertNull(entry.getTotpSecret()); - } - - public void testTotpUriInPassword() { - PasswordEntry entry = new PasswordEntry("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP"); - assertTrue(entry.hasTotp()); - assertEquals("JBSWY3DPEHPK3PXP", entry.getTotpSecret()); - } - - public void testTotpUriInContent() { - PasswordEntry entry = - new PasswordEntry( - "secret\nusername: test\notpauth://totp/test?secret=JBSWY3DPEHPK3PXP"); - assertTrue(entry.hasTotp()); - assertEquals("JBSWY3DPEHPK3PXP", entry.getTotpSecret()); - } - - public void testNoHotpUriPresent() { - PasswordEntry entry = new PasswordEntry("secret\nextra\nlogin: username\ncontent"); - assertFalse(entry.hasHotp()); - assertNull(entry.getHotpSecret()); - assertNull(entry.getHotpCounter()); - } - - public void testHotpUriInPassword() { - PasswordEntry entry = - new PasswordEntry("otpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25"); - assertTrue(entry.hasHotp()); - assertEquals("JBSWY3DPEHPK3PXP", entry.getHotpSecret()); - assertEquals(new Long(25), entry.getHotpCounter()); - } - - public void testHotpUriInContent() { - PasswordEntry entry = - new PasswordEntry( - "secret\nusername: test\notpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25"); - assertTrue(entry.hasHotp()); - assertEquals("JBSWY3DPEHPK3PXP", entry.getHotpSecret()); - assertEquals(new Long(25), entry.getHotpCounter()); - } -} diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.kt new file mode 100644 index 000000000..2f7028a40 --- /dev/null +++ b/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +package com.zeapo.pwdstore + +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.junit.Test + +class PasswordEntryTest { + @Test fun testGetPassword() { + assertEquals("fooooo", PasswordEntry("fooooo\nbla\n").password) + assertEquals("fooooo", PasswordEntry("fooooo\nbla").password) + assertEquals("fooooo", PasswordEntry("fooooo\n").password) + assertEquals("fooooo", PasswordEntry("fooooo").password) + assertEquals("", PasswordEntry("\nblubb\n").password) + assertEquals("", PasswordEntry("\nblubb").password) + assertEquals("", PasswordEntry("\n").password) + assertEquals("", PasswordEntry("").password) + } + + @Test fun testGetExtraContent() { + assertEquals("bla\n", PasswordEntry("fooooo\nbla\n").extraContent) + assertEquals("bla", PasswordEntry("fooooo\nbla").extraContent) + assertEquals("", PasswordEntry("fooooo\n").extraContent) + assertEquals("", PasswordEntry("fooooo").extraContent) + assertEquals("blubb\n", PasswordEntry("\nblubb\n").extraContent) + assertEquals("blubb", PasswordEntry("\nblubb").extraContent) + assertEquals("", PasswordEntry("\n").extraContent) + assertEquals("", PasswordEntry("").extraContent) + } + + @Test fun testGetUsername() { + assertEquals( + "username", + PasswordEntry("secret\nextra\nlogin: username\ncontent\n").username) + assertEquals( + "username", + PasswordEntry("\nextra\nusername: username\ncontent\n").username) + assertEquals( + "username", PasswordEntry("\nUSERNaMe: username\ncontent\n").username) + assertEquals("username", PasswordEntry("\nLOGiN:username").username) + assertNull(PasswordEntry("secret\nextra\ncontent\n").username) + } + + @Test fun testHasUsername() { + assertTrue(PasswordEntry("secret\nextra\nlogin: username\ncontent\n").hasUsername()) + assertFalse(PasswordEntry("secret\nextra\ncontent\n").hasUsername()) + assertFalse(PasswordEntry("secret\nlogin failed\n").hasUsername()) + assertFalse(PasswordEntry("\n").hasUsername()) + assertFalse(PasswordEntry("").hasUsername()) + } + + @Test fun testNoTotpUriPresent() { + val entry = PasswordEntry("secret\nextra\nlogin: username\ncontent") + assertFalse(entry.hasTotp()) + assertNull(entry.totpSecret) + } + + @Test fun testTotpUriInPassword() { + val entry = PasswordEntry("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP") + assertTrue(entry.hasTotp()) + assertEquals("JBSWY3DPEHPK3PXP", entry.totpSecret) + } + + @Test fun testTotpUriInContent() { + val entry = PasswordEntry( + "secret\nusername: test\notpauth://totp/test?secret=JBSWY3DPEHPK3PXP") + assertTrue(entry.hasTotp()) + assertEquals("JBSWY3DPEHPK3PXP", entry.totpSecret) + } + + @Test fun testNoHotpUriPresent() { + val entry = PasswordEntry("secret\nextra\nlogin: username\ncontent") + assertFalse(entry.hasHotp()) + assertNull(entry.hotpSecret) + assertNull(entry.hotpCounter) + } + + @Test fun testHotpUriInPassword() { + val entry = PasswordEntry("otpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25") + assertTrue(entry.hasHotp()) + assertEquals("JBSWY3DPEHPK3PXP", entry.hotpSecret) + assertEquals(25, entry.hotpCounter) + } + + @Test fun testHotpUriInContent() { + val entry = PasswordEntry( + "secret\nusername: test\notpauth://hotp/test?secret=JBSWY3DPEHPK3PXP&counter=25") + assertTrue(entry.hasHotp()) + assertEquals("JBSWY3DPEHPK3PXP", entry.hotpSecret) + assertEquals(25, entry.hotpCounter) + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f3d88b1c2faf2fc91d853cd5d4242b5547257070..490fda8577df6c95960ba7077c43220e5bb2c0d9 100644 GIT binary patch delta 6577 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 MViXjGx&OBR0?kH%b^rhX delta 6547 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