From 5abb9635dc65cc089140349cec127ae24d5c3e06 Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 11 Oct 2024 11:58:09 +0700 Subject: [PATCH 1/2] TF-2953 Set up Patrol tests --- .gitignore | 1 + android/app/build.gradle | 13 +++- .../android/tmail/MainActivityTest.java | 33 ++++++++++ docs/adr/0052-patrol-integration-test.md | 36 +++++++++++ integration_test/base/base_scenario.dart | 9 +++ integration_test/base/core_robot.dart | 15 +++++ integration_test/base/test_base.dart | 13 ++++ integration_test/robots/composer_robot.dart | 62 +++++++++++++++++++ integration_test/robots/login_robot.dart | 43 +++++++++++++ integration_test/robots/thread_robot.dart | 18 ++++++ .../scenarios/login_with_basic_auth.dart | 39 ++++++++++++ integration_test/scenarios/send_email.dart | 41 ++++++++++++ .../tests/compose/send_email_test.dart | 35 +++++++++++ .../login/login_with_basic_auth_test.dart | 29 +++++++++ .../utils/scenario_utils_mixin.dart | 9 +++ lib/main.dart | 32 +++++----- pubspec.lock | 22 ++++++- pubspec.yaml | 9 +++ 18 files changed, 441 insertions(+), 18 deletions(-) create mode 100644 android/app/src/androidTest/java/com/linagora/android/tmail/MainActivityTest.java create mode 100644 docs/adr/0052-patrol-integration-test.md create mode 100644 integration_test/base/base_scenario.dart create mode 100644 integration_test/base/core_robot.dart create mode 100644 integration_test/base/test_base.dart create mode 100644 integration_test/robots/composer_robot.dart create mode 100644 integration_test/robots/login_robot.dart create mode 100644 integration_test/robots/thread_robot.dart create mode 100644 integration_test/scenarios/login_with_basic_auth.dart create mode 100644 integration_test/scenarios/send_email.dart create mode 100644 integration_test/tests/compose/send_email_test.dart create mode 100644 integration_test/tests/login/login_with_basic_auth_test.dart create mode 100644 integration_test/utils/scenario_utils_mixin.dart 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/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 From 893e532e3aa1860c9e2dd681b8f252fcd619684e Mon Sep 17 00:00:00 2001 From: DatDang Date: Fri, 11 Oct 2024 11:58:55 +0700 Subject: [PATCH 2/2] TF-2953 Set up test environment for Patrol tests Made sure only 1 test using ngrok is running at a time --- .../workflows/patrol-integration-test.yaml | 56 +++++++ .../docker-compose.yaml | 17 +- backend-docker/imapserver.xml | 61 +++++++ backend-docker/jmap.properties | 1 + backend-docker/mailetcontainer.xml | 155 ++++++++++++++++++ .../patrol-integration-test-with-docker.sh | 63 +++++++ ...trol-local-integration-test-with-docker.sh | 62 +++++++ 7 files changed, 403 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/patrol-integration-test.yaml rename docker-compose.yaml => backend-docker/docker-compose.yaml (55%) create mode 100644 backend-docker/imapserver.xml create mode 100644 backend-docker/jmap.properties create mode 100644 backend-docker/mailetcontainer.xml create mode 100755 scripts/patrol-integration-test-with-docker.sh create mode 100755 scripts/patrol-local-integration-test-with-docker.sh 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/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 + + + + + + + + + + + + + bcc + ignore + + + 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/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