Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models/integrations')
-rw-r--r--spec/models/integrations/asana_spec.rb114
-rw-r--r--spec/models/integrations/assembla_spec.rb38
-rw-r--r--spec/models/integrations/bamboo_spec.rb287
-rw-r--r--spec/models/integrations/campfire_spec.rb87
-rw-r--r--spec/models/integrations/chat_message/alert_message_spec.rb58
-rw-r--r--spec/models/integrations/chat_message/base_message_spec.rb34
-rw-r--r--spec/models/integrations/chat_message/deployment_message_spec.rb164
-rw-r--r--spec/models/integrations/chat_message/issue_message_spec.rb124
-rw-r--r--spec/models/integrations/chat_message/merge_message_spec.rb144
-rw-r--r--spec/models/integrations/chat_message/note_message_spec.rb192
-rw-r--r--spec/models/integrations/chat_message/pipeline_message_spec.rb378
-rw-r--r--spec/models/integrations/chat_message/push_message_spec.rb215
-rw-r--r--spec/models/integrations/chat_message/wiki_page_message_spec.rb171
-rw-r--r--spec/models/integrations/confluence_spec.rb90
-rw-r--r--spec/models/integrations/datadog_spec.rb185
-rw-r--r--spec/models/integrations/emails_on_push_spec.rb163
16 files changed, 2444 insertions, 0 deletions
diff --git a/spec/models/integrations/asana_spec.rb b/spec/models/integrations/asana_spec.rb
new file mode 100644
index 00000000000..4473478910a
--- /dev/null
+++ b/spec/models/integrations/asana_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Asana do
+ describe 'Associations' do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :api_key }
+ end
+ end
+
+ describe 'Execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:gid) { "123456789ABCD" }
+
+ def create_data_for_commits(*messages)
+ {
+ object_kind: 'push',
+ ref: 'master',
+ user_name: user.name,
+ commits: messages.map do |m|
+ {
+ message: m,
+ url: 'https://gitlab.com/'
+ }
+ end
+ }
+ end
+
+ before do
+ @asana = described_class.new
+ allow(@asana).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ api_key: 'verySecret',
+ restrict_to_branch: 'master'
+ )
+ end
+
+ it 'calls Asana service to create a story' do
+ data = create_data_for_commits("Message from commit. related to ##{gid}")
+ expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
+
+ d1 = double('Asana::Resources::Task')
+ expect(d1).to receive(:add_comment).with(text: expected_message)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, gid).once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'calls Asana service to create a story and close a task' do
+ data = create_data_for_commits('fix #456789')
+ d1 = double('Asana::Resources::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'is able to close via url' do
+ data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
+ d1 = double('Asana::Resources::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'allows multiple matches per line' do
+ message = <<-EOF
+ minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
+ ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
+ EOF
+ data = create_data_for_commits(message)
+ d1 = double('Asana::Resources::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
+
+ d2 = double('Asana::Resources::Task')
+ expect(d2).to receive(:add_comment)
+ expect(d2).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
+
+ d3 = double('Asana::Resources::Task')
+ expect(d3).to receive(:add_comment)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
+
+ d4 = double('Asana::Resources::Task')
+ expect(d4).to receive(:add_comment)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
+
+ d5 = double('Asana::Resources::Task')
+ expect(d5).to receive(:add_comment)
+ expect(d5).to receive(:update).with(completed: true)
+ expect(::Asana::Resources::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
+
+ @asana.execute(data)
+ end
+ end
+end
diff --git a/spec/models/integrations/assembla_spec.rb b/spec/models/integrations/assembla_spec.rb
new file mode 100644
index 00000000000..bf9033416e9
--- /dev/null
+++ b/spec/models/integrations/assembla_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Assembla do
+ include StubRequests
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe "Execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ @assembla_service = described_class.new
+ allow(@assembla_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ token: 'verySecret',
+ subdomain: 'project_name'
+ )
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+ @api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
+ stub_full_request(@api_url, method: :post)
+ end
+
+ it "calls Assembla API" do
+ @assembla_service.execute(@sample_data)
+ expect(WebMock).to have_requested(:post, stubbed_hostname(@api_url)).with(
+ body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
+ ).once
+ end
+ end
+end
diff --git a/spec/models/integrations/bamboo_spec.rb b/spec/models/integrations/bamboo_spec.rb
new file mode 100644
index 00000000000..0ba1595bbd8
--- /dev/null
+++ b/spec/models/integrations/bamboo_spec.rb
@@ -0,0 +1,287 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Bamboo, :use_clean_rails_memory_store_caching do
+ include ReactiveCachingHelpers
+ include StubRequests
+
+ let(:bamboo_url) { 'http://gitlab.com/bamboo' }
+
+ let_it_be(:project) { create(:project) }
+
+ subject(:service) do
+ described_class.create!(
+ project: project,
+ properties: {
+ bamboo_url: bamboo_url,
+ username: 'mic',
+ password: 'password',
+ build_key: 'foo'
+ }
+ )
+ end
+
+ describe 'Associations' do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:build_key) }
+ it { is_expected.to validate_presence_of(:bamboo_url) }
+ it_behaves_like 'issue tracker service URL attribute', :bamboo_url
+
+ describe '#username' do
+ it 'does not validate the presence of username if password is nil' do
+ subject.password = nil
+
+ expect(subject).not_to validate_presence_of(:username)
+ end
+
+ it 'validates the presence of username if password is present' do
+ subject.password = 'secret'
+
+ expect(subject).to validate_presence_of(:username)
+ end
+ end
+
+ describe '#password' do
+ it 'does not validate the presence of password if username is nil' do
+ subject.username = nil
+
+ expect(subject).not_to validate_presence_of(:password)
+ end
+
+ it 'validates the presence of password if username is present' do
+ subject.username = 'john'
+
+ expect(subject).to validate_presence_of(:password)
+ end
+ end
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:build_key) }
+ it { is_expected.not_to validate_presence_of(:bamboo_url) }
+ it { is_expected.not_to validate_presence_of(:username) }
+ it { is_expected.not_to validate_presence_of(:password) }
+ end
+ end
+
+ describe 'Callbacks' do
+ describe 'before_update :reset_password' do
+ context 'when a password was previously set' do
+ it 'resets password if url changed' do
+ bamboo_service = service
+
+ bamboo_service.bamboo_url = 'http://gitlab1.com'
+ bamboo_service.save!
+
+ expect(bamboo_service.password).to be_nil
+ end
+
+ it 'does not reset password if username changed' do
+ bamboo_service = service
+
+ bamboo_service.username = 'some_name'
+ bamboo_service.save!
+
+ expect(bamboo_service.password).to eq('password')
+ end
+
+ it "does not reset password if new url is set together with password, even if it's the same password" do
+ bamboo_service = service
+
+ bamboo_service.bamboo_url = 'http://gitlab_edited.com'
+ bamboo_service.password = 'password'
+ bamboo_service.save!
+
+ expect(bamboo_service.password).to eq('password')
+ expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
+ end
+ end
+
+ it 'saves password if new url is set together with password when no password was previously set' do
+ bamboo_service = service
+ bamboo_service.password = nil
+
+ bamboo_service.bamboo_url = 'http://gitlab_edited.com'
+ bamboo_service.password = 'password'
+ bamboo_service.save!
+
+ expect(bamboo_service.password).to eq('password')
+ expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
+ end
+ end
+ end
+
+ describe '#execute' do
+ it 'runs update and build action' do
+ stub_update_and_build_request
+
+ subject.execute(Gitlab::DataBuilder::Push::SAMPLE_DATA)
+ end
+ end
+
+ describe '#build_page' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref')
+
+ expect(service.build_page('sha', 'ref')).to eq('foo')
+ end
+ end
+
+ describe '#commit_status' do
+ it 'returns the contents of the reactive cache' do
+ stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref')
+
+ expect(service.commit_status('sha', 'ref')).to eq('foo')
+ end
+ end
+
+ shared_examples 'reactive cache calculation' do
+ describe '#build_page' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:build_page] }
+
+ it 'returns a specific URL when status is 500' do
+ stub_request(status: 500)
+
+ is_expected.to eq('http://gitlab.com/bamboo/browse/foo')
+ end
+
+ it 'returns a specific URL when response has no results' do
+ stub_request(body: %q({"results":{"results":{"size":"0"}}}))
+
+ is_expected.to eq('http://gitlab.com/bamboo/browse/foo')
+ end
+
+ it 'returns a build URL when bamboo_url has no trailing slash' do
+ stub_request(body: bamboo_response)
+
+ is_expected.to eq('http://gitlab.com/bamboo/browse/42')
+ end
+
+ context 'bamboo_url has trailing slash' do
+ let(:bamboo_url) { 'http://gitlab.com/bamboo/' }
+
+ it 'returns a build URL' do
+ stub_request(body: bamboo_response)
+
+ is_expected.to eq('http://gitlab.com/bamboo/browse/42')
+ end
+ end
+ end
+
+ describe '#commit_status' do
+ subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] }
+
+ it 'sets commit status to :error when status is 500' do
+ stub_request(status: 500)
+
+ is_expected.to eq(:error)
+ end
+
+ it 'sets commit status to "pending" when status is 404' do
+ stub_request(status: 404)
+
+ is_expected.to eq('pending')
+ end
+
+ it 'sets commit status to "pending" when response has no results' do
+ stub_request(body: %q({"results":{"results":{"size":"0"}}}))
+
+ is_expected.to eq('pending')
+ end
+
+ it 'sets commit status to "success" when build state contains Success' do
+ stub_request(body: bamboo_response(build_state: 'YAY Success!'))
+
+ is_expected.to eq('success')
+ end
+
+ it 'sets commit status to "failed" when build state contains Failed' do
+ stub_request(body: bamboo_response(build_state: 'NO Failed!'))
+
+ is_expected.to eq('failed')
+ end
+
+ it 'sets commit status to "pending" when build state contains Pending' do
+ stub_request(body: bamboo_response(build_state: 'NO Pending!'))
+
+ is_expected.to eq('pending')
+ end
+
+ it 'sets commit status to :error when build state is unknown' do
+ stub_request(body: bamboo_response(build_state: 'FOO BAR!'))
+
+ is_expected.to eq(:error)
+ end
+
+ Gitlab::HTTP::HTTP_ERRORS.each do |http_error|
+ it "sets commit status to :error with a #{http_error.name} error" do
+ WebMock.stub_request(:get, 'http://gitlab.com/bamboo/rest/api/latest/result/byChangeset/123?os_authType=basic')
+ .to_raise(http_error)
+
+ expect(Gitlab::ErrorTracking)
+ .to receive(:log_exception)
+ .with(instance_of(http_error), project_id: project.id)
+
+ is_expected.to eq(:error)
+ end
+ end
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ context 'when Bamboo API returns single result' do
+ let(:bamboo_response_template) do
+ %q({"results":{"results":{"size":"1","result":{"buildState":"%{build_state}","planResultKey":{"key":"42"}}}}})
+ end
+
+ it_behaves_like 'reactive cache calculation'
+ end
+
+ context 'when Bamboo API returns an array of results and we only consider the last one' do
+ let(:bamboo_response_template) do
+ %q({"results":{"results":{"size":"2","result":[{"buildState":"%{build_state}","planResultKey":{"key":"41"}},{"buildState":"%{build_state}","planResultKey":{"key":"42"}}]}}})
+ end
+
+ it_behaves_like 'reactive cache calculation'
+ end
+ end
+
+ def stub_update_and_build_request(status: 200, body: nil)
+ bamboo_full_url = 'http://gitlab.com/bamboo/updateAndBuild.action?buildKey=foo&os_authType=basic'
+
+ stub_bamboo_request(bamboo_full_url, status, body)
+ end
+
+ def stub_request(status: 200, body: nil)
+ bamboo_full_url = 'http://gitlab.com/bamboo/rest/api/latest/result/byChangeset/123?os_authType=basic'
+
+ stub_bamboo_request(bamboo_full_url, status, body)
+ end
+
+ def stub_bamboo_request(url, status, body)
+ stub_full_request(url).to_return(
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body
+ ).with(basic_auth: %w(mic password))
+ end
+
+ def bamboo_response(build_state: 'success')
+ # reference: https://docs.atlassian.com/atlassian-bamboo/REST/6.2.5/#d2e786
+ bamboo_response_template % { build_state: build_state }
+ end
+end
diff --git a/spec/models/integrations/campfire_spec.rb b/spec/models/integrations/campfire_spec.rb
new file mode 100644
index 00000000000..b23edf03e8a
--- /dev/null
+++ b/spec/models/integrations/campfire_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Campfire do
+ include StubRequests
+
+ describe 'Associations' do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:token) }
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:token) }
+ end
+ end
+
+ describe "#execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ @campfire_service = described_class.new
+ allow(@campfire_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ token: 'verySecret',
+ subdomain: 'project-name',
+ room: 'test-room'
+ )
+ @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+ @rooms_url = 'https://project-name.campfirenow.com/rooms.json'
+ @auth = %w(verySecret X)
+ @headers = { 'Content-Type' => 'application/json; charset=utf-8' }
+ end
+
+ it "calls Campfire API to get a list of rooms and speak in a room" do
+ # make sure a valid list of rooms is returned
+ body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
+
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
+ body: body,
+ status: 200,
+ headers: @headers
+ )
+
+ # stub the speak request with the room id found in the previous request's response
+ speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
+ stub_full_request(speak_url, method: :post).with(basic_auth: @auth)
+
+ @campfire_service.execute(@sample_data)
+
+ expect(WebMock).to have_requested(:get, stubbed_hostname(@rooms_url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(speak_url))
+ .with(body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/).once
+ end
+
+ it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
+ # return a list of rooms that do not contain a room named 'test-room'
+ body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
+ body: body,
+ status: 200,
+ headers: @headers
+ )
+
+ @campfire_service.execute(@sample_data)
+
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.9/rooms.json').once
+ expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/alert_message_spec.rb b/spec/models/integrations/chat_message/alert_message_spec.rb
new file mode 100644
index 00000000000..9866b2d9185
--- /dev/null
+++ b/spec/models/integrations/chat_message/alert_message_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::AlertMessage do
+ subject { described_class.new(args) }
+
+ let_it_be(:start_time) { Time.current }
+
+ let(:alert) { create(:alert_management_alert, started_at: start_time) }
+
+ let(:args) do
+ {
+ project_name: 'project_name',
+ project_url: 'http://example.com'
+ }.merge(Gitlab::DataBuilder::Alert.build(alert))
+ end
+
+ describe '#message' do
+ it 'returns the correct message' do
+ expect(subject.message).to eq("Alert firing in #{args[:project_name]}")
+ end
+ end
+
+ describe '#attachments' do
+ it 'returns an array of one' do
+ expect(subject.attachments).to be_a(Array)
+ expect(subject.attachments.size).to eq(1)
+ end
+
+ it 'contains the correct attributes' do
+ attachments_item = subject.attachments.first
+ expect(attachments_item).to have_key(:title)
+ expect(attachments_item).to have_key(:title_link)
+ expect(attachments_item).to have_key(:color)
+ expect(attachments_item).to have_key(:fields)
+ end
+
+ it 'returns the correct color' do
+ expect(subject.attachments.first[:color]).to eq("#C95823")
+ end
+
+ it 'returns the correct attachment fields' do
+ attachments_item = subject.attachments.first
+ fields = attachments_item[:fields].map { |h| h[:title] }
+
+ expect(fields).to match_array(['Severity', 'Events', 'Status', 'Start time'])
+ end
+
+ it 'returns the correctly formatted time' do
+ time_item = subject.attachments.first[:fields].detect { |h| h[:title] == 'Start time' }
+
+ expected_time = start_time.strftime("%B #{start_time.day.ordinalize}, %Y %l:%M%p %Z")
+
+ expect(time_item[:value]).to eq(expected_time)
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/base_message_spec.rb b/spec/models/integrations/chat_message/base_message_spec.rb
new file mode 100644
index 00000000000..eada5d1031d
--- /dev/null
+++ b/spec/models/integrations/chat_message/base_message_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::BaseMessage do
+ let(:base_message) { described_class.new(args) }
+ let(:args) { { project_url: 'https://gitlab-domain.com' } }
+
+ describe '#fallback' do
+ subject { base_message.fallback }
+
+ before do
+ allow(base_message).to receive(:message).and_return(message)
+ end
+
+ context 'without relative links' do
+ let(:message) { 'Just another *markdown* message' }
+
+ it { is_expected.to eq(message) }
+ end
+
+ context 'with relative links' do
+ let(:message) { 'Check this out ![Screenshot1](/uploads/Screenshot1.png)' }
+
+ it { is_expected.to eq('Check this out https://gitlab-domain.com/uploads/Screenshot1.png') }
+ end
+
+ context 'with multiple relative links' do
+ let(:message) { 'Check this out ![Screenshot1](/uploads/Screenshot1.png). And this ![Screenshot2](/uploads/Screenshot2.png)' }
+
+ it { is_expected.to eq('Check this out https://gitlab-domain.com/uploads/Screenshot1.png. And this https://gitlab-domain.com/uploads/Screenshot2.png') }
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/deployment_message_spec.rb b/spec/models/integrations/chat_message/deployment_message_spec.rb
new file mode 100644
index 00000000000..ff255af11a3
--- /dev/null
+++ b/spec/models/integrations/chat_message/deployment_message_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::DeploymentMessage do
+ describe '#pretext' do
+ it 'returns a message with the data returned by the deployment data builder' do
+ environment = create(:environment, name: "myenvironment")
+ project = create(:project, :repository)
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
+ data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq("Deploy to myenvironment succeeded")
+ end
+
+ it 'returns a message for a successful deployment' do
+ data = {
+ status: 'success',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production succeeded')
+ end
+
+ it 'returns a message for a failed deployment' do
+ data = {
+ status: 'failed',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production failed')
+ end
+
+ it 'returns a message for a canceled deployment' do
+ data = {
+ status: 'canceled',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production canceled')
+ end
+
+ it 'returns a message for a deployment to another environment' do
+ data = {
+ status: 'success',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging succeeded')
+ end
+
+ it 'returns a message for a deployment with any other status' do
+ data = {
+ status: 'unknown',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging unknown')
+ end
+
+ it 'returns a message for a running deployment' do
+ data = {
+ status: 'running',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Starting deploy to production')
+ end
+ end
+
+ describe '#attachments' do
+ def deployment_data(params)
+ {
+ object_kind: "deployment",
+ status: "success",
+ deployable_id: 3,
+ deployable_url: "deployable_url",
+ environment: "sandbox",
+ project: {
+ name: "greatproject",
+ web_url: "project_web_url",
+ path_with_namespace: "project_path_with_namespace"
+ },
+ user: {
+ name: "Jane Person",
+ username: "jane"
+ },
+ user_url: "user_url",
+ short_sha: "12345678",
+ commit_url: "commit_url",
+ commit_title: "commit title text"
+ }.merge(params)
+ end
+
+ it 'returns attachments with the data returned by the deployment data builder' do
+ user = create(:user, name: "John Smith", username: "smith")
+ namespace = create(:namespace, name: "myspace")
+ project = create(:project, :repository, namespace: namespace, name: "myproject")
+ commit = project.commit('HEAD')
+ environment = create(:environment, name: "myenvironment", project: project)
+ ci_build = create(:ci_build, project: project)
+ deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha)
+ job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
+ commit_url = Gitlab::UrlBuilder.build(deployment.commit)
+ user_url = Gitlab::Routing.url_helpers.user_url(user)
+ data = Gitlab::DataBuilder::Deployment.build(deployment, Time.current)
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[myspace/myproject](#{project.web_url}) with job [##{ci_build.id}](#{job_url}) by [John Smith (smith)](#{user_url})\n[#{deployment.short_sha}](#{commit_url}): #{commit.title}",
+ color: "good"
+ }])
+ end
+
+ it 'returns attachments for a failed deployment' do
+ data = deployment_data(status: 'failed')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "danger"
+ }])
+ end
+
+ it 'returns attachments for a canceled deployment' do
+ data = deployment_data(status: 'canceled')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "warning"
+ }])
+ end
+
+ it 'uses a neutral color for a deployment with any other status' do
+ data = deployment_data(status: 'some-new-status-we-make-in-the-future')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "#334455"
+ }])
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/issue_message_spec.rb b/spec/models/integrations/chat_message/issue_message_spec.rb
new file mode 100644
index 00000000000..31b80ad3169
--- /dev/null
+++ b/spec/models/integrations/chat_message/issue_message_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::IssueMessage do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'test.user',
+ avatar_url: 'http://someavatar.com'
+ },
+ project_name: 'project_name',
+ project_url: 'http://somewhere.com',
+
+ object_attributes: {
+ title: 'Issue title',
+ id: 10,
+ iid: 100,
+ assignee_id: 1,
+ url: 'http://url.com',
+ action: 'open',
+ state: 'opened',
+ description: 'issue description'
+ }
+ }
+ end
+
+ context 'without markdown' do
+ let(:color) { '#C95823' }
+
+ describe '#initialize' do
+ before do
+ args[:object_attributes][:description] = nil
+ end
+
+ it 'returns a non-null description' do
+ expect(subject.description).to eq('')
+ end
+ end
+
+ context 'open' do
+ it 'returns a message regarding opening of issues' do
+ expect(subject.pretext).to eq(
+ '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> opened by Test User (test.user)')
+ expect(subject.attachments).to eq([
+ {
+ title: "#100 Issue title",
+ title_link: "http://url.com",
+ text: "issue description",
+ color: color
+ }
+ ])
+ end
+ end
+
+ context 'close' do
+ before do
+ args[:object_attributes][:action] = 'close'
+ args[:object_attributes][:state] = 'closed'
+ end
+
+ it 'returns a message regarding closing of issues' do
+ expect(subject.pretext). to eq(
+ '[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> closed by Test User (test.user)')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'reopen' do
+ before do
+ args[:object_attributes][:action] = 'reopen'
+ args[:object_attributes][:state] = 'opened'
+ end
+
+ it 'returns a message regarding reopening of issues' do
+ expect(subject.pretext)
+ .to eq('[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> opened by Test User (test.user)')
+ expect(subject.attachments).to be_empty
+ end
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ context 'open' do
+ it 'returns a message regarding opening of issues' do
+ expect(subject.pretext).to eq(
+ '[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) opened by Test User (test.user)')
+ expect(subject.attachments).to eq('issue description')
+ expect(subject.activity).to eq({
+ title: 'Issue opened by Test User (test.user)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: '[#100 Issue title](http://url.com)',
+ image: 'http://someavatar.com'
+ })
+ end
+ end
+
+ context 'close' do
+ before do
+ args[:object_attributes][:action] = 'close'
+ args[:object_attributes][:state] = 'closed'
+ end
+
+ it 'returns a message regarding closing of issues' do
+ expect(subject.pretext). to eq(
+ '[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) closed by Test User (test.user)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq({
+ title: 'Issue closed by Test User (test.user)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: '[#100 Issue title](http://url.com)',
+ image: 'http://someavatar.com'
+ })
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/merge_message_spec.rb b/spec/models/integrations/chat_message/merge_message_spec.rb
new file mode 100644
index 00000000000..ed1ad6837e2
--- /dev/null
+++ b/spec/models/integrations/chat_message/merge_message_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::MergeMessage do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'test.user',
+ avatar_url: 'http://someavatar.com'
+ },
+ project_name: 'project_name',
+ project_url: 'http://somewhere.com',
+
+ object_attributes: {
+ title: "Merge request title\nSecond line",
+ id: 10,
+ iid: 100,
+ assignee_id: 1,
+ url: 'http://url.com',
+ state: 'opened',
+ description: 'merge request description',
+ source_branch: 'source_branch',
+ target_branch: 'target_branch'
+ }
+ }
+ end
+
+ context 'without markdown' do
+ let(:color) { '#345' }
+
+ context 'open' do
+ it 'returns a message regarding opening of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) opened merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'close' do
+ before do
+ args[:object_attributes][:state] = 'closed'
+ end
+ it 'returns a message regarding closing of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) closed merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ context 'open' do
+ it 'returns a message regarding opening of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) opened merge request [!100 *Merge request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq({
+ title: 'Merge request opened by Test User (test.user)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: '[!100 *Merge request title*](http://somewhere.com/-/merge_requests/100)',
+ image: 'http://someavatar.com'
+ })
+ end
+ end
+
+ context 'close' do
+ before do
+ args[:object_attributes][:state] = 'closed'
+ end
+
+ it 'returns a message regarding closing of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) closed merge request [!100 *Merge request title*](http://somewhere.com/-/merge_requests/100) in [project_name](http://somewhere.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq({
+ title: 'Merge request closed by Test User (test.user)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: '[!100 *Merge request title*](http://somewhere.com/-/merge_requests/100)',
+ image: 'http://someavatar.com'
+ })
+ end
+ end
+ end
+
+ context 'approved' do
+ before do
+ args[:object_attributes][:action] = 'approved'
+ end
+
+ it 'returns a message regarding completed approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) approved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'unapproved' do
+ before do
+ args[:object_attributes][:action] = 'unapproved'
+ end
+
+ it 'returns a message regarding revocation of completed approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) unapproved merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'approval' do
+ before do
+ args[:object_attributes][:action] = 'approval'
+ end
+
+ it 'returns a message regarding added approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) added their approval to merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'unapproval' do
+ before do
+ args[:object_attributes][:action] = 'unapproval'
+ end
+
+ it 'returns a message regarding revoking approval of merge requests' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) removed their approval from merge request <http://somewhere.com/-/merge_requests/100|!100 *Merge request title*> '\
+ 'in <http://somewhere.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/note_message_spec.rb b/spec/models/integrations/chat_message/note_message_spec.rb
new file mode 100644
index 00000000000..668c0da26ae
--- /dev/null
+++ b/spec/models/integrations/chat_message/note_message_spec.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::NoteMessage do
+ subject { described_class.new(args) }
+
+ let(:color) { '#345' }
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'test.user',
+ avatar_url: 'http://fakeavatar'
+ },
+ project_name: 'project_name',
+ project_url: 'http://somewhere.com',
+ repository: {
+ name: 'project_name',
+ url: 'http://somewhere.com'
+ },
+ object_attributes: {
+ id: 10,
+ note: 'comment on a commit',
+ url: 'http://url.com',
+ noteable_type: 'Commit'
+ }
+ }
+ end
+
+ context 'commit notes' do
+ before do
+ args[:object_attributes][:note] = 'comment on a commit'
+ args[:object_attributes][:noteable_type] = 'Commit'
+ args[:commit] = {
+ id: '5f163b2b95e6f53cbd428f5f0b103702a52b9a23',
+ message: "Added a commit message\ndetails\n123\n"
+ }
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding notes on commits' do
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
+ "commit 5f163b2b> in <http://somewhere.com|project_name>: " \
+ "*Added a commit message*")
+ expect(subject.attachments).to eq([{
+ text: 'comment on a commit',
+ color: color
+ }])
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding notes on commits' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) [commented on commit 5f163b2b](http://url.com) in [project_name](http://somewhere.com): *Added a commit message*'
+ )
+ expect(subject.attachments).to eq('comment on a commit')
+ expect(subject.activity).to eq({
+ title: 'Test User (test.user) [commented on commit 5f163b2b](http://url.com)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: 'Added a commit message',
+ image: 'http://fakeavatar'
+ })
+ end
+ end
+ end
+
+ context 'merge request notes' do
+ before do
+ args[:object_attributes][:note] = 'comment on a merge request'
+ args[:object_attributes][:noteable_type] = 'MergeRequest'
+ args[:merge_request] = {
+ id: 1,
+ iid: 30,
+ title: "merge request title\ndetails\n"
+ }
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding notes on a merge request' do
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
+ "merge request !30> in <http://somewhere.com|project_name>: " \
+ "*merge request title*")
+ expect(subject.attachments).to eq([{
+ text: 'comment on a merge request',
+ color: color
+ }])
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding notes on a merge request' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) [commented on merge request !30](http://url.com) in [project_name](http://somewhere.com): *merge request title*')
+ expect(subject.attachments).to eq('comment on a merge request')
+ expect(subject.activity).to eq({
+ title: 'Test User (test.user) [commented on merge request !30](http://url.com)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: 'merge request title',
+ image: 'http://fakeavatar'
+ })
+ end
+ end
+ end
+
+ context 'issue notes' do
+ before do
+ args[:object_attributes][:note] = 'comment on an issue'
+ args[:object_attributes][:noteable_type] = 'Issue'
+ args[:issue] = {
+ id: 1,
+ iid: 20,
+ title: "issue title\ndetails\n"
+ }
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding notes on an issue' do
+ expect(subject.pretext).to eq(
+ "Test User (test.user) <http://url.com|commented on " \
+ "issue #20> in <http://somewhere.com|project_name>: " \
+ "*issue title*")
+ expect(subject.attachments).to eq([{
+ text: 'comment on an issue',
+ color: color
+ }])
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding notes on an issue' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) [commented on issue #20](http://url.com) in [project_name](http://somewhere.com): *issue title*')
+ expect(subject.attachments).to eq('comment on an issue')
+ expect(subject.activity).to eq({
+ title: 'Test User (test.user) [commented on issue #20](http://url.com)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: 'issue title',
+ image: 'http://fakeavatar'
+ })
+ end
+ end
+ end
+
+ context 'project snippet notes' do
+ before do
+ args[:object_attributes][:note] = 'comment on a snippet'
+ args[:object_attributes][:noteable_type] = 'Snippet'
+ args[:snippet] = {
+ id: 5,
+ title: "snippet title\ndetails\n"
+ }
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding notes on a project snippet' do
+ expect(subject.pretext).to eq("Test User (test.user) <http://url.com|commented on " \
+ "snippet $5> in <http://somewhere.com|project_name>: " \
+ "*snippet title*")
+ expect(subject.attachments).to eq([{
+ text: 'comment on a snippet',
+ color: color
+ }])
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding notes on a project snippet' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) [commented on snippet $5](http://url.com) in [project_name](http://somewhere.com): *snippet title*')
+ expect(subject.attachments).to eq('comment on a snippet')
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/pipeline_message_spec.rb b/spec/models/integrations/chat_message/pipeline_message_spec.rb
new file mode 100644
index 00000000000..a80d13d7f5d
--- /dev/null
+++ b/spec/models/integrations/chat_message/pipeline_message_spec.rb
@@ -0,0 +1,378 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::PipelineMessage do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ object_attributes: {
+ id: 123,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ tag: false,
+ ref: 'develop',
+ status: 'success',
+ detailed_status: nil,
+ duration: 7210,
+ finished_at: "2019-05-27 11:56:36 -0300"
+ },
+ project: {
+ id: 234,
+ name: "project_name",
+ path_with_namespace: 'group/project_name',
+ web_url: 'http://example.gitlab.com',
+ avatar_url: 'http://example.com/project_avatar'
+ },
+ user: {
+ id: 345,
+ name: "The Hacker",
+ username: "hacker",
+ email: "hacker@example.gitlab.com",
+ avatar_url: "http://example.com/avatar"
+ },
+ commit: {
+ id: "abcdef"
+ },
+ builds: nil,
+ markdown: false
+ }
+ end
+
+ let(:has_yaml_errors) { false }
+
+ before do
+ test_commit = double("A test commit", committer: args[:user], title: "A test commit message")
+ test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url])
+ allow(test_project).to receive(:avatar_url).with(no_args).and_return("/avatar")
+ allow(test_project).to receive(:avatar_url).with(only_path: false).and_return(args[:project][:avatar_url])
+ allow(Project).to receive(:find) { test_project }
+
+ test_pipeline = double("A test pipeline", has_yaml_errors?: has_yaml_errors,
+ yaml_errors: "yaml error description here")
+ allow(Ci::Pipeline).to receive(:find) { test_pipeline }
+
+ allow(Gitlab::UrlBuilder).to receive(:build).with(test_commit).and_return("http://example.com/commit")
+ allow(Gitlab::UrlBuilder).to receive(:build).with(args[:user]).and_return("http://example.gitlab.com/hacker")
+ end
+
+ it 'returns an empty pretext' do
+ expect(subject.pretext).to be_empty
+ end
+
+ it "returns the pipeline summary in the activity's title" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/-/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/-/commits/develop)" \
+ " by The Hacker (hacker) has passed"
+ )
+ end
+
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
+ end
+
+ it "returns the summary with a 'failed' status" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/-/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/-/commits/develop)" \
+ " by The Hacker (hacker) has failed"
+ )
+ end
+ end
+
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
+ end
+
+ it "returns the summary with a 'passed with warnings' status" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/-/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/-/commits/develop)" \
+ " by The Hacker (hacker) has passed with warnings"
+ )
+ end
+ end
+
+ context 'when no user is provided because the pipeline was triggered by the API' do
+ before do
+ args[:user] = nil
+ end
+
+ it "returns the summary with 'API' as the username" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/-/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/-/commits/develop)" \
+ " by API has passed"
+ )
+ end
+ end
+
+ it "returns a link to the project in the activity's subtitle" do
+ expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
+ end
+
+ it "returns the build duration in the activity's text property" do
+ expect(subject.activity[:text]).to eq("in 02:00:10")
+ end
+
+ it "returns the user's avatar image URL in the activity's image property" do
+ expect(subject.activity[:image]).to eq("http://example.com/avatar")
+ end
+
+ context 'when the user does not have an avatar' do
+ before do
+ args[:user][:avatar_url] = nil
+ end
+
+ it "returns an empty string in the activity's image property" do
+ expect(subject.activity[:image]).to be_empty
+ end
+ end
+
+ it "returns the pipeline summary as the attachment's fallback property" do
+ expect(subject.attachments.first[:fallback]).to eq(
+ "<http://example.gitlab.com|project_name>:" \
+ " Pipeline <http://example.gitlab.com/-/pipelines/123|#123>" \
+ " of branch <http://example.gitlab.com/-/commits/develop|develop>" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
+ end
+
+ it "returns 'good' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('good')
+ end
+
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
+ end
+
+ it "returns 'danger' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('danger')
+ end
+ end
+
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
+ end
+
+ it "returns 'warning' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('warning')
+ end
+ end
+
+ it "returns the committer's name and username as the attachment's author_name property" do
+ expect(subject.attachments.first[:author_name]).to eq('The Hacker (hacker)')
+ end
+
+ it "returns the committer's avatar URL as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to eq('http://example.com/avatar')
+ end
+
+ it "returns the committer's GitLab profile URL as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to eq('http://example.gitlab.com/hacker')
+ end
+
+ context 'when no user is provided because the pipeline was triggered by the API' do
+ before do
+ args[:user] = nil
+ end
+
+ it "returns the committer's name and username as the attachment's author_name property" do
+ expect(subject.attachments.first[:author_name]).to eq('API')
+ end
+
+ it "returns nil as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to be_nil
+ end
+
+ it "returns nil as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to be_nil
+ end
+ end
+
+ it "returns the pipeline ID, status, and duration as the attachment's title property" do
+ expect(subject.attachments.first[:title]).to eq("Pipeline #123 has passed in 02:00:10")
+ end
+
+ it "returns the pipeline URL as the attachment's title_link property" do
+ expect(subject.attachments.first[:title_link]).to eq("http://example.gitlab.com/-/pipelines/123")
+ end
+
+ it "returns two attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(2)
+ end
+
+ it "returns the commit message as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][0]).to eq({
+ title: "Branch",
+ value: "<http://example.gitlab.com/-/commits/develop|develop>",
+ short: true
+ })
+ end
+
+ it "returns the ref name and link as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][1]).to eq({
+ title: "Commit",
+ value: "<http://example.com/commit|A test commit message>",
+ short: true
+ })
+ end
+
+ context "when a job in the pipeline fails" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "rspec", status: "failed", stage: "test" },
+ { id: 2, name: "karma", status: "success", stage: "test" }
+ ]
+ end
+
+ it "returns four attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(4)
+ end
+
+ it "returns the stage name and link to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stage",
+ value: "<http://example.gitlab.com/-/pipelines/123/failures|test>",
+ short: true
+ })
+ end
+
+ it "returns the job name and link as the attachment's fourth field property" do
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed job",
+ value: "<http://example.gitlab.com/-/jobs/1|rspec>",
+ short: true
+ })
+ end
+ end
+
+ context "when lots of jobs across multiple stages fail" do
+ before do
+ args[:builds] = (1..25).map do |i|
+ { id: i, name: "job-#{i}", status: "failed", stage: "stage-" + ((i % 3) + 1).to_s }
+ end
+ end
+
+ it "returns the stage names and links to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stages",
+ value: "<http://example.gitlab.com/-/pipelines/123/failures|stage-2>, <http://example.gitlab.com/-/pipelines/123/failures|stage-1>, <http://example.gitlab.com/-/pipelines/123/failures|stage-3>",
+ short: true
+ })
+ end
+
+ it "returns the job names and links as the attachment's fourth field property" do
+ expected_jobs = 25.downto(16).map do |i|
+ "<http://example.gitlab.com/-/jobs/#{i}|job-#{i}>"
+ end
+
+ expected_jobs << "and <http://example.gitlab.com/-/pipelines/123/failures|15 more>"
+
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ })
+ end
+ end
+
+ context "when jobs succeed on retries" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
+ { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
+ { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 8, name: "job-1", status: "success", stage: "stage-1" }
+ ]
+ end
+
+ it "do not return a job which succeeded on retry" do
+ expected_jobs = [
+ "<http://example.gitlab.com/-/jobs/3|job-3>",
+ "<http://example.gitlab.com/-/jobs/2|job-2>"
+ ]
+
+ expect(subject.attachments.first[:fields][3]).to eq(
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ )
+ end
+ end
+
+ context "when jobs failed even on retries" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
+ { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
+ { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 8, name: "job-1", status: "failed", stage: "stage-1" }
+ ]
+ end
+
+ it "returns only first instance of the failed job" do
+ expected_jobs = [
+ "<http://example.gitlab.com/-/jobs/3|job-3>",
+ "<http://example.gitlab.com/-/jobs/2|job-2>",
+ "<http://example.gitlab.com/-/jobs/1|job-1>"
+ ]
+
+ expect(subject.attachments.first[:fields][3]).to eq(
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ )
+ end
+ end
+
+ context "when the CI config file contains a YAML error" do
+ let(:has_yaml_errors) { true }
+
+ it "returns three attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(3)
+ end
+
+ it "returns the YAML error deatils as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Invalid CI config YAML file",
+ value: "yaml error description here",
+ short: false
+ })
+ end
+ end
+
+ it "returns the project's name as the attachment's footer property" do
+ expect(subject.attachments.first[:footer]).to eq("project_name")
+ end
+
+ it "returns the project's avatar URL as the attachment's footer_icon property" do
+ expect(subject.attachments.first[:footer_icon]).to eq("http://example.com/project_avatar")
+ end
+
+ it "returns the pipeline's timestamp as the attachment's ts property" do
+ expected_ts = Time.parse(args[:object_attributes][:finished_at]).to_i
+ expect(subject.attachments.first[:ts]).to eq(expected_ts)
+ end
+
+ context 'when rendering markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns the pipeline summary as the attachments in markdown format' do
+ expect(subject.attachments).to eq(
+ "[project_name](http://example.gitlab.com):" \
+ " Pipeline [#123](http://example.gitlab.com/-/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/-/commits/develop)" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/push_message_spec.rb b/spec/models/integrations/chat_message/push_message_spec.rb
new file mode 100644
index 00000000000..167487449c3
--- /dev/null
+++ b/spec/models/integrations/chat_message/push_message_spec.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::PushMessage do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ after: 'after',
+ before: 'before',
+ project_name: 'project_name',
+ ref: 'refs/heads/master',
+ user_name: 'test.user',
+ user_avatar: 'http://someavatar.com',
+ project_url: 'http://url.com'
+ }
+ end
+
+ let(:color) { '#345' }
+
+ context 'push' do
+ before do
+ args[:commits] = [
+ { message: 'message1', title: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } },
+ {
+ message: 'message2' + ' w' * 100 + "\nsecondline",
+ title: 'message2 w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ...',
+ url: 'http://url2.com',
+ id: '123456789012',
+ author: { name: 'author2' }
+ }
+ ]
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding pushes' do
+ expect(subject.pretext).to eq(
+ 'test.user pushed to branch <http://url.com/commits/master|master> of '\
+ '<http://url.com|project_name> (<http://url.com/compare/before...after|Compare changes>)')
+ expect(subject.attachments).to eq([{
+ text: "<http://url1.com|abcdefgh>: message1 - author1\n\n"\
+ "<http://url2.com|12345678>: message2 w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ... - author2",
+ color: color
+ }])
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding pushes' do
+ expect(subject.pretext).to eq(
+ 'test.user pushed to branch [master](http://url.com/commits/master) of [project_name](http://url.com) ([Compare changes](http://url.com/compare/before...after))')
+ expect(subject.attachments).to eq(
+ "[abcdefgh](http://url1.com): message1 - author1\n\n[12345678](http://url2.com): message2 w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ... - author2")
+ expect(subject.activity).to eq(
+ title: 'test.user pushed to branch [master](http://url.com/commits/master)',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/before...after)',
+ image: 'http://someavatar.com'
+ )
+ end
+ end
+ end
+
+ context 'tag push' do
+ let(:args) do
+ {
+ after: 'after',
+ before: Gitlab::Git::BLANK_SHA,
+ project_name: 'project_name',
+ ref: 'refs/tags/new_tag',
+ user_name: 'test.user',
+ user_avatar: 'http://someavatar.com',
+ project_url: 'http://url.com'
+ }
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding pushes' do
+ expect(subject.pretext).to eq('test.user pushed new tag ' \
+ '<http://url.com/-/tags/new_tag|new_tag> to ' \
+ '<http://url.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding pushes' do
+ expect(subject.pretext).to eq(
+ 'test.user pushed new tag [new_tag](http://url.com/-/tags/new_tag) to [project_name](http://url.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq(
+ title: 'test.user pushed new tag [new_tag](http://url.com/-/tags/new_tag)',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ image: 'http://someavatar.com'
+ )
+ end
+ end
+ end
+
+ context 'removed tag' do
+ let(:args) do
+ {
+ after: Gitlab::Git::BLANK_SHA,
+ before: 'before',
+ project_name: 'project_name',
+ ref: 'refs/tags/new_tag',
+ user_name: 'test.user',
+ user_avatar: 'http://someavatar.com',
+ project_url: 'http://url.com'
+ }
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding removal of tags' do
+ expect(subject.pretext).to eq('test.user removed tag ' \
+ 'new_tag from ' \
+ '<http://url.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding removal of tags' do
+ expect(subject.pretext).to eq(
+ 'test.user removed tag new_tag from [project_name](http://url.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq(
+ title: 'test.user removed tag new_tag',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
+ image: 'http://someavatar.com'
+ )
+ end
+ end
+ end
+
+ context 'new branch' do
+ before do
+ args[:before] = Gitlab::Git::BLANK_SHA
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding a new branch' do
+ expect(subject.pretext).to eq(
+ 'test.user pushed new branch <http://url.com/commits/master|master> to '\
+ '<http://url.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding a new branch' do
+ expect(subject.pretext).to eq(
+ 'test.user pushed new branch [master](http://url.com/commits/master) to [project_name](http://url.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq(
+ title: 'test.user pushed new branch [master](http://url.com/commits/master)',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ image: 'http://someavatar.com'
+ )
+ end
+ end
+ end
+
+ context 'removed branch' do
+ before do
+ args[:after] = Gitlab::Git::BLANK_SHA
+ end
+
+ context 'without markdown' do
+ it 'returns a message regarding a removed branch' do
+ expect(subject.pretext).to eq(
+ 'test.user removed branch master from <http://url.com|project_name>')
+ expect(subject.attachments).to be_empty
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns a message regarding a removed branch' do
+ expect(subject.pretext).to eq(
+ 'test.user removed branch master from [project_name](http://url.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq(
+ title: 'test.user removed branch master',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/before...0000000000000000000000000000000000000000)',
+ image: 'http://someavatar.com'
+ )
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/chat_message/wiki_page_message_spec.rb b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
new file mode 100644
index 00000000000..e8672a0f9c8
--- /dev/null
+++ b/spec/models/integrations/chat_message/wiki_page_message_spec.rb
@@ -0,0 +1,171 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::ChatMessage::WikiPageMessage do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'test.user',
+ avatar_url: 'http://someavatar.com'
+ },
+ project_name: 'project_name',
+ project_url: 'http://somewhere.com',
+ object_attributes: {
+ title: 'Wiki page title',
+ url: 'http://url.com',
+ content: 'Wiki page content',
+ message: 'Wiki page commit message'
+ }
+ }
+ end
+
+ context 'without markdown' do
+ describe '#pretext' do
+ context 'when :action == "create"' do
+ before do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+
+ context 'when :action == "update"' do
+ before do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+ end
+
+ describe '#attachments' do
+ let(:color) { '#345' }
+
+ context 'when :action == "create"' do
+ before do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ it 'returns the commit message for a new wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page commit message",
+ color: color
+ }
+ ])
+ end
+ end
+
+ context 'when :action == "update"' do
+ before do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ it 'returns the commit message for an updated wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page commit message",
+ color: color
+ }
+ ])
+ end
+ end
+ end
+ end
+
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ describe '#pretext' do
+ context 'when :action == "create"' do
+ before do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ end
+ end
+
+ context 'when :action == "update"' do
+ before do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'Test User (test.user) edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ end
+ end
+ end
+
+ describe '#attachments' do
+ context 'when :action == "create"' do
+ before do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ it 'returns the commit message for a new wiki page' do
+ expect(subject.attachments).to eq('Wiki page commit message')
+ end
+ end
+
+ context 'when :action == "update"' do
+ before do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ it 'returns the commit message for an updated wiki page' do
+ expect(subject.attachments).to eq('Wiki page commit message')
+ end
+ end
+ end
+
+ describe '#activity' do
+ context 'when :action == "create"' do
+ before do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ it 'returns the attachment for a new wiki page' do
+ expect(subject.activity).to eq({
+ title: 'Test User (test.user) created [wiki page](http://url.com)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: 'Wiki page title',
+ image: 'http://someavatar.com'
+ })
+ end
+ end
+
+ context 'when :action == "update"' do
+ before do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ it 'returns the attachment for an updated wiki page' do
+ expect(subject.activity).to eq({
+ title: 'Test User (test.user) edited [wiki page](http://url.com)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: 'Wiki page title',
+ image: 'http://someavatar.com'
+ })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/integrations/confluence_spec.rb b/spec/models/integrations/confluence_spec.rb
new file mode 100644
index 00000000000..c217573f48d
--- /dev/null
+++ b/spec/models/integrations/confluence_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Confluence do
+ describe 'Associations' do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ before do
+ subject.active = active
+ end
+
+ context 'when service is active' do
+ let(:active) { true }
+
+ it { is_expected.not_to allow_value('https://example.com').for(:confluence_url) }
+ it { is_expected.not_to allow_value('example.com').for(:confluence_url) }
+ it { is_expected.not_to allow_value('foo').for(:confluence_url) }
+ it { is_expected.not_to allow_value('ftp://example.atlassian.net/wiki').for(:confluence_url) }
+ it { is_expected.not_to allow_value('https://example.atlassian.net').for(:confluence_url) }
+ it { is_expected.not_to allow_value('https://.atlassian.net/wiki').for(:confluence_url) }
+ it { is_expected.not_to allow_value('https://example.atlassian.net/wikifoo').for(:confluence_url) }
+ it { is_expected.not_to allow_value('').for(:confluence_url) }
+ it { is_expected.not_to allow_value(nil).for(:confluence_url) }
+ it { is_expected.not_to allow_value('😊').for(:confluence_url) }
+ it { is_expected.to allow_value('https://example.atlassian.net/wiki').for(:confluence_url) }
+ it { is_expected.to allow_value('http://example.atlassian.net/wiki').for(:confluence_url) }
+ it { is_expected.to allow_value('https://example.atlassian.net/wiki/').for(:confluence_url) }
+ it { is_expected.to allow_value('http://example.atlassian.net/wiki/').for(:confluence_url) }
+ it { is_expected.to allow_value('https://example.atlassian.net/wiki/foo').for(:confluence_url) }
+
+ it { is_expected.to validate_presence_of(:confluence_url) }
+ end
+
+ context 'when service is inactive' do
+ let(:active) { false }
+
+ it { is_expected.not_to validate_presence_of(:confluence_url) }
+ it { is_expected.to allow_value('foo').for(:confluence_url) }
+ end
+ end
+
+ describe '#help' do
+ it 'can correctly return a link to the project wiki when active' do
+ project = create(:project)
+ subject.project = project
+ subject.active = true
+
+ expect(subject.help).to include(Gitlab::Routing.url_helpers.project_wikis_url(project))
+ end
+
+ context 'when the project wiki is not enabled' do
+ it 'returns nil when both active or inactive', :aggregate_failures do
+ project = create(:project, :wiki_disabled)
+ subject.project = project
+
+ [true, false].each do |active|
+ subject.active = active
+
+ expect(subject.help).to be_nil
+ end
+ end
+ end
+ end
+
+ describe 'Caching has_confluence on project_settings' do
+ let(:project) { create(:project) }
+
+ subject { project.project_setting.has_confluence? }
+
+ it 'sets the property to true when service is active' do
+ create(:confluence_service, project: project, active: true)
+
+ is_expected.to be(true)
+ end
+
+ it 'sets the property to false when service is not active' do
+ create(:confluence_service, project: project, active: false)
+
+ is_expected.to be(false)
+ end
+
+ it 'creates a project_setting record if one was not already created' do
+ expect { create(:confluence_service) }.to change { ProjectSetting.count }.by(1)
+ end
+ end
+end
diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb
new file mode 100644
index 00000000000..165b21840e0
--- /dev/null
+++ b/spec/models/integrations/datadog_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+require 'securerandom'
+
+require 'spec_helper'
+
+RSpec.describe Integrations::Datadog do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
+ let_it_be(:build) { create(:ci_build, project: project) }
+
+ let(:active) { true }
+ let(:dd_site) { 'datadoghq.com' }
+ let(:default_url) { 'https://webhooks-http-intake.logs.datadoghq.com/v1/input/' }
+ let(:api_url) { '' }
+ let(:api_key) { SecureRandom.hex(32) }
+ let(:dd_env) { 'ci' }
+ let(:dd_service) { 'awesome-gitlab' }
+
+ let(:expected_hook_url) { default_url + api_key + "?env=#{dd_env}&service=#{dd_service}" }
+
+ let(:instance) do
+ described_class.new(
+ active: active,
+ project: project,
+ datadog_site: dd_site,
+ api_url: api_url,
+ api_key: api_key,
+ datadog_env: dd_env,
+ datadog_service: dd_service
+ )
+ end
+
+ let(:saved_instance) do
+ instance.save!
+ instance
+ end
+
+ let(:pipeline_data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
+ let(:build_data) { Gitlab::DataBuilder::Build.build(build) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_one(:service_hook) }
+ end
+
+ describe 'validations' do
+ subject { instance }
+
+ context 'when service is active' do
+ let(:active) { true }
+
+ it { is_expected.to validate_presence_of(:api_key) }
+ it { is_expected.to allow_value(api_key).for(:api_key) }
+ it { is_expected.not_to allow_value('87dab2403c9d462 87aec4d9214edb1e').for(:api_key) }
+ it { is_expected.not_to allow_value('................................').for(:api_key) }
+
+ context 'when selecting site' do
+ let(:dd_site) { 'datadoghq.com' }
+ let(:api_url) { '' }
+
+ it { is_expected.to validate_presence_of(:datadog_site) }
+ it { is_expected.not_to validate_presence_of(:api_url) }
+ it { is_expected.not_to allow_value('datadog hq.com').for(:datadog_site) }
+ end
+
+ context 'with custom api_url' do
+ let(:dd_site) { '' }
+ let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/v1/input/' }
+
+ it { is_expected.not_to validate_presence_of(:datadog_site) }
+ it { is_expected.to validate_presence_of(:api_url) }
+ it { is_expected.to allow_value(api_url).for(:api_url) }
+ it { is_expected.not_to allow_value('example.com').for(:api_url) }
+ end
+
+ context 'when missing site and api_url' do
+ let(:dd_site) { '' }
+ let(:api_url) { '' }
+
+ it { is_expected.not_to be_valid }
+ it { is_expected.to validate_presence_of(:datadog_site) }
+ it { is_expected.to validate_presence_of(:api_url) }
+ end
+
+ context 'when providing both site and api_url' do
+ let(:dd_site) { 'datadoghq.com' }
+ let(:api_url) { default_url }
+
+ it { is_expected.not_to allow_value('datadog hq.com').for(:datadog_site) }
+ it { is_expected.not_to allow_value('example.com').for(:api_url) }
+ end
+ end
+
+ context 'when service is not active' do
+ let(:active) { false }
+
+ it { is_expected.to be_valid }
+ it { is_expected.not_to validate_presence_of(:api_key) }
+ end
+ end
+
+ describe '#hook_url' do
+ subject { instance.hook_url }
+
+ context 'with standard site URL' do
+ it { is_expected.to eq(expected_hook_url) }
+ end
+
+ context 'with custom URL' do
+ let(:api_url) { 'https://webhooks-http-intake.logs.datad0g.com/v1/input/' }
+
+ it { is_expected.to eq(api_url + api_key + "?env=#{dd_env}&service=#{dd_service}") }
+
+ context 'blank' do
+ let(:api_url) { '' }
+
+ it { is_expected.to eq(expected_hook_url) }
+ end
+ end
+
+ context 'without optional params' do
+ let(:dd_service) { '' }
+ let(:dd_env) { '' }
+
+ it { is_expected.to eq(default_url + api_key) }
+ end
+ end
+
+ describe '#api_keys_url' do
+ subject { instance.api_keys_url }
+
+ it { is_expected.to eq("https://app.#{dd_site}/account/settings#api") }
+
+ context 'with unset datadog_site' do
+ let(:dd_site) { '' }
+
+ it { is_expected.to eq("https://docs.datadoghq.com/account_management/api-app-keys/") }
+ end
+ end
+
+ describe '#test' do
+ context 'when request is succesful' do
+ subject { saved_instance.test(pipeline_data) }
+
+ before do
+ stub_request(:post, expected_hook_url).to_return(body: 'OK')
+ end
+ it { is_expected.to eq({ success: true, result: 'OK' }) }
+ end
+
+ context 'when request fails' do
+ subject { saved_instance.test(pipeline_data) }
+
+ before do
+ stub_request(:post, expected_hook_url).to_return(body: 'CRASH!!!', status: 500)
+ end
+ it { is_expected.to eq({ success: false, result: 'CRASH!!!' }) }
+ end
+ end
+
+ describe '#execute' do
+ before do
+ stub_request(:post, expected_hook_url)
+ saved_instance.execute(data)
+ end
+
+ context 'with pipeline data' do
+ let(:data) { pipeline_data }
+ let(:expected_headers) do
+ { WebHookService::GITLAB_EVENT_HEADER => 'Pipeline Hook' }
+ end
+
+ it { expect(a_request(:post, expected_hook_url).with(headers: expected_headers)).to have_been_made }
+ end
+
+ context 'with job data' do
+ let(:data) { build_data }
+ let(:expected_headers) do
+ { WebHookService::GITLAB_EVENT_HEADER => 'Job Hook' }
+ end
+
+ it { expect(a_request(:post, expected_hook_url).with(headers: expected_headers)).to have_been_made }
+ end
+ end
+end
diff --git a/spec/models/integrations/emails_on_push_spec.rb b/spec/models/integrations/emails_on_push_spec.rb
new file mode 100644
index 00000000000..ca060f4155e
--- /dev/null
+++ b/spec/models/integrations/emails_on_push_spec.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::EmailsOnPush do
+ let_it_be(:project) { create_default(:project).freeze }
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:recipients) }
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:recipients) }
+ end
+
+ describe 'validates number of recipients' do
+ before do
+ stub_const("#{described_class}::RECIPIENTS_LIMIT", 2)
+ end
+
+ subject(:service) { described_class.new(project: project, recipients: recipients, active: true) }
+
+ context 'valid number of recipients' do
+ let(:recipients) { 'foo@bar.com duplicate@example.com Duplicate@example.com invalid-email' }
+
+ it 'does not count duplicates and invalid emails' do
+ is_expected.to be_valid
+ end
+ end
+
+ context 'invalid number of recipients' do
+ let(:recipients) { 'foo@bar.com bar@foo.com bob@gitlab.com' }
+
+ it { is_expected.not_to be_valid }
+
+ it 'adds an error message' do
+ service.valid?
+
+ expect(service.errors).to contain_exactly('Recipients can\'t exceed 2')
+ end
+
+ context 'when service is not active' do
+ before do
+ service.active = false
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+ end
+
+ describe '.new' do
+ context 'when properties is missing branches_to_be_notified' do
+ subject { described_class.new(properties: {}) }
+
+ it 'sets the default value to all' do
+ expect(subject.branches_to_be_notified).to eq('all')
+ end
+ end
+
+ context 'when branches_to_be_notified is already set' do
+ subject { described_class.new(properties: { branches_to_be_notified: 'protected' }) }
+
+ it 'does not overwrite it with the default value' do
+ expect(subject.branches_to_be_notified).to eq('protected')
+ end
+ end
+ end
+
+ describe '.valid_recipients' do
+ let(:recipients) { '<invalid> foobar Valid@recipient.com Dup@lica.te dup@lica.te Dup@Lica.te' }
+
+ it 'removes invalid email addresses and removes duplicates by keeping the original capitalization' do
+ expect(described_class.valid_recipients(recipients)).to contain_exactly('Valid@recipient.com', 'Dup@lica.te')
+ end
+ end
+
+ describe '#execute' do
+ let(:push_data) { { object_kind: 'push' } }
+ let(:project) { create(:project, :repository) }
+ let(:service) { create(:emails_on_push_service, project: project) }
+ let(:recipients) { 'test@gitlab.com' }
+
+ before do
+ subject.recipients = recipients
+ end
+
+ shared_examples 'sending email' do |branches_to_be_notified, branch_being_pushed_to|
+ let(:push_data) { { object_kind: 'push', object_attributes: { ref: branch_being_pushed_to } } }
+
+ before do
+ subject.branches_to_be_notified = branches_to_be_notified
+ end
+
+ it 'sends email' do
+ expect(EmailsOnPushWorker).not_to receive(:perform_async)
+
+ service.execute(push_data)
+ end
+ end
+
+ shared_examples 'not sending email' do |branches_to_be_notified, branch_being_pushed_to|
+ let(:push_data) { { object_kind: 'push', object_attributes: { ref: branch_being_pushed_to } } }
+
+ before do
+ subject.branches_to_be_notified = branches_to_be_notified
+ end
+
+ it 'does not send email' do
+ expect(EmailsOnPushWorker).not_to receive(:perform_async)
+
+ service.execute(push_data)
+ end
+ end
+
+ context 'when emails are disabled on the project' do
+ it 'does not send emails' do
+ expect(project).to receive(:emails_disabled?).and_return(true)
+ expect(EmailsOnPushWorker).not_to receive(:perform_async)
+
+ service.execute(push_data)
+ end
+ end
+
+ context 'when emails are enabled on the project' do
+ before do
+ create(:protected_branch, project: project, name: 'a-protected-branch')
+ expect(project).to receive(:emails_disabled?).and_return(true)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:case_name, :branches_to_be_notified, :branch_being_pushed_to, :expected_action) do
+ 'pushing to a random branch and notification configured for all branches' | 'all' | 'random' | 'sending email'
+ 'pushing to the default branch and notification configured for all branches' | 'all' | 'master' | 'sending email'
+ 'pushing to a protected branch and notification configured for all branches' | 'all' | 'a-protected-branch' | 'sending email'
+ 'pushing to a random branch and notification configured for default branch only' | 'default' | 'random' | 'not sending email'
+ 'pushing to the default branch and notification configured for default branch only' | 'default' | 'master' | 'sending email'
+ 'pushing to a protected branch and notification configured for default branch only' | 'default' | 'a-protected-branch' | 'not sending email'
+ 'pushing to a random branch and notification configured for protected branches only' | 'protected' | 'random' | 'not sending email'
+ 'pushing to the default branch and notification configured for protected branches only' | 'protected' | 'master' | 'not sending email'
+ 'pushing to a protected branch and notification configured for protected branches only' | 'protected' | 'a-protected-branch' | 'sending email'
+ 'pushing to a random branch and notification configured for default and protected branches only' | 'default_and_protected' | 'random' | 'not sending email'
+ 'pushing to the default branch and notification configured for default and protected branches only' | 'default_and_protected' | 'master' | 'sending email'
+ 'pushing to a protected branch and notification configured for default and protected branches only' | 'default_and_protected' | 'a-protected-branch' | 'sending email'
+ end
+
+ with_them do
+ include_examples params[:expected_action], branches_to_be_notified: params[:branches_to_be_notified], branch_being_pushed_to: params[:branch_being_pushed_to]
+ end
+ end
+ end
+end