Skip to content

[Mobile] Investigate session expiry and add auth diagnostics#3651

Merged
Baalmart merged 16 commits into
stagingfrom
mobile-session-persistence-fix
Jun 17, 2026
Merged

[Mobile] Investigate session expiry and add auth diagnostics#3651
Baalmart merged 16 commits into
stagingfrom
mobile-session-persistence-fix

Conversation

@Hassan-KreateStudio

@Hassan-KreateStudio Hassan-KreateStudio commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

What changed

This PR adds mobile auth investigation coverage for the session-expiry issue.

It includes:

  • auth-focused tests for AuthHelper
  • auth bloc tests for startup, session-expiry, and logout behavior
  • repository tests for authenticated 401 handling and refreshed token storage
  • user-preferences auth handling tests
  • integration coverage for the session-expiry flow

Why

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/auth

Notes

  • this PR is now test-only
  • simulator-only workaround changes were removed from the branch
  • no production auth files are changed in this PR

Kulubya Hassan added 14 commits June 10, 2026 07:59
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.
@coderabbitai

coderabbitai Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds seven new test files under src/mobile/test/app/auth/ covering the mobile authentication layer: token sanitization and storage, secure storage CRUD, AuthHelper refresh edge cases, BaseRepository authenticated response handling, UserPreferencesImpl auth fallback, AuthBloc state transitions, and an integration-level session-expiry flow.

Changes

Mobile Auth Test Suite

Layer / File(s) Summary
Token storage sanitization, persistence, and secure storage CRUD tests
src/mobile/test/app/auth/services/auth_token_storage_test.dart, src/mobile/test/app/auth/repository/secure_storage_repository_test.dart
AuthTokenStorage tests assert sanitizeToken strips scheme prefixes, saveAuthToken extracts sub from JWT and persists token + userId, and saveTokenFromHeaders handles the x-access-token header. SecureStorageRepository tests cover save/retrieve and delete/null cycles.
AuthHelper.refreshTokenIfNeeded edge case tests
src/mobile/test/app/auth/services/auth_helper_test.dart
Five tests cover refreshTokenIfNeeded returning null on HTTP 500, request timeout, unreachable host, and malformed token data; and returning the stored token when it has not yet expired.
BaseRepository authenticated response handling tests
src/mobile/test/app/auth/repository/base_repository_auth_test.dart
Three scenarios: session-expired 401 throws SessionExpiredException and fires the notifier once; unrelated 401 throws a non-session error without notifying; successful response with x-access-token header persists the refreshed token and userId.
UserPreferencesImpl token-refresh and 401 fallback tests
src/mobile/test/app/auth/repository/user_preferences_auth_test.dart
Validates expired-token path triggers refresh then authenticated preferences fetch with Hive caching, and a 401 preferences response falls back to previously cached Hive data.
AuthBloc state transition tests
src/mobile/test/app/auth/bloc/auth_bloc_test.dart
Covers AppStarted failure (AuthLoadingSessionExpiredStateGuestUser, clears authToken), AppStarted success (AuthLoadingAuthLoaded), SessionExpired event, and LogoutUser event — both clearing authToken and userId.
Integration session-expiry flow test
src/mobile/test/app/auth/integration/session_expiry_flow_test.dart
Seeds secure storage, wires AuthBloc with fake repositories and a real BaseRepository token refresher, mocks an HTTP GET returning a 401, and asserts SessionExpiredStateGuestUser with both stored values cleared.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • airqo-platform/AirQo-frontend#3477 — The AuthBloc session-expiry and token-cleanup state transitions tested in this PR directly validate the behavior introduced in that PR.

Suggested labels

mobile-app

Suggested reviewers

  • Baalmart

Poem

🧪 Tokens expire, servers respond with dread,
But now there are tests to catch what lies ahead.
SessionExpiredStateGuestUser it flows,
Storage gets cleared wherever the auth path goes.
No more ghost sessions haunting the app at night —
The test suite stands watch, and the green checks shine bright. ✅

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title references session expiry investigation and auth diagnostics, which aligns with the PR's primary objective of investigating session expiry issues and adding test coverage for authentication flows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mobile-session-persistence-fix

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/mobile/test/app/auth/services/auth_helper_test.dart (1)

53-167: ⚡ Quick win

Add 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 win

JWT helper duplication across test files.

This jwtToken helper 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 (as jwtToken)

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 jwtToken while the others use _jwtToken. Since it's defined inside main(), 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 unused meta dependency 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

📥 Commits

Reviewing files that changed from the base of the PR and between 62f8c1f and 45c1055.

⛔ Files ignored due to path filters (1)
  • src/mobile/pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (18)
  • src/mobile/.gitignore
  • src/mobile/ios/Runner.xcodeproj/project.pbxproj
  • src/mobile/lib/main.dart
  • src/mobile/lib/src/app/auth/bloc/auth_bloc.dart
  • src/mobile/lib/src/app/auth/lifecycle/app_resume_session_guard.dart
  • src/mobile/lib/src/app/auth/services/auth_helper.dart
  • src/mobile/local_packages/google_mlkit_translation_stub/README.md
  • src/mobile/local_packages/google_mlkit_translation_stub/lib/google_mlkit_translation.dart
  • src/mobile/local_packages/google_mlkit_translation_stub/pubspec.yaml
  • src/mobile/pubspec_overrides.yaml
  • src/mobile/test/app/auth/bloc/auth_bloc_test.dart
  • src/mobile/test/app/auth/integration/session_expiry_flow_test.dart
  • src/mobile/test/app/auth/lifecycle/app_resume_session_test.dart
  • src/mobile/test/app/auth/repository/base_repository_auth_test.dart
  • src/mobile/test/app/auth/repository/secure_storage_repository_test.dart
  • src/mobile/test/app/auth/repository/user_preferences_auth_test.dart
  • src/mobile/test/app/auth/services/auth_helper_test.dart
  • src/mobile/test/app/auth/services/auth_token_storage_test.dart
💤 Files with no reviewable changes (1)
  • src/mobile/ios/Runner.xcodeproj/project.pbxproj

Comment thread src/mobile/lib/src/app/auth/services/auth_helper.dart Outdated
Comment thread src/mobile/lib/src/app/auth/services/auth_helper.dart Outdated
Comment thread src/mobile/test/app/auth/lifecycle/app_resume_session_test.dart Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 TokenRefresher abstraction.
  • 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.

Comment on lines +17 to +25
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();
Comment on lines +239 to +249
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);
}
Comment on lines +202 to +210
final responseMessage = responseBody == null
? response.body
: (responseBody['message'] ??
responseBody['errors']?['message'] ??
responseBody['error'] ??
response.body)
.toString();
_logger.info('Silent refresh response message: $responseMessage');

Comment on lines +94 to +100
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();
});
Comment on lines +31 to +33
setUpAll(() async {
Hive.init('.');
});
Comment thread src/mobile/pubspec_overrides.yaml Outdated
Comment on lines +1 to +6
# 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
@Baalmart
Baalmart merged commit 51822fc into staging Jun 17, 2026
25 of 27 checks passed
@Baalmart
Baalmart deleted the mobile-session-persistence-fix branch June 17, 2026 17:22
@Baalmart Baalmart mentioned this pull request Jun 17, 2026
13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants