1package com.peachpasswords.vault23import com.peachpasswords.ui.vault.CsvExport4import com.peachpasswords.ui.vault.CsvImport5import com.peachpasswords.ui.vault.ImportParseResult6import com.peachpasswords.ui.vault.PeachScriptExport7import com.peachpasswords.ui.vault.PeachScriptImport8import org.junit.Assert.assertEquals9import org.junit.Assert.assertFalse10import org.junit.Assert.assertTrue11import org.junit.Test12131415161718class PeachScriptRoundTripTest {1920 private fun login(i: Int) = VaultEntry(21 id = java.util.UUID.nameUUIDFromBytes("acct-$i".toByteArray()).toString(), type = "login", title = "Acct $i",22 username = "user$i", password = "pw-$i-Ω", url = "https://site$i.example.com",23 notes = "note $i", createdAt = 1_500_000_000_000 + i, modifiedAt = 1_500_000_000_000 + i,24 )2526 2728 @Test fun large_vault_round_trips() {29 val input = (0 until 500).map { login(it) }30 val out = PeachPayloadCodec.buildPayload(input, "backup-pass")31 val back = PeachPayloadCodec.parsePayload(out, "backup-pass")32 assertEquals(500, back.size)33 assertEquals("Acct 0", back.first().title)34 assertEquals("Acct 499", back.last().title)35 assertEquals("pw-250-Ω", back[250].password)36 }3738 @Test fun tampered_ciphertext_fails() {39 val out = PeachPayloadCodec.buildPayload(listOf(login(1)), "pw").copyOf()40 41 val i = 40.coerceAtMost(out.size - 1)42 out[i] = (out[i].toInt() xor 0xFF).toByte()43 var threw = false44 try { PeachPayloadCodec.parsePayload(out, "pw") } catch (_: Exception) { threw = true }45 assertTrue("tampered ciphertext must fail auth", threw)46 }4748 @Test fun size_bucket_padding_hides_exact_entry_count() {49 50 51 val empty = PeachPayloadCodec.buildPayload(emptyList(), "pw")52 val one = PeachPayloadCodec.buildPayload(listOf(login(1)), "pw")53 assertEquals(empty.size, one.size)54 }5556 @Test fun each_export_uses_a_fresh_nonce() {57 val a = PeachPayloadCodec.buildPayload(listOf(login(1)), "pw")58 val b = PeachPayloadCodec.buildPayload(listOf(login(1)), "pw")59 assertFalse("salt/nonce randomization makes envelopes differ", a.contentEquals(b))60 }6162 6364 @Test fun text_export_is_peachscript_and_round_trips() {65 val text = PeachScriptExport.buildEncrypted(listOf(login(1), login(2)), "bp")66 assertTrue(PeachScriptImport.looksLikePeachScript(text))67 val r = PeachScriptImport.decryptAndParse(text, "bp")68 assertTrue(r is PeachScriptImport.DecryptResult.Success)69 r as PeachScriptImport.DecryptResult.Success70 assertEquals(2, r.entries.size)71 }7273 @Test fun text_export_wrong_password_does_not_yield_entries() {74 val text = PeachScriptExport.buildEncrypted(listOf(login(1)), "right")75 val r = PeachScriptImport.decryptAndParse(text, "wrong")76 assertFalse(r is PeachScriptImport.DecryptResult.Success)77 }7879 8081 @Test fun csv_header_columns_are_fixed_order() {82 val csv = CsvExport.toCsv(emptyList())83 assertEquals("title,username,password,url,notes", csv.lineSequence().first())84 }8586 @Test fun csv_uses_email_when_username_blank() {87 val e = VaultEntry(id = "", title = "X", type = "login", email = "[email protected]",88 createdAt = 0, modifiedAt = 0)89 val csv = CsvExport.toCsv(listOf(e))90 assertTrue(csv.contains("[email protected]"))91 }9293 @Test fun csv_formula_injection_is_guarded() {94 assertEquals("'=SUM(A1)", CsvExport.encodeField("=SUM(A1)"))95 assertEquals("'+1", CsvExport.encodeField("+1"))96 assertEquals("'-2", CsvExport.encodeField("-2"))97 assertEquals("'@cmd", CsvExport.encodeField("@cmd"))98 }99100 @Test fun csv_plain_field_is_unquoted() {101 assertEquals("hello", CsvExport.encodeField("hello"))102 }103104 @Test fun csv_quotes_fields_with_commas_and_quotes() {105 assertEquals("\"a,b\"", CsvExport.encodeField("a,b"))106 assertEquals("\"he\"\"llo\"", CsvExport.encodeField("he\"llo"))107 }108109 @Test fun csv_injection_guard_lands_inside_quotes_when_also_special() {110 111 assertEquals("\"'=a,b\"", CsvExport.encodeField("=a,b"))112 }113114 115116 @Test fun csv_round_trips_login_fields() {117 val entries = listOf(login(1), login(2))118 val csv = CsvExport.toCsv(entries)119 val parsed = CsvImport.detectAndParse(csv)120 assertTrue(parsed is ImportParseResult.Success)121 parsed as ImportParseResult.Success122 assertEquals(2, parsed.entries.size)123 val first = parsed.entries.first()124 assertEquals("Acct 1", first.title)125 assertEquals("user1", first.username)126 assertEquals("pw-1-Ω", first.password)127 assertEquals("https://site1.example.com", first.url)128 assertEquals("note 1", first.notes)129 }130131 @Test fun csv_round_trips_fields_needing_quotes() {132 val tricky = VaultEntry(133 id = "", title = "Comma, Inc", type = "login", username = "u",134 password = "p\"q", url = "https://x.example.com", notes = "line1\nline2",135 createdAt = 0, modifiedAt = 0,136 )137 val csv = CsvExport.toCsv(listOf(tricky))138 val parsed = CsvImport.detectAndParse(csv) as ImportParseResult.Success139 val e = parsed.entries.single()140 assertEquals("Comma, Inc", e.title)141 assertEquals("p\"q", e.password)142 assertEquals("line1\nline2", e.notes)143 }144}