diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index 76480ea1c12d7399a77b635e8d391f8c06cec1d7..f1f574c642483842c95dbbae41a283e22aa429f0 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -293,6 +293,7 @@ export default { :key="`${currentKey}-${field.name}`" v-bind="field" :is-validated="isValidated" + :data-qa-selector="`${field.name}_div`" /> </div> </div> diff --git a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue index 9e1ad24ae9fc62f4e88b94d8f2b516e9d192723e..b8fd8995744104b4526c8866c36895b882c7fa0a 100644 --- a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue +++ b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue @@ -33,6 +33,7 @@ export default { :key="`${currentKey}-${field.name}`" v-bind="field" :is-validated="isValidated" + :data-qa-selector="`${field.name}_div`" /> </div> </template> diff --git a/qa/qa/page/project/settings/integrations.rb b/qa/qa/page/project/settings/integrations.rb index 420dcb6391878220329e96938827302d66c06388..0d5515aacdf8bdcf4c577a39548a827c5bbea562 100644 --- a/qa/qa/page/project/settings/integrations.rb +++ b/qa/qa/page/project/settings/integrations.rb @@ -8,12 +8,17 @@ class Integrations < QA::Page::Base view 'app/assets/javascripts/integrations/index/components/integrations_table.vue' do element :prometheus_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern element :jira_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern + element :pipelines_email_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern end def click_on_prometheus_integration click_element :prometheus_link end + def click_pipelines_email_link + click_element :pipelines_email_link + end + def click_jira_link click_element :jira_link end diff --git a/qa/qa/page/project/settings/services/pipeline_status_emails.rb b/qa/qa/page/project/settings/services/pipeline_status_emails.rb new file mode 100644 index 0000000000000000000000000000000000000000..2f78577e3d5fc415229296976b0bd474ee394fc5 --- /dev/null +++ b/qa/qa/page/project/settings/services/pipeline_status_emails.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + module Services + class PipelineStatusEmails < QA::Page::Base + view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do + element :recipients_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern + element :notify_only_broken_pipelines_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern + element :save_changes_button + end + + def set_recipients(emails) + within_element :recipients_div do + fill_in 'Recipients', with: emails.join(',') + end + end + + def toggle_notify_broken_pipelines + within_element :notify_only_broken_pipelines_div do + uncheck 'Notify only broken pipelines', allow_label_click: true + end + end + + def click_save_button + click_element(:save_changes_button) + end + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb index f41e59856226b3ddc6312b699b3df7161bbacd93..b815186cd493e2cbb7ad7a589e226ac65a7a68b3 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb @@ -46,19 +46,22 @@ def mailhog_json total = mailhog_data.dig('total') subjects = mailhog_data.dig('items') .map(&method(:mailhog_item_subject)) - .join("\n") Runtime::Logger.debug(%Q[Total number of emails: #{total}]) - Runtime::Logger.debug(%Q[Subjects:\n#{subjects}]) + Runtime::Logger.debug(%Q[Subjects:\n#{subjects.join("\n")}]) # Expect at least two invitation messages: group and project - mailhog_data if total >= 2 + mailhog_data if mailhog_project_message_count(subjects) >= 1 end end def mailhog_item_subject(item) item.dig('Content', 'Headers', 'Subject', 0) end + + def mailhog_project_message_count(subjects) + subjects.count { |subject| subject.include?('project was granted') } + end end end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f4794b3a904a39827ba0d27088e7860f560ad9af --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module QA + RSpec.shared_examples 'notifies on a pipeline' do |exit_code| + before do + push_commit(exit_code: exit_code) + end + + it 'sends an email' do + meta = exit_code_meta(exit_code) + + project.visit! + Flow::Pipeline.wait_for_latest_pipeline(status: meta[:status]) + + messages = mail_hog_messages(mail_hog) + subjects = messages.map(&:subject) + targets = messages.map(&:to) + + aggregate_failures do + expect(subjects).to include(meta[:email_subject]) + expect(subjects).to include(/#{Regexp.escape(project.name)}/) + expect(targets).to include(*emails) + end + end + end + + RSpec.describe 'Verify', :orchestrated, :runner, :requires_admin, :smtp do + describe 'Pipeline status emails' do + let(:executor) { "qa-runner-#{Time.now.to_i}" } + let(:emails) { %w[foo@bar.com baz@buzz.com] } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'pipeline-status-project' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + let(:mail_hog) { Vendor::MailHog::API.new } + + before(:all) do + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) + end + + before do + setup_pipeline_emails(emails) + end + + describe 'when pipeline passes', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366240' do + include_examples 'notifies on a pipeline', 0 + end + + describe 'when pipeline fails', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366241' do + include_examples 'notifies on a pipeline', 1 + end + + def push_commit(exit_code: 0) + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: gitlab_ci_yaml(exit_code: exit_code) + } + ] + ) + end + end + + def setup_pipeline_emails(emails) + page.visit Runtime::Scenario.gitlab_address + Flow::Login.sign_in_unless_signed_in + + project.visit! + + Page::Project::Menu.perform(&:go_to_integrations_settings) + QA::Page::Project::Settings::Integrations.perform(&:click_pipelines_email_link) + + QA::Page::Project::Settings::Services::PipelineStatusEmails.perform do |pipeline_status_emails| + pipeline_status_emails.toggle_notify_broken_pipelines # notify on pass and fail + pipeline_status_emails.set_recipients(emails) + pipeline_status_emails.click_save_button + end + end + + def gitlab_ci_yaml(exit_code: 0, tag: executor) + <<~YAML + test-pipeline-email: + tags: + - #{tag} + script: sleep 5; exit #{exit_code}; + YAML + end + + private + + def exit_code_meta(exit_code) + { + 0 => { status: 'passed', email_subject: /Successful pipeline/ }, + 1 => { status: 'failed', email_subject: /Failed pipeline/ } + }[exit_code] + end + + def mail_hog_messages(mail_hog_api) + Support::Retrier.retry_until(sleep_interval: 1) do + Runtime::Logger.debug('Fetching email...') + + messages = mail_hog_api.fetch_messages + logs = messages.map { |m| "#{m.to}: #{m.subject}" } + + Runtime::Logger.debug("MailHog Logs: #{logs.join("\n")}") + + # for failing pipelines we have three messages + # one for the owner + # and one for each recipient + messages if mail_hog_pipeline_count(messages) >= 2 + end + end + + def mail_hog_pipeline_count(messages) + messages.count { |message| message.subject.include?('pipeline') } + end + end + end +end diff --git a/qa/qa/vendor/mail_hog/api.rb b/qa/qa/vendor/mail_hog/api.rb new file mode 100644 index 0000000000000000000000000000000000000000..85eb06316247c8a4ece6cc0ca1a41e70fe6837fe --- /dev/null +++ b/qa/qa/vendor/mail_hog/api.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module QA + module Vendor + module MailHog + # Represents a Set of messages from a MailHog response + class Messages + include Enumerable + + attr_reader :data + + def initialize(data) + @data = data + end + + def total + data.dig('total') + end + + def each + data.dig('items')&.each do |item| + yield MessageItem.new(item) + end + end + end + + # Represents an email item from a MailHog response + class MessageItem + attr_reader :data + + def initialize(data) + @data = data + end + + def to + data.dig('Content', 'Headers', 'To', 0) + end + + def subject + data.dig('Content', 'Headers', 'Subject', 0) + end + end + + class API + include Support::API + + attr_reader :hostname + + def initialize(hostname: QA::Runtime::Env.mailhog_hostname || 'localhost') + @hostname = hostname + end + + def base_url + "http://#{hostname}:8025" + end + + def api_messages_url(version: 2) + "#{base_url}/api/v#{version}/messages" + end + + def delete_messages + delete(api_messages_url(version: 1)) + end + + def fetch_messages + Messages.new(JSON.parse(fetch_messages_json)) + end + + def fetch_messages_json + get(api_messages_url).body + end + end + end + end +end