straw/strawApp/src/test/kotlin/com/sulkta/straw/util/LogDumpScrubTest.kt
Cobb 417456ad49
All checks were successful
build-apk / build-and-publish (push) Successful in 8m20s
diag: keep channel/playlist ids in the dogfood scrub profile
The dogfood profile already skips the channel/playlist/video-id replacements,
but the always-on LONG_TOKEN pass (20+ chars w/ a digit) then redacted the
24-char channel ids and long playlist ids to <token> — so channel_info logs
showed "[channel <token>]" instead of the real id. LONG_TOKEN on the dogfood
profile now keeps a token that is exactly a channel (UC…) or playlist
(PL/UU/LL/RD/OLAK5uy_…) id, while every other high-entropy run (visitorData,
hashes) still redacts. Full profile unchanged. +2 unit tests.
2026-08-06 09:48:32 -07:00

132 lines
5.4 KiB
Kotlin

/*
* SPDX-FileCopyrightText: 2026 Sulkta
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Unit tests for LogDump's two scrub profiles.
*
* These are pure-JVM tests over LogDump.scrubLine / scrubLineDogfood — the
* regexes have no Android dependency, so they run under the standard
* `:strawApp:testDebugUnitTest` task with junit on the test classpath.
*
* NOTE: this module had no test source set before; running these needs
* `testImplementation("junit:junit:4.13.2")` in strawApp/build.gradle.kts
* and a CI step that invokes the test task (build.yml only runs
* assembleDebug today, which does NOT compile/run this source set).
*/
package com.sulkta.straw.util
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class LogDumpScrubTest {
// ---- B1: IPv6-compressed `::` false positive -------------------------
@Test
fun rustPathWithDoubleColonSurvives() {
// The bug: `strawcore::stream` was scrubbed to `strawcor<ip>stream`
// because `e::` read as a compressed IPv6. It must now pass through
// untouched in BOTH profiles.
assertEquals("strawcore::stream", LogDump.scrubLine("strawcore::stream"))
assertEquals("strawcore::stream", LogDump.scrubLineDogfood("strawcore::stream"))
// A three-segment path — `::foo` must not clip `::f` (foo starts with
// the hex letter `f`).
assertEquals("strawcore::stream::foo", LogDump.scrubLine("strawcore::stream::foo"))
// Embedded in a realistic log line.
val line = "W strawcore: extracting strawcore::stream::foo failed"
assertEquals(line, LogDump.scrubLine(line))
}
@Test
fun realCompressedIpv6StillScrubs() {
// Loopback `::1` (a leading `::` — the OLD regex actually missed this),
// link-local, and documentation-range compressed forms all redact.
assertEquals("<ip>", LogDump.scrubLine("::1"))
assertEquals("<ip>", LogDump.scrubLine("fe80::1"))
assertEquals("<ip>", LogDump.scrubLine("2001:db8::1"))
assertEquals("<ip>", LogDump.scrubLine("2604:2dc0::1"))
// In-context, too.
assertTrue(LogDump.scrubLine("bound peer fe80::1 up").contains("<ip>"))
assertFalse(LogDump.scrubLine("bound peer fe80::1 up").contains("fe80"))
}
// ---- B2: dogfood profile KEEPS diagnostic identifiers ----------------
@Test
fun dogfoodKeepsBareVideoIdFullScrubs() {
val id = "dQw4w9WgXcQ"
assertEquals(id, LogDump.scrubLineDogfood(id))
assertEquals("<videoId>", LogDump.scrubLine(id))
}
@Test
fun dogfoodKeepsYoutubeShortLinkFullScrubs() {
val line = "open youtu.be/dQw4w9WgXcQ now"
// Dogfood: the youtu.be/<id> stays — the id is the diagnostic signal.
assertEquals(line, LogDump.scrubLineDogfood(line))
// Full: the schemeless YT link is redacted.
assertTrue(LogDump.scrubLine(line).contains("<scrubbed>"))
assertFalse(LogDump.scrubLine(line).contains("dQw4w9WgXcQ"))
}
@Test
fun dogfoodKeepsChannelAndPlaylistIdFullScrubs() {
// These are ≥20 chars so LONG_TOKEN would otherwise redact them even
// on the dogfood profile; the channel/playlist exemption keeps them.
val channelId = "UCuAXFkgsw1L7xaCfnd5JJOw" // UC + 22, has digits
val playlistId = "PLbpi6ZahtOH6Blw3RGYpWkSByi_T7Rygb"
assertEquals(channelId, LogDump.scrubLineDogfood(channelId))
assertEquals(playlistId, LogDump.scrubLineDogfood(playlistId))
// Full profile redacts them to their labelled placeholders.
assertEquals("<channelId>", LogDump.scrubLine(channelId))
assertEquals("<playlistId>", LogDump.scrubLine(playlistId))
}
@Test
fun dogfoodStillScrubsGenericHighEntropyToken() {
// visitorData-shaped: ≥20 chars with a digit, NOT a channel/playlist
// prefix → still `<token>` on the dogfood profile (it's a cred).
val visitorData = "CgtVQzEyMzQ1Njc4OTBhYg"
assertEquals("<token>", LogDump.scrubLineDogfood(visitorData))
assertEquals("<token>", LogDump.scrubLine(visitorData))
}
// ---- B2: dogfood profile STILL scrubs real credentials / PII ---------
@Test
fun dogfoodStillScrubsSignedGooglevideoUrl() {
val line =
"url=https://r5---sn-abc.googlevideo.com/videoplayback?expire=99&sig=DEADBEEFSIG&pot=SECRETPOT"
val out = LogDump.scrubLineDogfood(line)
assertTrue(out.contains("<scrubbed>"))
assertFalse(out.contains("DEADBEEFSIG"))
assertFalse(out.contains("SECRETPOT"))
}
@Test
fun dogfoodStillScrubsBearerToken() {
val out = LogDump.scrubLineDogfood("hdr Authorization: Bearer ya29.A0ARLongTokenValue123")
assertTrue(out.contains("<scrubbed>"))
assertFalse(out.contains("ya29"))
}
@Test
fun dogfoodStillScrubsSignedParams() {
val out = LogDump.scrubLineDogfood("req q&sig=DEADBEEF123&pot=POTVAL456 done")
assertTrue(out.contains("sig=<scrubbed>"))
assertTrue(out.contains("pot=<scrubbed>"))
assertFalse(out.contains("DEADBEEF123"))
assertFalse(out.contains("POTVAL456"))
}
@Test
fun dogfoodStillScrubsIpAddresses() {
val out = LogDump.scrubLineDogfood("peer 203.0.113.7 and fe80::1 up")
assertTrue(out.contains("<ip>"))
assertFalse(out.contains("203.0.113.7"))
assertFalse(out.contains("fe80"))
}
}