@@ -6,12 +6,17 @@ import com.facebook.react.bridge.JavaOnlyMap
66import okhttp3.mockwebserver.MockResponse
77import okhttp3.mockwebserver.MockWebServer
88import org.junit.After
9+ import org.junit.Assert.assertFalse
910import org.junit.Assert.assertTrue
1011import org.junit.Assert.fail
1112import org.junit.Before
1213import org.junit.Test
1314import java.io.IOException
1415import java.util.concurrent.TimeUnit
16+ import java.util.logging.Handler
17+ import java.util.logging.Level
18+ import java.util.logging.LogRecord
19+ import java.util.logging.Logger
1520import kotlin.system.measureTimeMillis
1621
1722// Proves that A0Auth0Module.buildNetworkingClient() genuinely threads networkingOptions
@@ -20,16 +25,66 @@ import kotlin.system.measureTimeMillis
2025class A0Auth0ModuleNetworkingOptionsTest {
2126
2227 private lateinit var server: MockWebServer
28+ private lateinit var logCapture: LogCapture
29+ private lateinit var okHttpLogger: Logger
2330
2431 @Before
2532 fun setUp () {
2633 server = MockWebServer ()
2734 server.start()
35+
36+ // Set up log capture for OkHttp's HttpLoggingInterceptor
37+ // Capture at root logger level to catch all logging regardless of hierarchy
38+ logCapture = LogCapture ()
39+ okHttpLogger = Logger .getLogger(" " ) // Root logger catches everything
40+ okHttpLogger.level = Level .ALL
41+ okHttpLogger.addHandler(logCapture)
2842 }
2943
3044 @After
3145 fun tearDown () {
3246 server.shutdown()
47+ okHttpLogger.removeHandler(logCapture)
48+ }
49+
50+ /* *
51+ * Captures java.util.logging records so we can verify Auth0.Android's
52+ * HttpLoggingInterceptor is (or isn't) writing request/response bodies.
53+ */
54+ private class LogCapture : Handler () {
55+ private val records = mutableListOf<LogRecord >()
56+
57+ override fun publish (record : LogRecord ) {
58+ records.add(record)
59+ }
60+
61+ override fun flush () {}
62+ override fun close () {}
63+
64+ fun hasRequestOrResponseBodyLogs (): Boolean {
65+ return records.any { record ->
66+ val message = record.message ? : " "
67+ val loggerName = record.loggerName ? : " "
68+
69+ (loggerName.contains(" okhttp" , ignoreCase = true ) ||
70+ loggerName.contains(" http" , ignoreCase = true )) &&
71+ (message.contains(" --> " ) || // Request line: "--> GET /token"
72+ message.contains(" <-- " ) || // Response line: "<-- 200 OK"
73+ message.contains(" Content-" ) || // Headers like Content-Type, Content-Length
74+ message.contains(" access_token" ) || // Response body content
75+ message.contains(" {\" " )) // JSON body start
76+ }
77+ }
78+
79+ fun getAllLogMessages (): String {
80+ return records.joinToString(" \n " ) { record ->
81+ " [${record.loggerName} ] ${record.message} "
82+ }
83+ }
84+
85+ fun clear () {
86+ records.clear()
87+ }
3388 }
3489
3590 @Test
@@ -78,21 +133,52 @@ class A0Auth0ModuleNetworkingOptionsTest {
78133
79134 @Test
80135 fun `enableLogging is ignored on a non-debuggable build even when requested` () {
81- server.enqueue(MockResponse ().setBody(" {}" ))
136+ server.enqueue(MockResponse ().setBody(""" {"access_token": "secret123"}""" ))
137+
138+ logCapture.clear()
82139
83140 // SECURITY: If the isDebuggable gate is ever removed, Auth0.Android attaches its
84141 // logging interceptor which logs full request/response bodies (including tokens).
85- // This test would crash ("Method android .util.Log not mocked") if that happens ,
86- // catching the security regression before it ships .
142+ // Auth0.Android's interceptor writes through java .util.logging, not android.util.Log ,
143+ // so we must assert on captured log records rather than relying on a crash .
87144 val client = A0Auth0Module .buildNetworkingClient(
88145 JavaOnlyMap .of(" enableLogging" , true ),
89146 isDebuggable = false
90147 )
91148
92- client.load(server.url(" /" ).toString(), RequestOptions (HttpMethod .GET ))
149+ client.load(server.url(" /token " ).toString(), RequestOptions (HttpMethod .GET ))
93150
94- val recordedRequest = server.takeRequest()
95- assertTrue(recordedRequest.method == " GET" )
151+ // Verify that NO request/response body logs were written. If the isDebuggable gate
152+ // is removed, this assertion will fail because the interceptor will log the response
153+ // body containing "access_token": "secret123".
154+ assertFalse(
155+ " Expected no HTTP body logs on non-debuggable build, but logging was enabled" ,
156+ logCapture.hasRequestOrResponseBodyLogs()
157+ )
158+ }
159+
160+ @Test
161+ fun `enableLogging actually logs request and response bodies on debuggable builds` () {
162+ server.enqueue(MockResponse ().setBody(""" {"access_token": "secret456"}""" ))
163+
164+ logCapture.clear()
165+
166+ // Positive test: verify that enableLogging actually works when isDebuggable = true.
167+ val client = A0Auth0Module .buildNetworkingClient(
168+ JavaOnlyMap .of(" enableLogging" , true ),
169+ isDebuggable = true
170+ )
171+
172+ client.load(server.url(" /token" ).toString(), RequestOptions (HttpMethod .GET ))
173+
174+ // The interceptor should have logged the request and response, proving that:
175+ // (a) our test harness correctly captures logs, and
176+ // (b) the enableLogging option genuinely enables logging when allowed.
177+ val allLogs = logCapture.getAllLogMessages()
178+ assertTrue(
179+ " Expected HTTP body logs on debuggable build with enableLogging=true. Captured logs:\n $allLogs " ,
180+ logCapture.hasRequestOrResponseBodyLogs()
181+ )
96182 }
97183
98184 @Test
0 commit comments