diff --git a/.github/workflows/integration-test.yaml b/.github/workflows/integration-test.yaml new file mode 100644 index 0000000000..2c56592260 --- /dev/null +++ b/.github/workflows/integration-test.yaml @@ -0,0 +1,55 @@ +name: Integration tests + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - "**/*.md" + +env: + JAVA_VERSION: 22 + +jobs: + integration_test: + name: Run integration tests + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: "temurin" + cache: maven + cache-dependency-path: "tmail_integration_test/pom.xml" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build local image + uses: docker/build-push-action@v6 + with: + load: true + tags: tmail-web:integration-test + + - uses: browser-actions/setup-chrome@v1 + with: + chrome-version: stable + install-chromedriver: true + + - uses: browser-actions/setup-firefox@v1 + - uses: browser-actions/setup-geckodriver@latest + + - name: Configure frontend + env: + FRONTEND_CONFIG: ${{ secrets.FRONTEND_CONFIG }} + FRONTEND_CREDS: ${{ secrets.FRONTEND_CREDS }} + run: ../scripts/setup-desktop-integration-tests.sh + working-directory: tmail_integration_test + + - name: Run integration tests + run: ../scripts/run-desktop-integration-tests.sh + working-directory: tmail_integration_test diff --git a/.gitignore b/.gitignore index 77a53b29b0..a1dcd66af6 100644 --- a/.gitignore +++ b/.gitignore @@ -120,3 +120,6 @@ app.*.symbols *.g.dart messages_*.dart *.mocks.dart + +# end-to-end test +config.properties \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart index 283946f7af..78001a39df 100644 --- a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -122,262 +122,268 @@ class _RecipientComposerWidgetState extends State { @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: RecipientComposerWidgetStyle.borderColor, - width: 1 + return Semantics( + value: 'Composer:${widget.prefix.name}', + child: Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: RecipientComposerWidgetStyle.borderColor, + width: 1 + ) ) - ) - ), - padding: widget.padding, - margin: widget.margin, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: RecipientComposerWidgetStyle.labelMargin, - child: Text( - '${widget.prefix.asName(context)}:', - key: Key('prefix_${widget.prefix.name}_recipient_composer_widget'), - style: RecipientComposerWidgetStyle.labelTextStyle + ), + padding: widget.padding, + margin: widget.margin, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: RecipientComposerWidgetStyle.labelMargin, + child: Text( + '${widget.prefix.asName(context)}:', + key: Key('prefix_${widget.prefix.name}_recipient_composer_widget'), + style: RecipientComposerWidgetStyle.labelTextStyle + ), ), - ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - Expanded( - child: FocusScope( - child: Focus( - onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), - onKeyEvent: PlatformInfo.isWeb ? _recipientInputOnKeyListener : null, - child: StatefulBuilder( - builder: (context, stateSetter) { - if (PlatformInfo.isWeb || widget.isTestingForWeb) { - return DragTarget( - builder: (context, candidateData, rejectedData) { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - enableBorder: _isDragging, - focusNodeKeyboard: widget.focusNodeKeyboard, - borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, - enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - autoHideTextInputField: true, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - onFocusTextInput: () { - if (_isCollapse) { - widget.onShowFullListEmailAddressAction?.call(widget.prefix); - } - }, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - index: index, - imagePaths: widget.imagePaths, - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: widget.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - imagePaths: widget.imagePaths, - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - }, - onAcceptWithDetails: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress.data, stateSetter), - onLeave: (draggableEmailAddress) { - if (_isDragging) { - stateSetter(() => _isDragging = false); - } - }, - onMove: (details) { - if (!_isDragging) { - stateSetter(() => _isDragging = true); - } - }, - ); - } else { - return TagEditor( - key: widget.keyTagEditor, - length: _collapsedListEmailAddress.length, - controller: widget.controller, - focusNode: widget.focusNode, - focusNodeKeyboard: widget.focusNodeKeyboard, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, - tagSpacing: RecipientComposerWidgetStyle.tagSpacing, - minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, - resetTextOnSubmitted: true, - autoScrollToInput: false, - autoHideTextInputField: true, - cursorColor: RecipientComposerWidgetStyle.cursorColor, - suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, - suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, - suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, - suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, - suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), - textStyle: RecipientComposerWidgetStyle.inputTextStyle, - onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), - onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), - onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), - onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), - onTapOutside: (_) {}, - onFocusTextInput: () { - if (_isCollapse) { - widget.onShowFullListEmailAddressAction?.call(widget.prefix); - } - }, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final currentEmailAddress = _currentListEmailAddress[index]; - final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; - - return RecipientTagItemWidget( - index: index, - imagePaths: widget.imagePaths, - prefix: widget.prefix, - currentEmailAddress: currentEmailAddress, - currentListEmailAddress: _currentListEmailAddress, - collapsedListEmailAddress: _collapsedListEmailAddress, - isLatestEmail: isLatestEmail, - isCollapsed: _isCollapse, - isLatestTagFocused: _lastTagFocused, - maxWidth: widget.maxWidth, - onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), - onShowFullAction: widget.onShowFullListEmailAddressAction, - ); - }, - onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { - return RecipientSuggestionItemWidget( - imagePaths: widget.imagePaths, - suggestionState: suggestionEmailAddress.state, - emailAddress: suggestionEmailAddress.emailAddress, - suggestionValid: suggestionValid, - highlight: highlight, - onSelectedAction: (emailAddress) { - stateSetter(() => _currentListEmailAddress.add(emailAddress)); - _updateListEmailAddressAction(); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ); - }, - ); - } - }, + const SizedBox(width: RecipientComposerWidgetStyle.space), + Expanded( + child: FocusScope( + child: Focus( + onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), + onKeyEvent: PlatformInfo.isWeb ? _recipientInputOnKeyListener : null, + child: StatefulBuilder( + builder: (context, stateSetter) { + if (PlatformInfo.isWeb || widget.isTestingForWeb) { + return DragTarget( + builder: (context, candidateData, rejectedData) { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + enableBorder: _isDragging, + focusNodeKeyboard: widget.focusNodeKeyboard, + borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, + enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + autoHideTextInputField: true, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + onFocusTextInput: () { + if (_isCollapse) { + widget.onShowFullListEmailAddressAction?.call(widget.prefix); + } + }, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return Semantics( + label: 'Composer:suggestion:${suggestionEmailAddress.emailAddress.email}', + child: RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ), + ); + }, + ); + }, + onAcceptWithDetails: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress.data, stateSetter), + onLeave: (draggableEmailAddress) { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + }, + onMove: (details) { + if (!_isDragging) { + stateSetter(() => _isDragging = true); + } + }, + ); + } else { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + focusNodeKeyboard: widget.focusNodeKeyboard, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + autoScrollToInput: false, + autoHideTextInputField: true, + cursorColor: RecipientComposerWidgetStyle.cursorColor, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(widget.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + onTapOutside: (_) {}, + onFocusTextInput: () { + if (_isCollapse) { + widget.onShowFullListEmailAddressAction?.call(widget.prefix); + } + }, + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + index: index, + imagePaths: widget.imagePaths, + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + maxWidth: widget.maxWidth, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + imagePaths: widget.imagePaths, + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); + } + }, + ) ) ) - ) - ), - const SizedBox(width: RecipientComposerWidgetStyle.space), - if (widget.prefix == PrefixEmailAddress.to) - if (PlatformInfo.isWeb || widget.isTestingForWeb) - ...[ - if (widget.fromState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - key: Key('prefix_${widget.prefix.name}_recipient_from_button'), - text: AppLocalizations.of(context).from_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), - ), - if (widget.ccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - key: Key('prefix_${widget.prefix.name}_recipient_cc_button'), - text: AppLocalizations.of(context).cc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), - ), - if (widget.bccState == PrefixRecipientState.disabled) - TMailButtonWidget.fromText( - key: Key('prefix_${widget.prefix.name}_recipient_bcc_button'), - text: AppLocalizations.of(context).bcc_email_address_prefix, - textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, - backgroundColor: Colors.transparent, - padding: RecipientComposerWidgetStyle.prefixButtonPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), - ), - ] - else if (PlatformInfo.isMobile) + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + if (widget.prefix == PrefixEmailAddress.to) + if (PlatformInfo.isWeb || widget.isTestingForWeb) + ...[ + if (widget.fromState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_from_button'), + text: AppLocalizations.of(context).from_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.from), + ), + if (widget.ccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_cc_button'), + text: AppLocalizations.of(context).cc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), + ), + if (widget.bccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + key: Key('prefix_${widget.prefix.name}_recipient_bcc_button'), + text: AppLocalizations.of(context).bcc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), + ), + ] + else if (PlatformInfo.isMobile) + TMailButtonWidget.fromIcon( + key: Key('prefix_${widget.prefix.name}_recipient_expand_button'), + icon: _isAllRecipientInputEnabled + ? widget.imagePaths.icChevronUp + : widget.imagePaths.icChevronDownOutline, + backgroundColor: Colors.transparent, + iconSize: 24, + padding: const EdgeInsets.all(5), + iconColor: AppColor.colorLabelComposer, + margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, + onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), + ) + else if (PlatformInfo.isWeb || widget.isTestingForWeb) TMailButtonWidget.fromIcon( - key: Key('prefix_${widget.prefix.name}_recipient_expand_button'), - icon: _isAllRecipientInputEnabled - ? widget.imagePaths.icChevronUp - : widget.imagePaths.icChevronDownOutline, + icon: widget.imagePaths.icClose, backgroundColor: Colors.transparent, - iconSize: 24, - padding: const EdgeInsets.all(5), - iconColor: AppColor.colorLabelComposer, - margin: RecipientComposerWidgetStyle.enableRecipientButtonMargin, - onTapActionCallback: () => widget.onEnableAllRecipientsInputAction?.call(_isAllRecipientInputEnabled), + iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, + iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, + padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), ) - else if (PlatformInfo.isWeb || widget.isTestingForWeb) - TMailButtonWidget.fromIcon( - icon: widget.imagePaths.icClose, - backgroundColor: Colors.transparent, - iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, - iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, - padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, - margin: RecipientComposerWidgetStyle.recipientMargin, - onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), - ) - ] + ] + ), ), ); } diff --git a/lib/features/composer/presentation/widgets/subject_composer_widget.dart b/lib/features/composer/presentation/widgets/subject_composer_widget.dart index cfc4d8a7a4..b164453aff 100644 --- a/lib/features/composer/presentation/widgets/subject_composer_widget.dart +++ b/lib/features/composer/presentation/widgets/subject_composer_widget.dart @@ -23,36 +23,39 @@ class SubjectComposerWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: SubjectComposerWidgetStyle.borderColor, - width: 1 - ) - ), - ), - margin: margin, - padding: padding, - child: Row( - children: [ - Text( - '${AppLocalizations.of(context).subject_email}:', - style: SubjectComposerWidgetStyle.labelTextStyle + return Semantics( + label: 'Composer:subject', + child: Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: SubjectComposerWidgetStyle.borderColor, + width: 1 + ) ), - const SizedBox(width:SubjectComposerWidgetStyle.space), - Expanded( - child: TextFieldBuilder( - cursorColor: SubjectComposerWidgetStyle.cursorColor, - focusNode: focusNode, - onTextChange: onTextChange, - maxLines: 1, - textDirection: DirectionUtils.getDirectionByLanguage(context), - textStyle: SubjectComposerWidgetStyle.inputTextStyle, - controller: textController, + ), + margin: margin, + padding: padding, + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).subject_email}:', + style: SubjectComposerWidgetStyle.labelTextStyle + ), + const SizedBox(width:SubjectComposerWidgetStyle.space), + Expanded( + child: TextFieldBuilder( + cursorColor: SubjectComposerWidgetStyle.cursorColor, + focusNode: focusNode, + onTextChange: onTextChange, + maxLines: 1, + textDirection: DirectionUtils.getDirectionByLanguage(context), + textStyle: SubjectComposerWidgetStyle.inputTextStyle, + controller: textController, + ) ) - ) - ] + ] + ), ), ); } diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart index c95ffd8bc3..6abcbb6e80 100644 --- a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -118,63 +118,66 @@ class _WebEditorState extends State { return ValueListenableBuilder( valueListenable: _htmlEditorHeight, builder: (context, height, _) { - return HtmlEditor( - key: Key('web_editor_$height'), - controller: _editorController, - htmlEditorOptions: HtmlEditorOptions( - shouldEnsureVisible: true, - hint: '', - darkMode: false, - initialText: widget.content, - customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction), - spellCheck: true, - disableDragAndDrop: true, - webInitialScripts: UnmodifiableListView([ - WebScript( - name: HtmlUtils.lineHeight100Percent.name, - script: HtmlUtils.lineHeight100Percent.script, - ), - WebScript( - name: HtmlUtils.registerDropListener.name, - script: HtmlUtils.registerDropListener.script, + return Semantics( + label: 'Composer:content', + child: HtmlEditor( + key: Key('web_editor_$height'), + controller: _editorController, + htmlEditorOptions: HtmlEditorOptions( + shouldEnsureVisible: true, + hint: '', + darkMode: false, + initialText: widget.content, + customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction), + spellCheck: true, + disableDragAndDrop: true, + webInitialScripts: UnmodifiableListView([ + WebScript( + name: HtmlUtils.lineHeight100Percent.name, + script: HtmlUtils.lineHeight100Percent.script, + ), + WebScript( + name: HtmlUtils.registerDropListener.name, + script: HtmlUtils.registerDropListener.script, + ), + WebScript( + name: HtmlUtils.unregisterDropListener.name, + script: HtmlUtils.unregisterDropListener.script, + ) + ]) + ), + htmlToolbarOptions: const HtmlToolbarOptions( + toolbarType: ToolbarType.hide, + defaultToolbarButtons: [], + ), + otherOptions: OtherOptions( + height: height, + // dropZoneWidth: dropZoneWidth, + // dropZoneHeight: dropZoneHeight, + ), + callbacks: Callbacks( + onBeforeCommand: widget.onChangeContent, + onChangeContent: widget.onChangeContent, + onInit: () { + widget.onInitial?.call(widget.content); + if (!_dropListenerRegistered) { + _editorController.evaluateJavascriptWeb( + HtmlUtils.registerDropListener.name); + _dropListenerRegistered = true; + } + }, + onFocus: widget.onFocus, + onBlur: widget.onUnFocus, + onMouseDown: () => widget.onMouseDown?.call(context), + onChangeSelection: widget.onEditorSettings, + onChangeCodeview: widget.onChangeContent, + onTextFontSizeChanged: widget.onEditorTextSizeChanged, + onPaste: () => _editorController.evaluateJavascriptWeb( + HtmlUtils.lineHeight100Percent.name ), - WebScript( - name: HtmlUtils.unregisterDropListener.name, - script: HtmlUtils.unregisterDropListener.script, - ) - ]) - ), - htmlToolbarOptions: const HtmlToolbarOptions( - toolbarType: ToolbarType.hide, - defaultToolbarButtons: [], - ), - otherOptions: OtherOptions( - height: height, - // dropZoneWidth: dropZoneWidth, - // dropZoneHeight: dropZoneHeight, - ), - callbacks: Callbacks( - onBeforeCommand: widget.onChangeContent, - onChangeContent: widget.onChangeContent, - onInit: () { - widget.onInitial?.call(widget.content); - if (!_dropListenerRegistered) { - _editorController.evaluateJavascriptWeb( - HtmlUtils.registerDropListener.name); - _dropListenerRegistered = true; - } - }, - onFocus: widget.onFocus, - onBlur: widget.onUnFocus, - onMouseDown: () => widget.onMouseDown?.call(context), - onChangeSelection: widget.onEditorSettings, - onChangeCodeview: widget.onChangeContent, - onTextFontSizeChanged: widget.onEditorTextSizeChanged, - onPaste: () => _editorController.evaluateJavascriptWeb( - HtmlUtils.lineHeight100Percent.name + onDragEnter: widget.onDragEnter, + onDragLeave: (types) {}, ), - onDragEnter: widget.onDragEnter, - onDragLeave: (_) {}, ), ); } diff --git a/lib/main.dart b/lib/main.dart index 1ff6496fef..511e74ea98 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/utils/theme_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; @@ -17,6 +18,7 @@ import 'package:worker_manager/worker_manager.dart'; void main() async { initLogger(() async { WidgetsFlutterBinding.ensureInitialized(); + SemanticsBinding.instance.ensureSemantics(); ThemeUtils.setSystemLightUIStyle(); await Future.wait([ diff --git a/scripts/run-desktop-integration-tests.sh b/scripts/run-desktop-integration-tests.sh new file mode 100755 index 0000000000..53a5f4fa5b --- /dev/null +++ b/scripts/run-desktop-integration-tests.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# docker run -d -p 2023:80 --name tmail-web -v "$(pwd)/env.file:/usr/share/nginx/html/assets/env.file" tmail-web:integration-test +./mvnw test \ No newline at end of file diff --git a/scripts/setup-desktop-integration-tests.sh b/scripts/setup-desktop-integration-tests.sh new file mode 100755 index 0000000000..2defadd664 --- /dev/null +++ b/scripts/setup-desktop-integration-tests.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +echo "$FRONTEND_CONFIG" > env.file +mkdir -p src/main/resources +echo "$FRONTEND_CREDS" > src/main/resources/config.properties diff --git a/tmail_integration_test/.gitignore b/tmail_integration_test/.gitignore new file mode 100644 index 0000000000..1213b4caa8 --- /dev/null +++ b/tmail_integration_test/.gitignore @@ -0,0 +1,2 @@ +# Default ignored files +/target/ \ No newline at end of file diff --git a/tmail_integration_test/.mvn/wrapper/maven-wrapper.properties b/tmail_integration_test/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..f95f1ee807 --- /dev/null +++ b/tmail_integration_test/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip diff --git a/tmail_integration_test/mvnw b/tmail_integration_test/mvnw new file mode 100755 index 0000000000..19529ddf8c --- /dev/null +++ b/tmail_integration_test/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/tmail_integration_test/mvnw.cmd b/tmail_integration_test/mvnw.cmd new file mode 100644 index 0000000000..249bdf3822 --- /dev/null +++ b/tmail_integration_test/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/tmail_integration_test/pom.xml b/tmail_integration_test/pom.xml new file mode 100644 index 0000000000..e7cff8bc20 --- /dev/null +++ b/tmail_integration_test/pom.xml @@ -0,0 +1,109 @@ + + + + 4.0.0 + + com.linagora + tmail + 0.0.1-SNAPSHOT + + tmail + + http://www.example.com + + + UTF-8 + 22 + 22 + + + + + org.junit.jupiter + junit-jupiter-api + 5.10.3 + + + org.junit.jupiter + junit-jupiter-engine + 5.10.3 + + + org.junit.jupiter + junit-jupiter-params + 5.10.3 + + + + com.microsoft.playwright + playwright + 1.45.0 + + + org.testcontainers + testcontainers + 1.20.0 + test + + + io.projectreactor + reactor-core + 3.6.8 + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + maven-resources-plugin + 3.0.2 + + + maven-compiler-plugin + 3.8.0 + + + maven-surefire-plugin + 2.22.1 + + false + + + + maven-jar-plugin + 3.0.2 + + + maven-install-plugin + 2.5.2 + + + maven-deploy-plugin + 2.8.2 + + + + maven-site-plugin + 3.7.1 + + + maven-project-info-reports-plugin + 3.0.0 + + + + + diff --git a/tmail_integration_test/src/main/java/com/tmail/base/BaseScenario.java b/tmail_integration_test/src/main/java/com/tmail/base/BaseScenario.java new file mode 100644 index 0000000000..38794f1532 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/BaseScenario.java @@ -0,0 +1,9 @@ +package com.tmail.base; + +import com.microsoft.playwright.Page; + +public abstract class BaseScenario { + public abstract void execute(Page page); + + public TestUtils testUtils = new TestUtils(); +} diff --git a/tmail_integration_test/src/main/java/com/tmail/base/CoreRobot.java b/tmail_integration_test/src/main/java/com/tmail/base/CoreRobot.java new file mode 100644 index 0000000000..efbb518388 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/CoreRobot.java @@ -0,0 +1,11 @@ +package com.tmail.base; + +import com.microsoft.playwright.Page; + +public abstract class CoreRobot { + protected Page page; + + public CoreRobot(Page page) { + this.page = page; + } +} diff --git a/tmail_integration_test/src/main/java/com/tmail/base/CustomParameterResolver.java b/tmail_integration_test/src/main/java/com/tmail/base/CustomParameterResolver.java new file mode 100644 index 0000000000..30fc136b2c --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/CustomParameterResolver.java @@ -0,0 +1,70 @@ +package com.tmail.base; + +import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Optional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter; +import org.junit.jupiter.engine.extension.ExtensionRegistry; + +public class CustomParameterResolver implements BeforeEachMethodAdapter, ParameterResolver { + + private ParameterResolver parameterisedTestParameterResolver = null; + + @Override + public void invokeBeforeEachMethod(ExtensionContext context, ExtensionRegistry registry) + throws Throwable { + Optional resolverOptional = registry.getExtensions(ParameterResolver.class) + .stream() + .filter(parameterResolver -> parameterResolver.getClass().getName() + .contains("ParameterizedTestParameterResolver")) + .findFirst(); + if (!resolverOptional.isPresent()) { + throw new IllegalStateException( + "ParameterizedTestParameterResolver missed in the registry. Probably it's not a Parameterized Test"); + } else { + parameterisedTestParameterResolver = resolverOptional.get(); + } + } + + @Override + public boolean supportsParameter(ParameterContext parameterContext, + ExtensionContext extensionContext) throws ParameterResolutionException { + if (isExecutedOnAfterOrBeforeMethod(parameterContext)) { + ParameterContext pContext = getMappedContext(parameterContext, extensionContext); + return parameterisedTestParameterResolver.supportsParameter(pContext, extensionContext); + } + return false; + } + + @Override + public Object resolveParameter(ParameterContext parameterContext, + ExtensionContext extensionContext) throws ParameterResolutionException { + return parameterisedTestParameterResolver.resolveParameter( + getMappedContext(parameterContext, extensionContext), extensionContext); + } + + private MappedParameterContext getMappedContext(ParameterContext parameterContext, + ExtensionContext extensionContext) { + return new MappedParameterContext( + parameterContext.getIndex(), + extensionContext.getRequiredTestMethod().getParameters()[parameterContext.getIndex()], + Optional.of(parameterContext.getTarget())); + } + + private boolean isExecutedOnAfterOrBeforeMethod(ParameterContext parameterContext) { + return Arrays.stream(parameterContext.getDeclaringExecutable().getDeclaredAnnotations()) + .anyMatch(this::isAfterEachOrBeforeEachAnnotation); + } + + private boolean isAfterEachOrBeforeEachAnnotation(Annotation annotation) { + return annotation.annotationType() == BeforeEach.class + || annotation.annotationType() == AfterEach.class; + } +} \ No newline at end of file diff --git a/tmail_integration_test/src/main/java/com/tmail/base/MappedParameterContext.java b/tmail_integration_test/src/main/java/com/tmail/base/MappedParameterContext.java new file mode 100644 index 0000000000..7d24b8915b --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/MappedParameterContext.java @@ -0,0 +1,53 @@ +package com.tmail.base; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Parameter; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.platform.commons.util.AnnotationUtils; + +public class MappedParameterContext implements ParameterContext { + + private final int index; + private final Parameter parameter; + private final Optional target; + + public MappedParameterContext(int index, Parameter parameter, + Optional target) { + this.index = index; + this.parameter = parameter; + this.target = target; + } + + @Override + public boolean isAnnotated(Class annotationType) { + return AnnotationUtils.isAnnotated(parameter, annotationType); + } + + @Override + public Optional findAnnotation(Class annotationType) { + return Optional.empty(); + } + + @Override + public List findRepeatableAnnotations(Class annotationType) { + return null; + } + + @Override + public int getIndex() { + return index; + } + + @Override + public Parameter getParameter() { + return parameter; + } + + @Override + public Optional getTarget() { + return target; + } +} diff --git a/tmail_integration_test/src/main/java/com/tmail/base/SupportedPlatform.java b/tmail_integration_test/src/main/java/com/tmail/base/SupportedPlatform.java new file mode 100644 index 0000000000..1bebc611bc --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/SupportedPlatform.java @@ -0,0 +1,6 @@ +package com.tmail.base; + +public enum SupportedPlatform { + CHROME, + FIREFOX +} diff --git a/tmail_integration_test/src/main/java/com/tmail/base/TestBase.java b/tmail_integration_test/src/main/java/com/tmail/base/TestBase.java new file mode 100644 index 0000000000..011a131195 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/TestBase.java @@ -0,0 +1,95 @@ +package com.tmail.base; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Properties; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.Browser.NewContextOptions; +import com.microsoft.playwright.BrowserContext; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.Tracing; + +@ExtendWith(CustomParameterResolver.class) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public abstract class TestBase { + private Playwright playwright; + private Browser browser; + private BrowserContext browserContext; + private Page page; + + protected BaseScenario scenario; + protected Properties properties; + + private static Boolean runHeadlessTest = true; + + public TestBase() { + properties = new Properties(); + ClassLoader loader = getClass().getClassLoader(); + try { + properties.load(loader.getResourceAsStream("config.properties")); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @BeforeAll + void setUpAll() { + playwright = Playwright.create(); + } + + @BeforeEach + public void setUp(SupportedPlatform supportedPlatform) { + browser = switch (supportedPlatform) { + case CHROME -> playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(runHeadlessTest)); + case FIREFOX -> playwright.firefox().launch(new BrowserType.LaunchOptions().setHeadless(runHeadlessTest)); + default -> throw new UnsupportedPlatformException(); + }; + + NewContextOptions contextOptions = new Browser.NewContextOptions() + .setViewportSize(1920, 1080); + browserContext = browser.newContext(contextOptions); + browserContext.tracing().start(new Tracing.StartOptions() + .setScreenshots(true) + .setSnapshots(true)); + + page = browserContext.newPage(); + } + + @AfterEach + public void tearDown() { + browserContext.tracing().stop(new Tracing.StopOptions() + .setPath(Paths.get("trace.zip"))); + page.close(); + browserContext.close(); + browser.close(); + } + + @AfterAll + void tearDownAll() { + playwright.close(); + } + + static Stream supportedPlatforms() { + return Arrays.asList(SupportedPlatform.values()).stream(); + } + + @ParameterizedTest + @MethodSource("supportedPlatforms") + public void testScenario(SupportedPlatform supportedPlatform) { + scenario.execute(page); + } +} diff --git a/tmail_integration_test/src/main/java/com/tmail/base/TestUtils.java b/tmail_integration_test/src/main/java/com/tmail/base/TestUtils.java new file mode 100644 index 0000000000..e09c20307c --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/TestUtils.java @@ -0,0 +1,9 @@ +package com.tmail.base; + +import com.microsoft.playwright.Page; + +public class TestUtils { + public void waitFor(int seconds, Page page) { + page.waitForTimeout(seconds * 1000); + } +} diff --git a/tmail_integration_test/src/main/java/com/tmail/base/UnsupportedPlatformException.java b/tmail_integration_test/src/main/java/com/tmail/base/UnsupportedPlatformException.java new file mode 100644 index 0000000000..90591ae33b --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/base/UnsupportedPlatformException.java @@ -0,0 +1,5 @@ +package com.tmail.base; + +public class UnsupportedPlatformException extends VirtualMachineError { + +} diff --git a/tmail_integration_test/src/main/java/com/tmail/robots/BasicAuthLoginRobot.java b/tmail_integration_test/src/main/java/com/tmail/robots/BasicAuthLoginRobot.java new file mode 100644 index 0000000000..5657d81f14 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/robots/BasicAuthLoginRobot.java @@ -0,0 +1,26 @@ +package com.tmail.robots; + +import com.microsoft.playwright.Page; +import com.tmail.base.CoreRobot; + +public class BasicAuthLoginRobot extends CoreRobot { + + public BasicAuthLoginRobot(Page page) { + super(page); + } + + public void enterUsername(String username) { + page.locator("#email").fill(username); + } + + public void enterPassword(String password) { + page.locator("input[aria-label='password']").click(); + page.waitForTimeout(1000); + page.locator("input[aria-label='password']").fill(password); + } + + public void clickLogin() { + page.getByText("Sign in").last().click(); + } + +} diff --git a/tmail_integration_test/src/main/java/com/tmail/robots/ComposerRobot.java b/tmail_integration_test/src/main/java/com/tmail/robots/ComposerRobot.java new file mode 100644 index 0000000000..744a2335a1 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/robots/ComposerRobot.java @@ -0,0 +1,42 @@ +package com.tmail.robots; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +import com.microsoft.playwright.FrameLocator; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.tmail.base.CoreRobot; + +public class ComposerRobot extends CoreRobot { + + public ComposerRobot(Page page) { + super(page); + } + + public void addReceipients(ArrayList recipients) { + Locator toFieldSelector = page.getByLabel("Composer:to").locator("input"); + toFieldSelector.click(); + for (String recipient : recipients) { + toFieldSelector.fill(recipient); + page.getByText(Pattern.compile("Composer:suggestion:" + recipient)).click(); + } + page.keyboard().press("Tab"); + page.waitForTimeout(1000); + } + + public void addSubject(String subject) { + page.getByLabel("Composer:subject").fill(subject); + page.keyboard().press("Tab"); + } + + public void addContent(String content) { + FrameLocator frameLocator = page.frameLocator("iFrame"); + frameLocator.locator(".note-editable").fill(content); + } + + public void clickSend() { + page.getByText("Send").click(); + } + +} diff --git a/tmail_integration_test/src/main/java/com/tmail/robots/HomeRobot.java b/tmail_integration_test/src/main/java/com/tmail/robots/HomeRobot.java new file mode 100644 index 0000000000..aa4deb8b0d --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/robots/HomeRobot.java @@ -0,0 +1,15 @@ +package com.tmail.robots; + +import com.microsoft.playwright.Page; +import com.tmail.base.CoreRobot; + +public class HomeRobot extends CoreRobot { + + public HomeRobot(Page page) { + super(page); + } + + public void navigateToTestSite(String url) { + page.navigate(url); + } +} diff --git a/tmail_integration_test/src/main/java/com/tmail/robots/MailboxDashboardRobot.java b/tmail_integration_test/src/main/java/com/tmail/robots/MailboxDashboardRobot.java new file mode 100644 index 0000000000..2eb9216389 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/robots/MailboxDashboardRobot.java @@ -0,0 +1,24 @@ +package com.tmail.robots; + +import com.microsoft.playwright.Page; +import com.tmail.base.CoreRobot; + +public class MailboxDashboardRobot extends CoreRobot { + + public MailboxDashboardRobot(Page page) { + super(page); + } + + public void openComposer() { + page.getByText("Compose").click(); + } + + public void waitForSendEmailSuccessToast() { + page.getByText("Message has been sent successfully").waitFor(); + } + + public void waitUntilExactLabelIsVisible(String string) { + page.getByText(string).waitFor(); + } + +} diff --git a/tmail_integration_test/src/main/java/com/tmail/robots/OidcLoginRobot.java b/tmail_integration_test/src/main/java/com/tmail/robots/OidcLoginRobot.java new file mode 100644 index 0000000000..3cb34e7377 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/robots/OidcLoginRobot.java @@ -0,0 +1,24 @@ +package com.tmail.robots; + +import com.microsoft.playwright.Page; +import com.tmail.base.CoreRobot; + +public class OidcLoginRobot extends CoreRobot { + + public OidcLoginRobot(Page page) { + super(page); + } + + public void enterUsername(String username) { + page.locator("#username").fill(username); + } + + public void enterPassword(String password) { + page.locator("#password").fill(password); + } + + public void clickLogin() { + page.locator("#kc-login").click(); + } + +} diff --git a/tmail_integration_test/src/main/java/com/tmail/scenarios/BasicAuthLoginScenario.java b/tmail_integration_test/src/main/java/com/tmail/scenarios/BasicAuthLoginScenario.java new file mode 100644 index 0000000000..36d90ea62c --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/scenarios/BasicAuthLoginScenario.java @@ -0,0 +1,35 @@ +package com.tmail.scenarios; + +import com.microsoft.playwright.Page; +import com.tmail.base.BaseScenario; +import com.tmail.robots.BasicAuthLoginRobot; +import com.tmail.robots.HomeRobot; +import com.tmail.robots.MailboxDashboardRobot; + +public class BasicAuthLoginScenario extends BaseScenario { + String testUrl; + String username; + String password; + + public BasicAuthLoginScenario(String testUrl, String username, String password) { + this.testUrl = testUrl; + this.username = username; + this.password = password; + } + + @Override + public void execute(Page page) { + HomeRobot homeRobot = new HomeRobot(page); + BasicAuthLoginRobot loginRobot = new BasicAuthLoginRobot(page); + MailboxDashboardRobot mailboxDashboardRobot = new MailboxDashboardRobot(page); + + homeRobot.navigateToTestSite(testUrl); + + loginRobot.enterUsername(username); + loginRobot.enterPassword(password); + loginRobot.clickLogin(); + + mailboxDashboardRobot.waitUntilExactLabelIsVisible("Compose"); + } + +} diff --git a/tmail_integration_test/src/main/java/com/tmail/scenarios/ComposeEmailScenario.java b/tmail_integration_test/src/main/java/com/tmail/scenarios/ComposeEmailScenario.java new file mode 100644 index 0000000000..fc1dc01b99 --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/scenarios/ComposeEmailScenario.java @@ -0,0 +1,42 @@ +package com.tmail.scenarios; + +import java.util.ArrayList; +import java.util.List; + +import com.microsoft.playwright.Page; +import com.tmail.base.BaseScenario; +import com.tmail.robots.ComposerRobot; +import com.tmail.robots.MailboxDashboardRobot; + +public class ComposeEmailScenario extends BaseScenario { + String testUrl; + String username; + String password; + String additionalReceipent; + + public ComposeEmailScenario(String testUrl, String username, String password, String additionalReceipent) { + this.testUrl = testUrl; + this.username = username; + this.password = password; + this.additionalReceipent = additionalReceipent; + } + + @Override + public void execute(Page page) { + MailboxDashboardRobot mailboxDashboardRobot = new MailboxDashboardRobot(page); + ComposerRobot composerRobot = new ComposerRobot(page); + + OidcLoginScenario loginUseCase = new OidcLoginScenario(testUrl, username, password); + loginUseCase.execute(page); + + mailboxDashboardRobot.openComposer(); + + composerRobot.addReceipients(new ArrayList(List.of(username, additionalReceipent))); + composerRobot.addSubject("Test subject"); + composerRobot.addContent("Test content"); + composerRobot.clickSend(); + + mailboxDashboardRobot.waitForSendEmailSuccessToast(); + } + +} diff --git a/tmail_integration_test/src/main/java/com/tmail/scenarios/OidcLoginScenario.java b/tmail_integration_test/src/main/java/com/tmail/scenarios/OidcLoginScenario.java new file mode 100644 index 0000000000..f79217d85e --- /dev/null +++ b/tmail_integration_test/src/main/java/com/tmail/scenarios/OidcLoginScenario.java @@ -0,0 +1,35 @@ +package com.tmail.scenarios; + +import com.microsoft.playwright.Page; +import com.tmail.base.BaseScenario; +import com.tmail.robots.HomeRobot; +import com.tmail.robots.MailboxDashboardRobot; +import com.tmail.robots.OidcLoginRobot; + +public class OidcLoginScenario extends BaseScenario { + String testUrl; + String username; + String password; + + public OidcLoginScenario(String testUrl, String username, String password) { + this.testUrl = testUrl; + this.username = username; + this.password = password; + } + + @Override + public void execute(Page page) { + HomeRobot homeRobot = new HomeRobot(page); + OidcLoginRobot loginRobot = new OidcLoginRobot(page); + MailboxDashboardRobot mailboxDashboardRobot = new MailboxDashboardRobot(page); + + homeRobot.navigateToTestSite(testUrl); + + loginRobot.enterUsername(username); + loginRobot.enterPassword(password); + loginRobot.clickLogin(); + + mailboxDashboardRobot.waitUntilExactLabelIsVisible("Compose"); + } + +} diff --git a/tmail_integration_test/src/test/java/com/tmail/preprod/basic_auth/login/BasicAuthLoginTest.java b/tmail_integration_test/src/test/java/com/tmail/preprod/basic_auth/login/BasicAuthLoginTest.java new file mode 100644 index 0000000000..baffbdef5d --- /dev/null +++ b/tmail_integration_test/src/test/java/com/tmail/preprod/basic_auth/login/BasicAuthLoginTest.java @@ -0,0 +1,24 @@ +package com.tmail.preprod.basic_auth.login; + +import static com.tmail.preprod.extension.Fixture.BOB; +import static com.tmail.preprod.extension.Fixture.PASSWORD; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.tmail.base.TestBase; +import com.tmail.preprod.extension.TmailExtension; +import com.tmail.scenarios.BasicAuthLoginScenario; + +public class BasicAuthLoginTest extends TestBase { + @RegisterExtension + TmailExtension tmailExtension = new TmailExtension(); + + @BeforeEach + void setupScenario() { + scenario = new BasicAuthLoginScenario( + tmailExtension.getTmailWebUrl().toString(), + BOB, + PASSWORD); + } +} diff --git a/tmail_integration_test/src/test/java/com/tmail/preprod/extension/Fixture.java b/tmail_integration_test/src/test/java/com/tmail/preprod/extension/Fixture.java new file mode 100644 index 0000000000..9c7fdfcd1d --- /dev/null +++ b/tmail_integration_test/src/test/java/com/tmail/preprod/extension/Fixture.java @@ -0,0 +1,7 @@ +package com.tmail.preprod.extension; + +public interface Fixture { + String DOMAIN = "domain.tld"; + String BOB = "bob@domain.tld"; + String PASSWORD = "password"; +} diff --git a/tmail_integration_test/src/test/java/com/tmail/preprod/extension/Runnables.java b/tmail_integration_test/src/test/java/com/tmail/preprod/extension/Runnables.java new file mode 100644 index 0000000000..b4070a19f0 --- /dev/null +++ b/tmail_integration_test/src/test/java/com/tmail/preprod/extension/Runnables.java @@ -0,0 +1,26 @@ +package com.tmail.preprod.extension; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +public class Runnables { + public static void runParallel(Runnable... runnables) { + Flux stream = Flux.just(runnables); + runParallel(stream); + } + + public static void runParallel(Flux runnables) { + runnables + .publishOn(Schedulers.boundedElastic()) + .parallel() + .runOn(Schedulers.boundedElastic()) + .flatMap(runnable -> { + runnable.run(); + return Mono.empty(); + }) + .sequential() + .then() + .block(); + } +} diff --git a/tmail_integration_test/src/test/java/com/tmail/preprod/extension/TmailExtension.java b/tmail_integration_test/src/test/java/com/tmail/preprod/extension/TmailExtension.java new file mode 100644 index 0000000000..fbf388f817 --- /dev/null +++ b/tmail_integration_test/src/test/java/com/tmail/preprod/extension/TmailExtension.java @@ -0,0 +1,332 @@ +package com.tmail.preprod.extension; + +import static com.tmail.preprod.extension.Fixture.BOB; +import static com.tmail.preprod.extension.Fixture.DOMAIN; +import static com.tmail.preprod.extension.Fixture.PASSWORD; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.utility.MountableFile; + +public class TmailExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback, AfterAllCallback { + + public enum IsolationLevel { + // Do not restart container after each test. Low isolation between tests, but fast pace for test suite. + // Suitable for either test suite that does not require strong isolation, or you are confident that backend data is well cleaned up each test. + // Note that in this mode, the TmailExtension already handles clean up some basic data after each test. You may need to clean up additional data yourself if needed. + LOW, + + // Always restart container after each test. Strong isolation between tests, but slow pace for test suite. + // Suitable for either test suite that requires strong isolation, or you are NOT confident that backend data is well cleaned up each test. + // Note that in this mode, you do not need to care about cleaning up data yourself. + HIGH + } + + private static final String TMAIL_WEB_LATEST_IMAGE_ENV = "tmail-web-latest-image"; // environment variable stores the docker image tag for the current branch. e.g., can set by CI or local dev + private static final WaitStrategy WAIT_STRATEGY = new LogMessageWaitStrategy().withRegEx(".*JAMES server started.*\\n").withTimes(1) + .withStartupTimeout(Duration.ofMinutes(3)); + private static final int JMAP_PORT = 80; + private static final int WEB_ADMIN_PORT = 8000; + private static final int NGINX_HTTP_PORT = 80; + private static final int TMAIL_WEB_PORT = 80; + + private final IsolationLevel isolationLevel; + private final GenericContainer tmailBackend; + private final GenericContainer tmailWeb; + private final GenericContainer nginx; + private final HttpClient httpClient; + + public TmailExtension(IsolationLevel isolationLevel) { + this.isolationLevel = isolationLevel; + + Network network = Network.newNetwork(); + + this.tmailBackend = new GenericContainer<>("linagora/tmail-backend:memory-0.10.2") + .withNetworkAliases("tmail-backend") + .withNetwork(network) + .withCopyFileToContainer(MountableFile.forClasspathResource("tmail-backend-conf/imapserver.xml"), "/root/conf/") + .withCopyFileToContainer(MountableFile.forClasspathResource("tmail-backend-conf/smtpserver.xml"), "/root/conf/") + .withCopyFileToContainer(MountableFile.forClasspathResource("tmail-backend-conf/jwt_privatekey"), "/root/conf/") + .withCopyFileToContainer(MountableFile.forClasspathResource("tmail-backend-conf/jwt_publickey"), "/root/conf/") + .withCopyFileToContainer(MountableFile.forClasspathResource("tmail-backend-conf/jmap.properties"), "/root/conf/") + .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withName("tmail-backend-memory-testing" + UUID.randomUUID())) + .waitingFor(WAIT_STRATEGY) + .withExposedPorts(JMAP_PORT, WEB_ADMIN_PORT); + + this.nginx = new GenericContainer<>("nginx:alpine") + .withNetwork(network) + .dependsOn(tmailBackend) + .withExposedPorts(NGINX_HTTP_PORT) + .waitingFor(Wait.forHttp("/").forStatusCode(200)); + + String tmailWebImageTag = Optional.ofNullable(System.getenv(TMAIL_WEB_LATEST_IMAGE_ENV)) + .orElse("tmail-web:integration-test"); + this.tmailWeb = new GenericContainer<>(tmailWebImageTag) + .withNetworkAliases("tmail-web") + .withNetwork(network) + .dependsOn(nginx) + .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withName("tmail-web-testing" + UUID.randomUUID())) + .waitingFor(Wait.forHttp("/").forStatusCode(200)) + .withExposedPorts(TMAIL_WEB_PORT); + + this.httpClient = HttpClient.newHttpClient(); + } + + public TmailExtension() { + this(IsolationLevel.LOW); + } + + @Override + public void beforeAll(ExtensionContext context) { + if (isolationLevel == IsolationLevel.LOW) { + startContainers(); + } + } + + @Override + public void beforeEach(ExtensionContext extensionContext) throws Exception { + if (isolationLevel == IsolationLevel.LOW) { + provisionBackendData(); + } + if (isolationLevel.equals(IsolationLevel.HIGH)) { + startContainers(); + provisionBackendData(); + } + } + + @Override + public void afterEach(ExtensionContext extensionContext) throws Exception { + if (isolationLevel == IsolationLevel.LOW) { + cleanupBackendData(); + } + if (isolationLevel.equals(IsolationLevel.HIGH)) { + stopContainers(); + } + } + + @Override + public void afterAll(ExtensionContext context) { + stopContainers(); + } + + private void startContainers() { + tmailBackend.start(); + + // we configure NGINX to tell tmail-backend how to response apiURL during runtime (as apiURL would be dynamic because of TestContainer random port nature) + nginx.start(); + configureNginxProxyForJmap(); + + setupTmailWebEnvFile(); + tmailWeb.start(); + } + + private void stopContainers() { + Runnables.runParallel( + tmailBackend::stop, + nginx::stop, + tmailWeb::stop); + } + + private void configureNginxProxyForJmap() { + try { + // Create a temporary file for the updated nginx.conf + Path tempNginxConf = Files.createTempFile("nginx", ".conf"); + + // Write the updated nginx configuration to the temporary file + Files.write(tempNginxConf, (""" + events { + worker_connections 1024; + } + + http { + upstream tmail-backend { + server tmail-backend:80; + } + + server { + listen 80; + + location / { + proxy_pass http://tmail-backend; + proxy_set_header X-JMAP-PREFIX %s; + proxy_set_header Host $host; + } + } + } + """.formatted(getProxiedJmapUrl())).getBytes()); + + // Copy the updated nginx.conf to the running container + nginx.copyFileToContainer(MountableFile.forHostPath(tempNginxConf), "/tmp/nginx.conf"); + + // Move the updated configuration to the nginx configuration directory + nginx.execInContainer("mv", "/tmp/nginx.conf", "/etc/nginx/nginx.conf"); + + // Reload nginx configuration + nginx.execInContainer("nginx", "-s", "reload"); + } catch (Exception e) { + throw new RuntimeException("Failed to create or copy temporary env.file", e); + } + } + + private void setupTmailWebEnvFile() { + try { + // Create a temporary file with the desired content + File tempFile = File.createTempFile("env", ".file"); + try (FileWriter writer = new FileWriter(tempFile)) { + writer.write("SERVER_URL=" + getProxiedJmapUrl() + "\n"); + writer.write("DOMAIN_REDIRECT_URL=http://localhost:3000\n"); + } + + // Copy the temporary file into the container + tmailWeb.withCopyFileToContainer(MountableFile.forHostPath(tempFile.getAbsolutePath()), "/usr/share/nginx/html/assets/env.file"); + } catch (IOException e) { + throw new RuntimeException("Failed to create or copy temporary env.file", e); + } + } + + public URL getTmailWebUrl() { + try { + return new URI("http://" + + tmailWeb.getHost() + ":" + + tmailWeb.getMappedPort(TMAIL_WEB_PORT)).toURL(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private URL getProxiedJmapUrl() { + try { + return new URI("http://" + + nginx.getHost() + ":" + + nginx.getMappedPort(NGINX_HTTP_PORT)).toURL(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void provisionBackendData() throws Exception { + createDomain(DOMAIN); + createUser(BOB, PASSWORD); + } + + public void createDomain(String domain) throws Exception { + tmailBackend.execInContainer("james-cli", "AddDomain", domain); + } + + public void createUser(String username, String password) throws IOException, InterruptedException { + tmailBackend.execInContainer("james-cli", "AddUser", username, password); + } + + private void cleanupBackendData() throws Exception { + deleteUser(BOB); + deleteDomain(DOMAIN); + } + + public void deleteDomain(String domain) throws Exception { + deleteUsersDataOfDomain(domain); + tmailBackend.execInContainer("james-cli", "RemoveDomain", domain); + } + + public void deleteUser(String username) throws Exception { + deleteUserData(username); + tmailBackend.execInContainer("james-cli", "RemoveUser", username); + } + + private URL getTMailBackendWebadminUrl() { + try { + return new URI("http://" + + tmailBackend.getHost() + ":" + + tmailBackend.getMappedPort(WEB_ADMIN_PORT)).toURL(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void deleteUserData(String username) throws Exception { + String deleteUserDataUrl = getTMailBackendWebadminUrl() + "/users/" + username + "?action=deleteData"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(deleteUserDataUrl)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 201) { + throw new RuntimeException("Failed to delete user data for username: " + username); + } + + // Extract taskId from response body + Pattern pattern = Pattern.compile("\"taskId\":\"([^\"]+)\""); + Matcher matcher = pattern.matcher(response.body()); + if (matcher.find()) { + String taskId = matcher.group(1); + awaitTaskCompletion(taskId); + } else { + throw new RuntimeException("Failed to extract taskId from response for username: " + username); + } + } + + private void deleteUsersDataOfDomain(String domain) throws Exception { + String deleteUsersDataOfDomainUrl = getTMailBackendWebadminUrl() + "/domains/" + domain + "?action=deleteData"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(deleteUsersDataOfDomainUrl)) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 201) { + throw new RuntimeException("Failed to delete user data for domain: " + domain); + } + + // Extract taskId from response body + Pattern pattern = Pattern.compile("\"taskId\":\"([^\"]+)\""); + Matcher matcher = pattern.matcher(response.body()); + if (matcher.find()) { + String taskId = matcher.group(1); + awaitTaskCompletion(taskId); + } else { + throw new RuntimeException("Failed to extract taskId from response for domain: " + domain); + } + } + + private void awaitTaskCompletion(String taskId) throws Exception { + String taskStatusUrl = getTMailBackendWebadminUrl() + "/tasks/" + taskId + "/await"; + HttpRequest request = HttpRequest.newBuilder() + .uri(new URI(taskStatusUrl)) + .GET() + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 200) { + throw new RuntimeException("Failed to get task status for taskId: " + taskId); + } + } +} diff --git a/tmail_integration_test/src/test/java/com/tmail/preprod/oidc/composeEmail/ComposeEmailTest.java b/tmail_integration_test/src/test/java/com/tmail/preprod/oidc/composeEmail/ComposeEmailTest.java new file mode 100644 index 0000000000..607a4e4776 --- /dev/null +++ b/tmail_integration_test/src/test/java/com/tmail/preprod/oidc/composeEmail/ComposeEmailTest.java @@ -0,0 +1,16 @@ +// package com.tmail.preprod.oidc.composeEmail; + +// import com.tmail.base.TestBase; +// import com.tmail.scenarios.ComposeEmailScenario; + +// public class ComposeEmailTest extends TestBase { + +// ComposeEmailTest() { +// scenario = new ComposeEmailScenario( +// properties.getProperty("app.hostUrl"), +// properties.getProperty("user.name"), +// properties.getProperty("user.password"), +// properties.getProperty("user.additionalMailRecipent")); +// } + +// } diff --git a/tmail_integration_test/src/test/java/com/tmail/preprod/oidc/login/OidcLoginTest.java b/tmail_integration_test/src/test/java/com/tmail/preprod/oidc/login/OidcLoginTest.java new file mode 100644 index 0000000000..ff429c284c --- /dev/null +++ b/tmail_integration_test/src/test/java/com/tmail/preprod/oidc/login/OidcLoginTest.java @@ -0,0 +1,15 @@ +// package com.tmail.preprod.oidc.login; + +// import com.tmail.base.TestBase; +// import com.tmail.scenarios.OidcLoginScenario; + +// public class OidcLoginTest extends TestBase { + +// OidcLoginTest() { +// scenario = new OidcLoginScenario( +// properties.getProperty("app.hostUrl"), +// properties.getProperty("user.name"), +// properties.getProperty("user.password")); +// } + +// } diff --git a/tmail_integration_test/src/test/resources/junit-platform.properties b/tmail_integration_test/src/test/resources/junit-platform.properties new file mode 100644 index 0000000000..4570caf069 --- /dev/null +++ b/tmail_integration_test/src/test/resources/junit-platform.properties @@ -0,0 +1,5 @@ +junit.jupiter.execution.parallel.enabled = true +junit.jupiter.execution.parallel.mode.default = same_thread +junit.jupiter.execution.parallel.mode.classes.default = concurrent +junit.jupiter.execution.parallel.config.strategy=dynamic +junit.jupiter.execution.parallel.config.dynamic.factor=0.5 \ No newline at end of file diff --git a/tmail_integration_test/src/test/resources/tmail-backend-conf/imapserver.xml b/tmail_integration_test/src/test/resources/tmail-backend-conf/imapserver.xml new file mode 100644 index 0000000000..db917d5958 --- /dev/null +++ b/tmail_integration_test/src/test/resources/tmail-backend-conf/imapserver.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tmail_integration_test/src/test/resources/tmail-backend-conf/jmap.properties b/tmail_integration_test/src/test/resources/tmail-backend-conf/jmap.properties new file mode 100644 index 0000000000..c1741f80ce --- /dev/null +++ b/tmail_integration_test/src/test/resources/tmail-backend-conf/jmap.properties @@ -0,0 +1,2 @@ +# So NGINX can override the dynamic tmail-backend JMAP URL via the session route +dynamic.jmap.prefix.resolution.enabled=true \ No newline at end of file diff --git a/tmail_integration_test/src/test/resources/tmail-backend-conf/jwt_privatekey b/tmail_integration_test/src/test/resources/tmail-backend-conf/jwt_privatekey new file mode 100644 index 0000000000..a4aa851ff5 --- /dev/null +++ b/tmail_integration_test/src/test/resources/tmail-backend-conf/jwt_privatekey @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDEV3BhLSBHuApH +GNlquIczzcvDOc0HoqczATsM7CeRp2SUicioxlItL2q6tKGTsK1zCcM+uKw1ydxd +AGDxpBD6kqIel/Quu/dRd8H6P/f4Hr0OOxGCgc78FYvjY6gcbPtaOmD9u/RMRqP6 +95s/DriPxPDYzA58yRH7NlhzJymthoZEtwIL/SuZ6e2huqnJdC11wEJgkwwkKO3x +01Y84V2y2EdRCvb5j9EOkCC5C4TGpjvSffD/4mcj+Z2VNTxQV0KdrlNxhBwwFEbL +TQ2B5hIkH2INDP6ubkxJeXmZ3iXyw+iNKtBUef9ZavIk4lZLKYNjZWX2eBg2cokT +p759oRoHAgMBAAECggEAdmyrCuH2C2wVPubdFIKygeuKEHnHkehoYtpGLLgv8al+ +gB1PG4VrQXfNL0oN/w/cvntP+X/X1yWnNa0py/YCi7Bv+nX6wUl8lfXe2TtGLLEV +pQS5vfbfyqqQUpnkZyjQvo5hvAlnA67D73bze6g8Z/MItir2PgvlPZl85g/kEpX3 +zt1yDTUFe+L8Ur+8Lied/0w2M53lUlebIYtsa2W7I05YzUBAVXkCIffnaIt3QtTC +tppBcVZXUacRtqULBMcUE2zc/yUKNqHzBjlkBv4VQ7nQDOjjUfW+VtccnTr+mLnl +R7VDy+POuZQ5u0gA8IwJNJFd5qdIm7l4tG7xa69rMQKBgQDmX1FF5Zty0Q7Acp1n +G9TZBOTTehzQPYsJwMynLR/b8mUAL+FTCh7Q7OJkGhcBxVDgc34OVPvc9wlFSUuU +ac0C0GtcD3BidatEfwMqVdpwcDnSEK47N13oSmaAL21mgC6/0ypV5TBgbkMRcxSb +h1GdeBWEG1RluVx6n1TflSvw6QKBgQDaLvj3fNbcIfJubx5oP2kmA1rw5jcSShK4 +UgM4Ifj98LOmsiB6qfY+36p6D78XINV0KpS3THi8rWzf31OuTN1BoZ5UpcyOOrDb +2pNnfGpC1VBP4rfWJMNpcGstj3YUNEV5pLyd5Cd6/gV8nRgiQ9ccEDJ1I5GXVWfV ++2a7/zddbwKBgHqWWi8xoWiVqp3p36yQeNEK86E9J7wAI86K09xZ/MwTzn8s+2Au +0HsossfFwlxk3Uay7m899dB9fGdsO1W8fyVyNs8EQC+EoiCO3eZXTSfr8DjCO5Sz +P7tua+DmW/bhWv8kpTCUBwwpYHMWo+6nMVz0G67yxBRlcLqnsohPXtSRAoGBAMMD +MxJ6Kc1OJlMgzKve6YvJefJRwq19Oag33ZrBerz29IwtMCyTV36xCb3Z7zGr7j3L +hWskVdJGrEaZZUEogKaV31/HZcNGoCeSASiBIrUj1ongmfI0n9jRW2q4jJDYe7ST +UudJMySSgbL08spFmrIBpCfhJ9N8ybeP4i5smj7PAoGBANSfn+DzQPICp2rldw7y +rPSCywIM6LzdoyykRMmX04sQAFVKgJwPei+oLrg8HcUjCZ1t8KJ4tHsvqFkQ0O4s +q8eh2eli5Qppg/Qmx1zT1W7+JYxlPsiXfmViBcY2+yNjNOHQPzyJyE+pvybW0DC+ +k9EJZv81OGrKInhjB/Ep6C76 +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/tmail_integration_test/src/test/resources/tmail-backend-conf/jwt_publickey b/tmail_integration_test/src/test/resources/tmail-backend-conf/jwt_publickey new file mode 100644 index 0000000000..ea8beb9de0 --- /dev/null +++ b/tmail_integration_test/src/test/resources/tmail-backend-conf/jwt_publickey @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxFdwYS0gR7gKRxjZariH +M83LwznNB6KnMwE7DOwnkadklInIqMZSLS9qurShk7CtcwnDPrisNcncXQBg8aQQ ++pKiHpf0Lrv3UXfB+j/3+B69DjsRgoHO/BWL42OoHGz7Wjpg/bv0TEaj+vebPw64 +j8Tw2MwOfMkR+zZYcycprYaGRLcCC/0rmentobqpyXQtdcBCYJMMJCjt8dNWPOFd +sthHUQr2+Y/RDpAguQuExqY70n3w/+JnI/mdlTU8UFdCna5TcYQcMBRGy00NgeYS +JB9iDQz+rm5MSXl5md4l8sPojSrQVHn/WWryJOJWSymDY2Vl9ngYNnKJE6e+faEa +BwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/tmail_integration_test/src/test/resources/tmail-backend-conf/smtpserver.xml b/tmail_integration_test/src/test/resources/tmail-backend-conf/smtpserver.xml new file mode 100644 index 0000000000..84834caa19 --- /dev/null +++ b/tmail_integration_test/src/test/resources/tmail-backend-conf/smtpserver.xml @@ -0,0 +1,2 @@ + +