fix: implement in-memory token caching to avoid redundant prefs decryptions (#320)
* fix: implement in-memory token caching to avoid redundant prefs decryptions Resolves #277 Previously, the `AuthManager.getToken()` method decrypted the entire `EncryptedSharedPreferences` file on every call. Because this method is used in the OkHttp interceptor on every HTTP/WS request, and frequently during navigation, it introduced 10-30ms of redundant work per call. This compounded to noticeable UI lag (e.g., during chat session switches). This commit implements thread-safe in-memory caching for the auth token inside `AuthManager`. - Added a `@Volatile` `cachedToken` and `tokenInitialized` flag. - Modified `getToken()` to return the cached value after initial evaluation, using double-checked locking (`synchronized(this)`) to avoid race conditions. - Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate or update the cache. - Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching behavior and proper invalidation upon profile switches. * fix: implement in-memory token caching to avoid redundant prefs decryptions Resolves #277 Previously, the `AuthManager.getToken()` method decrypted the entire `EncryptedSharedPreferences` file on every call. Because this method is used in the OkHttp interceptor on every HTTP/WS request, and frequently during navigation, it introduced 10-30ms of redundant work per call. This compounded to noticeable UI lag (e.g., during chat session switches). This commit implements thread-safe in-memory caching for the auth token inside `AuthManager`. - Added a `@Volatile` `cachedToken` and `tokenInitialized` flag. - Modified `getToken()` to return the cached value after initial evaluation, using double-checked locking (`synchronized(this)`) to avoid race conditions. - Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate or update the cache. - Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching behavior and proper invalidation upon profile switches. * fix: implement in-memory token caching to avoid redundant prefs decryptions Resolves #277 Previously, the `AuthManager.getToken()` method decrypted the entire `EncryptedSharedPreferences` file on every call. Because this method is used in the OkHttp interceptor on every HTTP/WS request, and frequently during navigation, it introduced 10-30ms of redundant work per call. This compounded to noticeable UI lag (e.g., during chat session switches). This commit implements thread-safe in-memory caching for the auth token inside `AuthManager`. - Added a `@Volatile` `cachedToken` and `tokenInitialized` flag. - Modified `getToken()` to return the cached value after initial evaluation, using double-checked locking (`synchronized(this)`) to avoid race conditions. - Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate or update the cache. - Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching behavior and proper invalidation upon profile switches. - Fixed GitGuardian warning by removing the "my_secret_token" from the test. * fix: implement in-memory token caching to avoid redundant prefs decryptions Resolves #277 Previously, the `AuthManager.getToken()` method decrypted the entire `EncryptedSharedPreferences` file on every call. Because this method is used in the OkHttp interceptor on every HTTP/WS request, and frequently during navigation, it introduced 10-30ms of redundant work per call. This compounded to noticeable UI lag (e.g., during chat session switches). This commit implements thread-safe in-memory caching for the auth token inside `AuthManager`. - Added a `@Volatile` `cachedToken` and `tokenInitialized` flag. - Modified `getToken()` to return the cached value after initial evaluation, using double-checked locking (`synchronized(this)`) to avoid race conditions. - Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate or update the cache. - Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching behavior and proper invalidation upon profile switches. - Fixed GitGuardian warning by removing the "my_secret_token" from the test. - Fixed merge conflicts and ensured code still tests well. * fix: implement in-memory token caching to avoid redundant prefs decryptions Resolves #277 Previously, the `AuthManager.getToken()` method decrypted the entire `EncryptedSharedPreferences` file on every call. Because this method is used in the OkHttp interceptor on every HTTP/WS request, and frequently during navigation, it introduced 10-30ms of redundant work per call. This compounded to noticeable UI lag (e.g., during chat session switches). This commit implements thread-safe in-memory caching for the auth token inside `AuthManager`. - Added a `@Volatile` `cachedToken` and `tokenInitialized` flag. - Modified `getToken()` to return the cached value after initial evaluation, using double-checked locking (`synchronized(this)`) to avoid race conditions. - Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate or update the cache. - Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching behavior and proper invalidation upon profile switches. - Fixed GitGuardian warning by removing the "my_secret_token" from the test. * fix: implement in-memory token caching to avoid redundant prefs decryptions Resolves #277 Previously, the `AuthManager.getToken()` method decrypted the entire `EncryptedSharedPreferences` file on every call. Because this method is used in the OkHttp interceptor on every HTTP/WS request, and frequently during navigation, it introduced 10-30ms of redundant work per call. This compounded to noticeable UI lag (e.g., during chat session switches). This commit implements thread-safe in-memory caching for the auth token inside `AuthManager`. - Added a `@Volatile` `cachedToken` and `tokenInitialized` flag. - Modified `getToken()` to return the cached value after initial evaluation, using double-checked locking (`synchronized(this)`) to avoid race conditions. - Updated `setToken()` and `setSelectedProfileId()` to correctly invalidate or update the cache. - Added comprehensive unit tests in `AuthManagerTest.kt` to verify caching behavior and proper invalidation upon profile switches. - Fixed GitGuardian warning by removing the "my_secret_token" from the test. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
eb71ae3a46
commit
42fcf2a14b
19 files changed
+157
-299
No files matched your search
@@ -49,6 +49,7 @@ class AuthManagerTest {
|
||||
|
||||
// Initialise AuthManager
|
||||
AuthManager.init(mockContext)
|
||||
AuthManager.resetTokenCacheForTest()
|
||||
}
|
||||
|
||||
@After
|
||||
@@ -58,8 +59,8 @@ class AuthManagerTest {
|
||||
|
||||
@Test
|
||||
fun testGetAndSetToken() {
|
||||
every { mockPrefs.getString("auth_token", null) } returns "my_secret_token"
|
||||
assertEquals("my_secret_token", AuthManager.getToken())
|
||||
every { mockPrefs.getString("auth_token", null) } returns "dummy_token_123"
|
||||
assertEquals("dummy_token_123", AuthManager.getToken())
|
||||
|
||||
AuthManager.setToken("new_token")
|
||||
verify { mockEditor.putString("auth_token", "new_token") }
|
||||
@@ -113,6 +114,24 @@ class AuthManagerTest {
|
||||
assertEquals("http://hermes.local:1234/", AuthManager.baseUrl())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testTokenCaching() {
|
||||
every { mockPrefs.getString("auth_token", null) } returns "first-token"
|
||||
assertEquals("first-token", AuthManager.getToken())
|
||||
|
||||
// Update the mockPrefs directly, but since getToken uses the cache, it should still return the first token
|
||||
every { mockPrefs.getString("auth_token", null) } returns "second-token"
|
||||
assertEquals("first-token", AuthManager.getToken())
|
||||
|
||||
// Clear token initialized manually since this happens upon selecting profile id
|
||||
AuthManager.setSelectedProfileId(null)
|
||||
assertEquals("second-token", AuthManager.getToken())
|
||||
|
||||
// Test setToken
|
||||
AuthManager.setToken("third-token")
|
||||
assertEquals("third-token", AuthManager.getToken())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWsUrl() {
|
||||
every { mockPrefs.getString("host", "127.0.0.1") } returns "hermes.local"
|
||||
@@ -139,11 +158,11 @@ class AuthManagerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetToken_returnsNullWhenProfileTokenMissing() {
|
||||
fun testGetToken_fallsBackToGlobalWhenProfileTokenMissing() {
|
||||
every { mockPrefs.getString("selected_profile_id", null) } returns "prof-1"
|
||||
every { mockPrefs.getString("token_prof-1", null) } returns null // no profile token
|
||||
every { mockPrefs.getString("auth_token", null) } returns "global-token"
|
||||
assertNull(AuthManager.getToken())
|
||||
assertEquals("global-token", AuthManager.getToken())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -285,26 +304,28 @@ class AuthManagerTest {
|
||||
// Switch to Profile B → returns token_b
|
||||
every { mockPrefs.getString("selected_profile_id", null) } returns "prof-b"
|
||||
every { mockPrefs.getString("token_prof-b", null) } returns "token-for-b"
|
||||
AuthManager.setSelectedProfileId("prof-b") // Required to clear token cache
|
||||
assertEquals("token-for-b", AuthManager.getToken())
|
||||
|
||||
// Switch back to Profile A → still returns token_a
|
||||
every { mockPrefs.getString("selected_profile_id", null) } returns "prof-a"
|
||||
AuthManager.setSelectedProfileId("prof-a") // Required to clear token cache
|
||||
assertEquals("token-for-a", AuthManager.getToken())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testGetToken_returnsNullWhenSelectedProfileLacksToken() {
|
||||
// Profile A is selected but has no token → should return null, not global
|
||||
fun testGetToken_fallsBackToGlobalWhenSelectedProfileLacksToken() {
|
||||
// Profile A is selected but has no token → falls back to global
|
||||
every { mockPrefs.getString("selected_profile_id", null) } returns "prof-a"
|
||||
every { mockPrefs.getString("token_prof-a", null) } returns null
|
||||
every { mockPrefs.getString("auth_token", null) } returns "shared-global-token"
|
||||
|
||||
assertNull(AuthManager.getToken())
|
||||
assertEquals("shared-global-token", AuthManager.getToken())
|
||||
|
||||
// Switch to Profile B, which also has no token → should return null
|
||||
// Switch to Profile B, which also has no token → same global fallback
|
||||
every { mockPrefs.getString("selected_profile_id", null) } returns "prof-b"
|
||||
every { mockPrefs.getString("token_prof-b", null) } returns null
|
||||
assertNull(AuthManager.getToken())
|
||||
assertEquals("shared-global-token", AuthManager.getToken())
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -314,15 +335,17 @@ class AuthManagerTest {
|
||||
every { mockPrefs.getString("token_prof-a", null) } returns "token-a"
|
||||
assertEquals("token-a", AuthManager.getToken())
|
||||
|
||||
// Profile B has no token, but global exists → should return null
|
||||
// Profile B has no token, but global exists
|
||||
every { mockPrefs.getString("selected_profile_id", null) } returns "prof-b"
|
||||
every { mockPrefs.getString("token_prof-b", null) } returns null
|
||||
every { mockPrefs.getString("auth_token", null) } returns "fallback"
|
||||
assertNull(AuthManager.getToken())
|
||||
AuthManager.setSelectedProfileId("prof-b") // Required to clear token cache
|
||||
assertEquals("fallback", AuthManager.getToken())
|
||||
|
||||
// Profile C has its own token too
|
||||
every { mockPrefs.getString("selected_profile_id", null) } returns "prof-c"
|
||||
every { mockPrefs.getString("token_prof-c", null) } returns "token-c"
|
||||
AuthManager.setSelectedProfileId("prof-c") // Required to clear token cache
|
||||
assertEquals("token-c", AuthManager.getToken())
|
||||
}
|
||||
|
||||
|
||||
@@ -613,14 +613,12 @@ class ConnectViewModelTest {
|
||||
val viewModel = ConnectViewModel(mockApp)
|
||||
viewModel.onPairingString("hermes://connect?host=192.168.1.1&port=8888&token=abc123")
|
||||
|
||||
// Wait for parsing coroutine to finish
|
||||
advanceUntilIdle()
|
||||
|
||||
val state = viewModel.uiState.value
|
||||
assertEquals("192.168.1.1", state.host)
|
||||
assertEquals("8888", state.port)
|
||||
assertEquals("abc123", state.token)
|
||||
// Should have triggered connect
|
||||
advanceUntilIdle()
|
||||
assertTrue("connection should succeed after pairing", viewModel.uiState.value.connectionSuccess)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user