Skip to content

Conversation

@M4dhav
Copy link
Contributor

@M4dhav M4dhav commented Aug 12, 2025

Description

This PR adds tests for ~50% of key app functionalities that can be efficiently tested using a combination of widget and unit tests. It also modifies controllers and adds constructors for them so that they can be mocked for said tests. Additionally, a workflow to automatically run said tests on all PRs is also added.

Fixes #508

Type of change

Please delete options that are not relevant.

  • New feature (non-breaking change which adds functionality)

How Has This Been Tested?

All Tests were run using flutter test and all passed.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Maintainer Checklist

Summary by CodeRabbit

  • New Features

    • None
  • Bug Fixes

    • Improved reliability when loading user ratings from data sources.
  • Refactor

    • Updated multiple controllers to use constructor-based dependency injection for better testability and maintainability.
  • Tests

    • Added comprehensive unit/widget tests for authentication, email change, profile editing, room creation, story exploration, and media playback.
    • Removed obsolete sample counter test.
  • Chores

    • Added CI workflow to automatically run tests on pull requests.
    • Enabled test execution in the Android store deployment pipeline.
    • Cleaned up obsolete build metadata.

@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

Walkthrough

Adds CI to run Flutter tests on PR open and enables tests in Android deploy workflow. Refactors several controllers to use constructor-based dependency injection. Adjusts ResonateUser JSON deserialization for userRating. Removes a build id. Adds comprehensive unit/widget tests with Mockito-generated mocks; deletes the default Flutter counter test.

Changes

Cohort / File(s) Summary
CI Workflows
.github/workflows/run_tests.yml, .github/workflows/store_deploy_android.yml
New PR test workflow runs Flutter tests with Java 17 and Flutter 3.32.5; Android deploy workflow now actively runs flutter test before build.
Controllers: DI Refactors
lib/controllers/auth_state_controller.dart, lib/controllers/change_email_controller.dart, lib/controllers/create_room_controller.dart, lib/controllers/edit_profile_controller.dart, lib/controllers/explore_story_controller.dart
Moved service resolution from field initializers/onInit to constructors with optional injected deps; removed AuthStateController.getAppwriteToken.
Models: JSON Deserialization
lib/models/resonate_user.freezed.dart, lib/models/resonate_user.g.dart
Applied @JsonKey(fromJson: _toDouble) to userRating and routed fromJson through _toDouble; toJson unchanged.
Android Build Meta
android/build/.last_build_id
Cleared file contents (build id removed).
Tests: Controllers
test/controllers/auth_state_controller_test.dart, test/controllers/change_email_controller_test.dart, test/controllers/chapter_player_controller_test.dart, test/controllers/create_room_controller_test.dart, test/controllers/edit_profile_controller_test.dart, test/controllers/explore_story_controller_test.dart
Added unit/widget tests covering auth state, email change, chapter player, room creation, profile editing, and story exploration flows.
Tests: Generated Mocks
test/controllers/*_test.mocks.dart
Added Mockito-generated mocks for Appwrite, FirebaseMessaging, ThemeController, etc., to support tests.
Tests: Cleanup
test/widget_test.dart
Removed default counter smoke test.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Dev as Developer
  participant GH as GitHub
  participant WF as RUN_SOFTWARE_TESTS
  participant R as GitHub Runner
  Dev->>GH: Open Pull Request
  GH-->>WF: trigger (pull_request_target: opened)
  WF->>R: Start job (ubuntu-latest)
  R->>R: actions/checkout@v4
  R->>R: actions/setup-java@v4 (Zulu JDK 17.0.12)
  R->>R: subosito/flutter-action@v2 (Flutter 3.32.5)
  R->>R: flutter pub get
  R->>R: flutter test
  R-->>GH: Report test results/status
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Add CI workflow to automatically run tests on PR open (#508)
Introduce/ensure automated software tests exist in the repo (#508)

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
Constructor-based DI and method removal in AuthStateController (lib/controllers/auth_state_controller.dart) Refactors production auth logic and removes getAppwriteToken, not required solely to add CI/tests per #508.
Apply @jsonkey(fromJson: _toDouble) to userRating (lib/models/resonate_user.freezed.dart, lib/models/resonate_user.g.dart) Alters model deserialization behavior; unrelated to adding automated tests/CI.
DI refactor in ChangeEmailController (lib/controllers/change_email_controller.dart) Production controller refactor beyond the scope of introducing CI test workflow.
DI refactor in CreateRoomController (lib/controllers/create_room_controller.dart) Production controller change not necessary for meeting #508.

Possibly related PRs

Poem

A carrot of code in the CI burrow,
I thump “flutter test!” with morning furrow.
Mocks hop in, controllers align,
Ratings convert—numbers refine.
PRs pop up, I wiggle with glee—
Green checks bloom like clover for me. 🥕🐇

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions
Copy link
Contributor

🎉 Welcome @M4dhav!
Thank you for your pull request! Our team will review it soon. 🔍

  • Please ensure your PR follows the contribution guidelines. ✅
  • All automated tests should pass before merging. 🔄
  • If this PR fixes an issue, link it in the description. 🔗

We appreciate your contribution! 🚀

@M4dhav M4dhav self-assigned this Aug 12, 2025
@M4dhav M4dhav added enhancement New feature or request gsoc Part of Google Summer of Code season labels Aug 12, 2025
@M4dhav M4dhav linked an issue Aug 12, 2025 that may be closed by this pull request
@M4dhav
Copy link
Contributor Author

M4dhav commented Aug 12, 2025

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@M4dhav M4dhav changed the title feat: tests written for key functionalities with ~50% code coverage Partial Implementation for Automated Software Tests Aug 12, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🔭 Outside diff range comments (17)
lib/controllers/create_room_controller.dart (1)

105-107: Regex validation bug: contains() with anchored pattern yields false positives.

Using String.contains with ^...$ allows substring matches, incorrectly validating inputs like "Football$". Use RegExp.hasMatch against the entire string instead.

Apply this fix:

-    if (!hashtag.contains(RegExp(r'^[a-zA-Z0-9_]+$'))) {
+    if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(hashtag)) {
lib/controllers/explore_story_controller.dart (3)

187-192: Bug: bucketId parameter ignored when creating file.

storage.createFile always uses storyBucketId, ignoring the bucketId argument. This is error-prone and breaks the function’s contract.

-      await storage.createFile(
-        bucketId: storyBucketId,
+      await storage.createFile(
+        bucketId: bucketId,
         fileId: fileId,
         file: InputFile.fromPath(path: filePath),
       );

544-549: Broken color-to-hex conversion (nonexistent properties a/r/g/b).

Flutter’s Color exposes alpha, red, green, blue (ints), not a/r/g/b, and no 0..1 scaling. This won’t compile or will be incorrect.

-  String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}'
-      '${(a * 255).toInt().toRadixString(16).padLeft(2, '0')}'
-      '${r.toInt().toRadixString(16).padLeft(2, '0')}'
-      '${g.toInt().toRadixString(16).padLeft(2, '0')}'
-      '${b.toInt().toRadixString(16).padLeft(2, '0')}';
+  String toHex({bool leadingHashSign = true}) => '${leadingHashSign ? '#' : ''}'
+      '${alpha.toRadixString(16).padLeft(2, '0')}'
+      '${red.toRadixString(16).padLeft(2, '0')}'
+      '${green.toRadixString(16).padLeft(2, '0')}'
+      '${blue.toRadixString(16).padLeft(2, '0')}';

419-456: Potential crash when unliking without a like document.

userLikeDocuments.first will throw if the query returns empty (e.g., due to race conditions). Guard for emptiness and only update likes after a successful delete; also no-op if nothing to delete.

-    try {
-      await databases.deleteDocument(
-        databaseId: storyDatabaseId,
-        collectionId: likeCollectionId,
-        documentId: userLikeDocuments.first.$id,
-      );
-    } on AppwriteException catch (e) {
-      log('Failed to Unlike i.e delete Like Document: ${e.message}');
-    }
-
-    try {
-      await databases.updateDocument(
-          databaseId: storyDatabaseId,
-          collectionId: storyCollectionId,
-          documentId: story.storyId,
-          data: {"likes": story.likesCount.value - 1});
-    } on AppwriteException catch (e) {
-      log("Failed to reduce one story like: ${e.message}");
-    }
+    if (userLikeDocuments.isEmpty) {
+      log('No like document found to delete for storyId=${story.storyId}');
+      return;
+    }
+    try {
+      await databases.deleteDocument(
+        databaseId: storyDatabaseId,
+        collectionId: likeCollectionId,
+        documentId: userLikeDocuments.first.$id,
+      );
+      // Only decrement likes after successful delete
+      try {
+        await databases.updateDocument(
+            databaseId: storyDatabaseId,
+            collectionId: storyCollectionId,
+            documentId: story.storyId,
+            data: {"likes": story.likesCount.value - 1});
+      } on AppwriteException catch (e) {
+        log("Failed to reduce one story like: ${e.message}");
+      }
+    } on AppwriteException catch (e) {
+      log('Failed to Unlike i.e delete Like Document: ${e.message}');
+    }
lib/controllers/change_email_controller.dart (3)

53-75: Consider handling partial-update inconsistency between the two collections

If the first update succeeds (users) and the second (username-email) fails, data becomes inconsistent. Appwrite doesn’t provide cross-document transactions, but you can:

  • Update the username-email doc first, then the users doc, or
  • Add compensating logic to revert the first update if the second fails, or
  • Log and surface a specific error to instruct the user to retry.

At minimum, reorder the operations to update the secondary mapping first, which is safer to roll forward/back.


94-97: Surface errors to the user on unexpected exceptions

The generic catch logs and returns false but provides no user feedback. Add a fallback snackbar + accessibility announcement for unexpected failures.

Apply this diff:

     } catch (e) {
       log(e.toString());
-      return false;
+      customSnackbar(
+        AppLocalizations.of(context)!.tryAgain,
+        e.toString(),
+        LogType.error,
+      );
+      SemanticsService.announce(
+        e.toString(),
+        TextDirection.ltr,
+      );
+      return false;
     }

143-200: Refactor nested futures to async/await and guarantee loading reset

The nested .then chains are hard to read and risk leaving isLoading = true on early returns/errors. Use async/await and a finally to ensure deterministic loading-state handling.

Apply this diff:

-  Future<void> changeEmail(BuildContext context) async {
-    if (changeEmailFormKey.currentState!.validate()) {
-      isLoading.value = true;
-
-      isEmailAvailable(emailController.text).then((status) {
-        if (status) {
-          changeEmailInAuth(
-                  emailController.text, passwordController.text, context)
-              .then((status) {
-            if (status) {
-              changeEmailInDatabases(emailController.text, context)
-                  .then((status) {
-                isLoading.value = false;
-
-                if (status) {
-                  customSnackbar(
-                    AppLocalizations.of(context)!.emailChanged,
-                    AppLocalizations.of(context)!.emailChangeSuccess,
-                    LogType.success,
-                  );
-                  SemanticsService.announce(
-                    AppLocalizations.of(context)!.emailChangeSuccess,
-                    TextDirection.ltr,
-                  );
-                } else {
-                  customSnackbar(
-                    AppLocalizations.of(context)!.failed,
-                    AppLocalizations.of(context)!.emailChangeFailed,
-                    LogType.error,
-                  );
-                  SemanticsService.announce(
-                    AppLocalizations.of(context)!.emailChangeFailed,
-                    TextDirection.ltr,
-                  );
-                }
-              });
-            } else {
-              isLoading.value = false;
-            }
-          });
-        } else {
-          customSnackbar(
-            AppLocalizations.of(context)!.oops,
-            AppLocalizations.of(context)!.emailExists,
-            LogType.error,
-          );
-          SemanticsService.announce(
-            AppLocalizations.of(context)!.emailExists,
-            TextDirection.ltr,
-          );
-          isLoading.value = false;
-        }
-      });
-    }
-  }
+  Future<void> changeEmail(BuildContext context) async {
+    if (!changeEmailFormKey.currentState!.validate()) return;
+    isLoading.value = true;
+    try {
+      final available = await isEmailAvailable(emailController.text);
+      if (!available) {
+        customSnackbar(
+          AppLocalizations.of(context)!.oops,
+          AppLocalizations.of(context)!.emailExists,
+          LogType.error,
+        );
+        SemanticsService.announce(
+          AppLocalizations.of(context)!.emailExists,
+          TextDirection.ltr,
+        );
+        return;
+      }
+
+      final authOk = await changeEmailInAuth(
+        emailController.text,
+        passwordController.text,
+        context,
+      );
+      if (!authOk) return;
+
+      final dbOk = await changeEmailInDatabases(
+        emailController.text,
+        context,
+      );
+      if (dbOk) {
+        customSnackbar(
+          AppLocalizations.of(context)!.emailChanged,
+          AppLocalizations.of(context)!.emailChangeSuccess,
+          LogType.success,
+        );
+        SemanticsService.announce(
+          AppLocalizations.of(context)!.emailChangeSuccess,
+          TextDirection.ltr,
+        );
+      } else {
+        customSnackbar(
+          AppLocalizations.of(context)!.failed,
+          AppLocalizations.of(context)!.emailChangeFailed,
+          LogType.error,
+        );
+        SemanticsService.announce(
+          AppLocalizations.of(context)!.emailChangeFailed,
+          TextDirection.ltr,
+        );
+      }
+    } finally {
+      isLoading.value = false;
+    }
+  }
lib/controllers/edit_profile_controller.dart (5)

95-99: Guard Get.back() to avoid popping when no dialog/route is open

Calling Get.back() unconditionally can throw if no route can pop (especially in tests). Guard with Get.isDialogOpen.

Apply this diff in both finally blocks:

-      // Close the loading dialog
-      Get.back();
+      // Close the loading dialog if open
+      if (Get.isDialogOpen == true) {
+        Get.back();
+      }

Also applies to: 131-134


141-153: Avoid null Get.context access inside _cropImage

Get.context may be null; dereferencing it will throw. Compute a safe title with a fallback.

Apply this diff:

-        AndroidUiSettings(
-          toolbarTitle: AppLocalizations.of(Get.context!)!.cropImage,
+        AndroidUiSettings(
+          toolbarTitle: (Get.context != null
+                  ? AppLocalizations.of(Get.context!)?.cropImage
+                  : null) ??
+              'Crop Image',
           // toolbarColor: themeController.primaryColor.value,
           // statusBarColor: themeController.primaryColor.value,
           // toolbarWidgetColor: Colors.black,
           // cropFrameColor: Colors.white,
           // activeControlsWidgetColor: themeController.primaryColor.value,
         ),
         IOSUiSettings(
           minimumAspectRatio: 1.0,
-          title: AppLocalizations.of(Get.context!)!.cropImage,
+          title: (Get.context != null
+                  ? AppLocalizations.of(Get.context!)?.cropImage
+                  : null) ??
+              'Crop Image',
         ),

209-217: Null-safe deletion of previous profile image file

profileImageID may be null/empty; guard before delete to avoid runtime exceptions.

Apply this diff:

-          await storage.deleteFile(
-              bucketId: userProfileImageBucketId,
-              fileId: authStateController.profileImageID!);
+          final oldId = authStateController.profileImageID;
+          if (oldId != null && oldId.isNotEmpty) {
+            await storage.deleteFile(
+              bucketId: userProfileImageBucketId,
+              fileId: oldId,
+            );
+          }

159-171: Differentiate “not found” from other errors when checking username availability

Treating any exception as “available” can approve usernames during transient failures. Inspect the AppwriteException and only return true on 404/not-found.

Apply this diff:

   Future<bool> isUsernameAvailable(String username) async {
     try {
       await databases.getDocument(
         databaseId: userDatabaseID,
         collectionId: usernameCollectionID,
         documentId: username,
       );
       return false;
-    } catch (e) {
-      log(e.toString());
-      return true;
+    } on AppwriteException catch (e) {
+      // 404 means document not found => username available
+      if (e.code == 404) {
+        return true;
+      }
+      log(e.toString());
+      return false;
+    } catch (e) {
+      log(e.toString());
+      return false;
     }
   }

38-43: Dispose TextEditingControllers to avoid leaks

imageController, nameController, and usernameController are never disposed. Dispose them in onClose().

Apply this diff near the end of the class:

   }
 }
+
+  @override
+  void onClose() {
+    imageController.dispose();
+    nameController.dispose();
+    usernameController.dispose();
+    super.onClose();
+  }

Also applies to: 375-375

lib/controllers/auth_state_controller.dart (5)

44-46: Avoid late initialization errors for rating fields.

ratingTotal and ratingCount are marked late but are only assigned when isUserProfileComplete == true. Accessing them elsewhere risks LateInitializationError.

Apply this diff to provide safe defaults:

-  late double ratingTotal;
-  late int ratingCount;
+  double ratingTotal = 0;
+  int ratingCount = 0;

160-162: Null/shape-safe numeric conversion for ratings.

userDataDoc.data["ratingTotal"].toDouble() will throw if the value is null or a non-numeric type (e.g., String). Use a safe cast to num before converting.

Apply this diff:

-        ratingTotal = userDataDoc.data["ratingTotal"].toDouble() ?? 5;
-        ratingCount = userDataDoc.data["ratingCount"] ?? 1;
+        ratingTotal =
+            (userDataDoc.data["ratingTotal"] as num?)?.toDouble() ?? 5.0;
+        ratingCount = (userDataDoc.data["ratingCount"] as num?)?.toInt() ?? 1;

72-86: Guard against negative index when payload doesn't match any room.

If no upcoming room name matches the payload, indexWhere returns -1, yielding a negative initialScrollOffset and potential UI issues.

Apply this diff:

     int index = upcomingRoomsController.upcomingRooms
         .indexWhere((upcomingRoom) => upcomingRoom.name == name);
 
-    upcomingRoomsController.upcomingRoomScrollController.value =
-        ScrollController(initialScrollOffset: UiSizes.height_170 * index);
+    if (index < 0) {
+      return;
+    }
+    upcomingRoomsController.upcomingRoomScrollController.value =
+        ScrollController(initialScrollOffset: UiSizes.height_170 * index);

195-232: Make token updates robust: handle null token, de-duplicate, and await updates.

  • getToken() can return null; current code force-unwraps.
  • Adds duplicates on every login.
  • updateDocument calls are not awaited, risking lost errors and early return.

Apply this refactor:

   Future<void> addRegistrationTokentoSubscribedandCreatedUpcomingRooms() async {
-    final fcmToken = await messaging.getToken();
+    final fcmToken = await messaging.getToken();
+    if (fcmToken == null || fcmToken.isEmpty) {
+      log('FCM token is null/empty. Skipping token updates.');
+      return;
+    }
 
     //subscribed Upcoming Rooms
-    List<Document> subscribedUpcomingRooms = await databases.listDocuments(
+    final List<Document> subscribedUpcomingRooms = await databases.listDocuments(
         databaseId: upcomingRoomsDatabaseId,
         collectionId: subscribedUserCollectionId,
         queries: [
           Query.equal("userID", [uid])
         ]).then((value) => value.documents);
-    for (var subscription in subscribedUpcomingRooms) {
-      List<dynamic> registrationTokens =
-          subscription.data['registrationTokens'];
-      registrationTokens.add(fcmToken!);
-      databases.updateDocument(
-          databaseId: upcomingRoomsDatabaseId,
-          collectionId: subscribedUserCollectionId,
-          documentId: subscription.$id,
-          data: {"registrationTokens": registrationTokens});
-    }
+    final List<Future<Document>> updates = [];
+    for (final subscription in subscribedUpcomingRooms) {
+      final tokens = List<String>.from(
+          (subscription.data['registrationTokens'] ?? const <String>[]));
+      if (!tokens.contains(fcmToken)) {
+        tokens.add(fcmToken);
+      }
+      updates.add(databases.updateDocument(
+          databaseId: upcomingRoomsDatabaseId,
+          collectionId: subscribedUserCollectionId,
+          documentId: subscription.$id,
+          data: {"registrationTokens": tokens}));
+    }
 
     //created Upcoming Rooms
-    List<Document> createdUpcomingRooms = await databases.listDocuments(
+    final List<Document> createdUpcomingRooms = await databases.listDocuments(
         databaseId: upcomingRoomsDatabaseId,
         collectionId: upcomingRoomsCollectionId,
         queries: [
           Query.equal("creatorUid", [uid])
         ]).then((value) => value.documents);
-    for (var upcomingRoom in createdUpcomingRooms) {
-      List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
-      creatorFcmTokens.add(fcmToken!);
-      databases.updateDocument(
-          databaseId: upcomingRoomsDatabaseId,
-          collectionId: upcomingRoomsCollectionId,
-          documentId: upcomingRoom.$id,
-          data: {"creator_fcm_tokens": creatorFcmTokens});
-    }
+    for (final upcomingRoom in createdUpcomingRooms) {
+      final tokens = List<String>.from(
+          (upcomingRoom.data['creator_fcm_tokens'] ?? const <String>[]));
+      if (!tokens.contains(fcmToken)) {
+        tokens.add(fcmToken);
+      }
+      updates.add(databases.updateDocument(
+          databaseId: upcomingRoomsDatabaseId,
+          collectionId: upcomingRoomsCollectionId,
+          documentId: upcomingRoom.$id,
+          data: {"creator_fcm_tokens": tokens}));
+    }
+    await Future.wait(updates);
   }

234-271: Symmetric fixes for token removal: handle null token, remove all occurrences, and await updates.

  • Guard against null token.
  • Use removeWhere to remove all duplicates.
  • Await updateDocument calls to ensure completion.

Apply this refactor:

   Future<void> removeRegistrationTokenFromSubscribedUpcomingRooms() async {
-    final fcmToken = await messaging.getToken();
+    final fcmToken = await messaging.getToken();
+    if (fcmToken == null || fcmToken.isEmpty) {
+      log('FCM token is null/empty. Skipping token removals.');
+      return;
+    }
 
     //subscribed Upcoming Rooms
-    List<Document> subscribedUpcomingRooms = await databases.listDocuments(
+    final List<Document> subscribedUpcomingRooms = await databases.listDocuments(
         databaseId: upcomingRoomsDatabaseId,
         collectionId: subscribedUserCollectionId,
         queries: [
           Query.equal("userID", [uid])
         ]).then((value) => value.documents);
-    for (var subscription in subscribedUpcomingRooms) {
-      List<dynamic> registrationTokens =
-          subscription.data['registrationTokens'];
-      registrationTokens.remove(fcmToken!);
-      databases.updateDocument(
-          databaseId: upcomingRoomsDatabaseId,
-          collectionId: subscribedUserCollectionId,
-          documentId: subscription.$id,
-          data: {"registrationTokens": registrationTokens});
-    }
+    final List<Future<Document>> updates = [];
+    for (final subscription in subscribedUpcomingRooms) {
+      final tokens = List<String>.from(
+          (subscription.data['registrationTokens'] ?? const <String>[]));
+      tokens.removeWhere((t) => t == fcmToken);
+      updates.add(databases.updateDocument(
+          databaseId: upcomingRoomsDatabaseId,
+          collectionId: subscribedUserCollectionId,
+          documentId: subscription.$id,
+          data: {"registrationTokens": tokens}));
+    }
 
     //created Upcoming Rooms
-    List<Document> createdUpcomingRooms = await databases.listDocuments(
+    final List<Document> createdUpcomingRooms = await databases.listDocuments(
         databaseId: upcomingRoomsDatabaseId,
         collectionId: upcomingRoomsCollectionId,
         queries: [
           Query.equal("creatorUid", [uid])
         ]).then((value) => value.documents);
-    for (var upcomingRoom in createdUpcomingRooms) {
-      List<dynamic> creatorFcmTokens = upcomingRoom.data['creator_fcm_tokens'];
-      creatorFcmTokens.remove(fcmToken!);
-      databases.updateDocument(
-          databaseId: upcomingRoomsDatabaseId,
-          collectionId: upcomingRoomsCollectionId,
-          documentId: upcomingRoom.$id,
-          data: {"creator_fcm_tokens": creatorFcmTokens});
-    }
+    for (final upcomingRoom in createdUpcomingRooms) {
+      final tokens = List<String>.from(
+          (upcomingRoom.data['creator_fcm_tokens'] ?? const <String>[]));
+      tokens.removeWhere((t) => t == fcmToken);
+      updates.add(databases.updateDocument(
+          databaseId: upcomingRoomsDatabaseId,
+          collectionId: upcomingRoomsCollectionId,
+          documentId: upcomingRoom.$id,
+          data: {"creator_fcm_tokens": tokens}));
+    }
+    await Future.wait(updates);
   }
🧹 Nitpick comments (22)
.github/workflows/run_tests.yml (2)

36-39: Speed up CI by enabling Flutter cache.

Enable caching in subosito/flutter-action to reuse SDK and pub cache across runs.

       - name: Setup Flutter
         uses: subosito/flutter-action@v2
         with:
           flutter-version: ${{ env.FLUTTER_VERSION }}
+          cache: true

10-45: Fix YAML lint issues (trailing spaces and missing EOF newline).

YAMLlint flagged trailing spaces (Lines 13, 16, 34) and missing newline at EOF (Line 45). Clean these to avoid CI style checks failing.

  • Remove trailing spaces on the flagged lines.
  • Ensure the file ends with a single newline.
test/controllers/chapter_player_controller_test.dart (2)

25-28: Assert non-null audioPlayer before dereference for clearer failures.

Add an explicit non-null check and use a non-null assertion to make the intent and failures crisper.

-    expect(chapterPlayerController.audioPlayer, isA<AudioPlayer>());
-    expect(chapterPlayerController.audioPlayer?.releaseMode, ReleaseMode.stop);
+    expect(chapterPlayerController.audioPlayer, isA<AudioPlayer>());
+    expect(chapterPlayerController.audioPlayer, isNotNull);
+    expect(chapterPlayerController.audioPlayer!.releaseMode, ReleaseMode.stop);

31-42: Consider faking/mocking AudioPlayer to avoid platform-channel dependencies.

Using the real AudioPlayer in widget tests can sometimes trigger platform-channel calls. If flakiness appears, replace with a simple fake or mock that satisfies the controller’s needs.

If needed, I can provide a minimal FakeAudioPlayer implementation compatible with the controller API.

lib/controllers/create_room_controller.dart (3)

65-65: Guard Get.back() to avoid unintended route pops.

If no dialog is open, Get.back() may pop a route unexpectedly. Guard it.

-      Get.back();
+      if (Get.isDialogOpen ?? false) {
+        Get.back();
+      }

88-88: Same here: guard Get.back() in error path.

Avoid popping routes when no dialog is present.

-      Get.back();
+      if (Get.isDialogOpen ?? false) {
+        Get.back();
+      }

35-41: Minor: avoid "null" in error messages for invalid/null tags.

When tag is null, error displays "... null". Prefer empty string.

-      return '${AppLocalizations.of(context)!.invalidTags} $tag'; // Return an error message for invalid tags
+      return '${AppLocalizations.of(context)!.invalidTags} ${tag ?? ''}'; // Return an error message for invalid tags
test/controllers/create_room_controller_test.dart (1)

6-6: Prefer stable Get import path.

Importing internal package paths is brittle. Use the public export.

-import 'package:get/get_navigation/src/root/get_material_app.dart';
+import 'package:get/get.dart';
test/controllers/edit_profile_controller_test.dart (2)

55-55: Redundant onInit call.

onInit() is already invoked in setUp(). Calling it again in the test is unnecessary and can lead to double-initialization side effects.

-  test('check onInit', () {
-    editProfileController.onInit();
+  test('check onInit', () {

24-52: Consider stubbing before onInit if it ever reads from databases.

Currently the databases.getDocument stub is set after onInit in setUp. If in future onInit starts hitting databases, tests may become flaky. Prefer registering stubs before invoking onInit.

lib/controllers/explore_story_controller.dart (3)

333-347: Optional: delete likes concurrently for better performance.

Deleting each like sequentially can be slow for stories with many likes. Batch with Future.wait.

-    for (Document like in storyLikeDocuments) {
-      await databases.deleteDocument(
-          databaseId: storyDatabaseId,
-          collectionId: likeCollectionId,
-          documentId: like.$id);
-    }
+    await Future.wait(storyLikeDocuments.map((like) {
+      return databases.deleteDocument(
+        databaseId: storyDatabaseId,
+        collectionId: likeCollectionId,
+        documentId: like.$id,
+      );
+    }));

24-24: Typo in property name: openedCategotyStories.

Consider renaming to openedCategoryStories for clarity. If used broadly, plan a safe rename across the codebase.


228-233: Typo in parameter name: desciption.

Use description for readability and consistency. If public API, coordinate a project-wide rename to avoid breakage.

Also applies to: 264-264

lib/controllers/change_email_controller.dart (1)

110-136: Handle unanticipated Appwrite errors in changeEmailInAuth

Only two error types are handled. For other AppwriteException types, users receive no feedback. Add a default branch to show a generic error.

Apply this diff:

       } else if (e.type == generalArgumentInvalid) {
         customSnackbar(
           AppLocalizations.of(context)!.tryAgain,
           AppLocalizations.of(context)!.passwordShort,
           LogType.error,
         );
         SemanticsService.announce(
           AppLocalizations.of(context)!.passwordShort,
           TextDirection.ltr,
         );
-      }
+      } else {
+        customSnackbar(
+          AppLocalizations.of(context)!.tryAgain,
+          e.toString(),
+          LogType.error,
+        );
+        SemanticsService.announce(
+          e.toString(),
+          TextDirection.ltr,
+        );
+      }
test/controllers/edit_profile_controller_test.mocks.dart (1)

13-17: Mockito-generated mocks use implementation imports; pin deps and regenerate on upgrades

The file imports package:appwrite/src/* and package:mockito/src/* (implementation imports). These can break on minor upgrades. Ensure:

  • appwrite and mockito versions are pinned in pubspec.yaml.
  • You re-run build_runner to regenerate mocks after dependency upgrades.

If the repo prefers not to commit generated code, consider generating in CI. Otherwise, keep this file and regenerate as needed.

Also applies to: 25-26

test/controllers/change_email_controller_test.dart (2)

153-196: Verify both database updates in the end-to-end changeEmail test

The production flow updates both the users and username-email documents. You currently verify only the latter here. Add a verify for the users doc to increase assertion strength (you already verify both in the dedicated changeEmailInDatabases test).

Apply this diff after verifying the username document:

     verify(mockDatabases.updateDocument(
         databaseId: userDatabaseID,
         collectionId: usernameCollectionID,
         documentId: 'TestUser',
         data: {
           'email': 'test2@test.com',
         })).called(1);
+    verify(mockDatabases.updateDocument(
+        databaseId: userDatabaseID,
+        collectionId: usersCollectionID,
+        documentId: '123',
+        data: {
+          'email': 'test2@test.com',
+        })).called(1);

106-151: Add negative-path tests (email exists, invalid password)

To fully exercise the controller:

  • Email already exists -> expect snackbar/announcement and no account.updateEmail call.
  • Invalid credentials (userInvalidCredentials) -> expect proper error snackbar and no DB update.
  • Short/invalid password (generalArgumentInvalid) -> same.

I can help scaffold these tests with proper stubs if you want.

test/controllers/explore_story_controller_test.dart (2)

174-191: Await the recommendation fetch and simplify loading assertions

With immediate futures, await the call and assert final state. If you still want to assert the transient loading flag, expose/await an internal future or use FakeAsync, but avoiding real sleeps is preferred.

Apply this diff:

-  test('test fetchStoryRecommendation', () async {
-    exploreStoryController.fetchStoryRecommendation();
-    expect(exploreStoryController.isLoadingRecommendedStories.value, true);
-    await Future.delayed(Duration(seconds: 3));
-    expect(exploreStoryController.isLoadingRecommendedStories.value, false);
+  test('test fetchStoryRecommendation', () async {
+    await exploreStoryController.fetchStoryRecommendation();
+    expect(exploreStoryController.isLoadingRecommendedStories.value, false);
     expect(exploreStoryController.recommendedStories.length, 2);
     for (int i = 0; i < exploreStoryController.recommendedStories.length; i++) {
       expect(mockStoriesList[i].storyId,
           exploreStoryController.recommendedStories[i].storyId);
       expect(mockStoriesList[i].title,
           exploreStoryController.recommendedStories[i].title);
       expect(mockStoriesList[i].description,
           exploreStoryController.recommendedStories[i].description);
       expect(mockStoriesList[i].userIsCreator,
           exploreStoryController.recommendedStories[i].userIsCreator);
       //Do not need to check everything as that is handled by the test above
     }
   });

127-145: Reduce brittleness of query matchers in stubs

Matching the full queries list ([Query.limit(10)] and specific equal queries) can make tests brittle if limits change. Consider using anyNamed('queries') with additional verify() assertions on the queries content, or custom argThat matchers to assert presence of expected filters without coupling to exact lists.

lib/controllers/edit_profile_controller.dart (1)

244-256: Also delete the old profile image file when removing image

Currently you blank the DB URL but leave the old file in storage, creating an orphan. Delete the file if present.

Apply this diff:

       if (removeImage) {
         imageController.text = "";
 
         // Update user profile picture URL in Database
         await databases.updateDocument(
           databaseId: userDatabaseID,
           collectionId: usersCollectionID,
           documentId: authStateController.uid!,
           data: {
             "profileImageUrl": imageController.text,
           },
         );
+        try {
+          final oldId = authStateController.profileImageID;
+          if (oldId != null && oldId.isNotEmpty) {
+            await storage.deleteFile(
+              bucketId: userProfileImageBucketId,
+              fileId: oldId,
+            );
+          }
+        } catch (e) {
+          log(e.toString());
+        }
       }
test/controllers/auth_state_controller_test.dart (1)

36-39: Swap $databaseId and $collectionId to match actual resources.

For subscribed-user documents, $collectionId should be subscribedUserCollectionId and $databaseId should be upcomingRoomsDatabaseId. They are currently inverted, which reduces test realism and could mislead future maintenance.

Apply this diff for both documents:

-      $collectionId: upcomingRoomsCollectionId,
-      $databaseId: subscribedUserCollectionId,
+      $collectionId: subscribedUserCollectionId,
+      $databaseId: upcomingRoomsDatabaseId,

Also applies to: 50-53

lib/controllers/auth_state_controller.dart (1)

21-26: Consider making injected fields immutable.

client and messaging are assigned once via DI and never reassigned. Making them final communicates intent and prevents accidental reassignment.

Apply this diff:

-  Client client;
+  final Client client;
@@
-  FirebaseMessaging messaging;
+  final FirebaseMessaging messaging;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e85d2f2 and b547404.

📒 Files selected for processing (21)
  • .github/workflows/run_tests.yml (1 hunks)
  • .github/workflows/store_deploy_android.yml (1 hunks)
  • android/build/.last_build_id (0 hunks)
  • lib/controllers/auth_state_controller.dart (1 hunks)
  • lib/controllers/change_email_controller.dart (1 hunks)
  • lib/controllers/create_room_controller.dart (1 hunks)
  • lib/controllers/edit_profile_controller.dart (2 hunks)
  • lib/controllers/explore_story_controller.dart (2 hunks)
  • lib/models/resonate_user.freezed.dart (8 hunks)
  • lib/models/resonate_user.g.dart (1 hunks)
  • test/controllers/auth_state_controller_test.dart (1 hunks)
  • test/controllers/auth_state_controller_test.mocks.dart (1 hunks)
  • test/controllers/change_email_controller_test.dart (1 hunks)
  • test/controllers/change_email_controller_test.mocks.dart (1 hunks)
  • test/controllers/chapter_player_controller_test.dart (1 hunks)
  • test/controllers/create_room_controller_test.dart (1 hunks)
  • test/controllers/create_room_controller_test.mocks.dart (1 hunks)
  • test/controllers/edit_profile_controller_test.dart (1 hunks)
  • test/controllers/edit_profile_controller_test.mocks.dart (1 hunks)
  • test/controllers/explore_story_controller_test.dart (1 hunks)
  • test/widget_test.dart (0 hunks)
💤 Files with no reviewable changes (2)
  • test/widget_test.dart
  • android/build/.last_build_id
🧰 Additional context used
🪛 YAMLlint (1.37.1)
.github/workflows/run_tests.yml

[error] 13-13: trailing spaces

(trailing-spaces)


[error] 16-16: trailing spaces

(trailing-spaces)


[error] 34-34: trailing spaces

(trailing-spaces)


[error] 45-45: no new line character at the end of file

(new-line-at-end-of-file)

🔇 Additional comments (14)
.github/workflows/run_tests.yml (1)

21-22: Flutter version 3.32.5 is valid

The workflow’s pinned Flutter version (3.32.5) is a valid stable release (June 25, 2025). The current latest stable release is 3.32.8 (July 25, 2025).

• You can continue pinning to 3.32.5 as-is.
• If you need the very latest fixes, update to 3.32.8.
• Alternatively, replace the version pin with

channel: stable

to automatically use the newest stable Flutter release.

test/controllers/auth_state_controller_test.mocks.dart (1)

1-4: Auto-generated mocks — OK to commit.

Looks good. Keep this generated file in sync with dependencies by re-running build_runner when interfaces change.

lib/models/resonate_user.g.dart (1)

18-19: Robust deserialization via _toDouble — LGTM.

Centralizing userRating conversion through _toDouble improves resilience to int/string/num inputs.

lib/controllers/create_room_controller.dart (1)

15-15: Good move to constructor-based DI.

Switching to a final field and initializing via the constructor improves testability and aligns with DI best practices across the repo.

Also applies to: 23-26

test/controllers/create_room_controller_test.dart (1)

59-62: Test will fail until tag regex is fixed in production code.

The case with "Football$" expects an invalid-tag error. With the current implementation using contains(), it will incorrectly pass validation. Apply the CreateRoomController.isValidTag fix (use RegExp.hasMatch) to align behavior with this test.

test/controllers/create_room_controller_test.mocks.dart (1)

1-327: Generated mocks look fine.

Mockito-generated file is consistent with ThemeController usage and supports constructor DI in tests. No action required.

lib/controllers/explore_story_controller.dart (1)

17-20: Constructor-based DI is a solid improvement.

Final fields with default providers keep previous behavior while enabling injection in tests. Nicely done.

Also applies to: 31-39

lib/controllers/change_email_controller.dart (1)

16-27: Constructor-based DI: solid change and consistent with PR objectives

Constructor injection with sensible Get.find/AppwriteService fallbacks improves testability and removes eager singletons. Aligned with the new testing strategy.

test/controllers/change_email_controller_test.dart (1)

18-35: Good use of constructor DI and comprehensive stubbing

The setup cleanly constructs controllers with mocks, matching the DI refactor in lib/. This unlocks realistic unit/widget tests.

lib/controllers/edit_profile_controller.dart (1)

44-54: Constructor-based DI and moving service initialization out of onInit: good improvement

This aligns the controller with the new testing and DI approach and removes hidden side-effects in onInit.

lib/models/resonate_user.freezed.dart (2)

26-27: LGTM: Defensive JSON conversion for userRating.

Annotating userRating with @JsonKey(fromJson: _toDouble) is a safe, backward-compatible way to handle heterogeneous numeric inputs.


353-366: Verify _toDouble exists and is in scope of resonate_user.dart.

This generated code references a private helper _toDouble. Ensure it is defined in lib/models/resonate_user.dart (the same library/part) so both freezed and json_serializable code can resolve it.

If missing, add this in resonate_user.dart:

double? _toDouble(dynamic v) {
  if (v == null) return null;
  if (v is num) return v.toDouble();
  if (v is String) return double.tryParse(v);
  return null;
}
test/controllers/change_email_controller_test.mocks.dart (1)

1-5: Generated mocks look good.

Mocks cover Databases, Account, Client, and FirebaseMessaging, enabling isolated controller tests without network calls. No manual changes needed.

lib/controllers/auth_state_controller.dart (1)

88-96: Async work in onInit is not awaited by GetX.

onInit returns void and is not awaited by the framework; exceptions in the async body may go unnoticed. Consider moving the async initialization to an explicit Future<void> init() called by the UI entry-point, or ensure robust try/catch and logging around awaited calls within onInit.

Would you like a small refactor that moves async initialization into a dedicated init() you call after constructing the controller (keeps behavior unchanged for tests)?

Comment on lines +7 to +9
pull_request_target:
types: [opened]

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don’t use pull_request_target to run untrusted PR code; switch to pull_request.

With pull_request_target and the default checkout, you’re testing the base branch, not the PR head. If you switch checkout to the PR head with pull_request_target, you’d run untrusted code with elevated permissions, which is unsafe. Use pull_request so tests run on the PR’s code with the default, safer permission model.

Apply this diff to trigger on PR updates appropriately:

-  pull_request_target:
-    types: [opened]
+  pull_request:
+    types: [opened, synchronize, reopened]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pull_request_target:
types: [opened]
pull_request:
types: [opened, synchronize, reopened]
🤖 Prompt for AI Agents
.github/workflows/run_tests.yml around lines 7 to 9: the workflow is using
pull_request_target which runs base-branch code and can execute untrusted PR
code with elevated permissions; change the event to pull_request (e.g., replace
pull_request_target with pull_request) and include appropriate types like
opened, synchronize, reopened so the workflow runs on the PR head and triggers
on PR updates; ensure you do not switch checkout behavior to the PR head under
pull_request_target and keep the default safer permissions.

Comment on lines +10 to +12
permissions:
pull-requests: write
contents: read
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Reduce permissions to least privilege.

Tests don’t need write access to PRs. Remove pull-requests: write to avoid unnecessary permissions.

-permissions:
-  pull-requests: write
-  contents: read
+permissions:
+  contents: read
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
permissions:
pull-requests: write
contents: read
permissions:
contents: read
🤖 Prompt for AI Agents
.github/workflows/run_tests.yml around lines 10 to 12: the workflow currently
grants pull-requests: write which is excessive for running tests; change the
permissions block to remove or downgrade pull-requests write access (e.g.,
delete the pull-requests entry or set pull-requests: read if needed) so the
workflow uses least privilege while keeping contents: read.

Comment on lines +71 to +72
- name: 📉 Run all app tests
run: flutter test
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Run tests before decoding secrets and building (fail-fast, minimize secret exposure).

Move the “Run all app tests” step to immediately after dependency install and before any secret decoding or signing steps. This reduces risk of leaking secrets via logs on failing tests and saves CI minutes by failing earlier.

Apply this diff here to remove the test step from its current position:

-      - name: 📉 Run all app tests
-        run: flutter test

Then add it right after “📦 Install dependencies” (outside the selected range), for example:

# Place this after "📦 Install dependencies" and before any "Decode ..." steps
- name: 📉 Run all app tests
  run: flutter test

Optional: enable caching for faster runs (outside the selected range):

- name: Setup Flutter
  uses: subosito/flutter-action@v2
  with:
    flutter-version: ${{ env.FLUTTER_VERSION }}
    cache: true
🤖 Prompt for AI Agents
.github/workflows/store_deploy_android.yml around lines 71-72: the "📉 Run all
app tests" step is currently placed after secret decoding and signing steps;
remove that step from lines 71-72 and re-insert it immediately after the "📦
Install dependencies" step (before any "Decode ..." or signing steps) so tests
run fail-fast without exposing secrets; keep the same step name and command
("flutter test"); optionally enable Flutter action caching by setting cache:
true on the Setup Flutter action (outside this range).

Comment on lines 18 to +20
final emailController = TextEditingController();
final passwordController = TextEditingController();
ChangeEmailController({
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Dispose TextEditingControllers to avoid leaks

emailController and passwordController are never disposed. Dispose them in onClose().

Apply this diff near the end of the class:

   }
 }
+
+  @override
+  void onClose() {
+    emailController.dispose();
+    passwordController.dispose();
+    super.onClose();
+  }

Also applies to: 199-201

🤖 Prompt for AI Agents
In lib/controllers/change_email_controller.dart around lines 18-20 (and also
applies to lines 199-201), the TextEditingController instances emailController
and passwordController are created but never disposed; add an override of
onClose() in the ChangeEmailController that calls emailController.dispose() and
passwordController.dispose(), then calls super.onClose(), to properly release
resources and avoid memory leaks.

Comment on lines +156 to +160
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
queries: [Query.equal("creatorUid", "123")]))
.thenAnswer((_) => Future.value(
DocumentList(total: 2, documents: mockUpcomingRoomsDocuments)));
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Stub mismatch: Query.equal value type should match production (use a list).

Production calls Query.equal("creatorUid", [uid]), but the stub uses a string. This prevents the when(...listDocuments...) from matching, causing the test to hit the real method or return null.

Apply this diff to align the stub with production:

-    when(mockDatabases.listDocuments(
-            databaseId: upcomingRoomsDatabaseId,
-            collectionId: upcomingRoomsCollectionId,
-            queries: [Query.equal("creatorUid", "123")]))
+    when(mockDatabases.listDocuments(
+            databaseId: upcomingRoomsDatabaseId,
+            collectionId: upcomingRoomsCollectionId,
+            queries: [Query.equal("creatorUid", ["123"])]))
         .thenAnswer((_) => Future.value(
             DocumentList(total: 2, documents: mockUpcomingRoomsDocuments)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
queries: [Query.equal("creatorUid", "123")]))
.thenAnswer((_) => Future.value(
DocumentList(total: 2, documents: mockUpcomingRoomsDocuments)));
when(mockDatabases.listDocuments(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
queries: [Query.equal("creatorUid", ["123"])]))
.thenAnswer((_) => Future.value(
DocumentList(total: 2, documents: mockUpcomingRoomsDocuments)));
🤖 Prompt for AI Agents
In test/controllers/auth_state_controller_test.dart around lines 156 to 160, the
stub for listDocuments uses Query.equal("creatorUid", "123") but production uses
a list value; update the stub to use a list (e.g. Query.equal("creatorUid",
["123"])) so the argument matcher matches the real call and the
when(...listDocuments...) returns the intended DocumentList; adjust any other
similar stubs in this test to use list values for Query.equal where production
passes a list.

Comment on lines +249 to +285
test('test removeRegistrationTokenFromSubscribedUpcomingRooms', () async {
await authStateController.setUserProfileData();
await authStateController
.removeRegistrationTokenFromSubscribedUpcomingRooms();
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: 'subUserDoc1',
data: {
'registrationTokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: 'subUserDoc2',
data: {
'registrationTokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: 'room1',
data: {
'creator_fcm_tokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: 'room2',
data: {
'creator_fcm_tokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix expectations: removal should not keep the removed token.

removeRegistrationTokenFromSubscribedUpcomingRooms removes the FCM token. Initial lists contain a single 'mockToken', so after removal, expected lists must not include 'mockToken'.

Apply this diff to correct expectations:

       data: {
-        'registrationTokens': ['token1', 'token2', 'mockToken']
+        'registrationTokens': ['token1', 'token2']
       },
@@
       data: {
-        'registrationTokens': ['token1', 'token2', 'mockToken']
+        'registrationTokens': ['token1', 'token2']
       },
@@
       data: {
-        'creator_fcm_tokens': ['token1', 'token2', 'mockToken']
+        'creator_fcm_tokens': ['token1', 'token2']
       },
@@
       data: {
-        'creator_fcm_tokens': ['token1', 'token2', 'mockToken']
+        'creator_fcm_tokens': ['token1', 'token2']
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('test removeRegistrationTokenFromSubscribedUpcomingRooms', () async {
await authStateController.setUserProfileData();
await authStateController
.removeRegistrationTokenFromSubscribedUpcomingRooms();
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: 'subUserDoc1',
data: {
'registrationTokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: 'subUserDoc2',
data: {
'registrationTokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: 'room1',
data: {
'creator_fcm_tokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: 'room2',
data: {
'creator_fcm_tokens': ['token1', 'token2', 'mockToken']
},
)).called(1);
});
test('test removeRegistrationTokenFromSubscribedUpcomingRooms', () async {
await authStateController.setUserProfileData();
await authStateController
.removeRegistrationTokenFromSubscribedUpcomingRooms();
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: 'subUserDoc1',
data: {
'registrationTokens': ['token1', 'token2']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: subscribedUserCollectionId,
documentId: 'subUserDoc2',
data: {
'registrationTokens': ['token1', 'token2']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: 'room1',
data: {
'creator_fcm_tokens': ['token1', 'token2']
},
)).called(1);
verify(mockDatabases.updateDocument(
databaseId: upcomingRoomsDatabaseId,
collectionId: upcomingRoomsCollectionId,
documentId: 'room2',
data: {
'creator_fcm_tokens': ['token1', 'token2']
},
)).called(1);
});
🤖 Prompt for AI Agents
In test/controllers/auth_state_controller_test.dart around lines 249 to 285, the
verifications currently expect the removed FCM token 'mockToken' to remain in
the arrays; update the expected data in all four verify(...) calls so the
registration/creator token lists do NOT include 'mockToken' (i.e., use
['token1', 'token2'] for each updateDocument verification) so the assertions
match the removal behavior.

Comment on lines +287 to +290
test('test signup', () async {
authStateController.signup('test@test.com', 'anyPassword');
expect(authStateController.email, 'test@test.com');
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Await the async signup to avoid flakiness.

The test callback is async but doesn't await signup(...). The assertion may run before the controller updates state.

Apply this diff:

-    authStateController.signup('test@test.com', 'anyPassword');
+    await authStateController.signup('test@test.com', 'anyPassword');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test('test signup', () async {
authStateController.signup('test@test.com', 'anyPassword');
expect(authStateController.email, 'test@test.com');
});
test('test signup', () async {
- authStateController.signup('test@test.com', 'anyPassword');
+ await authStateController.signup('test@test.com', 'anyPassword');
expect(authStateController.email, 'test@test.com');
});
🤖 Prompt for AI Agents
In test/controllers/auth_state_controller_test.dart around lines 287 to 290, the
async test calls authStateController.signup(...) but does not await it, causing
the expect to run before state updates; update the test to await the async
signup call (await authStateController.signup('test@test.com', 'anyPassword');)
so the controller has time to update before asserting authStateController.email
equals 'test@test.com'.

Comment on lines +8 to +15
void main() {
ChapterPlayerController chapterPlayerController = ChapterPlayerController();
test('check initial values', () {
expect(chapterPlayerController.currentPage.value, 0.0);
expect(chapterPlayerController.lyricProgress.value, 0.0);
expect(chapterPlayerController.sliderProgress.value, 0.0);
expect(chapterPlayerController.isPlaying.value, false);
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid shared state across tests; instantiate controller per test and dispose.

A single top-level ChapterPlayerController instance leaks state between tests and may retain resources (e.g., AudioPlayer). Initialize in setUp and dispose in tearDown to prevent flakiness and resource leaks.

 void main() {
-  ChapterPlayerController chapterPlayerController = ChapterPlayerController();
+  late ChapterPlayerController chapterPlayerController;
+
+  setUp(() {
+    chapterPlayerController = ChapterPlayerController();
+  });
+
+  tearDown(() async {
+    // Dispose audio resources to avoid leaks across tests
+    await chapterPlayerController.audioPlayer?.dispose();
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void main() {
ChapterPlayerController chapterPlayerController = ChapterPlayerController();
test('check initial values', () {
expect(chapterPlayerController.currentPage.value, 0.0);
expect(chapterPlayerController.lyricProgress.value, 0.0);
expect(chapterPlayerController.sliderProgress.value, 0.0);
expect(chapterPlayerController.isPlaying.value, false);
});
void main() {
late ChapterPlayerController chapterPlayerController;
setUp(() {
chapterPlayerController = ChapterPlayerController();
});
tearDown(() async {
// Dispose audio resources to avoid leaks across tests
await chapterPlayerController.audioPlayer?.dispose();
});
test('check initial values', () {
expect(chapterPlayerController.currentPage.value, 0.0);
expect(chapterPlayerController.lyricProgress.value, 0.0);
expect(chapterPlayerController.sliderProgress.value, 0.0);
expect(chapterPlayerController.isPlaying.value, false);
});
}
🤖 Prompt for AI Agents
In test/controllers/chapter_player_controller_test.dart around lines 8 to 15,
the test uses a single top-level ChapterPlayerController instance which can leak
state and resources between tests; change to create a new
ChapterPlayerController in setUp() before each test and assign it to a local
variable, and call the controller's dispose/cleanup method (or explicitly close
its AudioPlayer) in tearDown() after each test so no shared state or open
resources remain. Ensure tests reference the setup instance rather than a
file‑scope instance.

Comment on lines +127 to +145
when(databases.listDocuments(
databaseId: storyDatabaseId,
collectionId: storyCollectionId,
queries: [
Query.equal(
'creatorId', exploreStoryController.authStateController.uid)
])).thenAnswer((_) => Future.delayed(
Duration(seconds: 2),
() => DocumentList(total: 1, documents: [mockStoryDocuments[1]]),
));
when(databases
.listDocuments(
databaseId: storyDatabaseId,
collectionId: storyCollectionId,
queries: [Query.limit(10)])).thenAnswer((_) => Future.delayed(
Duration(seconds: 2),
() => DocumentList(total: 2, documents: mockStoryDocuments),
));
});
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid real multi-second delays in tests; return immediate futures

The 2-second delays slow the suite and CI. Return immediate futures instead. The loading-state test can await the method and assert before/after states.

Apply this diff to stubs:

-    when(databases.listDocuments(
+    when(databases.listDocuments(
         databaseId: storyDatabaseId,
         collectionId: storyCollectionId,
         queries: [
           Query.equal(
               'creatorId', exploreStoryController.authStateController.uid)
-        ])).thenAnswer((_) => Future.delayed(
-          Duration(seconds: 2),
-          () => DocumentList(total: 1, documents: [mockStoryDocuments[1]]),
-        ));
-    when(databases
-        .listDocuments(
+        ])).thenAnswer((_) async =>
+        DocumentList(total: 1, documents: [mockStoryDocuments[1]]));
+    when(databases.listDocuments(
             databaseId: storyDatabaseId,
             collectionId: storyCollectionId,
-            queries: [Query.limit(10)])).thenAnswer((_) => Future.delayed(
-          Duration(seconds: 2),
-          () => DocumentList(total: 2, documents: mockStoryDocuments),
-        ));
+            queries: [Query.limit(10)])).thenAnswer((_) async =>
+        DocumentList(total: 2, documents: mockStoryDocuments));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
when(databases.listDocuments(
databaseId: storyDatabaseId,
collectionId: storyCollectionId,
queries: [
Query.equal(
'creatorId', exploreStoryController.authStateController.uid)
])).thenAnswer((_) => Future.delayed(
Duration(seconds: 2),
() => DocumentList(total: 1, documents: [mockStoryDocuments[1]]),
));
when(databases
.listDocuments(
databaseId: storyDatabaseId,
collectionId: storyCollectionId,
queries: [Query.limit(10)])).thenAnswer((_) => Future.delayed(
Duration(seconds: 2),
() => DocumentList(total: 2, documents: mockStoryDocuments),
));
});
when(databases.listDocuments(
databaseId: storyDatabaseId,
collectionId: storyCollectionId,
queries: [
Query.equal(
'creatorId', exploreStoryController.authStateController.uid)
])).thenAnswer((_) async =>
DocumentList(total: 1, documents: [mockStoryDocuments[1]]));
when(databases.listDocuments(
databaseId: storyDatabaseId,
collectionId: storyCollectionId,
queries: [Query.limit(10)])).thenAnswer((_) async =>
DocumentList(total: 2, documents: mockStoryDocuments));
});
🤖 Prompt for AI Agents
In test/controllers/explore_story_controller_test.dart around lines 127 to 145,
the mocks use Future.delayed with 2-second waits which slow tests; change them
to return immediate futures instead (e.g., replace Future.delayed(..., () =>
DocumentList(...)) with Future.value(DocumentList(...)) or an async thenAnswer
that immediately returns the DocumentList) so the tests run fast while the
loading-state test can still await the call and assert before/after states.

@M4dhav M4dhav merged commit 1dbed55 into AOSSIE-Org:dev Aug 12, 2025
1 check passed
@github-actions
Copy link
Contributor

PR Closed - Thank You, @M4dhav!

  • If this PR was merged: Congratulations! Your contribution is now part of the project. 🚀
  • If this PR was closed without merging: Don’t worry! You can always improve it and submit again. 💪

We appreciate your effort and look forward to more contributions from you! 🤝

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request gsoc Part of Google Summer of Code season

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add automated software tests to check new PRs

1 participant