diff --git a/.github/workflows/patrol-integration-test.yaml b/.github/workflows/patrol-integration-test.yaml
new file mode 100644
index 0000000000..14affc60f4
--- /dev/null
+++ b/.github/workflows/patrol-integration-test.yaml
@@ -0,0 +1,56 @@
+name: Integration tests
+
+on:
+ workflow_dispatch:
+ schedule:
+ - cron: "0 0 * * *"
+
+env:
+ JAVA_VERSION: 17
+ FLUTTER_VERSION: 3.22.2
+
+jobs:
+ mobile_integration_test:
+ permissions:
+ contents: "read"
+ id-token: "write"
+
+ name: Run integration tests for mobile apps
+ runs-on: ubuntu-latest
+ concurrency:
+ group: ngrok
+ cancel-in-progress: false
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Authenticate to Google Cloud
+ uses: "google-github-actions/auth@v2"
+ with:
+ project_id: ${{ secrets.GOOGLE_CLOUD_PROJECT_ID }}
+ workload_identity_provider: ${{ secrets.GOOGLE_CLOUD_WORKLOAD_IDENTITY_PROVIDER_ID }}
+ service_account: ${{ secrets.GOOGLE_CLOUD_SERVICE_ACCOUNT }}
+
+ - name: Setup Cloud SDK
+ uses: google-github-actions/setup-gcloud@v2
+
+ - name: Setup Flutter
+ uses: subosito/flutter-action@v2
+ with:
+ flutter-version: ${{ env.FLUTTER_VERSION }}
+ channel: "stable"
+ cache: true
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ env.JAVA_VERSION }}
+ distribution: "temurin"
+
+ - name: Run prebuild
+ run: ./scripts/prebuild.sh
+
+ - name: Test
+ env:
+ NGROK_AUTHTOKEN: ${{ secrets.NGROK_AUTHTOKEN }}
+ run: ./scripts/patrol-integration-test-with-docker.sh
diff --git a/.gitignore b/.gitignore
index 77a53b29b0..f5d16ab74e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -120,3 +120,4 @@ app.*.symbols
*.g.dart
messages_*.dart
*.mocks.dart
+integration_test/test_bundle.dart
\ No newline at end of file
diff --git a/android/app/build.gradle b/android/app/build.gradle
index a9c72ee42d..2ac5b63a09 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -23,7 +23,7 @@ if (flutterVersionName == null) {
def flutterMinSdkVersion = localProperties.getProperty('flutter.minSdkVersion')
if (flutterMinSdkVersion == null) {
- flutterMinSdkVersion = '19'
+ flutterMinSdkVersion = '21'
}
apply plugin: 'com.android.application'
@@ -54,6 +54,8 @@ android {
manifestPlaceholders = [
'appAuthRedirectScheme': 'teammail.mobile'
]
+ testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner"
+ testInstrumentationRunnerArguments clearPackageData: "true"
}
compileOptions {
@@ -77,7 +79,15 @@ android {
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.release
}
+ debug {
+ minifyEnabled false
+ }
+ }
+
+ testOptions {
+ execution "ANDROIDX_TEST_ORCHESTRATOR"
}
+
}
flutter {
@@ -90,4 +100,5 @@ dependencies {
implementation 'androidx.work:work-runtime-ktx:2.7.0'
implementation 'com.android.support:multidex:1.0.3'
implementation 'androidx.window:window:1.0.0'
+ androidTestUtil "androidx.test:orchestrator:1.5.0"
}
diff --git a/android/app/src/androidTest/java/com/linagora/android/tmail/MainActivityTest.java b/android/app/src/androidTest/java/com/linagora/android/tmail/MainActivityTest.java
new file mode 100644
index 0000000000..678f6825aa
--- /dev/null
+++ b/android/app/src/androidTest/java/com/linagora/android/tmail/MainActivityTest.java
@@ -0,0 +1,33 @@
+package com.linagora.android.tmail;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import pl.leancode.patrol.PatrolJUnitRunner;
+
+@RunWith(Parameterized.class)
+public class MainActivityTest {
+ @Parameters(name = "{0}")
+ public static Object[] testCases() {
+ PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
+ // replace "MainActivity.class" with "io.flutter.embedding.android.FlutterActivity.class"
+ // if your AndroidManifest is using: android:name="io.flutter.embedding.android.FlutterActivity"
+ instrumentation.setUp(MainActivity.class);
+ instrumentation.waitForPatrolAppService();
+ return instrumentation.listDartTests();
+ }
+
+ public MainActivityTest(String dartTestName) {
+ this.dartTestName = dartTestName;
+ }
+
+ private final String dartTestName;
+
+ @Test
+ public void runDartTest() {
+ PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
+ instrumentation.runDartTest(dartTestName);
+ }
+}
diff --git a/docker-compose.yaml b/backend-docker/docker-compose.yaml
similarity index 55%
rename from docker-compose.yaml
rename to backend-docker/docker-compose.yaml
index 5727ef8ef6..2ce61d4f9d 100644
--- a/docker-compose.yaml
+++ b/backend-docker/docker-compose.yaml
@@ -1,26 +1,19 @@
version: "3"
services:
- tmail-frontend:
- image: linagora/tmail-web:master
- container_name: tmail-frontend
- ports:
- - "8080:80"
- volumes:
- - ./env.file:/usr/share/nginx/html/assets/env.file
- networks:
- - tmail
- depends_on:
- - tmail-backend
-
tmail-backend:
image: linagora/tmail-backend:memory-branch-master
container_name: tmail-backend
volumes:
- ./jwt_publickey:/root/conf/jwt_publickey
- ./jwt_privatekey:/root/conf/jwt_privatekey
+ - ./mailetcontainer.xml:/root/conf/mailetcontainer.xml
+ - ./imapserver.xml:/root/conf/imapserver.xml
+ - ./jmap.properties:/root/conf/jmap.properties
ports:
- "80:80"
+ environment:
+ - DOMAIN=example.com
networks:
- tmail
diff --git a/backend-docker/imapserver.xml b/backend-docker/imapserver.xml
new file mode 100644
index 0000000000..630cfbc26c
--- /dev/null
+++ b/backend-docker/imapserver.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+ imapserver
+ 0.0.0.0:143
+ 200
+
+
+ file://conf/keystore
+ james72laBalle
+ org.bouncycastle.jce.provider.BouncyCastleProvider
+
+ 0
+ 0
+ 120
+ SECONDS
+ true
+ false
+
+
+ imapserver-ssl
+ 0.0.0.0:993
+ 200
+
+
+ file://conf/keystore
+ james72laBalle
+ org.bouncycastle.jce.provider.BouncyCastleProvider
+
+ 0
+ 0
+ 120
+ SECONDS
+ true
+
+
\ No newline at end of file
diff --git a/backend-docker/jmap.properties b/backend-docker/jmap.properties
new file mode 100644
index 0000000000..e8ee83e6b8
--- /dev/null
+++ b/backend-docker/jmap.properties
@@ -0,0 +1 @@
+url.prefix=https://50e9-2402-9d80-85a-fe80-805b-e215-ab33-3def.ngrok-free.app
\ No newline at end of file
diff --git a/backend-docker/mailetcontainer.xml b/backend-docker/mailetcontainer.xml
new file mode 100644
index 0000000000..383173ef8b
--- /dev/null
+++ b/backend-docker/mailetcontainer.xml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+ postmaster
+
+
+
+ 20
+ memory://var/mail/error/
+
+
+
+
+
+
+
+ transport
+
+
+
+
+
+ mailetContainerErrors
+
+
+
+ memory://var/mail/error/
+ propagate
+
+
+
+
+
+
+
+
+
+
+
+
+
+ rrt-error
+
+
+ local-delivery
+
+
+ local-address-error
+ 550 - Requested action not taken: no such user here
+
+
+ relay
+
+
+ relay-denied
+
+
+
+
+
+
+
+
+
+ ContactAttribute1
+
+
+
+
+
+
+ outgoing
+ 5000, 100000, 500000
+ 3
+ 0
+ 10
+ true
+ bounces
+
+
+
+
+
+ mailetContainerLocalAddressError
+
+
+ none
+
+
+ memory://var/mail/address-error/
+
+
+
+
+
+ mailetContainerRelayDenied
+
+
+ none
+
+
+ memory://var/mail/relay-denied/
+ Warning: You are sending an e-mail to a remote server. You must be authenticated to perform such an operation
+
+
+
+
+
+ bounces
+
+
+ false
+
+
+
+
+
+ memory://var/mail/rrt-error/
+ true
+
+
+
+
+
+
+
+
+
diff --git a/docs/adr/0052-patrol-integration-test.md b/docs/adr/0052-patrol-integration-test.md
new file mode 100644
index 0000000000..72dc927aed
--- /dev/null
+++ b/docs/adr/0052-patrol-integration-test.md
@@ -0,0 +1,36 @@
+# 52. Patrol integration test
+
+Date: 2024-04-10
+
+## Status
+
+Accepted
+
+## Context
+
+- A need for integration testing for Twake Mail mobile arised.
+- The testing tool must be able to handle native UI and webview.
+
+## Decision
+
+- Patrol was chosen to write and test Twake Mail.
+
+## Consequences
+
+- Developers are now able to integration test Twake Mail
+- Set up:
+ - Run `dart pub global activate patrol_cli` to enable Patrol CLI
+ - Install ngrok and jq
+ - Open docker and Android emulator/connect Android device
+ - Remember to use `dart-define` neccessary for each test command
+ - Test individual test locally by edit `scripts/patrol-local-integration-test-with-docker.sh`
+ - Replace `patrol test -v` with `patrol test -v -t path/to/test/file`
+ - Run the `scripts/patrol-local-integration-test-with-docker.sh`
+ - Test every tests locally by running `scripts/patrol-local-integration-test-with-docker.sh` script
+ - Read more about Patrol in [Patrol homepage](https://patrol.leancode.co/)
+
+## Limitations
+
+- Backend docker container is initiated before Patrol tests run, and close after all tests have run. This lead to no data isolation between tests
+- Patrol gives no way of accessing Docker from system, due to when it runs, it bundle all tests into a single apk, and apk cannot access the system's terminal
+- Tried https://github.com/testcontainers/testcontainers-java but also failed, with the same reason as Patrol.
diff --git a/integration_test/base/base_scenario.dart b/integration_test/base/base_scenario.dart
new file mode 100644
index 0000000000..a9445902e2
--- /dev/null
+++ b/integration_test/base/base_scenario.dart
@@ -0,0 +1,9 @@
+import 'package:patrol/patrol.dart';
+
+abstract class BaseScenario {
+ final PatrolIntegrationTester $;
+
+ const BaseScenario(this.$);
+
+ Future execute();
+}
\ No newline at end of file
diff --git a/integration_test/base/core_robot.dart b/integration_test/base/core_robot.dart
new file mode 100644
index 0000000000..a116b25539
--- /dev/null
+++ b/integration_test/base/core_robot.dart
@@ -0,0 +1,15 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+abstract class CoreRobot {
+ final PatrolIntegrationTester $;
+
+ CoreRobot(this.$);
+
+ Future ensureViewVisible(PatrolFinder patrolFinder) async {
+ await $.waitUntilVisible(patrolFinder);
+ expect(patrolFinder, findsWidgets);
+ }
+
+ dynamic ignoreException() => $.tester.takeException();
+}
\ No newline at end of file
diff --git a/integration_test/base/test_base.dart b/integration_test/base/test_base.dart
new file mode 100644
index 0000000000..ac7229d4c1
--- /dev/null
+++ b/integration_test/base/test_base.dart
@@ -0,0 +1,13 @@
+import 'package:flutter/foundation.dart';
+import 'package:tmail_ui_user/main.dart' as app;
+
+class TestBase {
+ Future runTestApp() async {
+ await app.runTmail();
+ // https://github.com/leancodepl/patrol/issues/1602#issuecomment-1665317814
+ final originalOnError = FlutterError.onError!;
+ FlutterError.onError = (FlutterErrorDetails details) {
+ originalOnError(details);
+ };
+ }
+}
\ No newline at end of file
diff --git a/integration_test/robots/composer_robot.dart b/integration_test/robots/composer_robot.dart
new file mode 100644
index 0000000000..e8cd076a79
--- /dev/null
+++ b/integration_test/robots/composer_robot.dart
@@ -0,0 +1,62 @@
+import 'package:core/presentation/resources/image_paths.dart';
+import 'package:core/presentation/views/button/tmail_button_widget.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:model/email/prefix_email_address.dart';
+import 'package:rich_text_composer/rich_text_composer.dart';
+import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart';
+import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
+import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_editor_view.dart';
+import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart';
+import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart';
+import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart';
+import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart';
+
+import '../base/core_robot.dart';
+
+class ComposerRobot extends CoreRobot {
+ ComposerRobot(super.$);
+
+ Future addRecipient(String email) async {
+ await $(RecipientComposerWidget)
+ .which((widget) => widget.prefix == PrefixEmailAddress.to)
+ .enterText(email);
+ await $(RecipientSuggestionItemWidget)
+ .which((widget) => widget.emailAddress.email?.contains(email) ?? false)
+ .tap();
+ }
+
+ Future addSubject(String subject) async {
+ await $(SubjectComposerWidget).enterText(subject);
+ }
+
+ Future addContent(String content) async {
+ ComposerController? composerController;
+ await $(ComposerView)
+ .which((widget) {
+ composerController = widget.controller;
+ return true;
+ })
+ .$(MobileEditorView).$(HtmlEditor).$(InAppWebView).tap();
+
+ await composerController?.htmlEditorApi?.requestFocusLastChild();
+
+ await composerController!.htmlEditorApi!.insertHtml('$content
');
+ }
+
+ Future sendEmail() async {
+ await $(AppBarComposerWidget)
+ .$(TMailButtonWidget)
+ .which((widget) => widget.icon == ImagePaths().icSendMobile)
+ .tap();
+ }
+
+ Future expectSendEmailSuccessToast() async {
+ expect($('Message has been sent successfully'), findsOneWidget);
+ }
+
+ Future grantContactPermission() async {
+ if (await $.native.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
+ await $.native.grantPermissionWhenInUse();
+ }
+ }
+}
\ No newline at end of file
diff --git a/integration_test/robots/login_robot.dart b/integration_test/robots/login_robot.dart
new file mode 100644
index 0000000000..b079396fea
--- /dev/null
+++ b/integration_test/robots/login_robot.dart
@@ -0,0 +1,43 @@
+import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart';
+import 'package:flutter/material.dart';
+import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart';
+import 'package:tmail_ui_user/features/login/presentation/login_view.dart';
+import 'package:tmail_ui_user/features/login/presentation/widgets/login_text_input_builder.dart';
+
+import '../base/core_robot.dart';
+
+class LoginRobot extends CoreRobot {
+ LoginRobot(super.$);
+
+ Future expectLoginViewVisible() => ensureViewVisible($(LoginView));
+
+ Future enterEmail(String email) async {
+ final finder = $(LoginView).$(TextField);
+ await finder.enterText(email);
+ await $('Next').tap();
+ }
+
+ Future enterHostUrl(String url) async {
+ final finder = $(LoginView).$(TextField);
+ await finder.enterText(url);
+ await $('Next').tap();
+ }
+
+ Future enterBasicAuthEmail(String email) async {
+ await $(LoginView)
+ .$(TypeAheadFormFieldBuilder)
+ .$(TextField)
+ .enterText(email);
+ }
+
+ Future enterBasicAuthPassword(String password) async {
+ await $(LoginView)
+ .$(LoginTextInputBuilder)
+ .$(TextField)
+ .enterText(password);
+ }
+
+ Future loginBasicAuth() async {
+ await $(Container).$(ElevatedButton).tap();
+ }
+}
\ No newline at end of file
diff --git a/integration_test/robots/thread_robot.dart b/integration_test/robots/thread_robot.dart
new file mode 100644
index 0000000000..d2dd8ee3c4
--- /dev/null
+++ b/integration_test/robots/thread_robot.dart
@@ -0,0 +1,18 @@
+import 'package:flutter/material.dart';
+import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart';
+import 'package:tmail_ui_user/features/composer/presentation/composer_view.dart';
+import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart';
+
+import '../base/core_robot.dart';
+
+class ThreadRobot extends CoreRobot {
+ ThreadRobot(super.$);
+
+ Future expectThreadViewVisible() => ensureViewVisible($(ThreadView));
+
+ Future openComposer() async {
+ await $(ComposeFloatingButton).$(InkWell).tap();
+ }
+
+ Future expectComposerViewVisible() => ensureViewVisible($(ComposerView));
+}
\ No newline at end of file
diff --git a/integration_test/scenarios/login_with_basic_auth.dart b/integration_test/scenarios/login_with_basic_auth.dart
new file mode 100644
index 0000000000..7959c219fc
--- /dev/null
+++ b/integration_test/scenarios/login_with_basic_auth.dart
@@ -0,0 +1,39 @@
+import '../base/base_scenario.dart';
+import '../robots/login_robot.dart';
+import '../robots/thread_robot.dart';
+import '../utils/scenario_utils_mixin.dart';
+
+class LoginWithBasicAuth extends BaseScenario with ScenarioUtilsMixin {
+ const LoginWithBasicAuth(
+ super.$,
+ {
+ required this.username,
+ required this.hostUrl,
+ required this.email,
+ required this.password,
+ }
+ );
+
+ final String username;
+ final String hostUrl;
+ final String email;
+ final String password;
+
+ @override
+ Future execute() async {
+ final loginRobot = LoginRobot($);
+ final threadRobot = ThreadRobot($);
+
+ await loginRobot.expectLoginViewVisible();
+ await loginRobot.enterEmail(username);
+ await loginRobot.enterHostUrl(hostUrl);
+
+ await loginRobot.enterBasicAuthEmail(email);
+ await loginRobot.enterBasicAuthPassword(password);
+ await loginRobot.loginBasicAuth();
+
+ await grantNotificationPermission($.native);
+
+ await threadRobot.expectThreadViewVisible();
+ }
+}
\ No newline at end of file
diff --git a/integration_test/scenarios/send_email.dart b/integration_test/scenarios/send_email.dart
new file mode 100644
index 0000000000..fdf19288be
--- /dev/null
+++ b/integration_test/scenarios/send_email.dart
@@ -0,0 +1,41 @@
+import '../base/base_scenario.dart';
+import '../robots/composer_robot.dart';
+import '../robots/thread_robot.dart';
+import 'login_with_basic_auth.dart';
+
+class SendEmail extends BaseScenario {
+ const SendEmail(
+ super.$,
+ {
+ required this.loginWithBasicAuthScenario,
+ required this.additionalRecipient,
+ required this.subject,
+ required this.content
+ }
+ );
+
+ final LoginWithBasicAuth loginWithBasicAuthScenario;
+ final String additionalRecipient;
+ final String subject;
+ final String content;
+
+ @override
+ Future execute() async {
+ final threadRobot = ThreadRobot($);
+ final composerRobot = ComposerRobot($);
+
+ await loginWithBasicAuthScenario.execute();
+
+ await threadRobot.openComposer();
+ await threadRobot.expectComposerViewVisible();
+
+ await composerRobot.grantContactPermission();
+
+ await composerRobot.addRecipient(loginWithBasicAuthScenario.email);
+ await composerRobot.addRecipient(additionalRecipient);
+ await composerRobot.addSubject(subject);
+ await composerRobot.addContent(content);
+ await composerRobot.sendEmail();
+ await composerRobot.expectSendEmailSuccessToast();
+ }
+}
\ No newline at end of file
diff --git a/integration_test/tests/compose/send_email_test.dart b/integration_test/tests/compose/send_email_test.dart
new file mode 100644
index 0000000000..8793c47b46
--- /dev/null
+++ b/integration_test/tests/compose/send_email_test.dart
@@ -0,0 +1,35 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import '../../base/test_base.dart';
+import '../../scenarios/login_with_basic_auth.dart';
+import '../../scenarios/send_email.dart';
+
+void main() {
+ patrolTest(
+ 'Should see success toast when send email successfully',
+ config: const PatrolTesterConfig(
+ settlePolicy: SettlePolicy.trySettle,
+ visibleTimeout: Duration(minutes: 1)),
+ nativeAutomatorConfig: const NativeAutomatorConfig(
+ findTimeout: Duration(seconds: 10),
+ ),
+ framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive,
+ ($) async {
+ await TestBase().runTestApp();
+
+ final loginWithBasicAuthScenario = LoginWithBasicAuth($,
+ username: const String.fromEnvironment('USERNAME'),
+ hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'),
+ email: const String.fromEnvironment('BASIC_AUTH_EMAIL'),
+ password: const String.fromEnvironment('PASSWORD'),
+ );
+ final sendEmailScenario = SendEmail($,
+ loginWithBasicAuthScenario: loginWithBasicAuthScenario,
+ additionalRecipient: const String.fromEnvironment('ADDITIONAL_MAIL_RECIPIENT'),
+ subject: 'Test subject',
+ content: 'Test content');
+
+ await sendEmailScenario.execute();
+ });
+}
\ No newline at end of file
diff --git a/integration_test/tests/login/login_with_basic_auth_test.dart b/integration_test/tests/login/login_with_basic_auth_test.dart
new file mode 100644
index 0000000000..47b1c7a7a8
--- /dev/null
+++ b/integration_test/tests/login/login_with_basic_auth_test.dart
@@ -0,0 +1,29 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import '../../base/test_base.dart';
+import '../../scenarios/login_with_basic_auth.dart';
+
+void main() {
+ patrolTest(
+ 'Should see thread view when login with basic auth successfully',
+ config: const PatrolTesterConfig(
+ settlePolicy: SettlePolicy.trySettle,
+ visibleTimeout: Duration(minutes: 1)),
+ nativeAutomatorConfig: const NativeAutomatorConfig(
+ findTimeout: Duration(seconds: 10),
+ ),
+ framePolicy: LiveTestWidgetsFlutterBindingFramePolicy.benchmarkLive,
+ ($) async {
+ await TestBase().runTestApp();
+
+ final loginWithBasicAuthScenario = LoginWithBasicAuth($,
+ username: const String.fromEnvironment('USERNAME'),
+ hostUrl: const String.fromEnvironment('BASIC_AUTH_URL'),
+ email: const String.fromEnvironment('BASIC_AUTH_EMAIL'),
+ password: const String.fromEnvironment('PASSWORD'),
+ );
+
+ await loginWithBasicAuthScenario.execute();
+ });
+}
\ No newline at end of file
diff --git a/integration_test/utils/scenario_utils_mixin.dart b/integration_test/utils/scenario_utils_mixin.dart
new file mode 100644
index 0000000000..2b119c5101
--- /dev/null
+++ b/integration_test/utils/scenario_utils_mixin.dart
@@ -0,0 +1,9 @@
+import 'package:patrol/patrol.dart';
+
+mixin ScenarioUtilsMixin {
+ Future grantNotificationPermission(NativeAutomator nativeAutomator) async {
+ if (await nativeAutomator.isPermissionDialogVisible(timeout: const Duration(seconds: 5))) {
+ await nativeAutomator.grantPermissionWhenInUse();
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index 86435990a5..9ba0af392c 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -15,25 +15,29 @@ import 'package:tmail_ui_user/main/utils/app_utils.dart';
import 'package:url_strategy/url_strategy.dart';
import 'package:worker_manager/worker_manager.dart';
-void main() async {
+Future main() async {
initLogger(() async {
WidgetsFlutterBinding.ensureInitialized();
- ThemeUtils.setSystemLightUIStyle();
-
- await Future.wait([
- MainBindings().dependencies(),
- HiveCacheConfig.instance.setUp(),
- Executor().warmUp(log: BuildUtils.isDebugMode),
- AppUtils.loadEnvFile()
- ]);
- await HiveCacheConfig.instance.initializeEncryptionKey();
-
- setPathUrlStrategy();
-
- runApp(const TMailApp());
+ await runTmail();
});
}
+Future runTmail() async {
+ ThemeUtils.setSystemLightUIStyle();
+
+ await Future.wait([
+ MainBindings().dependencies(),
+ HiveCacheConfig.instance.setUp(),
+ Executor().warmUp(log: BuildUtils.isDebugMode),
+ AppUtils.loadEnvFile()
+ ]);
+ await HiveCacheConfig.instance.initializeEncryptionKey();
+
+ setPathUrlStrategy();
+
+ runApp(const TMailApp());
+}
+
class TMailApp extends StatelessWidget {
const TMailApp({Key? key}) : super(key: key);
diff --git a/pubspec.lock b/pubspec.lock
index 69a0d59200..84f1a92afd 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1252,10 +1252,10 @@ packages:
dependency: "direct main"
description:
name: json_annotation
- sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
+ sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
url: "https://pub.dev"
source: hosted
- version: "4.8.0"
+ version: "4.8.1"
json_serializable:
dependency: "direct dev"
description:
@@ -1480,6 +1480,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.1"
+ patrol:
+ dependency: "direct dev"
+ description:
+ name: patrol
+ sha256: ef07b0022f6eabee77655a3cde2364ff57cf22c29018d524476e972a5476724f
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.11.1"
+ patrol_finders:
+ dependency: transitive
+ description:
+ name: patrol_finders
+ sha256: "6bf2c3093fbccd02f80f73fafc1bd021d76410cbab6e329be220b5e3bc58f072"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
pattern_formatter:
dependency: transitive
description:
@@ -2192,4 +2208,4 @@ packages:
version: "3.1.2"
sdks:
dart: ">=3.3.0 <4.0.0"
- flutter: ">=3.20.0-7.0.pre.48"
+ flutter: ">=3.22.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index fa81fcad4d..9131540834 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -270,6 +270,8 @@ dev_dependencies:
http_mock_adapter: 0.4.2
+ patrol: 3.11.1
+
plugin_platform_interface: 2.1.8
dependency_overrides:
@@ -282,6 +284,8 @@ dependency_overrides:
url: https://github.com/linagora/flutter_file_picker
ref: email_supported_5.3.1
+ json_annotation: 4.8.1
+
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -355,3 +359,8 @@ flutter_native_splash:
cider:
link_template:
tag: https://github.com/linagora/tmail-flutter/releases/tag/v%tag% # initial release link template
+
+patrol:
+ app_name: Twake Mail
+ android:
+ package_name: com.linagora.android.teammail
diff --git a/scripts/patrol-integration-test-with-docker.sh b/scripts/patrol-integration-test-with-docker.sh
new file mode 100755
index 0000000000..5f00659308
--- /dev/null
+++ b/scripts/patrol-integration-test-with-docker.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+# Install ngrok
+echo "Installing ngrok..."
+curl -sSL https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null &&
+ echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list &&
+ sudo apt update && sudo apt install ngrok
+
+# Install patrol CLI
+echo "Installing patrol CLI..."
+dart pub global activate patrol_cli
+flutter build apk --config-only
+
+# Forward traffic to tmail-backend
+ngrok http http://localhost:80 --log=stdout >/dev/null &
+until [[ $(curl localhost:4040/api/status | jq -r ".status") == "online" ]]; do
+ echo "Waiting for ngrok to connect..."
+ sleep 2
+done
+
+export BASIC_AUTH_URL=$(curl -s localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url')
+
+cd backend-docker
+
+# Generate keys for tmail backend
+echo "Generating keys for tmail-backend..."
+openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -out jwt_privatekey
+openssl rsa -in jwt_privatekey -pubout -out jwt_publickey
+
+# Replace content of jmap.properties with url.prefix=$BASIC_AUTH_URL
+sed -i "s|url.prefix=.*|url.prefix=$BASIC_AUTH_URL|" jmap.properties
+
+echo "Starting services and adding users..."
+docker compose up -d
+# Wait till the service is started to add users
+until (docker compose logs tmail-backend | grep -i "JAMES server started"); do
+ echo "Waiting for tmail-backend to start..."
+ sleep 2
+done
+export BOB="bob"
+export ALICE="alice"
+export DOMAIN="example.com"
+docker exec tmail-backend james-cli AddUser "$BOB@$DOMAIN" "$BOB"
+docker exec tmail-backend james-cli AddUser "$ALICE@$DOMAIN" "$ALICE"
+
+cd ..
+
+echo "Building the app and running tests..."
+flutter build apk --config-only
+patrol build android -v \
+ --dart-define=USERNAME="$BOB" \
+ --dart-define=PASSWORD="$BOB" \
+ --dart-define=ADDITIONAL_MAIL_RECIPIENT="$ALICE@$DOMAIN" \
+ --dart-define=BASIC_AUTH_EMAIL="$BOB@$DOMAIN" \
+ --dart-define=BASIC_AUTH_URL="$BASIC_AUTH_URL"
+gcloud firebase test android run \
+ --type instrumentation \
+ --app build/app/outputs/apk/debug/app-debug.apk \
+ --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
+ --device 'model=oriole,version=33,locale=en,orientation=portrait' \
+ --timeout 10m \
+ --use-orchestrator \
+ --environment-variables clearPackageData=true
diff --git a/scripts/patrol-local-integration-test-with-docker.sh b/scripts/patrol-local-integration-test-with-docker.sh
new file mode 100755
index 0000000000..90f331d83a
--- /dev/null
+++ b/scripts/patrol-local-integration-test-with-docker.sh
@@ -0,0 +1,62 @@
+#!/bin/bash
+
+## Pre-requisites
+# Install ngrok
+# Install patrol CLI
+# Open android emulator
+
+# Stoping previous environment if any
+killall ngrok || true
+cd backend-docker
+docker compose down || true
+cd ..
+
+# Forward traffic to tmail-backend
+ngrok http http://localhost:80 --log=stdout >/dev/null &
+until [[ $(curl localhost:4040/api/status | jq -r ".status") == "online" ]]; do
+ echo "Waiting for ngrok to connect..."
+ sleep 2
+done
+
+export BASIC_AUTH_URL=$(curl -s localhost:4040/api/tunnels | jq -r '.tunnels[0].public_url')
+
+cd backend-docker
+
+# Generate keys for tmail backend
+echo "Generating keys for tmail-backend..."
+openssl genpkey -algorithm rsa -pkeyopt rsa_keygen_bits:4096 -out jwt_privatekey
+openssl rsa -in jwt_privatekey -pubout -out jwt_publickey
+
+# Replace content of jmap.properties with url.prefix=$BASIC_AUTH_URL
+sed -i '' "s|url.prefix=.*|url.prefix=$BASIC_AUTH_URL|" jmap.properties
+
+echo "Starting services and adding users..."
+docker compose up -d
+# Wait till the service is started to add users
+until (docker compose logs tmail-backend | grep -i "JAMES server started"); do
+ echo "Waiting for tmail-backend to start..."
+ sleep 2
+done
+export BOB="bob"
+export ALICE="alice"
+export DOMAIN="example.com"
+docker exec tmail-backend james-cli AddUser "$BOB@$DOMAIN" "$BOB"
+docker exec tmail-backend james-cli AddUser "$ALICE@$DOMAIN" "$ALICE"
+
+cd ..
+
+echo "Building the app and running tests..."
+flutter build apk --config-only
+patrol test -v \
+ --dart-define=USERNAME="$BOB" \
+ --dart-define=PASSWORD="$BOB" \
+ --dart-define=ADDITIONAL_MAIL_RECIPIENT="$ALICE@$DOMAIN" \
+ --dart-define=BASIC_AUTH_EMAIL="$BOB@$DOMAIN" \
+ --dart-define=BASIC_AUTH_URL="$BASIC_AUTH_URL"
+
+# Clean up
+echo "Cleaning up test environment..."
+killall ngrok
+cd backend-docker
+docker compose down
+cd ..
\ No newline at end of file