package com.peachpasswords.shared import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test /** * MF-8: a sensitive copy auto-clears after 30s — but only if our value is still on * the clipboard. Drives the pure [PeachClipboard.scheduleClear] seam on a virtual * scheduler (the Android ClipboardManager is faked with a plain string box). */ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class PeachClipboardTest { @Test fun clears_after_delay_when_value_unchanged() = runTest { var clipboard: String? = "s3cret" PeachClipboard.scheduleClear( scope = CoroutineScope(StandardTestDispatcher(testScheduler)), expected = "s3cret", readCurrent = { clipboard }, clear = { clipboard = "" }, ) advanceTimeBy(PeachClipboard.CLEAR_DELAY_MS - 1_000); runCurrent() assertEquals("not cleared before the window elapses", "s3cret", clipboard) advanceTimeBy(2_000); runCurrent() assertEquals("cleared once the 30s window elapses", "", clipboard) } @Test fun does_not_clear_when_user_copied_something_else() = runTest { var clipboard: String? = "s3cret" var cleared = false PeachClipboard.scheduleClear( scope = CoroutineScope(StandardTestDispatcher(testScheduler)), expected = "s3cret", readCurrent = { clipboard }, clear = { cleared = true }, ) clipboard = "user typed this after copying" // user copied something else within 30s advanceTimeBy(PeachClipboard.CLEAR_DELAY_MS + 1_000); runCurrent() assertFalse("must never wipe what the user copied afterwards", cleared) assertEquals("user typed this after copying", clipboard) } @Test fun clears_when_the_clipboard_cannot_be_read() = runTest { // From API 29 a backgrounded process reads null instead of the value it // wrote. That is the ordinary state after pasting into another app, and // it must not be mistaken for "someone else copied" (PB-26-119). var cleared = false PeachClipboard.scheduleClear( scope = CoroutineScope(StandardTestDispatcher(testScheduler)), expected = "s3cret", readCurrent = { null }, clear = { cleared = true }, ) advanceTimeBy(PeachClipboard.CLEAR_DELAY_MS + 1_000); runCurrent() assertTrue("a denied read must still clear our value", cleared) } @Test fun superseded_copy_is_left_to_its_own_timer() = runTest { var cleared = false PeachClipboard.scheduleClear( scope = CoroutineScope(StandardTestDispatcher(testScheduler)), expected = "s3cret", readCurrent = { "newer peach copy" }, clear = { cleared = true }, superseded = { true }, ) advanceTimeBy(PeachClipboard.CLEAR_DELAY_MS + 1_000); runCurrent() assertFalse("an older timer must not fire after a newer copy", cleared) } @Test fun thirty_second_default_matches_browser() { assertTrue(PeachClipboard.CLEAR_DELAY_MS == 30_000L) } @Test fun copy_feedback_bus_pulses_monotonically() { val before = CopyFeedbackBus.pulses.value CopyFeedbackBus.pulse() CopyFeedbackBus.pulse() assertEquals(before + 2, CopyFeedbackBus.pulses.value) } }