[Mobile] Investigate session expiry and add auth diagnostics#3651
Conversation
Add a local google_mlkit_translation stub through pubspec_overrides so the app can build on the iOS simulator without the native MLKit translation plugin. Also remove the project-level iphoneos-only platform restriction so the simulator remains a valid Xcode destination.
This commit documents and strengthens the mobile-side investigation for the\n"users logged out unexpectedly after login" issue.\n\nWhat I tested\n- Added and expanded auth-focused test coverage around the main client-side\n session expiry paths:\n - AuthHelper token refresh behavior\n - AuthBloc startup / session-expired / logout behavior\n - app resume session guard behavior\n - repository-level 401 handling\n - token storage behavior\n - user preferences auth handling\n - end-to-end session expiry integration flow\n- Simulated expired token, refresh timeout, network failure, malformed token,\n successful refresh, and repository-driven 401 scenarios.\n- Verified the full auth test suite passes with:\n flutter test test/app/auth\n\nWhat I observed\n- The normal client refresh path works as expected.\n- With a real logged-in user session, forcing the app down the expired-token\n branch successfully called the refresh endpoint, received HTTP 200, saved\n the new token, and kept the user logged in.\n- This weakens the hypothesis that the primary bug is the mobile app failing\n to refresh/store tokens in the normal expiry flow.\n\nWhat I added for investigation\n- Temporary runtime logging around auth refresh to capture:\n - whether the stored token is considered expired\n - refresh response status\n - refresh response message/body signal\n - whether a refreshed token is saved\n- Additional repository-level auth tests for user preferences, since later\n authenticated requests can also participate in session-expiry behavior.\n\nCurrent conclusion\n- I did not reproduce a failure in the core mobile token refresh flow.\n- The strongest remaining suspicion is now outside the simplest frontend token\n handling path, likely involving a longer-lived server/session policy,\n boundary condition, or backend-side expiry rule not matching the expected\n session duration.\n\nNext validation step\n- Live-device observation is now in progress: I am staying logged into the app\n and will re-check after about 24 hours to confirm whether real sessions are\n being ended earlier than the documented 365-day expectation.
📝 WalkthroughWalkthroughThis PR adds seven new test files under ChangesMobile Auth Test Suite
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/mobile/test/app/auth/services/auth_helper_test.dart (1)
53-167: ⚡ Quick winAdd a positive refresh-success test for the new parser/save path.
The suite exercises failure branches well, but it does not explicitly assert the changed 200/success/token parsing + persistence behavior.
✅ Suggested test case
group('AuthHelper.refreshTokenIfNeeded', () { + test('returns and persists refreshed token on successful refresh', () async { + final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); + ApiUtils.baseUrl = 'http://127.0.0.1:${server.port}'; + final freshToken = validToken(); + + server.listen((request) async { + expect(request.uri.path, '/api/v2/users/token/refresh'); + request.response.statusCode = HttpStatus.ok; + request.response + .write('{"success":true,"token":"$freshToken"}'); + await request.response.close(); + }); + + await SecureStorageRepository.instance.saveSecureData( + SecureStorageKeys.authToken, + expiredToken(), + ); + + final result = await AuthHelper.refreshTokenIfNeeded(); + final stored = await SecureStorageRepository.instance + .getSecureData(SecureStorageKeys.authToken); + + expect(result, isNotNull); + expect(stored, result); + + await server.close(force: true); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mobile/test/app/auth/services/auth_helper_test.dart` around lines 53 - 167, Add a new test case to the AuthHelper.refreshTokenIfNeeded group that verifies the successful refresh path is working correctly. Create a test that stores an expired token, mocks an HTTP server to respond with a 200 status code and a valid fresh token, calls AuthHelper.refreshTokenIfNeeded(), and then asserts that the method returns the fresh token (not null) and that the token has been properly saved to secure storage. This will ensure the changed parser and persistence logic for successful token refresh responses is explicitly covered by the test suite, complementing the existing failure case tests.src/mobile/test/app/auth/repository/user_preferences_auth_test.dart (1)
18-29: ⚡ Quick winJWT helper duplication across test files.
This
jwtTokenhelper is duplicated in three test files:
auth_token_storage_test.dart(as_jwtToken)base_repository_auth_test.dart(as_jwtToken)user_preferences_auth_test.dart(asjwtToken)Consider extracting this to a shared test utility file (e.g.,
test/helpers/jwt_test_helper.dart) to reduce duplication and improve maintainability.Minor: The naming is also inconsistent—this file uses
jwtTokenwhile the others use_jwtToken. Since it's defined insidemain(), both work, but consistency would be cleaner.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mobile/test/app/auth/repository/user_preferences_auth_test.dart` around lines 18 - 29, Extract the `jwtToken` helper function from user_preferences_auth_test.dart, auth_token_storage_test.dart (where it's named `_jwtToken`), and base_repository_auth_test.dart (also named `_jwtToken`) into a shared test utility file at test/helpers/jwt_test_helper.dart. Create the new utility file with a single public `jwtToken` function implementation and update all three test files to import and use this shared helper instead of their local duplicates. Also standardize the function naming to use `jwtToken` consistently across all locations.src/mobile/local_packages/google_mlkit_translation_stub/pubspec.yaml (1)
9-10: Remove the unusedmetadependency from pubspec.yaml.The stub implementation doesn't use any meta package annotations (
@required,@visibleForTesting, etc.), making this dependency unnecessary for a minimal stub. Removing it will keep the package lean without affecting functionality.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/mobile/local_packages/google_mlkit_translation_stub/pubspec.yaml` around lines 9 - 10, The meta dependency is declared in the dependencies section of pubspec.yaml but is not actually used by the stub implementation, as there are no meta package annotations anywhere in the code. Remove the `meta: ^1.9.1` line from the dependencies section to keep the stub package lean and remove unnecessary dependencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/mobile/lib/src/app/auth/services/auth_helper.dart`:
- Around line 202-210: The responseMessage extraction logic in the silent
refresh section can fall back to the raw response.body via the fallback chain,
which may contain the newly issued auth token and leak credentials into logs.
Modify the message extraction logic to prevent falling back to response.body
entirely. Instead, when none of the extracted fields (responseBody['message'],
responseBody['errors']?['message'], responseBody['error']) are available, use a
generic safe message (like a hardcoded string describing the refresh completion)
rather than falling back to the raw response.body. This ensures sensitive token
information is never included in the diagnostic log.
- Around line 239-246: The `_shouldTreatTokenAsExpired` method currently enables
the `FORCE_AUTH_TOKEN_EXPIRED` debug override in all builds, including release
builds, which can cause unintended auth behavior in production. Add a guard
condition to check that the build is not a release build (such as using
`kDebugMode` or equivalent Flutter/Dart constant) before allowing the
`forceExpiry` logic to take effect. Only when the build is a non-release build
AND the environment variable is set to 'true' should the token be treated as
expired for debugging purposes.
In `@src/mobile/test/app/auth/lifecycle/app_resume_session_test.dart`:
- Around line 182-191: The test uses a hardcoded 10ms delay with
Future<void>.delayed(const Duration(milliseconds: 10)) before asserting on
emittedStates, which creates a timing-sensitive test that can fail on slower CI
workers. Replace this hardcoded delay with deterministic event-queue draining
using a pump or similar mechanism that waits for all pending async work to
complete (such as pumpEventQueue() if available in your test framework). This
ensures the assertions for whereType<SessionExpiredState>() and
whereType<GuestUser>() execute only after all relevant state emissions have been
processed, regardless of system speed.
---
Nitpick comments:
In `@src/mobile/local_packages/google_mlkit_translation_stub/pubspec.yaml`:
- Around line 9-10: The meta dependency is declared in the dependencies section
of pubspec.yaml but is not actually used by the stub implementation, as there
are no meta package annotations anywhere in the code. Remove the `meta: ^1.9.1`
line from the dependencies section to keep the stub package lean and remove
unnecessary dependencies.
In `@src/mobile/test/app/auth/repository/user_preferences_auth_test.dart`:
- Around line 18-29: Extract the `jwtToken` helper function from
user_preferences_auth_test.dart, auth_token_storage_test.dart (where it's named
`_jwtToken`), and base_repository_auth_test.dart (also named `_jwtToken`) into a
shared test utility file at test/helpers/jwt_test_helper.dart. Create the new
utility file with a single public `jwtToken` function implementation and update
all three test files to import and use this shared helper instead of their local
duplicates. Also standardize the function naming to use `jwtToken` consistently
across all locations.
In `@src/mobile/test/app/auth/services/auth_helper_test.dart`:
- Around line 53-167: Add a new test case to the AuthHelper.refreshTokenIfNeeded
group that verifies the successful refresh path is working correctly. Create a
test that stores an expired token, mocks an HTTP server to respond with a 200
status code and a valid fresh token, calls AuthHelper.refreshTokenIfNeeded(),
and then asserts that the method returns the fresh token (not null) and that the
token has been properly saved to secure storage. This will ensure the changed
parser and persistence logic for successful token refresh responses is
explicitly covered by the test suite, complementing the existing failure case
tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 237cf7fa-24bb-4ccf-bde7-4cf26c596890
⛔ Files ignored due to path filters (1)
src/mobile/pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (18)
src/mobile/.gitignoresrc/mobile/ios/Runner.xcodeproj/project.pbxprojsrc/mobile/lib/main.dartsrc/mobile/lib/src/app/auth/bloc/auth_bloc.dartsrc/mobile/lib/src/app/auth/lifecycle/app_resume_session_guard.dartsrc/mobile/lib/src/app/auth/services/auth_helper.dartsrc/mobile/local_packages/google_mlkit_translation_stub/README.mdsrc/mobile/local_packages/google_mlkit_translation_stub/lib/google_mlkit_translation.dartsrc/mobile/local_packages/google_mlkit_translation_stub/pubspec.yamlsrc/mobile/pubspec_overrides.yamlsrc/mobile/test/app/auth/bloc/auth_bloc_test.dartsrc/mobile/test/app/auth/integration/session_expiry_flow_test.dartsrc/mobile/test/app/auth/lifecycle/app_resume_session_test.dartsrc/mobile/test/app/auth/repository/base_repository_auth_test.dartsrc/mobile/test/app/auth/repository/secure_storage_repository_test.dartsrc/mobile/test/app/auth/repository/user_preferences_auth_test.dartsrc/mobile/test/app/auth/services/auth_helper_test.dartsrc/mobile/test/app/auth/services/auth_token_storage_test.dart
💤 Files with no reviewable changes (1)
- src/mobile/ios/Runner.xcodeproj/project.pbxproj
There was a problem hiding this comment.
Pull request overview
This PR adds mobile-side coverage and diagnostics to investigate unexpected session expiry/logout behavior, including new tests around token storage/refresh flows, lifecycle resume handling, and repository 401/session-expiry escalation. It also introduces a temporary iOS simulator build workaround by overriding google_mlkit_translation with a local stub.
Changes:
- Added extensive auth-focused tests (token storage, refresh behavior, bloc/session-expiry flows, repository 401 handling, user-preferences auth handling).
- Added temporary auth refresh diagnostics + a debug-only “force token expired” hook, and refactored refresh calls behind a
TokenRefresherabstraction. - Introduced an app-resume guard + iOS simulator build workaround (Xcode project tweak + MLKit translation stub override).
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/mobile/lib/src/app/auth/services/auth_helper.dart | Adds refresh diagnostics and a debug hook for forcing “expired” behavior. |
| src/mobile/lib/src/app/auth/lifecycle/app_resume_session_guard.dart | New guard that triggers session-expiry on resume if refresh returns null. |
| src/mobile/lib/src/app/auth/bloc/auth_bloc.dart | Uses TokenRefresher abstraction for startup refresh path. |
| src/mobile/lib/main.dart | Delegates resume token-expiry handling to the new guard. |
| src/mobile/test/app/auth/services/auth_token_storage_test.dart | New tests for token sanitization and persistence (token + userId). |
| src/mobile/test/app/auth/services/auth_helper_test.dart | New tests for refresh outcomes (failure/timeout/network/malformed/valid). |
| src/mobile/test/app/auth/repository/secure_storage_repository_test.dart | New tests for secure storage save/get/delete behavior. |
| src/mobile/test/app/auth/repository/base_repository_auth_test.dart | New tests for repository 401 session-expiry escalation + header token persistence. |
| src/mobile/test/app/auth/repository/user_preferences_auth_test.dart | New tests for preferences repo behavior with refresh + 401/cached fallback. |
| src/mobile/test/app/auth/lifecycle/app_resume_session_test.dart | New tests for lifecycle resume session-expiry behavior and deduping. |
| src/mobile/test/app/auth/integration/session_expiry_flow_test.dart | New integration test wiring repository → global manager → bloc cleanup. |
| src/mobile/test/app/auth/bloc/auth_bloc_test.dart | New tests for bloc startup/session-expiry/logout flows. |
| src/mobile/pubspec_overrides.yaml | Adds local dependency override to use MLKit translation stub. |
| src/mobile/pubspec.lock | Resolves MLKit translation to local path stub; adjusts dependency graph. |
| src/mobile/local_packages/google_mlkit_translation_stub/* | Adds stub package to replace MLKit translation on simulator builds. |
| src/mobile/ios/Runner.xcodeproj/project.pbxproj | Removes SUPPORTED_PLATFORMS = iphoneos; to allow simulator builds. |
| src/mobile/.gitignore | Ignores additional local docs files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Future<void> handleAuthState( | ||
| AuthState currentState, { | ||
| required bool isMounted, | ||
| }) async { | ||
| if (currentState is! AuthLoaded) return; | ||
|
|
||
| final token = await _tokenRefresher.refreshTokenIfNeeded(); | ||
| if (token == null && isMounted) { | ||
| _sessionExpiryNotifier.notifySessionExpired(); |
| static bool _shouldTreatTokenAsExpired(String token) { | ||
| final forceExpiry = | ||
| (dotenv.env['FORCE_AUTH_TOKEN_EXPIRED'] ?? '').toLowerCase() == 'true'; | ||
| if (forceExpiry) { | ||
| _logger.warning( | ||
| 'FORCE_AUTH_TOKEN_EXPIRED is enabled - treating the stored token as expired for debugging'); | ||
| return true; | ||
| } | ||
|
|
||
| return JwtDecoder.isExpired(token); | ||
| } |
| final responseMessage = responseBody == null | ||
| ? response.body | ||
| : (responseBody['message'] ?? | ||
| responseBody['errors']?['message'] ?? | ||
| responseBody['error'] ?? | ||
| response.body) | ||
| .toString(); | ||
| _logger.info('Silent refresh response message: $responseMessage'); | ||
|
|
| server.listen((request) async { | ||
| expect(request.uri.path, '/api/v2/users/token/refresh'); | ||
| await Future<void>.delayed(const Duration(seconds: 11)); | ||
| request.response.statusCode = HttpStatus.ok; | ||
| request.response.write('{"success":true,"token":"fresh-token"}'); | ||
| await request.response.close(); | ||
| }); |
| setUpAll(() async { | ||
| Hive.init('.'); | ||
| }); |
| # Local-only simulator workaround: | ||
| # the real MLKit translation plugin does not currently build cleanly for iOS | ||
| # simulator, so we swap it with a stub package to unblock auth debugging. | ||
| dependency_overrides: | ||
| google_mlkit_translation: | ||
| path: local_packages/google_mlkit_translation_stub |
What changed
This PR adds mobile auth investigation coverage for the session-expiry issue.
It includes:
AuthHelperWhy
Users have reported being logged out unexpectedly after login. This PR narrows the branch to test-only investigation coverage so we can verify the main mobile-side session persistence and expiry paths without mixing in simulator-only workarounds or production auth changes.
What I validated
flutter test test/app/authNotes