Tests for encryptor.dart written

<!--
This section can be deleted after reading.

We employ the following branching strategy to simplify the development process and to ensure that only stable code is pushed to the `master` branch:

- `develop`: For unstable code: New features and bug fixes.
- `master`: Where the stable production ready code lies. Only security related bugs.

NOTE!!!

ONLY SUBMIT PRS AGAINST OUR `DEVELOP` BRANCH. THE DEFAULT IS `MAIN`, SO YOU WILL HAVE TO MODIFY THIS BEFORE SUBMITTING YOUR PR FOR REVIEW. PRS MADE AGAINST `MAIN` WILL BE CLOSED.
-->

<!--
Thanks for submitting a pull request! Please provide enough information so that others can review your pull request.
-->

**What kind of change does this PR introduce?**
This change is about:
1) Making the coverage of `lib/utils/encryptor.dart` from 2.94% to 100%
2) Covering all the test cases related to this file
3) Correcting the `generateRSAKeyPair()` function which previously was not generating keys but currently is!
4) Created AsymetricKeys Model to save Key Pairs in Hive which previously was not created, registered the respective Adapters in the `main.dart` file.
5) Created a Mock/Testing environment for Hive Data Base, so that Future Developers are able to write tests including Hive easily.

<!-- E.g. a bugfix, feature, refactoring, etc… -->

**Issue Number:** 2149

Fixes #<!--Add related issue number here.-->2149

**Did you add tests for your changes?** Yes

<!--Yes or No. Note: Add unit tests or automation tests for your code.-->

**Snapshots/Videos:**

<!--Add snapshots or videos wherever possible.-->

**If relevant, did you update the documentation?**

<!--Add link to Talawa-Docs.-->

**Summary**

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->
<!-- Try to link to an open issue for more information. -->

**Does this PR introduce a breaking change?** Yes, the Testing Environment for Hive is going to make life of Talawa Developers easy!

<!-- If this PR introduces a breaking change, please describe the impact and a migration path for existing applications. -->

**Other information**

<!--Add extra information about this PR here-->

**Have you read the [contributing guide](https://github.com/PalisadoesFoundation/talawa/blob/master/CONTRIBUTING.md)?** Yes

<!--Yes or No-->

Write tests for lib/utils/encryptor.dart
The Talawa code base needs to be 100% reliable. This means we need to have 100% unittest code coverage. No functionality must be removed to achieve this goal.

We will need unittests done for all methods, classes and functions found in this file.

Any widgets referenced in this file must also have unittests done

```
lib/utils/encryptor.dart

```

The current code coverage for this file can be found here:
-  https://app.codecov.io/gh/PalisadoesFoundation/talawa?displayType=list

@Ayush0Chaudhary  @noman2002 I would like to work on this issue.. can you please assign it to me?
Solve other issues first
@Ayush0Chaudhary @noman2002 Can you assign this issue to me?
There is a problem lying in the `generateRSAKeyPair()` function, the parameters passed in ` final random = SecureRandom('Fortuna')
      ..seed(secureRandom as CipherParameters);` are wrong as `secureRandom` is not a Cypher Parameter. When changed to the `KeyParametres` then to start AES Enginer, a key is required and also more configurations are required which can't be done without the assistance of Maintainers. See this link for more info: [AES-CBC with Pointy Castle](https://github.com/bcgit/pc-dart/blob/master/tutorials/aes-cbc.md)
If Maintainers can allow me, I can move forward with random configurations which can be modified further, otherwise test can't be written as parameters are wrong!
@literalEval Please answer this query.
```
AsymmetricKeyPair<PublicKey, PrivateKey> generateRSAKeyPair() {
    final secureRandom = FortunaRandom();
    final random = Random.secure();
    final List<int> seeds = [];
    for (int i = 0; i < 32; i++) {
      seeds.add(random.nextInt(255));
    }
    secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
    final rsapars = RSAKeyGeneratorParameters(BigInt.from(65537), 2048, 64);
  final params = ParametersWithRandom(rsapars, secureRandom);
  final keyGenerator = RSAKeyGenerator();
  keyGenerator.init(params);
  return keyGenerator.generateKeyPair();
  }
```
I have modified this function in this way with the help of the blog [Asymmetric Key Generation in Flutter](https://medium.com/flutter-community/asymmetric-key-generation-in-flutter-ad2b912f3309l). Kindly check if it is right as it is working fine!

diff --git a/lib/main.dart b/lib/main.dart
index 19350a4cc3..7ec3b18905 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -15,6 +15,7 @@ import 'package:talawa/constants/custom_theme.dart';
 import 'package:talawa/constants/quick_actions.dart';
 import 'package:talawa/firebase_options.dart';
 import 'package:talawa/locator.dart';
+import 'package:talawa/models/asymetric_keys/asymetric_keys.dart';
 import 'package:talawa/models/organization/org_info.dart';
 import 'package:talawa/models/user/user_info.dart';
 import 'package:talawa/plugins/fetch_plugin_list.dart';
@@ -105,11 +106,12 @@ Future<void> main() async {
   Hive
     ..init(dir.path)
     ..registerAdapter(UserAdapter())
-    ..registerAdapter(OrgInfoAdapter());
+    ..registerAdapter(OrgInfoAdapter())
+    ..registerAdapter(AsymetricKeysAdapter());

   await Hive.openBox<User>('currentUser');
   await Hive.openBox<OrgInfo>('currentOrg');
-
+  await Hive.openBox<AsymetricKeys>('user_keys');
   await Hive.openBox('pluginBox');
   await Hive.openBox('url');

diff --git a/lib/models/asymetric_keys/asymetric_keys.dart b/lib/models/asymetric_keys/asymetric_keys.dart
new file mode 100644
index 0000000000..5791136a4c
--- /dev/null
+++ b/lib/models/asymetric_keys/asymetric_keys.dart
@@ -0,0 +1,11 @@
+import 'package:hive/hive.dart';
+import 'package:pointycastle/api.dart';
+part 'asymetric_keys.g.dart';
+
+// This model is created to save Asymmetric Keys to the Hive as Hive doesn't allow to store objects other than Arrays, List, int, Strings etc.
+@HiveType(typeId: 7)
+class AsymetricKeys extends HiveObject {
+  AsymetricKeys({required this.keyPair});
+  @HiveField(0)
+  final AsymmetricKeyPair<PublicKey, PrivateKey> keyPair;
+}
diff --git a/lib/models/asymetric_keys/asymetric_keys.g.dart b/lib/models/asymetric_keys/asymetric_keys.g.dart
new file mode 100644
index 0000000000..5661ba4f03
--- /dev/null
+++ b/lib/models/asymetric_keys/asymetric_keys.g.dart
@@ -0,0 +1,41 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'asymetric_keys.dart';
+
+// **************************************************************************
+// TypeAdapterGenerator
+// **************************************************************************
+
+class AsymetricKeysAdapter extends TypeAdapter<AsymetricKeys> {
+  @override
+  final int typeId = 7;
+
+  @override
+  AsymetricKeys read(BinaryReader reader) {
+    final numOfFields = reader.readByte();
+    final fields = <int, dynamic>{
+      for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
+    };
+    return AsymetricKeys(
+      keyPair: fields[0] as AsymmetricKeyPair<PublicKey, PrivateKey>,
+    );
+  }
+
+  @override
+  void write(BinaryWriter writer, AsymetricKeys obj) {
+    writer
+      ..writeByte(1)
+      ..writeByte(0)
+      ..write(obj.keyPair);
+  }
+
+  @override
+  int get hashCode => typeId.hashCode;
+
+  @override
+  bool operator ==(Object other) =>
+      identical(this, other) ||
+      other is AsymetricKeysAdapter &&
+          runtimeType == other.runtimeType &&
+          typeId == other.typeId;
+}
diff --git a/lib/utils/encryptor.dart b/lib/utils/encryptor.dart
index 6435a25275..8bdef772d8 100644
--- a/lib/utils/encryptor.dart
+++ b/lib/utils/encryptor.dart
@@ -1,12 +1,15 @@
 import 'dart:convert';
+import 'dart:math';
 import 'dart:typed_data';

 import 'package:crypto/crypto.dart';
 import 'package:hive/hive.dart';
 import 'package:pointycastle/asymmetric/oaep.dart';
 import 'package:pointycastle/asymmetric/rsa.dart';
+import 'package:pointycastle/key_generators/rsa_key_generator.dart';
 import 'package:pointycastle/pointycastle.dart';
 import 'package:pointycastle/random/fortuna_random.dart';
+import 'package:talawa/models/asymetric_keys/asymetric_keys.dart';

 /// Handles all of the encryption tasks in the codebase.
 class Encryptor {
@@ -43,15 +46,17 @@ class Encryptor {
   /// * `AsymmetricKeyPair<PublicKey, PrivateKey>`: The generated
   /// public and private keys.
   AsymmetricKeyPair<PublicKey, PrivateKey> generateRSAKeyPair() {
-    final keyGen = RSAKeyGeneratorParameters(BigInt.parse('65537'), 2048, 64);
     final secureRandom = FortunaRandom();
-    final random = SecureRandom('Fortuna')
-      ..seed(secureRandom as CipherParameters);
-    final keyGenParams = ParametersWithRandom(keyGen, random);
-
-    final keyGenerator = KeyGenerator('RSA');
-    keyGenerator.init(keyGenParams);
-
+    final random = Random.secure();
+    final List<int> seeds = [];
+    for (int i = 0; i < 32; i++) {
+      seeds.add(random.nextInt(255));
+    }
+    secureRandom.seed(KeyParameter(Uint8List.fromList(seeds)));
+    final rsapars = RSAKeyGeneratorParameters(BigInt.from(65537), 2048, 64);
+    final params = ParametersWithRandom(rsapars, secureRandom);
+    final keyGenerator = RSAKeyGenerator();
+    keyGenerator.init(params);
     return keyGenerator.generateKeyPair();
   }

@@ -66,13 +71,12 @@ class Encryptor {
   /// * `Future<void>`: None
   Future<void> saveKeyPair(
     AsymmetricKeyPair<PublicKey, PrivateKey> keyPair,
+    HiveInterface hiveInterface,
   ) async {
     // TODO: Implement secure storage here
-
-    await Hive.openBox<AsymmetricKeyPair<PublicKey, PrivateKey>>('user_keys');
-    final keysBox = await Hive.openBox('user_keys');
-
-    keysBox.put('key_pair', keyPair);
+    final Box<AsymetricKeys> keysBox =
+        await hiveInterface.openBox<AsymetricKeys>('user_keys');
+    keysBox.put('key_pair', AsymetricKeys(keyPair: keyPair));
   }

   /// Loads secret keys from the Hive db.
@@ -83,12 +87,11 @@ class Encryptor {
   /// **returns**:
   /// * `Future<AsymmetricKeyPair<PublicKey, PrivateKey>>`: The public and
   /// private key pair
-  Future<AsymmetricKeyPair<PublicKey, PrivateKey>> loadKeyPair() async {
-    await Hive.openBox<AsymmetricKeyPair<PublicKey, PrivateKey>>('user_keys');
-    final keysBox = await Hive.openBox('user_keys');
-
-    return (await keysBox.get('key_pair'))
-        as AsymmetricKeyPair<PublicKey, PrivateKey>;
+  Future<AsymmetricKeyPair<PublicKey, PrivateKey>> loadKeyPair(
+    HiveInterface hive,
+  ) async {
+    final keysBox = await hive.openBox<AsymetricKeys>('user_keys');
+    return keysBox.get('key_pair')!.keyPair;
   }

   /// Encrypts the given string data with Recipient's Public Key.
@@ -136,15 +139,21 @@ class Encryptor {
   /// **returns**:
   /// * `Future<void>`: None
   /// TODO: Use this somewhere
-  Future<void> receiveMessage(Map<String, dynamic> message) async {
-    final privateKey = (await loadKeyPair()).privateKey;
-
-    final encryptedMessage = message['encryptedMessage'] as String;
-    final decryptedMessage = assymetricDecryptString(
-      encryptedMessage,
-      privateKey as RSAPrivateKey,
-    );
-
-    print('Decrypted Message: $decryptedMessage');
+  Future<void> receiveMessage(
+    Map<String, dynamic> message,
+    HiveInterface hive,
+  ) async {
+    try {
+      final encryptedMessage = message['encryptedMessage'] as String;
+      final privateKey = (await loadKeyPair(hive)).privateKey;
+      final decryptedMessage = assymetricDecryptString(
+        encryptedMessage,
+        privateKey as RSAPrivateKey,
+      );
+
+      print('Decrypted Message: $decryptedMessage');
+    } catch (e) {
+      throw Exception('Error');
+    }
   }
 }

diff --git a/test/helpers/setup_hive.dart b/test/helpers/setup_hive.dart
index cb9b87fbc5..55cc63be57 100644
--- a/test/helpers/setup_hive.dart
+++ b/test/helpers/setup_hive.dart
@@ -2,9 +2,22 @@
 // ignore_for_file: talawa_good_doc_comments

 import 'package:hive/hive.dart';
+import 'package:mockito/annotations.dart';
 import 'package:talawa/models/organization/org_info.dart';
 import 'package:talawa/models/user/user_info.dart';

+@GenerateMocks(
+  [
+    HiveInterface,
+    Box,
+  ],
+  customMocks: [
+    MockSpec<Box>(
+      as: Symbol('MockHiveBox'),
+      onMissingStub: OnMissingStub.returnDefault,
+    ),
+  ],
+)
 Future<Box> setUpUserInfoHive() async {
   Hive
     ..init("./temporaryPath")
diff --git a/test/helpers/setup_hive.mocks.dart b/test/helpers/setup_hive.mocks.dart
new file mode 100644
index 0000000000..dc67dfd0a5
--- /dev/null
+++ b/test/helpers/setup_hive.mocks.dart
@@ -0,0 +1,837 @@
+// Mocks generated by Mockito 5.4.3 from annotations
+// in talawa/test/utils/encryptor_test.dart.
+// Do not manually edit this file.
+
+// ignore_for_file: no_leading_underscores_for_library_prefixes
+import 'dart:async' as _i3;
+import 'dart:typed_data' as _i6;
+
+import 'package:hive/hive.dart' as _i2;
+import 'package:hive/src/box/default_compaction_strategy.dart' as _i5;
+import 'package:hive/src/box/default_key_comparator.dart' as _i4;
+import 'package:mockito/mockito.dart' as _i1;
+import 'package:mockito/src/dummies.dart' as _i7;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: deprecated_member_use
+// ignore_for_file: deprecated_member_use_from_same_package
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+// ignore_for_file: subtype_of_sealed_class
+
+class _FakeBox_0<E1> extends _i1.SmartFake implements _i2.Box<E1> {
+  _FakeBox_0(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+class _FakeLazyBox_1<E1> extends _i1.SmartFake implements _i2.LazyBox<E1> {
+  _FakeLazyBox_1(
+    Object parent,
+    Invocation parentInvocation,
+  ) : super(
+          parent,
+          parentInvocation,
+        );
+}
+
+/// A class which mocks [HiveInterface].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockHiveInterface extends _i1.Mock implements _i2.HiveInterface {
+  MockHiveInterface() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  void init(
+    String? path, {
+    _i2.HiveStorageBackendPreference? backendPreference =
+        _i2.HiveStorageBackendPreference.native,
+  }) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #init,
+          [path],
+          {#backendPreference: backendPreference},
+        ),
+        returnValueForMissingStub: null,
+      );
+
+  @override
+  _i3.Future<_i2.Box<E>> openBox<E>(
+    String? name, {
+    _i2.HiveCipher? encryptionCipher,
+    _i2.KeyComparator? keyComparator = _i4.defaultKeyComparator,
+    _i2.CompactionStrategy? compactionStrategy = _i5.defaultCompactionStrategy,
+    bool? crashRecovery = true,
+    String? path,
+    _i6.Uint8List? bytes,
+    String? collection,
+    List<int>? encryptionKey,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #openBox,
+          [name],
+          {
+            #encryptionCipher: encryptionCipher,
+            #keyComparator: keyComparator,
+            #compactionStrategy: compactionStrategy,
+            #crashRecovery: crashRecovery,
+            #path: path,
+            #bytes: bytes,
+            #collection: collection,
+            #encryptionKey: encryptionKey,
+          },
+        ),
+        returnValue: _i3.Future<_i2.Box<E>>.value(_FakeBox_0<E>(
+          this,
+          Invocation.method(
+            #openBox,
+            [name],
+            {
+              #encryptionCipher: encryptionCipher,
+              #keyComparator: keyComparator,
+              #compactionStrategy: compactionStrategy,
+              #crashRecovery: crashRecovery,
+              #path: path,
+              #bytes: bytes,
+              #collection: collection,
+              #encryptionKey: encryptionKey,
+            },
+          ),
+        )),
+      ) as _i3.Future<_i2.Box<E>>);
+
+  @override
+  _i3.Future<_i2.LazyBox<E>> openLazyBox<E>(
+    String? name, {
+    _i2.HiveCipher? encryptionCipher,
+    _i2.KeyComparator? keyComparator = _i4.defaultKeyComparator,
+    _i2.CompactionStrategy? compactionStrategy = _i5.defaultCompactionStrategy,
+    bool? crashRecovery = true,
+    String? path,
+    String? collection,
+    List<int>? encryptionKey,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #openLazyBox,
+          [name],
+          {
+            #encryptionCipher: encryptionCipher,
+            #keyComparator: keyComparator,
+            #compactionStrategy: compactionStrategy,
+            #crashRecovery: crashRecovery,
+            #path: path,
+            #collection: collection,
+            #encryptionKey: encryptionKey,
+          },
+        ),
+        returnValue: _i3.Future<_i2.LazyBox<E>>.value(_FakeLazyBox_1<E>(
+          this,
+          Invocation.method(
+            #openLazyBox,
+            [name],
+            {
+              #encryptionCipher: encryptionCipher,
+              #keyComparator: keyComparator,
+              #compactionStrategy: compactionStrategy,
+              #crashRecovery: crashRecovery,
+              #path: path,
+              #collection: collection,
+              #encryptionKey: encryptionKey,
+            },
+          ),
+        )),
+      ) as _i3.Future<_i2.LazyBox<E>>);
+
+  @override
+  _i2.Box<E> box<E>(String? name) => (super.noSuchMethod(
+        Invocation.method(
+          #box,
+          [name],
+        ),
+        returnValue: _FakeBox_0<E>(
+          this,
+          Invocation.method(
+            #box,
+            [name],
+          ),
+        ),
+      ) as _i2.Box<E>);
+
+  @override
+  _i2.LazyBox<E> lazyBox<E>(String? name) => (super.noSuchMethod(
+        Invocation.method(
+          #lazyBox,
+          [name],
+        ),
+        returnValue: _FakeLazyBox_1<E>(
+          this,
+          Invocation.method(
+            #lazyBox,
+            [name],
+          ),
+        ),
+      ) as _i2.LazyBox<E>);
+
+  @override
+  bool isBoxOpen(String? name) => (super.noSuchMethod(
+        Invocation.method(
+          #isBoxOpen,
+          [name],
+        ),
+        returnValue: false,
+      ) as bool);
+
+  @override
+  _i3.Future<void> close() => (super.noSuchMethod(
+        Invocation.method(
+          #close,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteBoxFromDisk(
+    String? name, {
+    String? path,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #deleteBoxFromDisk,
+          [name],
+          {#path: path},
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteFromDisk() => (super.noSuchMethod(
+        Invocation.method(
+          #deleteFromDisk,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  List<int> generateSecureKey() => (super.noSuchMethod(
+        Invocation.method(
+          #generateSecureKey,
+          [],
+        ),
+        returnValue: <int>[],
+      ) as List<int>);
+
+  @override
+  _i3.Future<bool> boxExists(
+    String? name, {
+    String? path,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #boxExists,
+          [name],
+          {#path: path},
+        ),
+        returnValue: _i3.Future<bool>.value(false),
+      ) as _i3.Future<bool>);
+
+  @override
+  void resetAdapters() => super.noSuchMethod(
+        Invocation.method(
+          #resetAdapters,
+          [],
+        ),
+        returnValueForMissingStub: null,
+      );
+
+  @override
+  void registerAdapter<T>(
+    _i2.TypeAdapter<T>? adapter, {
+    bool? internal = false,
+    bool? override = false,
+  }) =>
+      super.noSuchMethod(
+        Invocation.method(
+          #registerAdapter,
+          [adapter],
+          {
+            #internal: internal,
+            #override: override,
+          },
+        ),
+        returnValueForMissingStub: null,
+      );
+
+  @override
+  bool isAdapterRegistered(int? typeId) => (super.noSuchMethod(
+        Invocation.method(
+          #isAdapterRegistered,
+          [typeId],
+        ),
+        returnValue: false,
+      ) as bool);
+
+  @override
+  void ignoreTypeId<T>(int? typeId) => super.noSuchMethod(
+        Invocation.method(
+          #ignoreTypeId,
+          [typeId],
+        ),
+        returnValueForMissingStub: null,
+      );
+}
+
+/// A class which mocks [Box].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockBox<E> extends _i1.Mock implements _i2.Box<E> {
+  MockBox() {
+    _i1.throwOnMissingStub(this);
+  }
+
+  @override
+  Iterable<E> get values => (super.noSuchMethod(
+        Invocation.getter(#values),
+        returnValue: <E>[],
+      ) as Iterable<E>);
+
+  @override
+  String get name => (super.noSuchMethod(
+        Invocation.getter(#name),
+        returnValue: _i7.dummyValue<String>(
+          this,
+          Invocation.getter(#name),
+        ),
+      ) as String);
+
+  @override
+  bool get isOpen => (super.noSuchMethod(
+        Invocation.getter(#isOpen),
+        returnValue: false,
+      ) as bool);
+
+  @override
+  bool get lazy => (super.noSuchMethod(
+        Invocation.getter(#lazy),
+        returnValue: false,
+      ) as bool);
+
+  @override
+  Iterable<dynamic> get keys => (super.noSuchMethod(
+        Invocation.getter(#keys),
+        returnValue: <dynamic>[],
+      ) as Iterable<dynamic>);
+
+  @override
+  int get length => (super.noSuchMethod(
+        Invocation.getter(#length),
+        returnValue: 0,
+      ) as int);
+
+  @override
+  bool get isEmpty => (super.noSuchMethod(
+        Invocation.getter(#isEmpty),
+        returnValue: false,
+      ) as bool);
+
+  @override
+  bool get isNotEmpty => (super.noSuchMethod(
+        Invocation.getter(#isNotEmpty),
+        returnValue: false,
+      ) as bool);
+
+  @override
+  Iterable<E> valuesBetween({
+    dynamic startKey,
+    dynamic endKey,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #valuesBetween,
+          [],
+          {
+            #startKey: startKey,
+            #endKey: endKey,
+          },
+        ),
+        returnValue: <E>[],
+      ) as Iterable<E>);
+
+  @override
+  E? getAt(int? index) => (super.noSuchMethod(Invocation.method(
+        #getAt,
+        [index],
+      )) as E?);
+
+  @override
+  Map<dynamic, E> toMap() => (super.noSuchMethod(
+        Invocation.method(
+          #toMap,
+          [],
+        ),
+        returnValue: <dynamic, E>{},
+      ) as Map<dynamic, E>);
+
+  @override
+  dynamic keyAt(int? index) => super.noSuchMethod(Invocation.method(
+        #keyAt,
+        [index],
+      ));
+
+  @override
+  _i3.Stream<_i2.BoxEvent> watch({dynamic key}) => (super.noSuchMethod(
+        Invocation.method(
+          #watch,
+          [],
+          {#key: key},
+        ),
+        returnValue: _i3.Stream<_i2.BoxEvent>.empty(),
+      ) as _i3.Stream<_i2.BoxEvent>);
+
+  @override
+  bool containsKey(dynamic key) => (super.noSuchMethod(
+        Invocation.method(
+          #containsKey,
+          [key],
+        ),
+        returnValue: false,
+      ) as bool);
+
+  @override
+  _i3.Future<void> put(
+    dynamic key,
+    E? value,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #put,
+          [
+            key,
+            value,
+          ],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> putAt(
+    int? index,
+    E? value,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #putAt,
+          [
+            index,
+            value,
+          ],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> putAll(Map<dynamic, E>? entries) => (super.noSuchMethod(
+        Invocation.method(
+          #putAll,
+          [entries],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<int> add(E? value) => (super.noSuchMethod(
+        Invocation.method(
+          #add,
+          [value],
+        ),
+        returnValue: _i3.Future<int>.value(0),
+      ) as _i3.Future<int>);
+
+  @override
+  _i3.Future<Iterable<int>> addAll(Iterable<E>? values) => (super.noSuchMethod(
+        Invocation.method(
+          #addAll,
+          [values],
+        ),
+        returnValue: _i3.Future<Iterable<int>>.value(<int>[]),
+      ) as _i3.Future<Iterable<int>>);
+
+  @override
+  _i3.Future<void> delete(dynamic key) => (super.noSuchMethod(
+        Invocation.method(
+          #delete,
+          [key],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteAt(int? index) => (super.noSuchMethod(
+        Invocation.method(
+          #deleteAt,
+          [index],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteAll(Iterable<dynamic>? keys) => (super.noSuchMethod(
+        Invocation.method(
+          #deleteAll,
+          [keys],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> compact() => (super.noSuchMethod(
+        Invocation.method(
+          #compact,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<int> clear() => (super.noSuchMethod(
+        Invocation.method(
+          #clear,
+          [],
+        ),
+        returnValue: _i3.Future<int>.value(0),
+      ) as _i3.Future<int>);
+
+  @override
+  _i3.Future<void> close() => (super.noSuchMethod(
+        Invocation.method(
+          #close,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteFromDisk() => (super.noSuchMethod(
+        Invocation.method(
+          #deleteFromDisk,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> flush() => (super.noSuchMethod(
+        Invocation.method(
+          #flush,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+}
+
+/// A class which mocks [Box].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockHiveBox<E> extends _i1.Mock implements _i2.Box<E> {
+  @override
+  Iterable<E> get values => (super.noSuchMethod(
+        Invocation.getter(#values),
+        returnValue: <E>[],
+        returnValueForMissingStub: <E>[],
+      ) as Iterable<E>);
+
+  @override
+  String get name => (super.noSuchMethod(
+        Invocation.getter(#name),
+        returnValue: _i7.dummyValue<String>(
+          this,
+          Invocation.getter(#name),
+        ),
+        returnValueForMissingStub: _i7.dummyValue<String>(
+          this,
+          Invocation.getter(#name),
+        ),
+      ) as String);
+
+  @override
+  bool get isOpen => (super.noSuchMethod(
+        Invocation.getter(#isOpen),
+        returnValue: false,
+        returnValueForMissingStub: false,
+      ) as bool);
+
+  @override
+  bool get lazy => (super.noSuchMethod(
+        Invocation.getter(#lazy),
+        returnValue: false,
+        returnValueForMissingStub: false,
+      ) as bool);
+
+  @override
+  Iterable<dynamic> get keys => (super.noSuchMethod(
+        Invocation.getter(#keys),
+        returnValue: <dynamic>[],
+        returnValueForMissingStub: <dynamic>[],
+      ) as Iterable<dynamic>);
+
+  @override
+  int get length => (super.noSuchMethod(
+        Invocation.getter(#length),
+        returnValue: 0,
+        returnValueForMissingStub: 0,
+      ) as int);
+
+  @override
+  bool get isEmpty => (super.noSuchMethod(
+        Invocation.getter(#isEmpty),
+        returnValue: false,
+        returnValueForMissingStub: false,
+      ) as bool);
+
+  @override
+  bool get isNotEmpty => (super.noSuchMethod(
+        Invocation.getter(#isNotEmpty),
+        returnValue: false,
+        returnValueForMissingStub: false,
+      ) as bool);
+
+  @override
+  Iterable<E> valuesBetween({
+    dynamic startKey,
+    dynamic endKey,
+  }) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #valuesBetween,
+          [],
+          {
+            #startKey: startKey,
+            #endKey: endKey,
+          },
+        ),
+        returnValue: <E>[],
+        returnValueForMissingStub: <E>[],
+      ) as Iterable<E>);
+
+  @override
+  E? getAt(int? index) => (super.noSuchMethod(
+        Invocation.method(
+          #getAt,
+          [index],
+        ),
+        returnValueForMissingStub: null,
+      ) as E?);
+
+  @override
+  Map<dynamic, E> toMap() => (super.noSuchMethod(
+        Invocation.method(
+          #toMap,
+          [],
+        ),
+        returnValue: <dynamic, E>{},
+        returnValueForMissingStub: <dynamic, E>{},
+      ) as Map<dynamic, E>);
+
+  @override
+  dynamic keyAt(int? index) => super.noSuchMethod(
+        Invocation.method(
+          #keyAt,
+          [index],
+        ),
+        returnValueForMissingStub: null,
+      );
+
+  @override
+  _i3.Stream<_i2.BoxEvent> watch({dynamic key}) => (super.noSuchMethod(
+        Invocation.method(
+          #watch,
+          [],
+          {#key: key},
+        ),
+        returnValue: _i3.Stream<_i2.BoxEvent>.empty(),
+        returnValueForMissingStub: _i3.Stream<_i2.BoxEvent>.empty(),
+      ) as _i3.Stream<_i2.BoxEvent>);
+
+  @override
+  bool containsKey(dynamic key) => (super.noSuchMethod(
+        Invocation.method(
+          #containsKey,
+          [key],
+        ),
+        returnValue: false,
+        returnValueForMissingStub: false,
+      ) as bool);
+
+  @override
+  _i3.Future<void> put(
+    dynamic key,
+    E? value,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #put,
+          [
+            key,
+            value,
+          ],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> putAt(
+    int? index,
+    E? value,
+  ) =>
+      (super.noSuchMethod(
+        Invocation.method(
+          #putAt,
+          [
+            index,
+            value,
+          ],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> putAll(Map<dynamic, E>? entries) => (super.noSuchMethod(
+        Invocation.method(
+          #putAll,
+          [entries],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<int> add(E? value) => (super.noSuchMethod(
+        Invocation.method(
+          #add,
+          [value],
+        ),
+        returnValue: _i3.Future<int>.value(0),
+        returnValueForMissingStub: _i3.Future<int>.value(0),
+      ) as _i3.Future<int>);
+
+  @override
+  _i3.Future<Iterable<int>> addAll(Iterable<E>? values) => (super.noSuchMethod(
+        Invocation.method(
+          #addAll,
+          [values],
+        ),
+        returnValue: _i3.Future<Iterable<int>>.value(<int>[]),
+        returnValueForMissingStub: _i3.Future<Iterable<int>>.value(<int>[]),
+      ) as _i3.Future<Iterable<int>>);
+
+  @override
+  _i3.Future<void> delete(dynamic key) => (super.noSuchMethod(
+        Invocation.method(
+          #delete,
+          [key],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteAt(int? index) => (super.noSuchMethod(
+        Invocation.method(
+          #deleteAt,
+          [index],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteAll(Iterable<dynamic>? keys) => (super.noSuchMethod(
+        Invocation.method(
+          #deleteAll,
+          [keys],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> compact() => (super.noSuchMethod(
+        Invocation.method(
+          #compact,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<int> clear() => (super.noSuchMethod(
+        Invocation.method(
+          #clear,
+          [],
+        ),
+        returnValue: _i3.Future<int>.value(0),
+        returnValueForMissingStub: _i3.Future<int>.value(0),
+      ) as _i3.Future<int>);
+
+  @override
+  _i3.Future<void> close() => (super.noSuchMethod(
+        Invocation.method(
+          #close,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> deleteFromDisk() => (super.noSuchMethod(
+        Invocation.method(
+          #deleteFromDisk,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+
+  @override
+  _i3.Future<void> flush() => (super.noSuchMethod(
+        Invocation.method(
+          #flush,
+          [],
+        ),
+        returnValue: _i3.Future<void>.value(),
+        returnValueForMissingStub: _i3.Future<void>.value(),
+      ) as _i3.Future<void>);
+}
diff --git a/test/utils/encryptor_test.dart b/test/utils/encryptor_test.dart
new file mode 100644
index 0000000000..4c311a665c
--- /dev/null
+++ b/test/utils/encryptor_test.dart
@@ -0,0 +1,139 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:crypto/crypto.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:pointycastle/api.dart' as pointy;
+import 'package:pointycastle/asymmetric/oaep.dart';
+import 'package:pointycastle/asymmetric/rsa.dart';
+import 'package:pointycastle/pointycastle.dart';
+import 'package:talawa/models/asymetric_keys/asymetric_keys.dart';
+import 'package:talawa/utils/encryptor.dart';
+import '../helpers/setup_hive.mocks.dart';
+
+// This test is being written believing that in future when Encryptor class will get rid of shouldEncrypt variable then all the tests using that variable can be removed
+void main() {
+  group('When shouldEncrypt is true', () {
+    test('encryptString method should return the encrypted string', () {
+      Encryptor.shouldEncrypt = true;
+      const String inputString = 'password123';
+      final String outputString =
+          sha256.convert(utf8.encode(inputString)).toString();
+      expect(Encryptor.encryptString(inputString), outputString);
+    });
+  });
+// As the bool variable is not used anywhere except in this method so moving ahead with making this variable as false
+// On getting rid of this variable the above test can be removed
+  group('When shouldEncrypt is false', () {
+    late Encryptor encryptor;
+    late AsymmetricKeyPair<PublicKey, PrivateKey> keyPair;
+    late pointy.PublicKey publicKey;
+    late MockHiveInterface mockHiveInterface;
+    late MockBox<AsymetricKeys> mockHiveBox;
+    setUpAll(() {
+      encryptor = Encryptor();
+      Encryptor.shouldEncrypt = false;
+      keyPair = encryptor.generateRSAKeyPair();
+      publicKey = keyPair.publicKey;
+      mockHiveInterface = MockHiveInterface();
+      mockHiveBox = MockBox();
+    });
+    test('encryptString method should return the same string', () {
+      const String inputString = 'password123';
+      expect(Encryptor.encryptString(inputString), inputString);
+    });
+    test(
+        'Correct security options are added while generating keys from generateRSAKeyPair method',
+        () {
+      expect(publicKey, isA<RSAPublicKey>());
+      final RSAPublicKey key = publicKey as RSAPublicKey;
+      expect(key.publicExponent, BigInt.parse('65537'));
+      expect(key.modulus!.bitLength, 2048);
+    });
+    test(
+        'Checking whether Box Names and Key Names are appropriate for Hive or not',
+        () async {
+      when(mockHiveInterface.openBox<AsymetricKeys>('user_keys'))
+          .thenAnswer((realInvocation) async {
+        return mockHiveBox;
+      });
+      await encryptor.saveKeyPair(keyPair, mockHiveInterface);
+      verify(mockHiveInterface.openBox('user_keys'));
+      verify(mockHiveBox.put('key_pair', any));
+    });
+    test('For loadPairKey()', () async {
+      when(mockHiveInterface.openBox<AsymetricKeys>('user_keys'))
+          .thenAnswer((realInvocation) async {
+        return mockHiveBox;
+      });
+      when(mockHiveBox.get('key_pair'))
+          .thenAnswer((realInvocation) => AsymetricKeys(keyPair: keyPair));
+      await encryptor.loadKeyPair(mockHiveInterface);
+      verify(mockHiveBox.get('key_pair'));
+    });
+    test(
+        'For checking whether the data is properly encrypted through RSA by assymetricEncryptString()',
+        () {
+      const String data = 'Hello Talawa';
+      final output = encryptor.assymetricEncryptString(
+        data,
+        keyPair.publicKey as RSAPublicKey,
+      );
+      final cipher = OAEPEncoding(RSAEngine())
+        ..init(false, PrivateKeyParameter<RSAPrivateKey>(keyPair.privateKey));
+      final decryptedBytes = cipher.process(base64Decode(output));
+      expect(String.fromCharCodes(decryptedBytes), data);
+    });
+    test(
+        'For checking whether the data is getting decrypted properly through assymetricDecryptString()',
+        () {
+      const String data = 'Hello Talawa';
+      final cipher = OAEPEncoding(RSAEngine())
+        ..init(true, PublicKeyParameter<RSAPublicKey>(keyPair.publicKey));
+      final encryptedBytes = cipher.process(Uint8List.fromList(data.codeUnits));
+      final output = encryptor.assymetricDecryptString(
+        base64Encode(encryptedBytes),
+        keyPair.privateKey as RSAPrivateKey,
+      );
+      expect(output, data);
+    });
+    test('Test for wrong message sent to receiveMessage()', () {
+      const Map<String, dynamic> message = {"message": "Hello Talawa"};
+      expect(
+        encryptor.receiveMessage(message, mockHiveInterface),
+        throwsException,
+      );
+    });
+    test('Unencrypted message sent to receiveMessage()', () {
+      const Map<String, dynamic> message = {"encryptedMessage": "Hello Talawa"};
+      when(mockHiveInterface.openBox<AsymetricKeys>('user_keys'))
+          .thenAnswer((realInvocation) async {
+        return mockHiveBox;
+      });
+      when(mockHiveBox.get('key_pair'))
+          .thenAnswer((realInvocation) => AsymetricKeys(keyPair: keyPair));
+      expect(
+        encryptor.receiveMessage(message, mockHiveInterface),
+        throwsException,
+      );
+    });
+    test('Encrypted Message sent to receiveMessage()', () {
+      const String data = 'Hello Talawa';
+      final cipher = OAEPEncoding(RSAEngine())
+        ..init(true, PublicKeyParameter<RSAPublicKey>(keyPair.publicKey));
+      final encryptedBytes = cipher.process(Uint8List.fromList(data.codeUnits));
+      final String encryptedMessge = base64Encode(encryptedBytes);
+      final Map<String, dynamic> message = {
+        "encryptedMessage": encryptedMessge,
+      };
+      when(mockHiveInterface.openBox<AsymetricKeys>('user_keys'))
+          .thenAnswer((realInvocation) async {
+        return mockHiveBox;
+      });
+      when(mockHiveBox.get('key_pair'))
+          .thenAnswer((realInvocation) => AsymetricKeys(keyPair: keyPair));
+      expect(encryptor.receiveMessage(message, mockHiveInterface), completes);
+    });
+  });
+}


Fixing bug to join or switch to any joined org

**What kind of change does this PR introduce?**

Fix the bug to switch or join new org.

**Issue Number:**

Fixes #2199

**Did you add tests for your changes?**

No

**Have you read the [contributing guide](https://github.com/PalisadoesFoundation/talawa/blob/master/CONTRIBUTING.md)?**

Yes

Switching organizations fails when organization exists
**Describe the bug**
- Please work with @Ayush0Chaudhary for comments, advice and review of your PR.
- Switching organizations fails even when the organization exists
- A `No organization found! Please contact your admin` message is seen

**To Reproduce**
Steps to reproduce the behavior:

1.  Log into the app
2. Access the left drawer to switch organizations
3. Click Join new organization
4. Select organization
5. Error occurs

**Expected behavior**
- No error should occur
- If the user is already a member of the organization, then the transition must be seamless
- If the user is not a member, then a registration screen or something similar must be viewed

**Actual behavior**
- Error occurs

**Screenshots**
- https://www.youtube.com/watch?v=86AzK_Y_GCQ&t=711s
- https://youtu.be/86AzK_Y_GCQ?t=764 This could be related to the Firebase implementation. Ideally we don't want Firebase integration. You may have to work with an API and Admin person on this.

**Additional details**
- You must get the test **_code coverage up to 100%_** for any edited files.

**Potential internship candidates**
Please read this if you are planning to apply for a Palisadoes Foundation internship https://github.com/PalisadoesFoundation/talawa/issues/359

I would like to work on this issue. Can you please assign it to me?

I would like to work on this @noman2002 @palisadoes

diff --git a/lib/models/organization/org_info.dart b/lib/models/organization/org_info.dart
index a72873f562..1ab789ad52 100644
--- a/lib/models/organization/org_info.dart
+++ b/lib/models/organization/org_info.dart
@@ -66,15 +66,25 @@ class OrgInfo {
   ///
   /// **returns**:
   /// * `List<OrgInfo>`: returning the OrgInfo object containing the json data
-  List<OrgInfo> fromJsonToList(List<dynamic> json) {
+  List<OrgInfo> fromJsonToList(dynamic json) {
     final List<OrgInfo> orgList = [];
-    json.forEach((element) {
-      if (element is Map<String, dynamic>) {
-        // print(68);
-        final OrgInfo org = OrgInfo.fromJson(element);
-        orgList.add(org);
+
+    if (json is List) {
+      for (final dynamic outerElement in json) {
+        if (outerElement is List) {
+          for (final dynamic innerElement in outerElement) {
+            if (innerElement is Map<String, dynamic>) {
+              final OrgInfo org = OrgInfo.fromJson(innerElement);
+              orgList.add(org);
+            }
+          }
+        } else if (outerElement is Map<String, dynamic>) {
+          final OrgInfo org = OrgInfo.fromJson(outerElement);
+          orgList.add(org);
+        }
       }
-    });
+    }
+
     return orgList;
   }

diff --git a/lib/widgets/organization_list.dart b/lib/widgets/organization_list.dart
index 1228d8d13c..a2a01ad79b 100644
--- a/lib/widgets/organization_list.dart
+++ b/lib/widgets/organization_list.dart
@@ -63,7 +63,7 @@ class OrganizationList extends StatelessWidget {
             if (!result.isLoading) {
               // print(result.data!['organizationsConnection']);
               model.organizations = OrgInfo().fromJsonToList(
-                result.data!['organizationsConnection'] as List,
+                result.data!['organizationsConnection'],
               );
             }

diff --git a/lib/widgets/organization_search_list.dart b/lib/widgets/organization_search_list.dart
index 9998e4a293..7b432b1bd3 100644
--- a/lib/widgets/organization_search_list.dart
+++ b/lib/widgets/organization_search_list.dart
@@ -1,6 +1,3 @@
-// ignore_for_file: talawa_api_doc
-// ignore_for_file: talawa_good_doc_comments
-
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:graphql_flutter/graphql_flutter.dart';
@@ -13,10 +10,11 @@ import 'package:talawa/view_model/pre_auth_view_models/select_organization_view_
 import 'package:talawa/widgets/custom_list_tile.dart';
 import 'package:visibility_detector/visibility_detector.dart';

-/// OrganizationSearchList class return a widget that shows all
-/// the matching organizations searched on the search bar.
+/// This class returns the widget that shows all the matching orgs searched in the search bar.
 class OrganizationSearchList extends StatelessWidget {
   const OrganizationSearchList({required this.model, super.key});
+
+  /// model constructor for the selectOrganisation widget.
   final SelectOrganizationViewModel model;

   @override
@@ -53,7 +51,7 @@ class OrganizationSearchList extends StatelessWidget {
             // If the result is still loading!
             if (!result.isLoading) {
               model.organizations = OrgInfo().fromJsonToList(
-                result.data!['organizationsConnection'] as List,
+                result.data!['organizationsConnection'],
               );
             }
             // return the Scroll bar widget for scrolling down the organizations.
diff --git a/lib/widgets/talawa_error_dialog.dart b/lib/widgets/talawa_error_dialog.dart
index a37a0abac0..7b0e0be8ec 100644
--- a/lib/widgets/talawa_error_dialog.dart
+++ b/lib/widgets/talawa_error_dialog.dart
@@ -1,20 +1,23 @@
-// ignore_for_file: talawa_api_doc
-// ignore_for_file: talawa_good_doc_comments
-
 import 'package:auto_size_text/auto_size_text.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_svg/svg.dart';

 import 'package:talawa/enums/enums.dart';
+import 'package:talawa/services/size_config.dart';
 import 'package:talawa/utils/app_localization.dart';

+/// Common Error Dialog box for the whole talawa app.
 class TalawaErrorDialog extends StatelessWidget {
   const TalawaErrorDialog(
     this.errorMessage, {
     super.key,
     required this.messageType,
   });
+
+  /// error message for the talawaDialogBox.
   final String errorMessage;
+
+  /// enum to what is the type of error message.
   final MessageType messageType;

   @override
@@ -22,8 +25,8 @@ class TalawaErrorDialog extends StatelessWidget {
     return SizedBox(
       child: AlertDialog(
         content: SizedBox(
-          width: 200,
-          height: 135,
+          width: SizeConfig.screenWidth! * 0.65,
+          height: SizeConfig.screenWidth! * 0.38,
           child: Column(
             children: [
               SvgPicture.asset(
@@ -74,10 +77,12 @@ class TalawaErrorDialog extends StatelessWidget {
               const SizedBox(
                 height: 5,
               ),
-              AutoSizeText(
-                AppLocalizations.of(context)!.strictTranslate(errorMessage),
-                style: const TextStyle(fontSize: 16),
-                maxLines: 3,
+              Expanded(
+                child: AutoSizeText(
+                  AppLocalizations.of(context)!.strictTranslate(errorMessage),
+                  style: const TextStyle(fontSize: 16),
+                  maxLines: 3,
+                ),
               ),
               const SizedBox(
                 height: 19,

diff --git a/test/model_tests/organization/org_info_test.dart b/test/model_tests/organization/org_info_test.dart
index 7e1ad7c1e1..fb96537579 100644
--- a/test/model_tests/organization/org_info_test.dart
+++ b/test/model_tests/organization/org_info_test.dart
@@ -106,5 +106,70 @@ void main() {
         expect(result.admins![0].id, 'user_id');
       }
     });
+
+    test('Test fromJsonToList with nested structures', () {
+      final List<dynamic> nestedJson = [
+        [json1, json2], // Nested list with org information
+        json1, // Single org information
+      ];
+
+      final List<OrgInfo> res = OrgInfo().fromJsonToList(nestedJson);
+
+      expect(
+        res.length,
+        3,
+      ); // There are two orgs in the nested list and one in the single org information
+
+      expect(res[0].id, '123');
+      expect(res[0].image, 'image_url');
+      expect(res[0].name, 'Name');
+      expect(res[0].description, 'Description');
+      expect(res[0].isPublic, true);
+      expect(res[0].creatorInfo!.authToken, ' ');
+      expect(res[0].creatorInfo!.refreshToken, ' ');
+      expect(res[0].creatorInfo!.id, 'user_id');
+      expect(res[0].members!.length, 1);
+      expect(res[0].members![0].authToken, ' ');
+      expect(res[0].members![0].refreshToken, ' ');
+      expect(res[0].members![0].id, 'user_id');
+      expect(res[0].admins!.length, 1);
+      expect(res[0].admins![0].authToken, ' ');
+      expect(res[0].admins![0].refreshToken, ' ');
+      expect(res[0].admins![0].id, 'user_id');
+
+      expect(res[1].id, '321');
+      expect(res[1].image, 'image_url');
+      expect(res[1].name, 'Name');
+      expect(res[1].description, 'Description');
+      expect(res[1].isPublic, true);
+      expect(res[1].creatorInfo!.authToken, ' ');
+      expect(res[1].creatorInfo!.refreshToken, ' ');
+      expect(res[1].creatorInfo!.id, 'user_id');
+      expect(res[1].members!.length, 1);
+      expect(res[1].members![0].authToken, ' ');
+      expect(res[1].members![0].refreshToken, ' ');
+      expect(res[1].members![0].id, 'user_id');
+      expect(res[1].admins!.length, 1);
+      expect(res[1].admins![0].authToken, ' ');
+      expect(res[1].admins![0].refreshToken, ' ');
+      expect(res[1].admins![0].id, 'user_id');
+
+      expect(res[2].id, '123');
+      expect(res[2].image, 'image_url');
+      expect(res[2].name, 'Name');
+      expect(res[2].description, 'Description');
+      expect(res[2].isPublic, true);
+      expect(res[2].creatorInfo!.authToken, ' ');
+      expect(res[2].creatorInfo!.refreshToken, ' ');
+      expect(res[2].creatorInfo!.id, 'user_id');
+      expect(res[2].members!.length, 1);
+      expect(res[2].members![0].authToken, ' ');
+      expect(res[2].members![0].refreshToken, ' ');
+      expect(res[2].members![0].id, 'user_id');
+      expect(res[2].admins!.length, 1);
+      expect(res[2].admins![0].authToken, ' ');
+      expect(res[2].admins![0].refreshToken, ' ');
+      expect(res[2].admins![0].id, 'user_id');
+    });
   });
 }


compose: Fix bug where topic input not filled on quote-and-reply

Fixes #1469.

compose: On recent servers, quote-and-reply doesn't fill empty topic input
To reproduce:

1. Connect to CZO or another realm with feature level 334+
2. Enter a channel narrow
3. Long-press a message; tap "Quote and reply"
4. See the content input update with the quoted message

Expected: the topic input is automatically filled with the message's topic

Actual: the topic input remains blank

-----

This looks like a regression from the recent "general chat" work. To check if the topic input was left empty, the quote-and-reply button checks the condition `composeBoxController.topic.textNormalized == kNoTopicTopic`. Before 769cc7df405562d5f99a869ed009662e2b5801b6, that condition was a reliable test of whether the topic input was left empty. But it isn't reliable anymore; on recent servers, if the topic input is left empty, `composeBoxController.topic.textNormalized` will be the empty string.

Assigning @PIG208 as the author of 769cc7df405562d5f99a869ed009662e2b5801b6.

flutter test test/widgets/action_sheet_test.dart --reporter expanded 2>&1

diff --git a/lib/widgets/action_sheet.dart b/lib/widgets/action_sheet.dart
index 30bbf433f5..c56e41d5c2 100644
--- a/lib/widgets/action_sheet.dart
+++ b/lib/widgets/action_sheet.dart
@@ -1409,7 +1409,7 @@ class QuoteAndReplyButton extends MessageActionSheetMenuItemButton {
     composeBoxController!;
     if (
       composeBoxController is StreamComposeBoxController
-      && composeBoxController.topic.textNormalized == kNoTopicTopic
+      && composeBoxController.topic.isTopicVacuous
       && message is StreamMessage
     ) {
       composeBoxController.topic.setTopic(message.topic);

diff --git a/test/model/autocomplete_checks.dart b/test/model/autocomplete_checks.dart
index cb08378e77..65e3aaf755 100644
--- a/test/model/autocomplete_checks.dart
+++ b/test/model/autocomplete_checks.dart
@@ -10,6 +10,7 @@ extension ComposeContentControllerChecks on Subject<ComposeContentController> {

 extension ComposeTopicControllerChecks on Subject<ComposeTopicController> {
   Subject<AutocompleteIntent<TopicAutocompleteQuery>?> get autocompleteIntent => has((c) => c.autocompleteIntent(), 'autocompleteIntent');
+  Subject<String> get textNormalized => has((c) => c.textNormalized, 'textNormalized');
 }

 extension AutocompleteIntentChecks on Subject<AutocompleteIntent<AutocompleteQuery>> {
diff --git a/test/widgets/action_sheet_test.dart b/test/widgets/action_sheet_test.dart
index 621f759f28..c43c6daab7 100644
--- a/test/widgets/action_sheet_test.dart
+++ b/test/widgets/action_sheet_test.dart
@@ -41,6 +41,7 @@ import '../api/fake_api.dart';
 import '../api/model/model_checks.dart';
 import '../example_data.dart' as eg;
 import '../flutter_checks.dart';
+import '../model/autocomplete_checks.dart';
 import '../model/binding.dart';
 import '../model/content_test.dart';
 import '../model/test_store.dart';
@@ -1674,8 +1675,33 @@ void main() {
         check(contentController).not((it) => it.validationErrors.contains(ContentValidationError.quoteAndReplyInProgress));
       }

-      testWidgets('in channel narrow', (tester) async {
-        final message = eg.streamMessage();
+      testWidgets('in channel narrow with different, non-vacuous topic', (tester) async {
+        final message = eg.streamMessage(topic: 'some topic');
+        await setupToMessageActionSheet(tester, message: message, narrow: ChannelNarrow(message.streamId));
+
+        final composeBoxController = findComposeBoxController(tester) as StreamComposeBoxController;
+        final contentController = composeBoxController.content;
+
+        // Ensure channel-topics are loaded before testing quote & reply behavior
+        connection.prepare(body:
+          jsonEncode(GetStreamTopicsResult(topics: [eg.getStreamTopicsEntry()]).toJson()));
+        final topicController = composeBoxController.topic;
+        topicController.value = TextEditingValue(text: 'other topic');
+
+        final valueBefore = contentController.value;
+        prepareRawContentResponseSuccess(message: message, rawContent: 'Hello world');
+        await tapQuoteAndReplyButton(tester);
+        checkLoadingState(store, contentController, valueBefore: valueBefore, message: message);
+        await tester.pump(Duration.zero); // message is fetched; compose box updates
+        check(composeBoxController.contentFocusNode.hasFocus).isTrue();
+        checkSuccessState(store, contentController,
+          valueBefore: valueBefore, message: message, rawContent: 'Hello world');
+        check(topicController).textNormalized.equals('other topic');
+      });
+
+      testWidgets('in channel narrow with empty topic', (tester) async {
+        // Regression test for https://github.com/zulip/zulip-flutter/issues/1469
+        final message = eg.streamMessage(topic: 'some topic');
         await setupToMessageActionSheet(tester, message: message, narrow: ChannelNarrow(message.streamId));

         final composeBoxController = findComposeBoxController(tester) as StreamComposeBoxController;
@@ -1685,7 +1711,7 @@ void main() {
         connection.prepare(body:
           jsonEncode(GetStreamTopicsResult(topics: [eg.getStreamTopicsEntry()]).toJson()));
         final topicController = composeBoxController.topic;
-        topicController.value = const TextEditingValue(text: kNoTopicTopic);
+        topicController.value = const TextEditingValue(text: '');

         final valueBefore = contentController.value;
         prepareRawContentResponseSuccess(message: message, rawContent: 'Hello world');
@@ -1695,6 +1721,7 @@ void main() {
         check(composeBoxController.contentFocusNode.hasFocus).isTrue();
         checkSuccessState(store, contentController,
           valueBefore: valueBefore, message: message, rawContent: 'Hello world');
+        check(topicController).textNormalized.equals('some topic');
       });

       group('in topic narrow', () {


all_channels: Replace three dot menu with long press gesture and add on tap to feed

Fixes #1914

Before and After still

### Page View
| Before | After |
| --- | --- |
| |  |

After Max Sys Font

| After Max Sys Font |
| --- |
|

Long Press View

| Long Press on Public Channel |  ~~Long Press on Private Channel~~ Not part of this PR anymore |
| --- | --- |
|  |  |

Gesture Press Views

## Gesture press in default sys font
https://github.com/user-attachments/assets/de182d27-9f3f-4304-8ffe-5856f0669f6b

## ~Unsub from private channel 200 milliseconds fade~. Not part of PR
https://github.com/user-attachments/assets/7f1b0f99-8ae5-47bf-8b05-764614e53a39

Make "All channels" view more similar to "Channels"

We should make the UI of the "All channels" view match the "Channels" view better:

- Click on the row should navigate to the channel.
- Long press should open the channel menu.
- Remove the `...` buttons, as users can follow the regular pattern of opening the menu with a long press when needed.

flutter test test/widgets/all_channels_test.dart --reporter expanded 2>&1

diff --git a/lib/widgets/all_channels.dart b/lib/widgets/all_channels.dart
index 3aaf0d8baf..7d662bcc2d 100644
--- a/lib/widgets/all_channels.dart
+++ b/lib/widgets/all_channels.dart
@@ -5,11 +5,13 @@ import '../api/route/channels.dart';
 import '../generated/l10n/zulip_localizations.dart';
 import '../log.dart';
 import '../model/channel.dart';
+import '../model/narrow.dart';
 import 'action_sheet.dart';
 import 'actions.dart';
 import 'app_bar.dart';
 import 'button.dart';
 import 'icons.dart';
+import 'message_list.dart';
 import 'page.dart';
 import 'remote_settings.dart';
 import 'store.dart';
@@ -96,28 +98,30 @@ class AllChannelsListEntry extends StatelessWidget {
     final Subscription? subscription = channel is Subscription ? channel : null;
     final hasContentAccess = store.selfHasContentAccess(channel);

-    return Padding(
-      padding: EdgeInsetsDirectional.only(start: 8, end: 4),
-      child: Row(spacing: 6, children: [
-        Icon(
-          size: 20,
-          color: colorSwatchFor(context, subscription).iconOnPlainBackground,
-          iconDataForStream(channel)),
-        Expanded(
-          child: Text(
-            style: TextStyle(
-              color: designVariables.textMessage,
-              fontSize: 17,
-              height: 20 / 17,
-            ).merge(weightVariableTextStyle(context, wght: 600)),
-            channel.name)),
-        if (hasContentAccess) _SubscribeToggle(channel: channel),
-        ZulipIconButton(
-          icon: ZulipIcons.more_horizontal,
-          onPressed: () {
-            showChannelActionSheet(context, channelId: channel.streamId);
-          }),
-      ]));
+    return InkWell(
+      onTap: !hasContentAccess ? null : () => Navigator.push(context,
+        MessageListPage.buildRoute(context: context,
+          narrow: ChannelNarrow(channel.streamId))),
+      onLongPress: () => showChannelActionSheet(context, channelId: channel.streamId),
+      child: ConstrainedBox(constraints: const BoxConstraints(minHeight: 44),
+        child: Padding(padding: const EdgeInsetsDirectional.only(start: 8, end: 12),
+          child: Row(spacing: 6, children: [
+            Icon(
+              size: 20,
+              color: colorSwatchFor(context, subscription).iconOnPlainBackground,
+              iconDataForStream(channel)),
+            Expanded(
+              child: Text(
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+                style: TextStyle(
+                  color: designVariables.textMessage,
+                  fontSize: 17,
+                  height: 20 / 17,
+                ).merge(weightVariableTextStyle(context, wght: 600)),
+                channel.name)),
+            if (hasContentAccess) _SubscribeToggle(channel: channel),
+          ]))));
   }
 }

diff --git a/test/widgets/all_channels_test.dart b/test/widgets/all_channels_test.dart
index b47546be40..f6e1092781 100644
--- a/test/widgets/all_channels_test.dart
+++ b/test/widgets/all_channels_test.dart
@@ -12,6 +12,7 @@ import 'package:zulip/widgets/app_bar.dart';
 import 'package:zulip/widgets/button.dart';
 import 'package:zulip/widgets/home.dart';
 import 'package:zulip/widgets/icons.dart';
+import 'package:zulip/widgets/message_list.dart';
 import 'package:zulip/widgets/page.dart';
 import 'package:zulip/widgets/remote_settings.dart';
 import 'package:zulip/widgets/theme.dart';
@@ -203,20 +204,37 @@ void main() {
         check(maybeToggle).isNull();
       }

-      check(findInRow(find.byIcon(ZulipIcons.more_horizontal))).findsOne();
+      final touchTargetSize = tester.getSize(findElement);
+      check(touchTargetSize.height).equals(44);
     }
   });

-  testWidgets('tapping three-dots button opens channel action sheet', (tester) async {
+  testWidgets('open channel action sheet on long press', (tester) async {
     await setupAllChannelsPage(tester, channels: [eg.stream()]);

-    await tester.tap(find.byIcon(ZulipIcons.more_horizontal));
+    await tester.longPress(find.byType(AllChannelsListEntry));
     await tester.pump();
     await transitionDurationObserver.pumpPastTransition(tester);

     check(find.byType(BottomSheet)).findsOne();
   });

+  testWidgets('navigate to channel feed on tap', (tester) async {
+    final channel = eg.stream(name: 'some-channel');
+    await setupAllChannelsPage(tester, channels: [channel]);
+
+    connection.prepare(json: eg.newestGetMessagesResult(
+      foundOldest: true, messages: [eg.streamMessage(stream: channel)]).toJson());
+    await tester.tap(find.byType(AllChannelsListEntry));
+    await tester.pump();
+    await transitionDurationObserver.pumpPastTransition(tester);
+
+    check(find.descendant(
+      of: find.byType(MessageListPage),
+      matching: find.text('some-channel')),
+    ).findsOne();
+  });
+
   testWidgets('use toggle switch to subscribe/unsubscribe', (tester) async {
     final channel = eg.stream();
     await setupAllChannelsPage(tester, channels: [channel]);


settings: Make page scrollable

| Before | After (Screenshot) |
| ------- | ------------------ |
| ![column_l](https://github.com/user-attachments/assets/5218af2f-3fe8-445a-8380-69694b0f7301) | ![listview_l](https://github.com/user-attachments/assets/21f84210-aeba-49cb-859d-1c7389e6bb84) |
| ![column_d](https://github.com/user-attachments/assets/d4722bf7-5ecd-419f-9013-2799a3022111) | ![listview_d](https://github.com/user-attachments/assets/d949e2bb-199e-470c-8361-a99187f3848b) |

### Video Recordings

### Before

https://github.com/user-attachments/assets/03716367-b5d9-4f83-84f4-3716abb59829

### After (Normal)

https://github.com/user-attachments/assets/92a05eab-6789-4811-bb95-271838c864f3

### After (Increased Text Size to unreasonably high in android settings to test edge case)

https://github.com/user-attachments/assets/c070507f-8a4f-4fcf-8f4f-5731bef5f9d3

Fixes #1904

Settings page should scroll

Settings page should scroll
The settings page should be scrollable if the content doesn't all fit on the screen.

Currently there are only a few settings, so for most users this doesn't happen. But it can happen if the user has a very small screen (https://github.com/zulip/zulip-flutter/issues/1898#issuecomment-3386032763) or have their display zoomed in for accessibility. In the future it'll probably happen for more users, as we add more settings.

flutter test test/widgets/settings_test.dart --reporter expanded 2>&1

diff --git a/lib/widgets/settings.dart b/lib/widgets/settings.dart
index 5995cdcbfe..bad16c336b 100644
--- a/lib/widgets/settings.dart
+++ b/lib/widgets/settings.dart
@@ -20,7 +20,7 @@ class SettingsPage extends StatelessWidget {
     return Scaffold(
       appBar: ZulipAppBar(
         title: Text(zulipLocalizations.settingsPageTitle)),
-      body: Column(children: [
+      body: ListView(children: [
         const _ThemeSetting(),
         const _BrowserPreferenceSetting(),
         const _VisitFirstUnreadSetting(),

diff --git a/test/widgets/settings_test.dart b/test/widgets/settings_test.dart
index e889fa0d3d..a56b93d7d9 100644
--- a/test/widgets/settings_test.dart
+++ b/test/widgets/settings_test.dart
@@ -64,6 +64,21 @@ void main() {
         hasCheckedState: true, isChecked: expectedIsChecked);
   }

+  testWidgets('SettingsPage is scrollable when taller than a screenful', (tester) async {
+    tester.view.physicalSize = const Size(200, 200);
+    tester.view.devicePixelRatio = 1.0;
+    addTearDown(tester.view.reset);
+    await prepare(tester);
+
+    final lastElementFinder = GlobalSettingsStore.experimentalFeatureFlags.isNotEmpty
+      ? find.text("Experimental features")
+      : find.text("Mark messages as read on scroll");
+    check(lastElementFinder).findsNothing();
+
+    await tester.scrollUntilVisible(lastElementFinder, 100);
+    check(lastElementFinder).findsOne();
+  });
+
   group('ThemeSetting', () {
     void checkThemeSetting(WidgetTester tester, {
       required ThemeSetting? expectedThemeSetting,


Open uploaded files using authed temporary URLs

Fixes #1732.

Open uploaded files without logging in again
Currently if you're looking at a Zulip message that contains an uploaded file (other than an image or video, which we handle directly in the message feed) — for example a PDF — and you click the link to try to open the file, you get a Zulip login page.

This ultimately works; you just have to do the login, and then you can see the file. But it'd be smoother if the file opened without that step; after all, you were already logged in in the Zulip app.

The legacy app had this feature; see https://github.com/zulip/zulip-mobile/issues/3303 where it was requested, and https://github.com/zulip/zulip-mobile/pull/4089 for the implementation. We'll use the same feature of the Zulip server API as the legacy app did for this; see [getFileTemporaryUrl.js](https://github.com/zulip/zulip-mobile/blob/eb8505c4a0ccfb7bdd817ca9022c11ce57ab69d8/src/api/messages/getFileTemporaryUrl.js).

### References
* Chat thread: [#mobile > can't open uploaded PDF](https://chat.zulip.org/#narrow/channel/48-mobile/topic/can't.20open.20uploaded.20PDF/with/2226578)
* Old zulip-mobile issue: zulip/zulip-mobile#3303
* Implementation in zulip-mobile: zulip/zulip-mobile#4089
* API documentation: partial docs at https://zulip.com/api/get-file-temporary-url ; additional details in zulip-mobile's API binding at [zulip-mobile:src/api/messages/getFileTemporaryUrl.js](https://github.com/zulip/zulip-mobile/blob/eb8505c4a0ccfb7bdd817ca9022c11ce57ab69d8/src/api/messages/getFileTemporaryUrl.js).

Describing this with different words to help with finding it in search (after #1834; thanks @varkor): you might say you're trying to open or download a file that's been shared in a Zulip message.

flutter test test/model/internal_link_test.dart test/widgets/actions_test.dart test/widgets/content_test.dart --reporter expanded 2>&1

diff --git a/assets/l10n/app_en.arb b/assets/l10n/app_en.arb
index 9048fa98de..1f52ac32ba 100644
--- a/assets/l10n/app_en.arb
+++ b/assets/l10n/app_en.arb
@@ -329,6 +329,10 @@
   "@errorCouldNotFetchMessageSource": {
     "description": "Error message when the source of a message could not be fetched."
   },
+  "errorCouldNotAccessUploadedFileTitle": "Could not access uploaded file",
+  "@errorCouldNotAccessUploadedFileTitle": {
+    "description": "Error title on failure in opening a file someone previously uploaded to Zulip"
+  },
   "errorCopyingFailed": "Copying failed",
   "@errorCopyingFailed": {
     "description": "Error message when copying the text of a message to the user's system clipboard failed."
diff --git a/lib/api/route/messages.dart b/lib/api/route/messages.dart
index 6d0487f0d7..73ecb02af9 100644
--- a/lib/api/route/messages.dart
+++ b/lib/api/route/messages.dart
@@ -285,6 +285,29 @@ class UploadFileResult {
   Map<String, dynamic> toJson() => _$UploadFileResultToJson(this);
 }

+/// https://zulip.com/api/get-file-temporary-url
+Future<GetFileTemporaryUrlResult> getFileTemporaryUrl(ApiConnection connection, {
+  required int realmId,
+  required String filename,
+}) {
+  return connection.get('getFileTemporaryUrl', GetFileTemporaryUrlResult.fromJson,
+    'user_uploads/$realmId/$filename', {});
+}
+
+@JsonSerializable(fieldRename: FieldRename.snake)
+class GetFileTemporaryUrlResult {
+  final String url;
+
+  GetFileTemporaryUrlResult({
+    required this.url,
+  });
+
+  factory GetFileTemporaryUrlResult.fromJson(Map<String, dynamic> json) =>
+    _$GetFileTemporaryUrlResultFromJson(json);
+
+  Map<String, dynamic> toJson() => _$GetFileTemporaryUrlResultToJson(this);
+}
+
 /// https://zulip.com/api/add-reaction
 Future<void> addReaction(ApiConnection connection, {
   required int messageId,
diff --git a/lib/api/route/messages.g.dart b/lib/api/route/messages.g.dart
index 4169634701..bf07f1f153 100644
--- a/lib/api/route/messages.g.dart
+++ b/lib/api/route/messages.g.dart
@@ -55,6 +55,14 @@ UploadFileResult _$UploadFileResultFromJson(Map<String, dynamic> json) =>
 Map<String, dynamic> _$UploadFileResultToJson(UploadFileResult instance) =>
     <String, dynamic>{'uri': instance.url};

+GetFileTemporaryUrlResult _$GetFileTemporaryUrlResultFromJson(
+  Map<String, dynamic> json,
+) => GetFileTemporaryUrlResult(url: json['url'] as String);
+
+Map<String, dynamic> _$GetFileTemporaryUrlResultToJson(
+  GetFileTemporaryUrlResult instance,
+) => <String, dynamic>{'url': instance.url};
+
 UpdateMessageFlagsResult _$UpdateMessageFlagsResultFromJson(
   Map<String, dynamic> json,
 ) => UpdateMessageFlagsResult(
diff --git a/lib/generated/l10n/zulip_localizations.dart b/lib/generated/l10n/zulip_localizations.dart
index 7a08e9d986..d1d40b2010 100644
--- a/lib/generated/l10n/zulip_localizations.dart
+++ b/lib/generated/l10n/zulip_localizations.dart
@@ -593,6 +593,12 @@ abstract class ZulipLocalizations {
   /// **'Could not fetch message source.'**
   String get errorCouldNotFetchMessageSource;

+  /// Error title on failure in opening a file someone previously uploaded to Zulip
+  ///
+  /// In en, this message translates to:
+  /// **'Could not access uploaded file'**
+  String get errorCouldNotAccessUploadedFileTitle;
+
   /// Error message when copying the text of a message to the user's system clipboard failed.
   ///
   /// In en, this message translates to:
diff --git a/lib/generated/l10n/zulip_localizations_ar.dart b/lib/generated/l10n/zulip_localizations_ar.dart
index 759cd06a01..1a292881ec 100644
--- a/lib/generated/l10n/zulip_localizations_ar.dart
+++ b/lib/generated/l10n/zulip_localizations_ar.dart
@@ -277,6 +277,10 @@ class ZulipLocalizationsAr extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Could not fetch message source.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Copying failed';

diff --git a/lib/generated/l10n/zulip_localizations_de.dart b/lib/generated/l10n/zulip_localizations_de.dart
index 4975e37387..83cc660772 100644
--- a/lib/generated/l10n/zulip_localizations_de.dart
+++ b/lib/generated/l10n/zulip_localizations_de.dart
@@ -285,6 +285,10 @@ class ZulipLocalizationsDe extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Konnte Nachrichtenquelle nicht abrufen.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Kopieren fehlgeschlagen';

diff --git a/lib/generated/l10n/zulip_localizations_en.dart b/lib/generated/l10n/zulip_localizations_en.dart
index 4ad9cf4f51..80c1c2c475 100644
--- a/lib/generated/l10n/zulip_localizations_en.dart
+++ b/lib/generated/l10n/zulip_localizations_en.dart
@@ -277,6 +277,10 @@ class ZulipLocalizationsEn extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Could not fetch message source.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Copying failed';

diff --git a/lib/generated/l10n/zulip_localizations_fr.dart b/lib/generated/l10n/zulip_localizations_fr.dart
index 402619efec..7572aa5d3f 100644
--- a/lib/generated/l10n/zulip_localizations_fr.dart
+++ b/lib/generated/l10n/zulip_localizations_fr.dart
@@ -284,6 +284,10 @@ class ZulipLocalizationsFr extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Impossible d\'atteindre le message source.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Échec de la copie';

diff --git a/lib/generated/l10n/zulip_localizations_it.dart b/lib/generated/l10n/zulip_localizations_it.dart
index ce18a1d568..e42ef1f08e 100644
--- a/lib/generated/l10n/zulip_localizations_it.dart
+++ b/lib/generated/l10n/zulip_localizations_it.dart
@@ -283,6 +283,10 @@ class ZulipLocalizationsIt extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Impossibile recuperare l\'origine del messaggio.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Copia non riuscita';

diff --git a/lib/generated/l10n/zulip_localizations_ja.dart b/lib/generated/l10n/zulip_localizations_ja.dart
index 4e75bf6568..eae3a0c3eb 100644
--- a/lib/generated/l10n/zulip_localizations_ja.dart
+++ b/lib/generated/l10n/zulip_localizations_ja.dart
@@ -272,6 +272,10 @@ class ZulipLocalizationsJa extends ZulipLocalizations {
   @override
   String get errorCouldNotFetchMessageSource => 'メッセージのソースを取得できませんでした。';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'コピーに失敗しました';

diff --git a/lib/generated/l10n/zulip_localizations_nb.dart b/lib/generated/l10n/zulip_localizations_nb.dart
index 344bb0372b..cc2c5e1852 100644
--- a/lib/generated/l10n/zulip_localizations_nb.dart
+++ b/lib/generated/l10n/zulip_localizations_nb.dart
@@ -277,6 +277,10 @@ class ZulipLocalizationsNb extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Could not fetch message source.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Copying failed';

diff --git a/lib/generated/l10n/zulip_localizations_pl.dart b/lib/generated/l10n/zulip_localizations_pl.dart
index 45e9fb2cac..c7c14cf77e 100644
--- a/lib/generated/l10n/zulip_localizations_pl.dart
+++ b/lib/generated/l10n/zulip_localizations_pl.dart
@@ -288,6 +288,10 @@ class ZulipLocalizationsPl extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Nie można uzyskać źródłowej wiadomości.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Nie udało się skopiować';

diff --git a/lib/generated/l10n/zulip_localizations_ru.dart b/lib/generated/l10n/zulip_localizations_ru.dart
index 817d6b03f8..ae8ce816c4 100644
--- a/lib/generated/l10n/zulip_localizations_ru.dart
+++ b/lib/generated/l10n/zulip_localizations_ru.dart
@@ -289,6 +289,10 @@ class ZulipLocalizationsRu extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Не удалось извлечь источник сообщения.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Сбой копирования';

diff --git a/lib/generated/l10n/zulip_localizations_sk.dart b/lib/generated/l10n/zulip_localizations_sk.dart
index 2504894b25..ca321e40a6 100644
--- a/lib/generated/l10n/zulip_localizations_sk.dart
+++ b/lib/generated/l10n/zulip_localizations_sk.dart
@@ -278,6 +278,10 @@ class ZulipLocalizationsSk extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Nepodarilo sa nahrať zdroj správy';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Kopírovanie zlyhalo';

diff --git a/lib/generated/l10n/zulip_localizations_sl.dart b/lib/generated/l10n/zulip_localizations_sl.dart
index 933971c93a..3e72b30aac 100644
--- a/lib/generated/l10n/zulip_localizations_sl.dart
+++ b/lib/generated/l10n/zulip_localizations_sl.dart
@@ -286,6 +286,10 @@ class ZulipLocalizationsSl extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Ni bilo mogoče pridobiti vira sporočila.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Kopiranje ni uspelo';

diff --git a/lib/generated/l10n/zulip_localizations_uk.dart b/lib/generated/l10n/zulip_localizations_uk.dart
index 24076bc706..993cb65cdb 100644
--- a/lib/generated/l10n/zulip_localizations_uk.dart
+++ b/lib/generated/l10n/zulip_localizations_uk.dart
@@ -287,6 +287,10 @@ class ZulipLocalizationsUk extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Не вдалося отримати джерело повідомлення.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Помилка копіювання';

diff --git a/lib/generated/l10n/zulip_localizations_zh.dart b/lib/generated/l10n/zulip_localizations_zh.dart
index 4084a19aba..6d62f29892 100644
--- a/lib/generated/l10n/zulip_localizations_zh.dart
+++ b/lib/generated/l10n/zulip_localizations_zh.dart
@@ -277,6 +277,10 @@ class ZulipLocalizationsZh extends ZulipLocalizations {
   String get errorCouldNotFetchMessageSource =>
       'Could not fetch message source.';

+  @override
+  String get errorCouldNotAccessUploadedFileTitle =>
+      'Could not access uploaded file';
+
   @override
   String get errorCopyingFailed => 'Copying failed';

diff --git a/lib/model/internal_link.dart b/lib/model/internal_link.dart
index d46942f6d5..bb40fb98fa 100644
--- a/lib/model/internal_link.dart
+++ b/lib/model/internal_link.dart
@@ -128,6 +128,47 @@ class NarrowLink extends InternalLink {
   final int? nearMessageId;
 }

+/// A parsed link to an uploaded file in Zulip.
+///
+/// The structure mirrors the data required for [getFileTemporaryUrl]:
+///   https://zulip.com/api/get-file-temporary-url
+class UserUploadLink extends InternalLink {
+  UserUploadLink(this.realmId, this.path, {required super.realmUrl});
+
+  static UserUploadLink? _tryParse(String urlPath, Uri realmUrl) {
+    final match = _urlPathRegexp.matchAsPrefix(urlPath);
+    if (match == null) return null;
+    final realmId = int.parse(match.group(1)!, radix: 10);
+    return UserUploadLink(realmId, match.group(2)!, realmUrl: realmUrl);
+  }
+
+  static const _urlPathPrefix = '/user_uploads/';
+  static final _urlPathRegexp = RegExp(r'^/user_uploads/(\d+)/(.+)$');
+
+  final int realmId;
+
+  /// The remaining path components after the realm ID.
+  ///
+  /// This value excludes the slash that separates the realm ID from the
+  /// next component, but includes the rest of the URL path after that slash.
+  ///
+  /// This value should only be used as part of a URL, not elsewhere
+  /// in the Zulip API.  Other uses are likely to cause a new version
+  /// of the following issue:
+  ///   https://github.com/zulip/zulip-flutter/issues/1709
+  /// Concretely, this string might differ from the corresponding substring of
+  /// the original URL string (e.g., one found in the HTML of a Zulip message):
+  /// if there were non-ASCII characters in the original string,
+  /// then [Uri.parse] will have converted them to percent-encoded form.
+  /// This is fine as part of a URL, because then the HTTP client would
+  /// otherwise have had to percent-encode those characters anyway.
+  ///
+  /// This corresponds to `filename` in the arguments to [getFileTemporaryUrl];
+  /// but it's typically several path components,
+  /// not just one as that name would suggest.
+  final String path;
+}
+
 /// Try to parse the given URL as a page in this app, on `store`'s realm.
 ///
 /// `url` must already be a result from [PerAccountStore.tryResolveUrl]
@@ -148,27 +189,37 @@ class NarrowLink extends InternalLink {
 ///   #narrow/stream/1-announce/stream/1-announce (duplicated operator)
 // TODO(#1661): handle all valid narrow links, returning a search narrow
 InternalLink? parseInternalLink(Uri url, PerAccountStore store) {
-  if (!_isInternalLink(url, store.realmUrl)) return null;
-
-  final (category, segments) = _getCategoryAndSegmentsFromFragment(url.fragment);
-  switch (category) {
-    case 'narrow':
-      if (segments.isEmpty || !segments.length.isEven) return null;
-      return _interpretNarrowSegments(segments, store);
+  if (!_sameOrigin(url, store.realmUrl)) return null;
+
+  if ((url.hasEmptyPath || url.path == '/')) {
+    if (url.hasQuery) return null;
+    if (!url.hasFragment) return null;
+    // The URL is of the form `/#…` relative to the realm URL,
+    // the shape used for representing a state within the web app.
+    final (category, segments) = _getCategoryAndSegmentsFromFragment(url.fragment);
+    switch (category) {
+      case 'narrow':
+        if (segments.isEmpty || !segments.length.isEven) return null;
+        return _interpretNarrowSegments(segments, store);
+    }
+  } else if (url.path.startsWith(UserUploadLink._urlPathPrefix)) {
+    return UserUploadLink._tryParse(url.path, store.realmUrl);
   }
+
   return null;
 }

-/// Check if `url` is an internal link on the given `realmUrl`.
-bool _isInternalLink(Uri url, Uri realmUrl) {
+/// Check if `url` has the same origin as `realmUrl`.
+bool _sameOrigin(Uri url, Uri realmUrl) {
   try {
-    if (url.origin != realmUrl.origin) return false;
+    return url.origin == realmUrl.origin;
   } on StateError {
+    // The getter [Uri.origin] throws if the scheme is not "http" or "https".
+    // (Also if the URL is relative or certain kinds of malformed, but those
+    // should be impossible as `url` came from [PerAccountStore.tryResolveUrl]).
+    // In that case `url` has no "origin", and certainly not the realm's origin.
     return false;
   }
-  return (url.hasEmptyPath || url.path == '/')
-    && !url.hasQuery
-    && url.hasFragment;
 }

 /// Split `fragment` of arbitrary segments and handle trailing slashes
diff --git a/lib/widgets/actions.dart b/lib/widgets/actions.dart
index 39232847b3..bd8274613f 100644
--- a/lib/widgets/actions.dart
+++ b/lib/widgets/actions.dart
@@ -7,9 +7,11 @@ import '../api/exception.dart';
 import '../api/model/model.dart';
 import '../api/model/narrow.dart';
 import '../api/route/messages.dart';
+import '../api/route/messages.dart' as messages_api;
 import '../api/route/channels.dart' as channels_api;
 import '../generated/l10n/zulip_localizations.dart';
 import '../model/binding.dart';
+import '../model/internal_link.dart';
 import '../model/narrow.dart';
 import '../model/realm.dart';
 import 'dialog.dart';
@@ -241,6 +243,42 @@ abstract final class ZulipAction {
     return fetchedMessage?.content;
   }

+  /// Fetch and parse a URL from [messages_api.getFileTemporaryUrl];
+  /// on failure, show an error dialog and return null.
+  static Future<Uri?> getFileTemporaryUrl(BuildContext context, UserUploadLink link) async {
+    final zulipLocalizations = ZulipLocalizations.of(context);
+
+    String? resultUrl;
+    try {
+      final store = PerAccountStoreWidget.of(context);
+      resultUrl = (await messages_api.getFileTemporaryUrl(store.connection,
+        realmId: link.realmId, filename: link.path)).url;
+    } catch (e) {
+      if (!context.mounted) return null;
+      final errorMessage = switch (e) {
+        ZulipApiException() => e.message,
+        // TODO specific messages for common errors, like network errors
+        //   (support with reusable code)
+        _ => null,
+      };
+      showErrorDialog(context: context,
+        title: zulipLocalizations.errorCouldNotAccessUploadedFileTitle,
+        message: errorMessage);
+      return null;
+    }
+    if (!context.mounted) return null;
+
+    final tempUrl = PerAccountStoreWidget.of(context).tryResolveUrl(resultUrl);
+    if (tempUrl == null) {
+      showErrorDialog(context: context,
+        title: zulipLocalizations.errorCouldNotAccessUploadedFileTitle,
+        message: zulipLocalizations.errorInvalidResponse);
+      return null;
+    }
+
+    return tempUrl;
+  }
+
   static Future<void> subscribeToChannel(BuildContext context, {
     required int channelId,
   }) async {
diff --git a/lib/widgets/content.dart b/lib/widgets/content.dart
index 2816e90233..4544fc91c4 100644
--- a/lib/widgets/content.dart
+++ b/lib/widgets/content.dart
@@ -1436,6 +1436,12 @@ void _launchUrl(BuildContext context, String urlString) async {
           narrow: internalLink.narrow,
           initAnchorMessageId: internalLink.nearMessageId)));

+    case UserUploadLink():
+      final tempUrl = await ZulipAction.getFileTemporaryUrl(context, internalLink);
+      if (!context.mounted) return null;
+      if (tempUrl == null) return;
+      await PlatformActions.launchUrl(context, tempUrl);
+
     case null:
       await PlatformActions.launchUrl(context, url);
   }

diff --git a/test/model/internal_link_test.dart b/test/model/internal_link_test.dart
index 74345f9811..c256286b6a 100644
--- a/test/model/internal_link_test.dart
+++ b/test/model/internal_link_test.dart
@@ -586,6 +586,34 @@ void main() {
       });
     });
   });
+
+  group('parseInternalLink uploads', () {
+    for (final (expectParse, urlPath) in [
+      (true,  '/user_uploads/123/abc.pdf'),
+      (true,  '/user_uploads/123/4/ab/5/cde'),
+      (false, '/user_uploads/123/'),
+      (false, '/user_uploads/123'),
+      (false, '/user_uploads/'),
+      (false, '/user_uploads'),
+      (false, '/user_uploads//abc.pdf'),
+      (false, '/user_uploads/-123/abc.pdf'),
+      (false, '/user_upload/123/abc.pdf'),
+    ]) {
+      test('$urlPath -> ${expectParse ? 'parse' : 'reject'}', () {
+        final store = eg.store();
+        final url = store.tryResolveUrl(urlPath)!;
+        final result = parseInternalLink(url, store);
+        if (!expectParse) {
+          check(result).isNull();
+        } else {
+          check(result).isA<UserUploadLink>();
+          result as UserUploadLink;
+          final reconstructedPath = '/user_uploads/${result.realmId}/${result.path}';
+          check(reconstructedPath).equals(urlPath);
+        }
+      });
+    }
+  });
 }

 extension InternalLinkChecks on Subject<InternalLink> {
diff --git a/test/widgets/actions_test.dart b/test/widgets/actions_test.dart
index 2aa7432280..7c292aa62e 100644
--- a/test/widgets/actions_test.dart
+++ b/test/widgets/actions_test.dart
@@ -11,6 +11,7 @@ import 'package:zulip/api/model/model.dart';
 import 'package:zulip/api/model/narrow.dart';
 import 'package:zulip/api/route/messages.dart';
 import 'package:zulip/model/binding.dart';
+import 'package:zulip/model/internal_link.dart';
 import 'package:zulip/model/localizations.dart';
 import 'package:zulip/model/narrow.dart';
 import 'package:zulip/model/settings.dart';
@@ -244,6 +245,27 @@ void main() {
         check(await didPass).isFalse();
       });
     });
+
+    group('getFileTemporaryUrl', () {
+      testWidgets('smoke', (tester) async {
+        await prepare(tester);
+        connection.prepare(json: GetFileTemporaryUrlResult(
+          url: '/temp/s3kr1t-auth-token/paper.pdf').toJson());
+        final link = parseInternalLink(
+          store.tryResolveUrl('/user_uploads/123/ab/paper.pdf')!, store);
+
+        final future = ZulipAction.getFileTemporaryUrl(context,
+          link as UserUploadLink);
+        await tester.pump(Duration.zero);
+        check(connection.lastRequest).isA<http.Request>()
+          ..method.equals('GET')
+          ..url.path.equals('/api/v1/user_uploads/123/ab/paper.pdf')
+          ..url.query.isEmpty()
+          ..body.isEmpty();
+        check(await future).equals(
+          store.tryResolveUrl('/temp/s3kr1t-auth-token/paper.pdf')!);
+      });
+    });
   });

   group('PlatformActions', () {
diff --git a/test/widgets/content_test.dart b/test/widgets/content_test.dart
index 28af496c59..cf3d7a4e1b 100644
--- a/test/widgets/content_test.dart
+++ b/test/widgets/content_test.dart
@@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart';
 import 'package:zulip/api/core.dart';
 import 'package:zulip/api/model/initial_snapshot.dart';
 import 'package:zulip/api/model/model.dart';
+import 'package:zulip/api/route/messages.dart';
 import 'package:zulip/model/content.dart';
 import 'package:zulip/model/narrow.dart';
 import 'package:zulip/model/settings.dart';
@@ -21,6 +22,7 @@ import 'package:zulip/widgets/page.dart';
 import 'package:zulip/widgets/store.dart';
 import 'package:zulip/widgets/text.dart';

+import '../api/fake_api.dart';
 import '../example_data.dart' as eg;
 import '../flutter_checks.dart';
 import '../model/binding.dart';
@@ -944,6 +946,9 @@ void main() {
   });

   group('LinkNode on internal links', () {
+    late PerAccountStore store;
+    late FakeApiConnection connection;
+
     Future<List<Route<dynamic>>> prepare(WidgetTester tester, String html) async {
       final pushedRoutes = <Route<dynamic>>[];
       final testNavObserver = TestNavigatorObserver()
@@ -959,12 +964,13 @@ void main() {
       assert(pushedRoutes.length == 1);
       pushedRoutes.removeLast();

-      final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
+      store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
+      connection = store.connection as FakeApiConnection;
       await store.addStream(eg.stream(name: 'stream'));
       return pushedRoutes;
     }

-    testWidgets('valid internal links are navigated to within app', (tester) async {
+    testWidgets('narrow links are navigated to within app', (tester) async {
       final pushedRoutes = await prepare(tester,
         '
stream
');

@@ -976,6 +982,22 @@ void main() {

     // TODO(#1570): test links with /near/ go to the specific message

+    testWidgets('uploaded-file links are opened with temporary authed URL', (tester) async {
+      final pushedRoutes = await prepare(tester,
+        '
paper.pdf
');
+
+      final tempUrlString = '/temp/s3kr1t-auth-token/paper.pdf';
+      final expectedUrl = eg.realmUrl.resolve(tempUrlString);
+
+      connection.prepare(json: GetFileTemporaryUrlResult(
+        url: tempUrlString).toJson());
+      await tapText(tester, find.text('paper.pdf'));
+      await tester.pump(Duration.zero);
+      check(testBinding.takeLaunchUrlCalls())
+        .single.equals((url: expectedUrl, mode: LaunchMode.inAppBrowserView));
+      check(pushedRoutes).isEmpty();
+    });
+
     testWidgets('invalid internal links are opened in browser', (tester) async {
       // Link is invalid due to `topic` operator missing an operand.
       final pushedRoutes = await prepare(tester,
@@ -1190,10 +1212,14 @@ void main() {
     }

     testWidgets('tapping on audio link opens it in browser', (tester) async {
-      final url = eg.realmUrl.resolve('/user_uploads/2/f2/a_WnijOXIeRnI6OSxo9F6gZM/crab-rave.mp3');
       await prepare(tester, ContentExample.audioInline.html);
+      final store = await testBinding.globalStore.perAccount(eg.selfAccount.id);
+      final connection = store.connection as FakeApiConnection;

+      final url = eg.realmUrl.resolve('/temp/token/crab-rave.mp3');
+      connection.prepare(json: GetFileTemporaryUrlResult(url: url.path).toJson());
       await tapText(tester, find.text('crab-rave.mp3'));
+      await tester.pump(Duration.zero);

       final expectedLaunchMode = defaultTargetPlatform == TargetPlatform.iOS ?
         LaunchMode.externalApplication : LaunchMode.inAppBrowserView;