All checks were successful
build-apk / build-and-publish (push) Successful in 8m20s
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.
132 lines
5.4 KiB
Kotlin
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"))
|
|
}
|
|
}
|