From a28aae6e4a269bbae78b77014fb849e72b2ddef9 Mon Sep 17 00:00:00 2001 From: xXJsonDeruloXx Date: Sun, 27 Sep 2026 10:52:01 -0400 Subject: [PATCH] test: add unit tests for archive paths and log redaction --- .github/workflows/build.yml | 3 + app/build.gradle | 7 +++ .../launcher/core/ArchivePathsTest.kt | 46 +++++++++++++++ .../launcher/core/LogRedactorTest.kt | 58 +++++++++++++++++++ 4 files changed, 114 insertions(+) create mode 100644 app/src/test/java/com/droiddeck/launcher/core/ArchivePathsTest.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c2d479537..cf1cf715e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -295,6 +295,9 @@ jobs: set -o pipefail ./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log + - name: Unit tests + run: ./gradlew testReleaseUnitTest --console=plain -PndkVersion="$NDK_VERSION" + - name: Check the native dependency closure run: | set -euo pipefail diff --git a/app/build.gradle b/app/build.gradle index 568a8ab49..7e86ee1eb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -140,6 +140,11 @@ android { buildConfig true } + testOptions { + // Plain JVM tests: a stray android.util.Log call returns a default instead of throwing. + unitTests.returnDefaultValues = true + } + compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 @@ -169,4 +174,6 @@ dependencies { implementation 'com.flyfishxu:kadb-android:1.2.1' // Android terminal view/emulator used by the in-session secondary-display Linux shell. implementation 'com.termux.termux-app:terminal-view:0.118.0' + + testImplementation 'junit:junit:4.13.2' } diff --git a/app/src/test/java/com/droiddeck/launcher/core/ArchivePathsTest.kt b/app/src/test/java/com/droiddeck/launcher/core/ArchivePathsTest.kt new file mode 100644 index 000000000..0f8ac08b4 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/core/ArchivePathsTest.kt @@ -0,0 +1,46 @@ +package com.droiddeck.launcher.core + +import java.io.File +import java.nio.file.Files +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test + +class ArchivePathsTest { + private lateinit var root: File + private lateinit var dest: File + + @Before fun setUp() { + root = Files.createTempDirectory("archive-paths").toFile() + dest = File(root, "dest").apply { mkdirs() } + } + + @After fun tearDown() { + root.deleteRecursively() + } + + @Test fun plainEntryLandsInside() { + assertEquals(File(dest, "usr/bin/sh").canonicalPath, ArchivePaths.inside(dest, "usr/bin/sh")!!.canonicalPath) + } + + @Test fun dotEntryIsTheDestination() { + assertEquals(dest.canonicalPath, ArchivePaths.inside(dest, "./")!!.canonicalPath) + } + + @Test fun parentTraversalIsRefused() { + assertNull(ArchivePaths.inside(dest, "../escape")) + assertNull(ArchivePaths.inside(dest, "usr/../../escape")) + } + + @Test fun siblingWithSharedPrefixIsRefused() { + File(root, "dest-other").mkdirs() + assertNull(ArchivePaths.inside(dest, "../dest-other/x")) + } + + @Test fun symlinkOutOfTheTreeIsRefused() { + Files.createSymbolicLink(File(dest, "link").toPath(), root.toPath()) + assertNull(ArchivePaths.inside(dest, "link/escape")) + } +} diff --git a/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt b/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt new file mode 100644 index 000000000..88f2a8703 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/core/LogRedactorTest.kt @@ -0,0 +1,58 @@ +package com.droiddeck.launcher.core + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class LogRedactorTest { + @Test fun labelledJwtIsRedacted() { + val out = LogRedactor.redact("Using JWT 25484942796017334") + assertFalse(out.contains("25484942796017334")) + assertTrue(out.contains("")) + } + + @Test fun secretPairKeepsItsKey() { + val out = LogRedactor.redact("GET /x?sessionid=abcdef123456 200") + assertFalse(out.contains("abcdef123456")) + assertTrue(out.contains("sessionid=")) + } + + @Test fun plainKeyValueSurvives() { + assertEquals("language key=english", LogRedactor.redact("language key=english")) + } + + @Test fun steamIdIsMaskedNotDeleted() { + assertEquals("user 76561********1234", LogRedactor.redact("user 76561198012341234")) + } + + @Test fun webApiKeyIsRedactedButDepotChunkIsKept() { + val key = "0123456789abcdef0123456789abcdef" + val chunk = "0123456789abcdef0123456789abcdef01234567" + assertFalse(LogRedactor.redact("key $key").contains(key)) + assertTrue(LogRedactor.redact("chunk $chunk").contains(chunk)) + } + + @Test fun emailIsRedacted() { + assertEquals("login ok", LogRedactor.redact("login someone@example.com ok")) + } + + @Test fun clockTimeIsNotAnAddress() { + val line = "[2026-09-27 10:03:05] Startup" + assertEquals(line, LogRedactor.redact(line)) + } + + @Test fun redactingTwiceChangesNothing() { + val lines = listOf( + "Using JWT 25484942796017334", + "token=abcdefghijklmnop sessionid=qrstuvwxyz123", + "user 76561198012341234 [U:1:52075506]", + "external address 2607:f8b0:4005:80a::200e", + "OnLoginStateChange someaccount 2 1 0 0", + ) + for (line in lines) { + val once = LogRedactor.redact(line) + assertEquals(once, LogRedactor.redact(once)) + } + } +}