diff options
Diffstat (limited to 'spec/models/integrations')
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 |