package com.peachpasswords.autofill import com.peachpasswords.autofill.AutofillResponseBuilder.FillMode import com.peachpasswords.vault.VaultEntry import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Test class AutofillPreviewLabelsTest { private fun entry( title: String = "GitHub", username: String? = "june", email: String? = null, url: String? = "https://github.com", ) = VaultEntry( id = title, title = title, username = username, email = email, url = url, createdAt = 0, modifiedAt = 0, ) @Test fun normal_mode_shows_entry_title_and_identity() { val labels = AutofillResponseBuilder.previewLabels(false, entry(), "github.com") assertEquals("GitHub", labels.title) assertEquals("june", labels.subtitle) assertEquals(FillMode.LOGIN, labels.fillMode) } @Test fun normal_mode_falls_back_email_then_domain() { val emailOnly = entry(username = null, email = "june@example.com") assertEquals("june@example.com", AutofillResponseBuilder.previewLabels(false, emailOnly, null).subtitle) val bare = entry(username = null, email = null) assertEquals("github.com", AutofillResponseBuilder.previewLabels(false, bare, null).subtitle) } @Test fun private_mode_hides_title_and_identity() { val labels = AutofillResponseBuilder.previewLabels(true, entry(), "github.com") assertEquals("Peach", labels.title) assertEquals("github.com", labels.subtitle) } @Test fun private_mode_without_origin_has_blank_subtitle() { val labels = AutofillResponseBuilder.previewLabels(true, entry(), null) assertEquals("Peach", labels.title) assertEquals("", labels.subtitle) } // --- OTP_FILL fillMode: the chip surfaces the actual TOTP code --- @Test fun otpFill_mode_shows_chunked_code_as_title_and_entry_as_subtitle() { val labels = AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = "github.com", fillMode = FillMode.OTP_FILL, otpCode = "482901", ) assertEquals("482 901", labels.title) assertEquals("2FA", labels.subtitle) assertEquals("GitHub", labels.iconSource) assertEquals(FillMode.OTP_FILL, labels.fillMode) } @Test fun otpFill_mode_passes_through_long_codes_unchunked() { val labels = AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = null, fillMode = FillMode.OTP_FILL, otpCode = "JBSWY3DPEHPK3PXP", ) // 8-char Steam-style code — chunk only fires on <=6 digits, this passes through. assertEquals("JBSWY3DPEHPK3PXP", labels.title) assertEquals("2FA", labels.subtitle) } @Test fun otpFill_mode_never_leaks_digits_in_private_preview() { val labels = AutofillResponseBuilder.previewLabels( privatePreview = true, entry = entry(), originDomain = "github.com", fillMode = FillMode.OTP_FILL, otpCode = "482901", ) // Privacy always wins over fillMode — chip says "Peach" not the digits. assertEquals("Peach", labels.title) assertEquals("github.com", labels.subtitle) } @Test(expected = IllegalArgumentException::class) fun otpFill_mode_requires_otpCode() { AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = "github.com", fillMode = FillMode.OTP_FILL, otpCode = null, ) } @Test fun otpFill_mode_subtitle_appends_countdown_when_secondsRemaining_is_set() { val labels = AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = "github.com", fillMode = FillMode.OTP_FILL, otpCode = "482901", secondsRemaining = 23, ) assertEquals("2FA · 23s", labels.subtitle) assertEquals("482 901", labels.title) } @Test fun otpFill_mode_subtitle_says_expired_when_secondsRemaining_is_zero() { val labels = AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = "github.com", fillMode = FillMode.OTP_FILL, otpCode = "482901", secondsRemaining = 0, ) assertEquals("2FA · expired", labels.subtitle) } @Test fun otpFill_mode_subtitle_says_expired_when_secondsRemaining_is_negative() { // A new FillRequest issued right at the rollover can briefly carry a // negative remaining value. The chip should still render "expired" // -- the code is stale either way. val labels = AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = null, fillMode = FillMode.OTP_FILL, otpCode = "482901", secondsRemaining = -1, ) assertEquals("2FA · expired", labels.subtitle) } @Test fun otpFill_mode_subtitle_is_just_2fa_when_secondsRemaining_is_null() { // Back-compat: pre-countdown callers may build OTP_FILL labels without // a countdown. Subtitle stays the original "2FA". val labels = AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = null, fillMode = FillMode.OTP_FILL, otpCode = "482901", secondsRemaining = null, ) assertEquals("2FA", labels.subtitle) assertNull("no end icon when secondsRemaining is unknown", labels.endIcon) } @Test fun chunkOtpForDisplay_groups_only_6digit_codes_with_space() { // 6-digit only: grouped into 3+3. assertEquals("482 901", AutofillResponseBuilder.chunkOtpForDisplay("482901")) // 5-digit and 7-digit: pass through (no sane chunking). assertEquals("12345", AutofillResponseBuilder.chunkOtpForDisplay("12345")) assertEquals("1234567", AutofillResponseBuilder.chunkOtpForDisplay("1234567")) // Alphanumeric (Steam-style base32): pass through unchanged — never strip letters. assertEquals("JBSWY3DPEHPK3PXP", AutofillResponseBuilder.chunkOtpForDisplay("JBSWY3DPEHPK3PXP")) } } // --- Countdown subtitle on the 2FA chip --- class AutofillPreviewLabelsCountdownTest { private fun entry(title: String = "GitHub") = VaultEntry( id = title, title = title, username = "june", createdAt = 0, modifiedAt = 0, ) @Test fun otpFill_chip_counts_down_seconds_remaining_in_subtitle() { val labels = AutofillResponseBuilder.previewLabels( privatePreview = false, entry = entry(), originDomain = "github.com", fillMode = FillMode.OTP_FILL, otpCode = "482901", secondsRemaining = 23, ) assertEquals("482 901", labels.title) assertEquals("2FA · 23s", labels.subtitle) } @Test fun otpFill_chip_amber_zone_at_five_to_nine_seconds() { val labels = AutofillResponseBuilder.previewLabels( false, entry(), null, fillMode = FillMode.OTP_FILL, otpCode = "123456", secondsRemaining = 7, ) assertEquals("2FA · 7s", labels.subtitle) } @Test fun otpFill_chip_red_zone_at_or_below_four_seconds() { val labels = AutofillResponseBuilder.previewLabels( false, entry(), null, fillMode = FillMode.OTP_FILL, otpCode = "123456", secondsRemaining = 3, ) assertEquals("2FA · 3s", labels.subtitle) } @Test fun otpFill_chip_at_zero_or_below_shows_expired() { val labels = AutofillResponseBuilder.previewLabels( false, entry(), null, fillMode = FillMode.OTP_FILL, otpCode = "123456", secondsRemaining = 0, ) assertEquals("2FA · expired", labels.subtitle) } @Test fun otpFill_chip_without_secondsRemaining_keeps_legacy_subtitle() { // Back-compat: callers that don't pass secondsRemaining get the same // label as v2.14.4. No end-icon either — purely visual change gated // on the freshness signal. val labels = AutofillResponseBuilder.previewLabels( false, entry(), "github.com", fillMode = FillMode.OTP_FILL, otpCode = "482901", ) assertEquals("2FA", labels.subtitle) assertEquals(null, labels.endIcon) } @Test fun private_mode_still_hides_digits_even_with_countdown() { // Privacy wins: "Peach" not the digits, no countdown leak either. val labels = AutofillResponseBuilder.previewLabels( true, entry(), "github.com", fillMode = FillMode.OTP_FILL, otpCode = "482901", secondsRemaining = 7, ) assertEquals("Peach", labels.title) assertEquals("github.com", labels.subtitle) } }