aboutsummaryrefslogtreecommitdiffstats
path: root/format-common/src/test/kotlin/app/passwordstore/data
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2022-12-19 16:47:17 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2022-12-19 16:47:17 +0530
commit8cfe6ec84c1bd8119fd59fbb76d729da284ced27 (patch)
tree7035f03675f8924bfc3870b68b6345faf020aaac /format-common/src/test/kotlin/app/passwordstore/data
parent39a0284cd55298eb4930db681e813260aea58867 (diff)
downloadHardenedPass-8cfe6ec84c1bd8119fd59fbb76d729da284ced27.tar.gz
HardenedPass-8cfe6ec84c1bd8119fd59fbb76d729da284ced27.zip
refactor: merge format-common-impl into format-common
Also converts the format-common module into an Android library since UriTotpFinder requires the Android SDK.
Diffstat (limited to 'format-common/src/test/kotlin/app/passwordstore/data')
-rw-r--r--format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt221
1 files changed, 221 insertions, 0 deletions
diff --git a/format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt b/format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt
new file mode 100644
index 00000000..31ced9f3
--- /dev/null
+++ b/format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.data.passfile
+
+import app.cash.turbine.Event
+import app.cash.turbine.test
+import app.passwordstore.test.CoroutineTestRule
+import app.passwordstore.util.time.TestUserClock
+import app.passwordstore.util.time.UserClock
+import app.passwordstore.util.totp.UriTotpFinder
+import java.util.Locale
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.ExperimentalTime
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
+@RunWith(RobolectricTestRunner::class)
+class PasswordEntryTest {
+
+ @get:Rule val coroutineTestRule: CoroutineTestRule = CoroutineTestRule()
+ private val totpFinder = UriTotpFinder()
+
+ private fun makeEntry(content: String, clock: UserClock = fakeClock) =
+ PasswordEntry(
+ clock,
+ totpFinder,
+ content.encodeToByteArray(),
+ )
+
+ @Test
+ fun getPassword() {
+ assertEquals("fooooo", makeEntry("fooooo\nbla\n").password)
+ assertEquals("fooooo", makeEntry("fooooo\nbla").password)
+ assertEquals("fooooo", makeEntry("fooooo\n").password)
+ assertEquals("fooooo", makeEntry("fooooo").password)
+ assertEquals("", makeEntry("\nblubb\n").password)
+ assertEquals("", makeEntry("\nblubb").password)
+ assertEquals("", makeEntry("\n").password)
+ assertEquals("", makeEntry("").password)
+ for (field in PasswordEntry.PASSWORD_FIELDS) {
+ assertEquals("fooooo", makeEntry("\n$field fooooo").password)
+ assertEquals("fooooo", makeEntry("\n${field.uppercase(Locale.getDefault())} fooooo").password)
+ assertEquals("fooooo", makeEntry("GOPASS-SECRET-1.0\n$field fooooo").password)
+ assertEquals("fooooo", makeEntry("someFirstLine\nUsername: bar\n$field fooooo").password)
+ }
+ }
+
+ @Test
+ fun getExtraContent() {
+ assertEquals("bla\n", makeEntry("fooooo\nbla\n").extraContentString)
+ assertEquals("bla", makeEntry("fooooo\nbla").extraContentString)
+ assertEquals("", makeEntry("fooooo\n").extraContentString)
+ assertEquals("", makeEntry("fooooo").extraContentString)
+ assertEquals("blubb\n", makeEntry("\nblubb\n").extraContentString)
+ assertEquals("blubb", makeEntry("\nblubb").extraContentString)
+ assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContentString)
+ assertEquals("blubb", makeEntry("password: foo\nblubb").extraContentString)
+ assertEquals(
+ "blubb\nusername: bar",
+ makeEntry("blubb\npassword: foo\nusername: bar").extraContentString
+ )
+ assertEquals("", makeEntry("\n").extraContentString)
+ assertEquals("", makeEntry("").extraContentString)
+ }
+
+ @Test
+ fun parseExtraContentWithoutAuth() {
+ var entry = makeEntry("username: abc\npassword: abc\ntest: abcdef")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test"))
+ assertEquals("abcdef", entry.extraContent["test"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest: :abcdef:")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test"))
+ assertEquals(":abcdef:", entry.extraContent["test"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest : ::abc:def::")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test"))
+ assertEquals("::abc:def::", entry.extraContent["test"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\ntest2: ghijkl")
+ assertEquals(2, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test2"))
+ assertEquals("ghijkl", entry.extraContent["test2"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\n: ghijkl\n mnopqr:")
+ assertEquals(2, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("Extra Content"))
+ assertEquals(": ghijkl\n mnopqr:", entry.extraContent["Extra Content"])
+
+ entry = makeEntry("username: abc\npassword: abc\n:\n\n")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("Extra Content"))
+ assertEquals(":", entry.extraContent["Extra Content"])
+ }
+
+ @Test
+ fun getUsername() {
+ for (field in PasswordEntry.USERNAME_FIELDS) {
+ assertEquals("username", makeEntry("\n$field username").username)
+ assertEquals(
+ "username",
+ makeEntry("\n${field.uppercase(Locale.getDefault())} username").username
+ )
+ }
+ assertEquals("username", makeEntry("secret\nextra\nlogin: username\ncontent\n").username)
+ assertEquals("username", makeEntry("\nextra\nusername: username\ncontent\n").username)
+ assertEquals("username", makeEntry("\nUSERNaMe: username\ncontent\n").username)
+ assertEquals("username", makeEntry("\nlogin: username").username)
+ assertEquals("foo@example.com", makeEntry("\nemail: foo@example.com").username)
+ assertEquals("username", makeEntry("\nidentity: username\nlogin: another_username").username)
+ assertEquals("username", makeEntry("\nLOGiN:username").username)
+ assertEquals("foo@example.com", makeEntry("pass\nmail: foo@example.com").username)
+ assertNull(makeEntry("secret\nextra\ncontent\n").username)
+ }
+
+ @Test
+ fun hasUsername() {
+ assertNotNull(makeEntry("secret\nextra\nlogin: username\ncontent\n").username)
+ assertNull(makeEntry("secret\nextra\ncontent\n").username)
+ assertNull(makeEntry("secret\nlogin failed\n").username)
+ assertNull(makeEntry("\n").username)
+ assertNull(makeEntry("").username)
+ }
+
+ @Test
+ fun generatesOtpFromTotpUri() = runTest {
+ val entry = makeEntry("secret\nextra\n$TOTP_URI")
+ assertTrue(entry.hasTotp())
+ entry.totp.test {
+ val otp = expectMostRecentItem()
+ assertEquals("818800", otp.value)
+ assertEquals(30.seconds, otp.remainingTime)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ /**
+ * Same as [generatesOtpFromTotpUri], but advances the clock by 5 seconds. This exercises the
+ * [Totp.remainingTime] calculation logic, and acts as a regression test to resolve the bug which
+ * blocked https://github.com/Android-Password-Store/Android-Password-Store/issues/1550.
+ */
+ @Test
+ fun generatedOtpHasCorrectRemainingTime() = runTest {
+ val entry = makeEntry("secret\nextra\n$TOTP_URI", TestUserClock.withAddedSeconds(5))
+ assertTrue(entry.hasTotp())
+ entry.totp.test {
+ val otp = expectMostRecentItem()
+ assertEquals("818800", otp.value)
+ assertEquals(25.seconds, otp.remainingTime)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun generatesOtpWithOnlyUriInFile() = runTest {
+ val entry = makeEntry(TOTP_URI)
+ assertNull(entry.password)
+ entry.totp.test {
+ val otp = expectMostRecentItem()
+ assertEquals("818800", otp.value)
+ assertEquals(30.seconds, otp.remainingTime)
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun emitsTotpEverySecond() = runTest {
+ val entry = makeEntry(TOTP_URI)
+ entry.totp.test {
+ delay(3000L)
+ val events = cancelAndConsumeRemainingEvents()
+ assertEquals(3, events.size)
+ assertTrue { events.all { event -> event is Event.Item<Totp> } }
+ }
+ }
+
+ @Test
+ fun onlyLooksForUriInFirstLine() {
+ val entry = makeEntry("id:\n$TOTP_URI")
+ assertNotNull(entry.password)
+ assertTrue(entry.hasTotp())
+ assertNull(entry.username)
+ }
+
+ // https://github.com/android-password-store/Android-Password-Store/issues/1190
+ @Test
+ fun extraContentWithMultipleUsernameFields() {
+ val entry = makeEntry("pass\nuser: user\nid: id\n$TOTP_URI")
+ assertTrue(entry.extraContent.isNotEmpty())
+ assertTrue(entry.hasTotp())
+ assertNotNull(entry.username)
+ assertEquals("pass", entry.password)
+ assertEquals("user", entry.username)
+ assertEquals(mapOf("id" to "id"), entry.extraContent)
+ }
+
+ companion object {
+
+ @Suppress("MaxLineLength")
+ const val TOTP_URI =
+ "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"
+
+ val fakeClock = TestUserClock()
+ }
+}