summaryrefslogtreecommitdiff
path: root/spec/models
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2017-05-23 02:10:29 +0800
committerLin Jen-Shin <godfat@godfat.org>2017-05-23 02:10:29 +0800
commit1a4130d3a6cfb4956f8bb1186cc499ea549d8e18 (patch)
tree076adcb3e6f3800a1a7bbc6809839d5cb3b3f372 /spec/models
parent3c8a6fba67998eb17240b15db85f8d1c8aff338e (diff)
parent18a6d9c5326bc2b90a1f0cc8664d638a39885924 (diff)
downloadgitlab-ce-27377-preload-pipeline-entity.tar.gz
Merge remote-tracking branch 'upstream/master' into 27377-preload-pipeline-entity27377-preload-pipeline-entity
* upstream/master: (2534 commits) Update VERSION to 9.3.0-pre Update CHANGELOG.md for 9.2.0 removes unnecessary redundacy in usage ping doc Respect the typo as rubocop said Add a test to ensure this works on MySQL Change pipelines schedules help page path change domain to hostname in usage ping doc Fixes broken MySQL migration for retried Show password field mask while editing service settings Add notes for supported schedulers and cloud providers Move environment monitoring to environments doc Add docs for change of Cache/Artifact restore order" Avoid resource intensive login checks if password is not provided Change translation for 'coding' by 'desarrollo' for Spanish Add to docs: issues multiple assignees rename "Add emoji" and "Award emoji" to "Add reaction" where appropriate Add project and group notification settings info 32570 Fix border-bottom for project activity tab Add users endpoint to frontend API class Rename users on mysql ...
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/abuse_report_spec.rb3
-rw-r--r--spec/models/application_setting_spec.rb65
-rw-r--r--spec/models/award_emoji_spec.rb14
-rw-r--r--spec/models/blob_spec.rb348
-rw-r--r--spec/models/blob_viewer/base_spec.rb177
-rw-r--r--spec/models/blob_viewer/changelog_spec.rb27
-rw-r--r--spec/models/blob_viewer/composer_json_spec.rb25
-rw-r--r--spec/models/blob_viewer/gemspec_spec.rb25
-rw-r--r--spec/models/blob_viewer/gitlab_ci_yml_spec.rb32
-rw-r--r--spec/models/blob_viewer/license_spec.rb34
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb25
-rw-r--r--spec/models/blob_viewer/podspec_json_spec.rb25
-rw-r--r--spec/models/blob_viewer/podspec_spec.rb25
-rw-r--r--spec/models/blob_viewer/route_map_spec.rb38
-rw-r--r--spec/models/blob_viewer/server_side_spec.rb41
-rw-r--r--spec/models/ci/artifact_blob_spec.rb44
-rw-r--r--spec/models/ci/build_spec.rb382
-rw-r--r--spec/models/ci/group_spec.rb44
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb112
-rw-r--r--spec/models/ci/pipeline_spec.rb202
-rw-r--r--spec/models/ci/pipeline_status_spec.rb173
-rw-r--r--spec/models/ci/stage_spec.rb37
-rw-r--r--spec/models/ci/trigger_spec.rb4
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb34
-rw-r--r--spec/models/commit_status_spec.rb51
-rw-r--r--spec/models/concerns/awardable_spec.rb4
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb280
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb24
-rw-r--r--spec/models/concerns/has_status_spec.rb12
-rw-r--r--spec/models/concerns/ignorable_column_spec.rb38
-rw-r--r--spec/models/concerns/issuable_spec.rb144
-rw-r--r--spec/models/concerns/mentionable_spec.rb49
-rw-r--r--spec/models/concerns/milestoneish_spec.rb6
-rw-r--r--spec/models/concerns/noteable_spec.rb261
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb2
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb548
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb329
-rw-r--r--spec/models/concerns/routable_spec.rb179
-rw-r--r--spec/models/concerns/spammable_spec.rb4
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb2
-rw-r--r--spec/models/container_repository_spec.rb234
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb4
-rw-r--r--spec/models/cycle_analytics/production_spec.rb2
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb2
-rw-r--r--spec/models/cycle_analytics/test_spec.rb1
-rw-r--r--spec/models/deployment_spec.rb28
-rw-r--r--spec/models/diff_discussion_spec.rb86
-rw-r--r--spec/models/diff_note_spec.rb349
-rw-r--r--spec/models/discussion_spec.rb623
-rw-r--r--spec/models/environment_spec.rb82
-rw-r--r--spec/models/event_spec.rb46
-rw-r--r--spec/models/global_milestone_spec.rb2
-rw-r--r--spec/models/group_spec.rb88
-rw-r--r--spec/models/hooks/system_hook_spec.rb21
-rw-r--r--spec/models/issue_collection_spec.rb2
-rw-r--r--spec/models/issue_spec.rb88
-rw-r--r--spec/models/label_spec.rb16
-rw-r--r--spec/models/legacy_diff_discussion_spec.rb33
-rw-r--r--spec/models/legacy_diff_note_spec.rb101
-rw-r--r--spec/models/member_spec.rb27
-rw-r--r--spec/models/members/group_member_spec.rb21
-rw-r--r--spec/models/merge_request_spec.rb368
-rw-r--r--spec/models/milestone_spec.rb12
-rw-r--r--spec/models/namespace_spec.rb71
-rw-r--r--spec/models/network/graph_spec.rb36
-rw-r--r--spec/models/note_spec.rb369
-rw-r--r--spec/models/project_authorization_spec.rb2
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb96
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb75
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb242
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb133
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb136
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb143
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb20
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb91
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb277
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb (renamed from spec/models/project_services/pipeline_email_service_spec.rb)0
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb2
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb35
-rw-r--r--spec/models/project_snippet_spec.rb3
-rw-r--r--spec/models/project_spec.rb258
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/models/project_wiki_spec.rb21
-rw-r--r--spec/models/protectable_dropdown_spec.rb25
-rw-r--r--spec/models/protected_branch/merge_access_level_spec.rb5
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb5
-rw-r--r--spec/models/protected_branch_spec.rb67
-rw-r--r--spec/models/protected_tag_spec.rb12
-rw-r--r--spec/models/redirect_route_spec.rb27
-rw-r--r--spec/models/repository_spec.rb194
-rw-r--r--spec/models/route_spec.rb114
-rw-r--r--spec/models/sent_notification_spec.rb174
-rw-r--r--spec/models/service_spec.rb55
-rw-r--r--spec/models/snippet_blob_spec.rb47
-rw-r--r--spec/models/snippet_spec.rb53
-rw-r--r--spec/models/spam_log_spec.rb11
-rw-r--r--spec/models/todo_spec.rb46
-rw-r--r--spec/models/user_spec.rb306
101 files changed, 6480 insertions, 2786 deletions
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 4e71597521d..ced93c8f762 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -29,7 +29,8 @@ RSpec.describe AbuseReport, type: :model do
it 'lets a worker delete the user' do
expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id,
- delete_solo_owned_groups: true)
+ delete_solo_owned_groups: true,
+ hard_delete: true)
subject.remove_user(deleted_by: user)
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 01ca1584ed2..fa229542f70 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -4,6 +4,7 @@ describe ApplicationSetting, models: true do
let(:setting) { ApplicationSetting.create_from_defaults }
it { expect(setting).to be_valid }
+ it { expect(setting.uuid).to be_present }
describe 'validations' do
let(:http) { 'http://example.com' }
@@ -88,7 +89,7 @@ describe ApplicationSetting, models: true do
storages = {
'custom1' => 'tmp/tests/custom_repositories_1',
'custom2' => 'tmp/tests/custom_repositories_2',
- 'custom3' => 'tmp/tests/custom_repositories_3',
+ 'custom3' => 'tmp/tests/custom_repositories_3'
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
@@ -210,4 +211,66 @@ describe ApplicationSetting, models: true do
expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
end
end
+
+ describe 'usage ping settings' do
+ context 'when the usage ping is disabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false)
+ end
+
+ it 'does not allow the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_falsey
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+ end
+
+ context 'when the usage ping is enabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true)
+ end
+
+ it 'allows the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_truthy
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns true for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index cb3c592f8cd..2a9a27752c1 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -25,6 +25,20 @@ describe AwardEmoji, models: true do
expect(new_award).not_to be_valid
end
+
+ # Assume User A and User B both created award emoji of the same name
+ # on the same awardable. When User A is deleted, User A's award emoji
+ # is moved to the ghost user. When User B is deleted, User B's award emoji
+ # also needs to be moved to the ghost user - this cannot happen unless
+ # the uniqueness validation is disabled for ghost users.
+ it "allows duplicate award emoji for ghost users" do
+ user = create(:user, :ghost)
+ issue = create(:issue)
+ create(:award_emoji, user: user, awardable: issue)
+ new_award = build(:award_emoji, user: user, awardable: issue)
+
+ expect(new_award).to be_valid
+ end
end
end
end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 552229e9b07..f19e1af65a6 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -2,6 +2,14 @@
require 'rails_helper'
describe Blob do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
describe '.decorate' do
it 'returns NilClass when given nil' do
expect(described_class.decorate(nil)).to be_nil
@@ -12,7 +20,7 @@ describe Blob do
context 'using a binary blob' do
it 'returns the data as-is' do
data = "\n\xFF\xB9\xC3"
- blob = described_class.new(double(binary?: true, data: data))
+ blob = fake_blob(binary: true, data: data)
expect(blob.data).to eq(data)
end
@@ -20,142 +28,330 @@ describe Blob do
context 'using a text blob' do
it 'converts the data to UTF-8' do
- blob = described_class.new(double(binary?: false, data: "\n\xFF\xB9\xC3"))
+ blob = fake_blob(binary: false, data: "\n\xFF\xB9\xC3")
expect(blob.data).to eq("\n���")
end
end
end
- describe '#svg?' do
- it 'is falsey when not text' do
- git_blob = double(text?: false)
+ describe '#external_storage_error?' do
+ context 'if the blob is stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
- expect(described_class.decorate(git_blob)).not_to be_svg
+ context 'when the project has LFS enabled' do
+ it 'returns false' do
+ expect(blob.external_storage_error?).to be_falsey
+ end
+ end
+
+ context 'when the project does not have LFS enabled' do
+ before do
+ project.lfs_enabled = false
+ end
+
+ it 'returns true' do
+ expect(blob.external_storage_error?).to be_truthy
+ end
+ end
end
- it 'is falsey when no language is detected' do
- git_blob = double(text?: true, language: nil)
+ context 'if the blob is not stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.md') }
- expect(described_class.decorate(git_blob)).not_to be_svg
+ it 'returns false' do
+ expect(blob.external_storage_error?).to be_falsey
+ end
end
+ end
+
+ describe '#stored_externally?' do
+ context 'if the blob is stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ context 'when the project has LFS enabled' do
+ it 'returns true' do
+ expect(blob.stored_externally?).to be_truthy
+ end
+ end
- it' is falsey when language is not SVG' do
- git_blob = double(text?: true, language: double(name: 'XML'))
+ context 'when the project does not have LFS enabled' do
+ before do
+ project.lfs_enabled = false
+ end
- expect(described_class.decorate(git_blob)).not_to be_svg
+ it 'returns false' do
+ expect(blob.stored_externally?).to be_falsey
+ end
+ end
end
- it 'is truthy when language is SVG' do
- git_blob = double(text?: true, language: double(name: 'SVG'))
+ context 'if the blob is not stored in LFS' do
+ let(:blob) { fake_blob(path: 'file.md') }
- expect(described_class.decorate(git_blob)).to be_svg
+ it 'returns false' do
+ expect(blob.stored_externally?).to be_falsey
+ end
end
end
- describe '#ipython_notebook?' do
- it 'is falsey when language is not Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'JSON'))
+ describe '#raw_binary?' do
+ context 'if the blob is stored externally' do
+ context 'if the extension has a rich viewer' do
+ context 'if the viewer is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+
+ context 'if the viewer is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md', lfs: true)
- expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
+
+ context "if the extension doesn't have a rich viewer" do
+ context 'if the extension has a text mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.txt', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.ics', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
+
+ context 'if the extension has a binary mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.rb', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.exe', lfs: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+ end
+
+ context 'if the extension has an unknown mime type' do
+ context 'if the extension is for a programming language' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.ini', lfs: true)
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
+
+ context 'if the extension is not for a programming language' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.wtf', lfs: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+ end
+ end
+ end
+
+ context 'if the blob is not stored externally' do
+ context 'if the blob is binary' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
+
+ expect(blob.raw_binary?).to be_truthy
+ end
+ end
+
+ context 'if the blob is text-based' do
+ it 'return false' do
+ blob = fake_blob(path: 'file.md')
+
+ expect(blob.raw_binary?).to be_falsey
+ end
+ end
end
+ end
- it 'is truthy when language is Jupyter Notebook' do
- git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
+ describe '#extension' do
+ it 'returns the extension' do
+ blob = fake_blob(path: 'file.md')
- expect(described_class.decorate(git_blob)).to be_ipython_notebook
+ expect(blob.extension).to eq('md')
end
end
- describe '#video?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+ describe '#simple_viewer' do
+ context 'when the blob is empty' do
+ it 'returns an empty viewer' do
+ blob = fake_blob(data: '', size: 0)
+
+ expect(blob.simple_viewer).to be_a(BlobViewer::Empty)
+ end
+ end
- expect(described_class.decorate(git_blob)).not_to be_video
+ context 'when the file represented by the blob is binary' do
+ it 'returns a download viewer' do
+ blob = fake_blob(binary: true)
+
+ expect(blob.simple_viewer).to be_a(BlobViewer::Download)
+ end
end
- UploaderHelper::VIDEO_EXT.each do |ext|
- it "is truthy when extension is .#{ext}" do
- git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
+ context 'when the file represented by the blob is text-based' do
+ it 'returns a text viewer' do
+ blob = fake_blob
- expect(described_class.decorate(git_blob)).to be_video
+ expect(blob.simple_viewer).to be_a(BlobViewer::Text)
end
end
end
- describe '#to_partial_path' do
- let(:project) { double(lfs_enabled?: true) }
+ describe '#rich_viewer' do
+ context 'when the blob has an external storage error' do
+ before do
+ project.lfs_enabled = false
+ end
+
+ it 'returns nil' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
+
+ expect(blob.rich_viewer).to be_nil
+ end
+ end
+
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
+
+ expect(blob.rich_viewer).to be_nil
+ end
+ end
- def stubbed_blob(overrides = {})
- overrides.reverse_merge!(
- image?: false,
- language: nil,
- lfs_pointer?: false,
- svg?: false,
- text?: false
- )
+ context 'when the blob is stored externally' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- described_class.decorate(double).tap do |blob|
- allow(blob).to receive_messages(overrides)
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
end
end
- it 'handles LFS pointers with LFS enabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- expect(blob.to_partial_path(project)).to eq 'download'
+ context 'when the blob is binary' do
+ it 'returns a matching binary viewer' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
+
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles LFS pointers with LFS disabled' do
- blob = stubbed_blob(lfs_pointer?: true, text?: true)
- project = double(lfs_enabled?: false)
- expect(blob.to_partial_path(project)).to eq 'text'
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'file.md')
+
+ expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
+ end
end
+ end
+
+ describe '#auxiliary_viewer' do
+ context 'when the blob has an external storage error' do
+ before do
+ project.lfs_enabled = false
+ end
- it 'handles SVGs' do
- blob = stubbed_blob(text?: true, svg?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
+ it 'returns nil' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
end
- it 'handles images' do
- blob = stubbed_blob(image?: true)
- expect(blob.to_partial_path(project)).to eq 'image'
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
end
- it 'handles text' do
- blob = stubbed_blob(text?: true)
- expect(blob.to_partial_path(project)).to eq 'text'
+ context 'when the blob is stored externally' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
+
+ expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
+ end
end
- it 'defaults to download' do
- blob = stubbed_blob
- expect(blob.to_partial_path(project)).to eq 'download'
+ context 'when the blob is binary' do
+ it 'returns nil' do
+ blob = fake_blob(path: 'LICENSE', binary: true)
+
+ expect(blob.auxiliary_viewer).to be_nil
+ end
end
- it 'handles iPython notebooks' do
- blob = stubbed_blob(text?: true, ipython_notebook?: true)
- expect(blob.to_partial_path(project)).to eq 'notebook'
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'LICENSE')
+
+ expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
+ end
end
end
- describe '#size_within_svg_limits?' do
- let(:blob) { described_class.decorate(double(:blob)) }
+ describe '#rendered_as_text?' do
+ context 'when ignoring errors' do
+ context 'when the simple viewer is text-based' do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
+
+ expect(blob.rendered_as_text?).to be_truthy
+ end
+ end
- it 'returns true when the blob size is smaller than the SVG limit' do
- expect(blob).to receive(:size).and_return(42)
+ context 'when the simple viewer is binary' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
+ expect(blob.rendered_as_text?).to be_falsey
+ end
+ end
end
- it 'returns true when the blob size is equal to the SVG limit' do
- expect(blob).to receive(:size).and_return(Blob::MAXIMUM_SVG_SIZE)
+ context 'when not ignoring errors' do
+ context 'when the viewer has render errors' do
+ it 'returns false' do
+ blob = fake_blob(path: 'file.md', size: 100.megabytes)
- expect(blob.size_within_svg_limits?).to eq(true)
- end
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_falsey
+ end
+ end
- it 'returns false when the blob size is larger than the SVG limit' do
- expect(blob).to receive(:size).and_return(1.terabyte)
+ context "when the viewer doesn't have render errors" do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md')
- expect(blob.size_within_svg_limits?).to eq(false)
+ expect(blob.rendered_as_text?(ignore_errors: false)).to be_truthy
+ end
+ end
end
end
end
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
new file mode 100644
index 00000000000..92fbf64a6b7
--- /dev/null
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -0,0 +1,177 @@
+require 'spec_helper'
+
+describe BlobViewer::Base, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(described_class) do
+ include BlobViewer::ServerSide
+
+ self.extensions = %w(pdf)
+ self.binary = true
+ self.overridable_max_size = 1.megabyte
+ self.max_size = 5.megabytes
+ end
+ end
+
+ let(:viewer) { viewer_class.new(blob) }
+
+ describe '.can_render?' do
+ context 'when the extension is supported' do
+ context 'when the binaryness matches' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true) }
+
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ let(:blob) { fake_blob(path: 'file.pdf', binary: false) }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the file type is supported' do
+ before do
+ viewer_class.file_types = %i(license)
+ viewer_class.binary = false
+ end
+
+ context 'when the binaryness matches' do
+ let(:blob) { fake_blob(path: 'LICENSE', binary: false) }
+
+ it 'returns true' do
+ expect(viewer_class.can_render?(blob)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ let(:blob) { fake_blob(path: 'LICENSE', binary: true) }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the extension and file type are not supported' do
+ let(:blob) { fake_blob(path: 'file.txt') }
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(blob)).to be_falsey
+ end
+ end
+ end
+
+ describe '#exceeds_overridable_max_size?' do
+ context 'when the blob size is larger than the overridable max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.exceeds_overridable_max_size?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the overridable max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.exceeds_overridable_max_size?).to be_falsey
+ end
+ end
+ end
+
+ describe '#exceeds_max_size?' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.exceeds_max_size?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.exceeds_max_size?).to be_falsey
+ end
+ end
+ end
+
+ describe '#can_override_max_size?' do
+ context 'when the blob size is larger than the overridable max size' do
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.can_override_max_size?).to be_truthy
+ end
+ end
+ end
+
+ context 'when the blob size is smaller than the overridable max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.can_override_max_size?).to be_falsey
+ end
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the max size is overridden' do
+ before do
+ viewer.override_max_size = true
+ end
+
+ context 'when the blob size is larger than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+
+ context 'when the max size is not overridden' do
+ context 'when the blob size is larger than the overridable max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the blob size is smaller than the overridable max size' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/changelog_spec.rb b/spec/models/blob_viewer/changelog_spec.rb
new file mode 100644
index 00000000000..9066c5a05ac
--- /dev/null
+++ b/spec/models/blob_viewer/changelog_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe BlobViewer::Changelog, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: 'CHANGELOG') }
+ subject { described_class.new(blob) }
+
+ describe '#render_error' do
+ context 'when there are no tags' do
+ before do
+ allow(project.repository).to receive(:tag_count).and_return(0)
+ end
+
+ it 'returns :no_tags' do
+ expect(subject.render_error).to eq(:no_tags)
+ end
+ end
+
+ context 'when there are tags' do
+ it 'returns nil' do
+ expect(subject.render_error).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/composer_json_spec.rb b/spec/models/blob_viewer/composer_json_spec.rb
new file mode 100644
index 00000000000..df4f1f4815c
--- /dev/null
+++ b/spec/models/blob_viewer/composer_json_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::ComposerJson, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "laravel/laravel",
+ "homepage": "https://laravel.com/"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'composer.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('laravel/laravel')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/gemspec_spec.rb b/spec/models/blob_viewer/gemspec_spec.rb
new file mode 100644
index 00000000000..81e932de290
--- /dev/null
+++ b/spec/models/blob_viewer/gemspec_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::Gemspec, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = "activerecord"
+ end
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'activerecord.gemspec', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('activerecord')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
new file mode 100644
index 00000000000..0c6c24ece21
--- /dev/null
+++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe BlobViewer::GitlabCiYml, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
+ let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#validation_message' do
+ it 'calls prepare! on the viewer' do
+ expect(subject).to receive(:prepare!)
+
+ subject.validation_message
+ end
+
+ context 'when the configuration is valid' do
+ it 'returns nil' do
+ expect(subject.validation_message).to be_nil
+ end
+ end
+
+ context 'when the configuration is invalid' do
+ let(:data) { 'oof' }
+
+ it 'returns the error message' do
+ expect(subject.validation_message).to eq('Invalid configuration format')
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/license_spec.rb b/spec/models/blob_viewer/license_spec.rb
new file mode 100644
index 00000000000..944ddd32b92
--- /dev/null
+++ b/spec/models/blob_viewer/license_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe BlobViewer::License, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: 'LICENSE') }
+ subject { described_class.new(blob) }
+
+ describe '#license' do
+ it 'returns the blob project repository license' do
+ expect(subject.license).not_to be_nil
+ expect(subject.license).to eq(project.repository.license)
+ end
+ end
+
+ describe '#render_error' do
+ context 'when there is no license' do
+ before do
+ allow(project.repository).to receive(:license).and_return(nil)
+ end
+
+ it 'returns :unknown_license' do
+ expect(subject.render_error).to eq(:unknown_license)
+ end
+ end
+
+ context 'when there is a license' do
+ it 'returns nil' do
+ expect(subject.render_error).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
new file mode 100644
index 00000000000..5c9a9c81963
--- /dev/null
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::PackageJson, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "module-name",
+ "version": "10.3.1"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'package.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('module-name')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/podspec_json_spec.rb b/spec/models/blob_viewer/podspec_json_spec.rb
new file mode 100644
index 00000000000..42a00940bc5
--- /dev/null
+++ b/spec/models/blob_viewer/podspec_json_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::PodspecJson, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "AFNetworking",
+ "version": "2.0.0"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'AFNetworking.podspec.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('AFNetworking')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/podspec_spec.rb b/spec/models/blob_viewer/podspec_spec.rb
new file mode 100644
index 00000000000..6c9f0f42d53
--- /dev/null
+++ b/spec/models/blob_viewer/podspec_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe BlobViewer::Podspec, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ Pod::Spec.new do |spec|
+ spec.name = 'Reachability'
+ spec.version = '3.1.0'
+ end
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'Reachability.podspec', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_name' do
+ it 'returns the package name' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_name).to eq('Reachability')
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/route_map_spec.rb b/spec/models/blob_viewer/route_map_spec.rb
new file mode 100644
index 00000000000..4854e0262d9
--- /dev/null
+++ b/spec/models/blob_viewer/route_map_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe BlobViewer::RouteMap, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:project) }
+ let(:data) do
+ <<-MAP.strip_heredoc
+ # Team data
+ - source: 'data/team.yml'
+ public: 'team/'
+ MAP
+ end
+ let(:blob) { fake_blob(path: '.gitlab/route-map.yml', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#validation_message' do
+ it 'calls prepare! on the viewer' do
+ expect(subject).to receive(:prepare!)
+
+ subject.validation_message
+ end
+
+ context 'when the configuration is valid' do
+ it 'returns nil' do
+ expect(subject.validation_message).to be_nil
+ end
+ end
+
+ context 'when the configuration is invalid' do
+ let(:data) { 'oof' }
+
+ it 'returns the error message' do
+ expect(subject.validation_message).to eq('Route map is not an array')
+ end
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/server_side_spec.rb b/spec/models/blob_viewer/server_side_spec.rb
new file mode 100644
index 00000000000..f047953d540
--- /dev/null
+++ b/spec/models/blob_viewer/server_side_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe BlobViewer::ServerSide, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { build(:empty_project) }
+
+ let(:viewer_class) do
+ Class.new(BlobViewer::Base) do
+ include BlobViewer::ServerSide
+ end
+ end
+
+ subject { viewer_class.new(blob) }
+
+ describe '#prepare!' do
+ let(:blob) { fake_blob(path: 'file.txt') }
+
+ it 'loads all blob data' do
+ expect(blob).to receive(:load_all_data!)
+
+ subject.prepare!
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the blob is stored externally' do
+ let(:project) { build(:empty_project, lfs_enabled: true) }
+
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
+
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ it 'return :server_side_but_stored_externally' do
+ expect(subject.render_error).to eq(:server_side_but_stored_externally)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
new file mode 100644
index 00000000000..968593d7e9b
--- /dev/null
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Ci::ArtifactBlob, models: true do
+ let(:build) { create(:ci_build, :artifacts) }
+ let(:entry) { build.artifacts_metadata_entry('other_artifacts_0.1.2/another-subdirectory/banana_sample.gif') }
+
+ subject { described_class.new(entry) }
+
+ describe '#id' do
+ it 'returns a hash of the path' do
+ expect(subject.id).to eq(Digest::SHA1.hexdigest(entry.path))
+ end
+ end
+
+ describe '#name' do
+ it 'returns the entry name' do
+ expect(subject.name).to eq(entry.name)
+ end
+ end
+
+ describe '#path' do
+ it 'returns the entry path' do
+ expect(subject.path).to eq(entry.path)
+ end
+ end
+
+ describe '#size' do
+ it 'returns the entry size' do
+ expect(subject.size).to eq(entry.metadata[:size])
+ end
+ end
+
+ describe '#mode' do
+ it 'returns the entry mode' do
+ expect(subject.mode).to eq(entry.metadata[:mode])
+ end
+ end
+
+ describe '#external_storage' do
+ it 'returns :build_artifact' do
+ expect(subject.external_storage).to eq(:build_artifact)
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 8dbcf50ee0c..e971b4bc3f9 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -17,8 +17,9 @@ describe Ci::Build, :models do
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
it { is_expected.to have_many(:deployments) }
- it { is_expected.to validate_presence_of :ref }
- it { is_expected.to respond_to :trace_html }
+ it { is_expected.to validate_presence_of(:ref) }
+ it { is_expected.to respond_to(:has_trace?) }
+ it { is_expected.to respond_to(:trace) }
describe '#actionize' do
context 'when build is a created' do
@@ -78,32 +79,6 @@ describe Ci::Build, :models do
end
end
- describe '#append_trace' do
- subject { build.trace_html }
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- build.append_trace(token, 0)
- end
-
- it { is_expected.not_to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- build.append_trace(token, 0)
- end
-
- it { is_expected.not_to include(token) }
- end
- end
-
describe '#artifacts?' do
subject { build.artifacts? }
@@ -272,15 +247,101 @@ describe Ci::Build, :models do
describe '#update_coverage' do
context "regarding coverage_regex's value," do
- it "saves the correct extracted coverage value" do
+ before do
build.coverage_regex = '\(\d+.\d+\%\) covered'
- allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
- expect(build).to receive(:update_attributes).with(coverage: 98.29) { true }
- expect(build.update_coverage).to be true
+ build.trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+ end
+
+ it "saves the correct extracted coverage value" do
+ expect(build.update_coverage).to be(true)
+ expect(build.coverage).to eq(98.29)
end
end
end
+ describe '#trace' do
+ subject { build.trace }
+
+ it { is_expected.to be_a(Gitlab::Ci::Trace) }
+ end
+
+ describe '#has_trace?' do
+ subject { build.has_trace? }
+
+ it "expect to call exist? method" do
+ expect_any_instance_of(Gitlab::Ci::Trace).to receive(:exist?)
+ .and_return(true)
+
+ is_expected.to be(true)
+ end
+ end
+
+ describe '#trace=' do
+ it "expect to fail trace=" do
+ expect { build.trace = "new" }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#old_trace' do
+ subject { build.old_trace }
+
+ before do
+ build.update_column(:trace, 'old trace')
+ end
+
+ it "expect to receive data from database" do
+ is_expected.to eq('old trace')
+ end
+ end
+
+ describe '#erase_old_trace!' do
+ subject { build.send(:read_attribute, :trace) }
+
+ before do
+ build.send(:write_attribute, :trace, 'old trace')
+ end
+
+ it "expect to receive data from database" do
+ build.erase_old_trace!
+
+ is_expected.to be_nil
+ end
+ end
+
+ describe '#hide_secrets' do
+ let(:subject) { build.hide_secrets(data) }
+
+ context 'hide runners token' do
+ let(:data) { 'new token data'}
+
+ before do
+ build.project.update(runners_token: 'token')
+ end
+
+ it { is_expected.to eq('new xxxxx data') }
+ end
+
+ context 'hide build token' do
+ let(:data) { 'new token data'}
+
+ before do
+ build.update(token: 'token')
+ end
+
+ it { is_expected.to eq('new xxxxx data') }
+ end
+
+ context 'hide build token' do
+ let(:data) { 'new token data'}
+
+ before do
+ build.update(token: 'token')
+ end
+
+ it { is_expected.to eq('new xxxxx data') }
+ end
+ end
+
describe 'deployment' do
describe '#last_deployment' do
subject { build.last_deployment }
@@ -438,7 +499,7 @@ describe Ci::Build, :models do
end
it 'erases build trace in trace file' do
- expect(build.trace).to be_empty
+ expect(build).not_to have_trace
end
it 'sets erased to true' do
@@ -532,38 +593,6 @@ describe Ci::Build, :models do
end
end
- describe '#extract_coverage' do
- context 'valid content & regex' do
- subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to eq(98.29) }
- end
-
- context 'valid content & bad regex' do
- subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') }
-
- it { is_expected.to be_nil }
- end
-
- context 'no coverage content & regex' do
- subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to be_nil }
- end
-
- context 'multiple results in content & regex' do
- subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') }
-
- it { is_expected.to eq(98.29) }
- end
-
- context 'using a regex capture' do
- subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') }
-
- it { is_expected.to eq(65) }
- end
- end
-
describe '#first_pending' do
let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) }
let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') }
@@ -735,40 +764,6 @@ describe Ci::Build, :models do
end
end
- describe '#has_commands?' do
- context 'when build has commands' do
- let(:build) do
- create(:ci_build, commands: 'rspec')
- end
-
- it 'has commands' do
- expect(build).to have_commands
- end
- end
-
- context 'when does not have commands' do
- context 'when commands are an empty string' do
- let(:build) do
- create(:ci_build, commands: '')
- end
-
- it 'has no commands' do
- expect(build).not_to have_commands
- end
- end
-
- context 'when commands are not set at all' do
- let(:build) do
- create(:ci_build, commands: nil)
- end
-
- it 'has no commands' do
- expect(build).not_to have_commands
- end
- end
- end
- end
-
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
@@ -902,22 +897,26 @@ describe Ci::Build, :models do
end
describe '#persisted_environment' do
- before do
- @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+ let!(:environment) do
+ create(:environment, project: project, name: "foo-#{project.default_branch}")
end
subject { build.persisted_environment }
- context 'referenced literally' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+ context 'when referenced literally' do
+ let(:build) do
+ create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}")
+ end
- it { is_expected.to eq(@environment) }
+ it { is_expected.to eq(environment) }
end
- context 'referenced with a variable' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") }
+ context 'when referenced with a variable' do
+ let(:build) do
+ create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME")
+ end
- it { is_expected.to eq(@environment) }
+ it { is_expected.to eq(environment) }
end
end
@@ -928,26 +927,8 @@ describe Ci::Build, :models do
project.add_developer(user)
end
- context 'when build is manual' do
- it 'enqueues a build' do
- new_build = build.play(user)
-
- expect(new_build).to be_pending
- expect(new_build).to eq(build)
- end
- end
-
- context 'when build is passed' do
- before do
- build.update(status: 'success')
- end
-
- it 'creates a new build' do
- new_build = build.play(user)
-
- expect(new_build).to be_pending
- expect(new_build).not_to eq(build)
- end
+ it 'enqueues the build' do
+ expect(build.play(user)).to be_pending
end
end
@@ -983,32 +964,6 @@ describe Ci::Build, :models do
it { is_expected.to eq(project.name) }
end
- describe '#raw_trace' do
- subject { build.raw_trace }
-
- context 'when build.trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.project.update(runners_token: token)
- build.update(trace: token)
- end
-
- it { is_expected.not_to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(token: token)
- build.update(trace: token)
- end
-
- it { is_expected.not_to include(token) }
- end
- end
-
describe '#ref_slug' do
{
'master' => 'master',
@@ -1017,7 +972,7 @@ describe Ci::Build, :models do
'fix-1-foo' => 'fix-1-foo',
'a' * 63 => 'a' * 63,
'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
+ 'FOO' => 'foo'
}.each do |ref, slug|
it "transforms #{ref} to #{slug}" do
build.ref = ref
@@ -1074,61 +1029,6 @@ describe Ci::Build, :models do
end
end
- describe '#trace' do
- it 'obfuscates project runners token' do
- allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
-
- expect(build.trace).to eq("Test: xxxxxxxxxxxxxxxxxxxx")
- end
-
- it 'empty project runners token' do
- allow(build).to receive(:raw_trace).and_return(test_trace)
- # runners_token can't normally be set to nil
- allow(build.project).to receive(:runners_token).and_return(nil)
-
- expect(build.trace).to eq(test_trace)
- end
-
- context 'when build does not have trace' do
- it 'is is empty' do
- expect(build.trace).to be_nil
- end
- end
-
- context 'when trace contains text' do
- let(:text) { 'example output' }
- before do
- build.trace = text
- end
-
- it { expect(build.trace).to eq(text) }
- end
-
- context 'when trace hides runners token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(trace: token)
- build.project.update(runners_token: token)
- end
-
- it { expect(build.trace).not_to include(token) }
- it { expect(build.raw_trace).to include(token) }
- end
-
- context 'when build.trace hides build token' do
- let(:token) { 'my_secret_token' }
-
- before do
- build.update(trace: token)
- build.update(token: token)
- end
-
- it { expect(build.trace).not_to include(token) }
- it { expect(build.raw_trace).to include(token) }
- end
- end
-
describe '#has_expiring_artifacts?' do
context 'when artifacts have expiration date set' do
before { build.update(artifacts_expire_at: 1.day.from_now) }
@@ -1147,66 +1047,6 @@ describe Ci::Build, :models do
end
end
- describe '#has_trace_file?' do
- context 'when there is no trace' do
- it { expect(build.has_trace_file?).to be_falsey }
- it { expect(build.trace).to be_nil }
- end
-
- context 'when there is a trace' do
- context 'when trace is stored in file' do
- let(:build_with_trace) { create(:ci_build, :trace) }
-
- it { expect(build_with_trace.has_trace_file?).to be_truthy }
- it { expect(build_with_trace.trace).to eq('BUILD TRACE') }
- end
-
- context 'when trace is stored in old file' do
- before do
- allow(build.project).to receive(:ci_id).and_return(999)
- allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
- allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(true)
- allow(File).to receive(:read).with(build.old_path_to_trace).and_return(test_trace)
- end
-
- it { expect(build.has_trace_file?).to be_truthy }
- it { expect(build.trace).to eq(test_trace) }
- end
-
- context 'when trace is stored in DB' do
- before do
- allow(build.project).to receive(:ci_id).and_return(nil)
- allow(build).to receive(:read_attribute).with(:trace).and_return(test_trace)
- allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
- allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(false)
- end
-
- it { expect(build.has_trace_file?).to be_falsey }
- it { expect(build.trace).to eq(test_trace) }
- end
- end
- end
-
- describe '#trace_file_path' do
- context 'when trace is stored in file' do
- before do
- allow(build).to receive(:has_trace_file?).and_return(true)
- allow(build).to receive(:has_old_trace_file?).and_return(false)
- end
-
- it { expect(build.trace_file_path).to eq(build.path_to_trace) }
- end
-
- context 'when trace is stored in old file' do
- before do
- allow(build).to receive(:has_trace_file?).and_return(true)
- allow(build).to receive(:has_old_trace_file?).and_return(true)
- end
-
- it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
- end
- end
-
describe '#update_project_statistics' do
let!(:build) { create(:ci_build, artifacts_size: 23) }
@@ -1304,7 +1144,7 @@ describe Ci::Build, :models do
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
{ key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
{ key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
- { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }
]
end
@@ -1460,7 +1300,7 @@ describe Ci::Build, :models do
{ key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
end
let(:ci_registry_image) do
- { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true }
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true }
end
context 'and is disabled for project' do
diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb
new file mode 100644
index 00000000000..62e15093089
--- /dev/null
+++ b/spec/models/ci/group_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Ci::Group, models: true do
+ subject do
+ described_class.new('test', name: 'rspec', jobs: jobs)
+ end
+
+ let!(:jobs) { build_list(:ci_build, 1, :success) }
+
+ it { is_expected.to include_module(StaticModel) }
+
+ it { is_expected.to respond_to(:stage) }
+ it { is_expected.to respond_to(:name) }
+ it { is_expected.to respond_to(:jobs) }
+ it { is_expected.to respond_to(:status) }
+
+ describe '#size' do
+ it 'returns the number of statuses in the group' do
+ expect(subject.size).to eq(1)
+ end
+ end
+
+ describe '#detailed_status' do
+ context 'when there is only one item in the group' do
+ it 'calls the status from the object itself' do
+ expect(jobs.first).to receive(:detailed_status)
+
+ expect(subject.detailed_status(double(:user)))
+ end
+ end
+
+ context 'when there are more than one commit status in the group' do
+ let(:jobs) do
+ [create(:ci_build, :failed),
+ create(:ci_build, :success)]
+ end
+
+ it 'fabricates a new detailed status object' do
+ expect(subject.detailed_status(double(:user)))
+ .to be_a(Gitlab::Ci::Status::Failed)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
new file mode 100644
index 00000000000..822b98c5f6c
--- /dev/null
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Ci::PipelineSchedule, models: true do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:owner) }
+
+ it { is_expected.to have_many(:pipelines) }
+
+ it { is_expected.to respond_to(:ref) }
+ it { is_expected.to respond_to(:cron) }
+ it { is_expected.to respond_to(:cron_timezone) }
+ it { is_expected.to respond_to(:description) }
+ it { is_expected.to respond_to(:next_run_at) }
+ it { is_expected.to respond_to(:deleted_at) }
+
+ describe 'validations' do
+ it 'does not allow invalid cron patters' do
+ pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *')
+
+ expect(pipeline_schedule).not_to be_valid
+ end
+
+ it 'does not allow invalid cron patters' do
+ pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid')
+
+ expect(pipeline_schedule).not_to be_valid
+ end
+ end
+
+ describe '#set_next_run_at' do
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+
+ context 'when creates new pipeline schedule' do
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone).
+ next_time_from(Time.now)
+ end
+
+ it 'updates next_run_at automatically' do
+ expect(Ci::PipelineSchedule.last.next_run_at).to eq(expected_next_run_at)
+ end
+ end
+
+ context 'when updates cron of exsisted pipeline schedule' do
+ let(:new_cron) { '0 0 1 1 *' }
+
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone).
+ next_time_from(Time.now)
+ end
+
+ it 'updates next_run_at automatically' do
+ pipeline_schedule.update!(cron: new_cron)
+
+ expect(Ci::PipelineSchedule.last.next_run_at).to eq(expected_next_run_at)
+ end
+ end
+ end
+
+ describe '#schedule_next_run!' do
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+
+ context 'when reschedules after 10 days from now' do
+ let(:future_time) { 10.days.from_now }
+
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone).
+ next_time_from(future_time)
+ end
+
+ it 'points to proper next_run_at' do
+ Timecop.freeze(future_time) do
+ pipeline_schedule.schedule_next_run!
+
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
+ end
+ end
+ end
+ end
+
+ describe '#real_next_run' do
+ subject do
+ described_class.last.real_next_run(worker_cron: worker_cron,
+ worker_time_zone: worker_time_zone)
+ end
+
+ context 'when GitLab time_zone is UTC' do
+ before do
+ allow(Time).to receive(:zone)
+ .and_return(ActiveSupport::TimeZone[worker_time_zone])
+ end
+
+ let(:worker_time_zone) { 'UTC' }
+
+ context 'when cron_timezone is Eastern Time (US & Canada)' do
+ before do
+ create(:ci_pipeline_schedule, :nightly,
+ cron_timezone: 'Eastern Time (US & Canada)')
+ end
+
+ let(:worker_cron) { '0 1 2 3 *' }
+
+ it 'returns the next time worker executes' do
+ expect(subject.min).to eq(0)
+ expect(subject.hour).to eq(1)
+ expect(subject.day).to eq(2)
+ expect(subject.month).to eq(3)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 53282b999dc..56b24ce62f3 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -12,10 +12,14 @@ describe Ci::Pipeline, models: true do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:user) }
+ it { is_expected.to belong_to(:auto_canceled_by) }
+ it { is_expected.to belong_to(:pipeline_schedule) }
it { is_expected.to have_many(:statuses) }
it { is_expected.to have_many(:trigger_requests) }
it { is_expected.to have_many(:builds) }
+ it { is_expected.to have_many(:auto_canceled_pipelines) }
+ it { is_expected.to have_many(:auto_canceled_jobs) }
it { is_expected.to validate_presence_of :sha }
it { is_expected.to validate_presence_of :status }
@@ -56,8 +60,8 @@ describe Ci::Pipeline, models: true do
subject { pipeline.retried }
before do
- @build1 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
- @build2 = FactoryGirl.create :ci_build, pipeline: pipeline, name: 'deploy'
+ @build1 = create(:ci_build, pipeline: pipeline, name: 'deploy', retried: true)
+ @build2 = create(:ci_build, pipeline: pipeline, name: 'deploy')
end
it 'returns old builds' do
@@ -66,31 +70,31 @@ describe Ci::Pipeline, models: true do
end
describe "coverage" do
- let(:project) { FactoryGirl.create :empty_project, build_coverage_regex: "/.*/" }
- let(:pipeline) { FactoryGirl.create :ci_empty_pipeline, project: project }
+ let(:project) { create(:empty_project, build_coverage_regex: "/.*/") }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it "calculates average when there are two builds with coverage" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there are two builds with coverage and one with nil" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
- FactoryGirl.create :ci_build, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
+ create(:ci_build, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there are two builds with coverage and one is retried" do
- FactoryGirl.create :ci_build, name: "rspec", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, pipeline: pipeline
- FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, pipeline: pipeline
+ create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline)
+ create(:ci_build, name: "rubocop", coverage: 30, pipeline: pipeline, retried: true)
+ create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline)
expect(pipeline.coverage).to eq("35.00")
end
it "calculates average when there is one build without coverage" do
- FactoryGirl.create :ci_build, pipeline: pipeline
+ FactoryGirl.create(:ci_build, pipeline: pipeline)
expect(pipeline.coverage).to be_nil
end
end
@@ -134,6 +138,43 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#auto_canceled?' do
+ subject { pipeline.auto_canceled? }
+
+ context 'when it is canceled' do
+ before do
+ pipeline.cancel
+ end
+
+ context 'when there is auto_canceled_by' do
+ before do
+ pipeline.update(auto_canceled_by: create(:ci_empty_pipeline))
+ end
+
+ it 'is auto canceled' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when there is no auto_canceled_by' do
+ it 'is not auto canceled' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'when it is retried and canceled manually' do
+ before do
+ pipeline.enqueue
+ pipeline.cancel
+ end
+
+ it 'is not auto canceled' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+ end
+
describe 'pipeline stages' do
before do
create(:commit_status, pipeline: pipeline,
@@ -181,13 +222,15 @@ describe Ci::Pipeline, models: true do
%w(deploy running)])
end
- context 'when commit status is retried' do
+ context 'when commit status is retried' do
before do
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'success')
+
+ pipeline.process!
end
it 'ignores the previous state' do
@@ -256,32 +299,56 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create_build('build1', 0) }
- let(:build_b) { create_build('build2', 0) }
- let(:build_c) { create_build('build3', 0) }
+ let(:build) { create_build('build1', queued_at: 0) }
+ let(:build_b) { create_build('build2', queued_at: 0) }
+ let(:build_c) { create_build('build3', queued_at: 0) }
describe '#duration' do
- before do
- travel_to(current + 30) do
- build.run!
- build.success!
- build_b.run!
- build_c.run!
- end
+ context 'when multiple builds are finished' do
+ before do
+ travel_to(current + 30) do
+ build.run!
+ build.success!
+ build_b.run!
+ build_c.run!
+ end
- travel_to(current + 40) do
- build_b.drop!
+ travel_to(current + 40) do
+ build_b.drop!
+ end
+
+ travel_to(current + 70) do
+ build_c.success!
+ end
end
- travel_to(current + 70) do
- build_c.success!
+ it 'matches sum of builds duration' do
+ pipeline.reload
+
+ expect(pipeline.duration).to eq(40)
end
end
- it 'matches sum of builds duration' do
- pipeline.reload
+ context 'when pipeline becomes blocked' do
+ let!(:build) { create_build('build:1') }
+ let!(:action) { create_build('manual:action', :manual) }
+
+ before do
+ travel_to(current + 1.minute) do
+ build.run!
+ end
+
+ travel_to(current + 5.minutes) do
+ build.success!
+ end
+ end
+
+ it 'recalculates pipeline duration' do
+ pipeline.reload
- expect(pipeline.duration).to eq(40)
+ expect(pipeline).to be_manual
+ expect(pipeline.duration).to eq 4.minutes
+ end
end
end
@@ -335,12 +402,21 @@ describe Ci::Pipeline, models: true do
end
end
- def create_build(name, queued_at = current, started_from = 0)
- create(:ci_build,
+ describe 'pipeline caching' do
+ it 'performs ExpirePipelinesCacheWorker' do
+ expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
+
+ pipeline.cancel
+ end
+ end
+
+ def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
+ create(:ci_build, *traits,
name: name,
pipeline: pipeline,
queued_at: queued_at,
- started_at: queued_at + started_from)
+ started_at: queued_at + started_from,
+ **opts)
end
end
@@ -415,6 +491,10 @@ describe Ci::Pipeline, models: true do
context 'there are multiple of the same name' do
let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') }
+ before do
+ manual.update(retried: true)
+ end
+
it 'returns latest one' do
is_expected.to contain_exactly(manual2)
end
@@ -774,6 +854,16 @@ describe Ci::Pipeline, models: true do
end
end
end
+
+ context 'when there is a manual action present in the pipeline' do
+ before do
+ create(:ci_build, :manual, pipeline: pipeline)
+ end
+
+ it 'is not cancelable' do
+ expect(pipeline).not_to be_cancelable
+ end
+ end
end
describe '#cancel_running' do
@@ -966,11 +1056,12 @@ describe Ci::Pipeline, models: true do
end
describe "#merge_requests" do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') }
it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do
- merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { 'a288a022a53a5a944fae87bcec6efc87b7061808' }
+ merge_request = create(:merge_request, source_project: project, head_pipeline: pipeline, source_branch: pipeline.ref)
expect(pipeline.merge_requests).to eq([merge_request])
end
@@ -989,6 +1080,23 @@ describe Ci::Pipeline, models: true do
end
end
+ describe "#all_merge_requests" do
+ let(:project) { create(:empty_project) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') }
+
+ it "returns all merge requests having the same source branch" do
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+
+ expect(pipeline.all_merge_requests).to eq([merge_request])
+ end
+
+ it "doesn't return merge requests having a different source branch" do
+ create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
+
+ expect(pipeline.all_merge_requests).to be_empty
+ end
+ end
+
describe '#stuck?' do
before do
create(:ci_build, :pending, pipeline: pipeline)
@@ -1031,19 +1139,6 @@ describe Ci::Pipeline, models: true do
end
end
- describe '#update_status' do
- let(:pipeline) { create(:ci_pipeline, sha: '123456') }
-
- it 'updates the cached status' do
- fake_status = double
- # after updating the status, the status is set to `skipped` for this pipeline's builds
- expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status)
- expect(fake_status).to receive(:store_in_cache_if_needed)
-
- pipeline.update_status
- end
- end
-
describe 'notifications when pipeline success or failed' do
let(:project) { create(:project, :repository) }
@@ -1055,10 +1150,13 @@ describe Ci::Pipeline, models: true do
end
before do
- reset_delivered_emails!
-
project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
+ pipeline.user.global_notification_setting.
+ update(level: 'custom', failed_pipeline: true, success_pipeline: true)
+
+ reset_delivered_emails!
+
perform_enqueued_jobs do
pipeline.enqueue
pipeline.run
diff --git a/spec/models/ci/pipeline_status_spec.rb b/spec/models/ci/pipeline_status_spec.rb
deleted file mode 100644
index bc5b71666c2..00000000000
--- a/spec/models/ci/pipeline_status_spec.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-require 'spec_helper'
-
-describe Ci::PipelineStatus do
- let(:project) { create(:project) }
- let(:pipeline_status) { described_class.new(project) }
-
- describe '.load_for_project' do
- it "loads the status" do
- expect_any_instance_of(described_class).to receive(:load_status)
-
- described_class.load_for_project(project)
- end
- end
-
- describe '#has_status?' do
- it "is false when the status wasn't loaded yet" do
- expect(pipeline_status.has_status?).to be_falsy
- end
-
- it 'is true when all status information was loaded' do
- fake_commit = double
- allow(fake_commit).to receive(:status).and_return('failed')
- allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6')
- allow(pipeline_status).to receive(:commit).and_return(fake_commit)
- allow(pipeline_status).to receive(:has_cache?).and_return(false)
-
- pipeline_status.load_status
-
- expect(pipeline_status.has_status?).to be_truthy
- end
- end
-
- describe '#load_status' do
- it 'loads the status from the cache when there is one' do
- expect(pipeline_status).to receive(:has_cache?).and_return(true)
- expect(pipeline_status).to receive(:load_from_cache)
-
- pipeline_status.load_status
- end
-
- it 'loads the status from the project commit when there is no cache' do
- allow(pipeline_status).to receive(:has_cache?).and_return(false)
-
- expect(pipeline_status).to receive(:load_from_commit)
-
- pipeline_status.load_status
- end
-
- it 'stores the status in the cache when it loading it from the project' do
- allow(pipeline_status).to receive(:has_cache?).and_return(false)
- allow(pipeline_status).to receive(:load_from_commit)
-
- expect(pipeline_status).to receive(:store_in_cache)
-
- pipeline_status.load_status
- end
-
- it 'sets the state to loaded' do
- pipeline_status.load_status
-
- expect(pipeline_status).to be_loaded
- end
-
- it 'only loads the status once' do
- expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1)
- expect(pipeline_status).to receive(:load_from_cache).exactly(1)
-
- pipeline_status.load_status
- pipeline_status.load_status
- end
- end
-
- describe "#load_from_commit" do
- let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
-
- it 'reads the status from the pipeline for the commit' do
- pipeline_status.load_from_commit
-
- expect(pipeline_status.status).to eq('success')
- expect(pipeline_status.sha).to eq(project.commit.sha)
- end
-
- it "doesn't fail for an empty project" do
- status_for_empty_commit = described_class.new(create(:empty_project))
-
- status_for_empty_commit.load_status
-
- expect(status_for_empty_commit).to be_loaded
- end
- end
-
- describe "#store_in_cache", :redis do
- it "sets the object in redis" do
- pipeline_status.sha = '123456'
- pipeline_status.status = 'failed'
-
- pipeline_status.store_in_cache
- read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
-
- expect(read_sha).to eq('123456')
- expect(read_status).to eq('failed')
- end
- end
-
- describe '#store_in_cache_if_needed', :redis do
- it 'stores the state in the cache when the sha is the HEAD of the project' do
- create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
- build_status = described_class.load_for_project(project)
-
- build_status.store_in_cache_if_needed
- sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
-
- expect(sha).not_to be_nil
- expect(status).not_to be_nil
- end
-
- it "doesn't store the status in redis when the sha is not the head of the project" do
- other_status = described_class.new(project, sha: "123456", status: "failed")
-
- other_status.store_in_cache_if_needed
- sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
-
- expect(sha).to be_nil
- expect(status).to be_nil
- end
-
- it "deletes the cache if the repository doesn't have a head commit" do
- empty_project = create(:empty_project)
- Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) }
- other_status = described_class.new(empty_project, sha: "123456", status: "failed")
-
- other_status.store_in_cache_if_needed
- sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) }
-
- expect(sha).to be_nil
- expect(status).to be_nil
- end
- end
-
- describe "with a status in redis", :redis do
- let(:status) { 'success' }
- let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
-
- before do
- Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) }
- end
-
- describe '#load_from_cache' do
- it 'reads the status from redis' do
- pipeline_status.load_from_cache
-
- expect(pipeline_status.sha).to eq(sha)
- expect(pipeline_status.status).to eq(status)
- end
- end
-
- describe '#has_cache?' do
- it 'knows the status is cached' do
- expect(pipeline_status.has_cache?).to be_truthy
- end
- end
-
- describe '#delete_from_cache' do
- it 'deletes values from redis' do
- pipeline_status.delete_from_cache
-
- key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") }
-
- expect(key_exists).to be_falsy
- end
- end
- end
-end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index c38faf32f7d..8f6ab908987 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -28,6 +28,35 @@ describe Ci::Stage, models: true do
end
end
+ describe '#groups' do
+ before do
+ create_job(:ci_build, name: 'rspec 0 2')
+ create_job(:ci_build, name: 'rspec 0 1')
+ create_job(:ci_build, name: 'spinach 0 1')
+ create_job(:commit_status, name: 'aaaaa')
+ end
+
+ it 'returns an array of three groups' do
+ expect(stage.groups).to be_a Array
+ expect(stage.groups).to all(be_a Ci::Group)
+ expect(stage.groups.size).to eq 3
+ end
+
+ it 'returns groups with correctly ordered statuses' do
+ expect(stage.groups.first.jobs.map(&:name))
+ .to eq ['aaaaa']
+ expect(stage.groups.second.jobs.map(&:name))
+ .to eq ['rspec 0 1', 'rspec 0 2']
+ expect(stage.groups.third.jobs.map(&:name))
+ .to eq ['spinach 0 1']
+ end
+
+ it 'returns groups with correct names' do
+ expect(stage.groups.map(&:name))
+ .to eq %w[aaaaa rspec spinach]
+ end
+ end
+
describe '#statuses_count' do
before do
create_job(:ci_build)
@@ -73,6 +102,10 @@ describe Ci::Stage, models: true do
context 'and builds are retried' do
let!(:new_build) { create_job(:ci_build, status: :success) }
+ before do
+ stage_build.update(retried: true)
+ end
+
it "returns status of latest build" do
is_expected.to eq('success')
end
@@ -223,7 +256,7 @@ describe Ci::Stage, models: true do
end
end
- def create_job(type, status: 'success', stage: stage_name)
- create(type, pipeline: pipeline, stage: stage, status: status)
+ def create_job(type, status: 'success', stage: stage_name, **opts)
+ create(type, pipeline: pipeline, stage: stage, status: status, **opts)
end
end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 1bcb673cb16..92c15c13c18 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -16,8 +16,8 @@ describe Ci::Trigger, models: true do
expect(trigger.token).not_to be_nil
end
- it 'does not set an random token if one provided' do
- trigger = create(:ci_trigger, project: project)
+ it 'does not set a random token if one provided' do
+ trigger = create(:ci_trigger, project: project, token: 'token')
expect(trigger.token).to eq('token')
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 048d25869bc..fe8c52d5353 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::Variable, models: true do
- subject { Ci::Variable.new }
+ subject { build(:ci_variable) }
let(:secret_value) { 'secret' }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index befafcf457c..a239f8e165c 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -212,7 +212,7 @@ eos
end
end
- describe '#latest_pipeline' do
+ describe '#last_pipeline' do
let!(:first_pipeline) do
create(:ci_empty_pipeline,
project: project,
@@ -226,8 +226,8 @@ eos
status: 'success')
end
- it 'returns latest pipeline' do
- expect(commit.latest_pipeline).to eq second_pipeline
+ it 'returns last pipeline' do
+ expect(commit.last_pipeline).to eq second_pipeline
end
end
@@ -388,32 +388,4 @@ eos
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
-
- describe '#raw_diffs' do
- context 'Gitaly commit_raw_diffs feature enabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
- end
-
- context 'when a truthy deltas_only is not passed to args' do
- it 'fetches diffs from Gitaly server' do
- expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
- with(commit)
-
- commit.raw_diffs
- end
- end
-
- context 'when a truthy deltas_only is passed to args' do
- it 'fetches diffs using Rugged' do
- opts = { deltas_only: true }
-
- expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
- expect(commit.raw).to receive(:diffs).with(opts)
-
- commit.raw_diffs(opts)
- end
- end
- end
- end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 7343b735a74..6947affcc1e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -16,6 +16,7 @@ describe CommitStatus, :models do
it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:auto_canceled_by) }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_inclusion_of(:status).in_array(%w(pending running failed success canceled)) }
@@ -101,6 +102,32 @@ describe CommitStatus, :models do
end
end
+ describe '#auto_canceled?' do
+ subject { commit_status.auto_canceled? }
+
+ context 'when it is canceled' do
+ before do
+ commit_status.update(status: 'canceled')
+ end
+
+ context 'when there is auto_canceled_by' do
+ before do
+ commit_status.update(auto_canceled_by: create(:ci_empty_pipeline))
+ end
+
+ it 'is auto canceled' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when there is no auto_canceled_by' do
+ it 'is not auto canceled' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+ end
+
describe '#duration' do
subject { commit_status.duration }
@@ -130,9 +157,9 @@ describe CommitStatus, :models do
subject { described_class.latest.order(:id) }
let(:statuses) do
- [create_status(name: 'aa', ref: 'bb', status: 'running'),
- create_status(name: 'cc', ref: 'cc', status: 'pending'),
- create_status(name: 'aa', ref: 'cc', status: 'success'),
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
create_status(name: 'cc', ref: 'bb', status: 'success'),
create_status(name: 'aa', ref: 'bb', status: 'success')]
end
@@ -142,6 +169,22 @@ describe CommitStatus, :models do
end
end
+ describe '.retried' do
+ subject { described_class.retried.order(:id) }
+
+ let(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running', retried: true),
+ create_status(name: 'cc', ref: 'cc', status: 'pending', retried: true),
+ create_status(name: 'aa', ref: 'cc', status: 'success', retried: true),
+ create_status(name: 'cc', ref: 'bb', status: 'success'),
+ create_status(name: 'aa', ref: 'bb', status: 'success')]
+ end
+
+ it 'returns unique statuses' do
+ is_expected.to contain_exactly(*statuses.values_at(0, 1, 2))
+ end
+ end
+
describe '.running_or_pending' do
subject { described_class.running_or_pending.order(:id) }
@@ -154,7 +197,7 @@ describe CommitStatus, :models do
end
it 'returns statuses that are running or pending' do
- is_expected.to eq(statuses.values_at(0, 1))
+ is_expected.to contain_exactly(*statuses.values_at(0, 1))
end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index de791abdf3d..63ad3a3630b 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -1,10 +1,12 @@
require 'spec_helper'
-describe Issue, "Awardable" do
+describe Awardable do
let!(:issue) { create(:issue) }
let!(:award_emoji) { create(:award_emoji, :downvote, awardable: issue) }
describe "Associations" do
+ subject { build(:issue) }
+
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 6151d53cd91..40bbb10eaac 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -1,9 +1,6 @@
require 'spec_helper'
describe CacheMarkdownField do
- caching_classes = CacheMarkdownField::CACHING_CLASSES
- CacheMarkdownField::CACHING_CLASSES = ["ThingWithMarkdownFields"].freeze
-
# The minimum necessary ActiveModel to test this concern
class ThingWithMarkdownFields
include ActiveModel::Model
@@ -21,24 +18,25 @@ describe CacheMarkdownField do
end
extend ActiveModel::Callbacks
- define_model_callbacks :save
+ define_model_callbacks :create, :update
include CacheMarkdownField
cache_markdown_field :foo
cache_markdown_field :baz, pipeline: :single_line
- def self.add_attr(attr_name)
- self.attribute_names += [attr_name]
- define_attribute_methods(attr_name)
- attr_reader(attr_name)
- define_method("#{attr_name}=") do |val|
- send("#{attr_name}_will_change!") unless val == send(attr_name)
- instance_variable_set("@#{attr_name}", val)
+ def self.add_attr(name)
+ self.attribute_names += [name]
+ define_attribute_methods(name)
+ attr_reader(name)
+ define_method("#{name}=") do |value|
+ write_attribute(name, value)
end
end
- [:foo, :foo_html, :bar, :baz, :baz_html].each do |attr_name|
- add_attr(attr_name)
+ add_attr :cached_markdown_version
+
+ [:foo, :foo_html, :bar, :baz, :baz_html].each do |name|
+ add_attr(name)
end
def initialize(*)
@@ -48,134 +46,258 @@ describe CacheMarkdownField do
clear_changes_information
end
+ def read_attribute(name)
+ instance_variable_get("@#{name}")
+ end
+
+ def write_attribute(name, value)
+ send("#{name}_will_change!") unless value == read_attribute(name)
+ instance_variable_set("@#{name}", value)
+ end
+
def save
- run_callbacks :save do
+ run_callbacks :update do
changes_applied
end
end
end
- CacheMarkdownField::CACHING_CLASSES = caching_classes
-
def thing_subclass(new_attr)
Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
end
- let(:markdown) { "`Foo`" }
- let(:html) { "<p><code>Foo</code></p>" }
+ let(:markdown) { '`Foo`' }
+ let(:html) { '<p dir="auto"><code>Foo</code></p>' }
- let(:updated_markdown) { "`Bar`" }
- let(:updated_html) { "<p dir=\"auto\"><code>Bar</code></p>" }
+ let(:updated_markdown) { '`Bar`' }
+ let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
- subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
+ let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) }
- describe ".attributes" do
- it "excludes cache attributes" do
- expect(thing_subclass(:qux).new.attributes.keys.sort).to eq(%w[bar baz foo qux])
+ describe '.attributes' do
+ it 'excludes cache attributes' do
+ expect(thing.attributes.keys.sort).to eq(%w[bar baz foo])
end
end
- describe ".cache_markdown_field" do
- it "refuses to allow untracked classes" do
- expect { thing_subclass(:qux).__send__(:cache_markdown_field, :qux) }.to raise_error(RuntimeError)
+ context 'an unchanged markdown field' do
+ before do
+ thing.foo = thing.foo
+ thing.save
end
+
+ it { expect(thing.foo).to eq(markdown) }
+ it { expect(thing.foo_html).to eq(html) }
+ it { expect(thing.foo_html_changed?).not_to be_truthy }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end
- context "an unchanged markdown field" do
+ context 'a changed markdown field' do
before do
- subject.foo = subject.foo
- subject.save
+ thing.foo = updated_markdown
+ thing.save
end
- it { expect(subject.foo).to eq(markdown) }
- it { expect(subject.foo_html).to eq(html) }
- it { expect(subject.foo_html_changed?).not_to be_truthy }
+ it { expect(thing.foo_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end
- context "a changed markdown field" do
+ context 'a non-markdown field changed' do
before do
- subject.foo = updated_markdown
- subject.save
+ thing.bar = 'OK'
+ thing.save
end
- it { expect(subject.foo_html).to eq(updated_html) }
+ it { expect(thing.bar).to eq('OK') }
+ it { expect(thing.foo).to eq(markdown) }
+ it { expect(thing.foo_html).to eq(html) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end
- context "a non-markdown field changed" do
+ context 'version is out of date' do
+ let(:thing) { ThingWithMarkdownFields.new(foo: updated_markdown, foo_html: html, cached_markdown_version: nil) }
+
before do
- subject.bar = "OK"
- subject.save
+ thing.save
end
- it { expect(subject.bar).to eq("OK") }
- it { expect(subject.foo).to eq(markdown) }
- it { expect(subject.foo_html).to eq(html) }
+ it { expect(thing.foo_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
+ end
+
+ describe '#cached_html_up_to_date?' do
+ subject { thing.cached_html_up_to_date?(:foo) }
+
+ it 'returns false when the version is absent' do
+ thing.cached_markdown_version = nil
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns false when the version is too early' do
+ thing.cached_markdown_version -= 1
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns false when the version is too late' do
+ thing.cached_markdown_version += 1
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns true when the version is just right' do
+ thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false if markdown has been changed but html has not' do
+ thing.foo = updated_html
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns true if markdown has not been changed but html has' do
+ thing.foo_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns true if markdown and html have both been changed' do
+ thing.foo = updated_markdown
+ thing.foo_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false if the markdown field is set but the html is not' do
+ thing.foo_html = nil
+
+ is_expected.to be_falsy
+ end
+ end
+
+ describe '#refresh_markdown_cache!' do
+ before do
+ thing.foo = updated_markdown
+ end
+
+ context 'do_update: false' do
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!
+
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
+
+ it 'does not save the result' do
+ expect(thing).not_to receive(:update_columns)
+
+ thing.refresh_markdown_cache!
+ end
+
+ it 'updates the markdown cache version' do
+ thing.cached_markdown_version = nil
+ thing.refresh_markdown_cache!
+
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
+ end
+ end
+
+ context 'do_update: true' do
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!(do_update: true)
+
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.foo_html_changed?).to be_truthy
+ expect(thing.baz_html_changed?).to be_truthy
+ end
+
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
+
+ thing.refresh_markdown_cache!(do_update: true)
+ end
+
+ it 'saves the changes using #update_columns' do
+ expect(thing).to receive(:persisted?).and_return(true)
+ expect(thing).to receive(:update_columns)
+ .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
+
+ thing.refresh_markdown_cache!(do_update: true)
+ end
+ end
end
describe '#banzai_render_context' do
- it "sets project to nil if the object lacks a project" do
- context = subject.banzai_render_context(:foo)
- expect(context).to have_key(:project)
+ subject(:context) { thing.banzai_render_context(:foo) }
+
+ it 'sets project to nil if the object lacks a project' do
+ is_expected.to have_key(:project)
expect(context[:project]).to be_nil
end
- it "excludes author if the object lacks an author" do
- context = subject.banzai_render_context(:foo)
- expect(context).not_to have_key(:author)
+ it 'excludes author if the object lacks an author' do
+ is_expected.not_to have_key(:author)
end
- it "raises if the context for an unrecognised field is requested" do
- expect{subject.banzai_render_context(:not_found)}.to raise_error(ArgumentError)
+ it 'raises if the context for an unrecognised field is requested' do
+ expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
end
- it "includes the pipeline" do
- context = subject.banzai_render_context(:baz)
- expect(context[:pipeline]).to eq(:single_line)
+ it 'includes the pipeline' do
+ baz = thing.banzai_render_context(:baz)
+
+ expect(baz[:pipeline]).to eq(:single_line)
end
- it "returns copies of the context template" do
- template = subject.cached_markdown_fields[:baz]
- copy = subject.banzai_render_context(:baz)
+ it 'returns copies of the context template' do
+ template = thing.cached_markdown_fields[:baz]
+ copy = thing.banzai_render_context(:baz)
+
expect(copy).not_to be(template)
end
- context "with a project" do
- subject { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project) }
+ context 'with a project' do
+ let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project_value) }
- it "sets the project in the context" do
- context = subject.banzai_render_context(:foo)
- expect(context).to have_key(:project)
- expect(context[:project]).to eq(:project)
+ it 'sets the project in the context' do
+ is_expected.to have_key(:project)
+ expect(context[:project]).to eq(:project_value)
end
- it "invalidates the cache when project changes" do
- subject.project = :new_project
+ it 'invalidates the cache when project changes' do
+ thing.project = :new_project
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
- subject.save
+ thing.save
- expect(subject.foo_html).to eq(updated_html)
- expect(subject.baz_html).to eq(updated_html)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.baz_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
end
- context "with an author" do
- subject { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author) }
+ context 'with an author' do
+ let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) }
- it "sets the author in the context" do
- context = subject.banzai_render_context(:foo)
- expect(context).to have_key(:author)
- expect(context[:author]).to eq(:author)
+ it 'sets the author in the context' do
+ is_expected.to have_key(:author)
+ expect(context[:author]).to eq(:author_value)
end
- it "invalidates the cache when author changes" do
- subject.author = :new_author
+ it 'invalidates the cache when author changes' do
+ thing.author = :new_author
allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
- subject.save
+ thing.save
- expect(subject.foo_html).to eq(updated_html)
- expect(subject.baz_html).to eq(updated_html)
+ expect(thing.foo_html).to eq(updated_html)
+ expect(thing.baz_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION)
end
end
end
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
new file mode 100644
index 00000000000..8571e85627c
--- /dev/null
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe DiscussionOnDiff, model: true do
+ subject { create(:diff_note_on_merge_request).to_discussion }
+
+ describe "#truncated_diff_lines" do
+ let(:truncated_lines) { subject.truncated_diff_lines }
+
+ context "when diff is greater than allowed number of truncated diff lines " do
+ it "returns fewer lines" do
+ expect(subject.diff_lines.count).to be > DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
+
+ expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES
+ end
+ end
+
+ context "when some diff lines are meta" do
+ it "returns no meta lines" do
+ expect(subject.diff_lines).to include(be_meta)
+ expect(truncated_lines).not_to include(be_meta)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 82abad0e2f6..67dae7cf4c0 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -231,6 +231,18 @@ describe HasStatus do
end
end
+ describe '.created_or_pending' do
+ subject { CommitStatus.created_or_pending }
+
+ %i[created pending].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[running failed success].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
+
describe '.finished' do
subject { CommitStatus.finished }
diff --git a/spec/models/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb
new file mode 100644
index 00000000000..dba9fe43327
--- /dev/null
+++ b/spec/models/concerns/ignorable_column_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe IgnorableColumn do
+ let :base_class do
+ Class.new do
+ def self.columns
+ # This method does not have access to "double"
+ [Struct.new(:name).new('id'), Struct.new(:name).new('title')]
+ end
+ end
+ end
+
+ let :model do
+ Class.new(base_class) do
+ include IgnorableColumn
+ end
+ end
+
+ describe '.columns' do
+ it 'returns the columns, excluding the ignored ones' do
+ model.ignore_column(:title)
+
+ expect(model.columns.map(&:name)).to eq(%w(id))
+ end
+ end
+
+ describe '.ignored_columns' do
+ it 'returns a Set' do
+ expect(model.ignored_columns).to be_an_instance_of(Set)
+ end
+
+ it 'returns the names of the ignored columns' do
+ model.ignore_column(:title)
+
+ expect(model.ignored_columns).to eq(Set.new(%w(title)))
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 4522206fab1..27890e33b49 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -1,13 +1,15 @@
require 'spec_helper'
-describe Issue, "Issuable" do
+describe Issuable do
+ let(:issuable_class) { Issue }
let(:issue) { create(:issue) }
let(:user) { create(:user) }
describe "Associations" do
+ subject { build(:issue) }
+
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:author) }
- it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
@@ -23,10 +25,14 @@ describe Issue, "Issuable" do
end
describe 'Included modules' do
+ let(:described_class) { issuable_class }
+
it { is_expected.to include_module(Awardable) }
end
describe "Validation" do
+ subject { build(:issue) }
+
before do
allow(subject).to receive(:set_iid).and_return(false)
end
@@ -39,9 +45,11 @@ describe Issue, "Issuable" do
end
describe "Scope" do
- it { expect(described_class).to respond_to(:opened) }
- it { expect(described_class).to respond_to(:closed) }
- it { expect(described_class).to respond_to(:assigned) }
+ subject { build(:issue) }
+
+ it { expect(issuable_class).to respond_to(:opened) }
+ it { expect(issuable_class).to respond_to(:closed) }
+ it { expect(issuable_class).to respond_to(:assigned) }
end
describe 'author_name' do
@@ -57,74 +65,20 @@ describe Issue, "Issuable" do
end
end
- describe 'assignee_name' do
- it 'is delegated to assignee' do
- issue.update!(assignee: create(:user))
-
- expect(issue.assignee_name).to eq issue.assignee.name
- end
-
- it 'returns nil when assignee is nil' do
- issue.assignee_id = nil
- issue.save(validate: false)
-
- expect(issue.assignee_name).to eq nil
- end
- end
-
- describe "before_save" do
- describe "#update_cache_counts" do
- context "when previous assignee exists" do
- before do
- assignee = create(:user)
- issue.project.team << [assignee, :developer]
- issue.update(assignee: assignee)
- end
-
- it "updates cache counts for new assignee" do
- user = create(:user)
-
- expect(user).to receive(:update_cache_counts)
-
- issue.update(assignee: user)
- end
-
- it "updates cache counts for previous assignee" do
- old_assignee = issue.assignee
- allow(User).to receive(:find_by_id).with(old_assignee.id).and_return(old_assignee)
-
- expect(old_assignee).to receive(:update_cache_counts)
-
- issue.update(assignee: nil)
- end
- end
-
- context "when previous assignee does not exist" do
- before{ issue.update(assignee: nil) }
-
- it "updates cache count for the new assignee" do
- expect_any_instance_of(User).to receive(:update_cache_counts)
-
- issue.update(assignee: user)
- end
- end
- end
- end
-
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
it 'returns notes with a matching title' do
- expect(described_class.search(searchable_issue.title)).
+ expect(issuable_class.search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
- expect(described_class.search('able')).to eq([searchable_issue])
+ expect(issuable_class.search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
- expect(described_class.search(searchable_issue.title.upcase)).
+ expect(issuable_class.search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
end
@@ -135,31 +89,31 @@ describe Issue, "Issuable" do
end
it 'returns notes with a matching title' do
- expect(described_class.full_search(searchable_issue.title)).
+ expect(issuable_class.full_search(searchable_issue.title)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching title' do
- expect(described_class.full_search('able')).to eq([searchable_issue])
+ expect(issuable_class.full_search('able')).to eq([searchable_issue])
end
it 'returns notes with a matching title regardless of the casing' do
- expect(described_class.full_search(searchable_issue.title.upcase)).
+ expect(issuable_class.full_search(searchable_issue.title.upcase)).
to eq([searchable_issue])
end
it 'returns notes with a matching description' do
- expect(described_class.full_search(searchable_issue.description)).
+ expect(issuable_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a partially matching description' do
- expect(described_class.full_search(searchable_issue.description)).
+ expect(issuable_class.full_search(searchable_issue.description)).
to eq([searchable_issue])
end
it 'returns notes with a matching description regardless of the casing' do
- expect(described_class.full_search(searchable_issue.description.upcase)).
+ expect(issuable_class.full_search(searchable_issue.description.upcase)).
to eq([searchable_issue])
end
end
@@ -298,7 +252,20 @@ describe Issue, "Issuable" do
end
context "issue is assigned" do
- before { issue.update_attribute(:assignee, user) }
+ before { issue.assignees << user }
+
+ it "returns correct hook data" do
+ expect(data[:assignees].first).to eq(user.hook_attrs)
+ end
+ end
+
+ context "merge_request is assigned" do
+ let(:merge_request) { create(:merge_request) }
+ let(:data) { merge_request.to_hook_data(user) }
+
+ before do
+ merge_request.update_attribute(:assignee, user)
+ end
it "returns correct hook data" do
expect(data[:object_attributes]['assignee_id']).to eq(user.id)
@@ -320,24 +287,6 @@ describe Issue, "Issuable" do
include_examples 'deprecated repository hook data'
end
- describe '#card_attributes' do
- it 'includes the author name' do
- allow(issue).to receive(:author).and_return(double(name: 'Robert'))
- allow(issue).to receive(:assignee).and_return(nil)
-
- expect(issue.card_attributes).
- to eq({ 'Author' => 'Robert', 'Assignee' => nil })
- end
-
- it 'includes the assignee name' do
- allow(issue).to receive(:author).and_return(double(name: 'Robert'))
- allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
-
- expect(issue.card_attributes).
- to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
- end
- end
-
describe '#labels_array' do
let(:project) { create(:empty_project) }
let(:bug) { create(:label, project: project, title: 'bug') }
@@ -466,27 +415,6 @@ describe Issue, "Issuable" do
end
end
- describe '#assignee_or_author?' do
- let(:user) { build(:user, id: 1) }
- let(:issue) { build(:issue) }
-
- it 'returns true for a user that is assigned to an issue' do
- issue.assignee = user
-
- expect(issue.assignee_or_author?(user)).to eq(true)
- end
-
- it 'returns true for a user that is the author of an issue' do
- issue.author = user
-
- expect(issue.assignee_or_author?(user)).to eq(true)
- end
-
- it 'returns false for a user that is not the assignee or author' do
- expect(issue.assignee_or_author?(user)).to eq(false)
- end
- end
-
describe '#spend_time' do
let(:user) { create(:user) }
let(:issue) { create(:issue) }
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 2092576e981..e382c7120de 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -163,3 +163,52 @@ describe Issue, "Mentionable" do
end
end
end
+
+describe Commit, 'Mentionable' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:commit) { project.commit }
+
+ describe '#matches_cross_reference_regex?' do
+ it "is false when message doesn't reference anything" do
+ allow(commit.raw).to receive(:message).and_return "WIP: Do something"
+
+ expect(commit.matches_cross_reference_regex?).to be false
+ end
+
+ it 'is true if issue #number mentioned in title' do
+ allow(commit.raw).to receive(:message).and_return "#1"
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ it 'is true if references an MR' do
+ allow(commit.raw).to receive(:message).and_return "See merge request !12"
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ it 'is true if references a commit' do
+ allow(commit.raw).to receive(:message).and_return "a1b2c3d4"
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ it 'is true if issue referenced by url' do
+ issue = create(:issue, project: project)
+
+ allow(commit.raw).to receive(:message).and_return Gitlab::UrlBuilder.build(issue)
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+
+ context 'with external issue tracker' do
+ let(:project) { create(:jira_project) }
+
+ it 'is true if external issues referenced' do
+ allow(commit.raw).to receive(:message).and_return 'JIRA-123'
+
+ expect(commit.matches_cross_reference_regex?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 68e4c0a522b..675b730c557 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -11,13 +11,13 @@ describe Milestone, 'Milestoneish' do
let(:milestone) { create(:milestone, project: project) }
let!(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
- let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) }
+ let!(:security_issue_2) { create(:issue, :confidential, project: project, assignees: [assignee], milestone: milestone) }
let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
- let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignees: [assignee], milestone: milestone) }
let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) }
- let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) }
+ let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignees: [assignee], milestone: milestone) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
new file mode 100644
index 00000000000..bdae742ff1d
--- /dev/null
+++ b/spec/models/concerns/noteable_spec.rb
@@ -0,0 +1,261 @@
+require 'spec_helper'
+
+describe Noteable, model: true do
+ let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
+ let(:project) { active_diff_note1.project }
+ subject { active_diff_note1.noteable }
+ let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: subject, in_reply_to: active_diff_note1) }
+ let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: active_position2) }
+ let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: subject, position: outdated_position) }
+ let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: subject, in_reply_to: outdated_diff_note1) }
+ let!(:discussion_note1) { create(:discussion_note_on_merge_request, project: project, noteable: subject) }
+ let!(:discussion_note2) { create(:discussion_note_on_merge_request, in_reply_to: discussion_note1) }
+ let!(:commit_diff_note1) { create(:diff_note_on_commit, project: project) }
+ let!(:commit_diff_note2) { create(:diff_note_on_commit, project: project, in_reply_to: commit_diff_note1) }
+ let!(:commit_note1) { create(:note_on_commit, project: project) }
+ let!(:commit_note2) { create(:note_on_commit, project: project) }
+ let!(:commit_discussion_note1) { create(:discussion_note_on_commit, project: project) }
+ let!(:commit_discussion_note2) { create(:discussion_note_on_commit, in_reply_to: commit_discussion_note1) }
+ let!(:commit_discussion_note3) { create(:discussion_note_on_commit, project: project) }
+ let!(:note1) { create(:note, project: project, noteable: subject) }
+ let!(:note2) { create(:note, project: project, noteable: subject) }
+
+ let(:active_position2) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: 16,
+ new_line: 22,
+ diff_refs: subject.diff_refs
+ )
+ end
+
+ let(:outdated_position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs
+ )
+ end
+
+ describe '#discussions' do
+ let(:discussions) { subject.discussions }
+
+ it 'includes discussions for diff notes, commit diff notes, commit notes, and regular notes' do
+ expect(discussions).to eq([
+ DiffDiscussion.new([active_diff_note1, active_diff_note2], subject),
+ DiffDiscussion.new([active_diff_note3], subject),
+ DiffDiscussion.new([outdated_diff_note1, outdated_diff_note2], subject),
+ Discussion.new([discussion_note1, discussion_note2], subject),
+ DiffDiscussion.new([commit_diff_note1, commit_diff_note2], subject),
+ OutOfContextDiscussion.new([commit_note1, commit_note2], subject),
+ Discussion.new([commit_discussion_note1, commit_discussion_note2], subject),
+ Discussion.new([commit_discussion_note3], subject),
+ IndividualNoteDiscussion.new([note1], subject),
+ IndividualNoteDiscussion.new([note2], subject)
+ ])
+ end
+ end
+
+ describe '#grouped_diff_discussions' do
+ let(:grouped_diff_discussions) { subject.grouped_diff_discussions }
+
+ it "includes active discussions" do
+ discussions = grouped_diff_discussions.values.flatten
+
+ expect(discussions.count).to eq(2)
+ expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
+ expect(discussions.all?(&:active?)).to be true
+
+ expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
+ expect(discussions.last.notes).to eq([active_diff_note3])
+ end
+
+ it "doesn't include outdated discussions" do
+ expect(grouped_diff_discussions.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ end
+
+ it "groups the discussions by line code" do
+ expect(grouped_diff_discussions[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
+ expect(grouped_diff_discussions[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
+ end
+
+ context "discussion status" do
+ let(:first_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion }
+ let(:second_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion }
+ let(:third_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion }
+
+ before do
+ allow(subject).to receive(:resolvable_discussions).and_return([first_discussion, second_discussion, third_discussion])
+ end
+
+ describe "#discussions_resolvable?" do
+ context "when all discussions are unresolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(false)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolvable?).to be false
+ end
+ end
+
+ context "when some discussions are unresolvable and some discussions are resolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+
+ context "when all discussions are resolvable" do
+ before do
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(true)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolvable?).to be true
+ end
+ end
+ end
+
+ describe "#discussions_resolved?" do
+ context "when discussions are not resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolved?).to be false
+ end
+ end
+
+ context "when discussions are resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(true)
+
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable discussions are resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_resolved?).to be true
+ end
+ end
+
+ context "when some resolvable discussions are not resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#discussions_to_be_resolved?" do
+ context "when discussions are not resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_to_be_resolved?).to be false
+ end
+ end
+
+ context "when discussions are resolvable" do
+ before do
+ allow(subject).to receive(:discussions_resolvable?).and_return(true)
+
+ allow(first_discussion).to receive(:resolvable?).and_return(true)
+ allow(second_discussion).to receive(:resolvable?).and_return(false)
+ allow(third_discussion).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable discussions are resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.discussions_to_be_resolved?).to be false
+ end
+ end
+
+ context "when some resolvable discussions are not resolved" do
+ before do
+ allow(first_discussion).to receive(:resolved?).and_return(true)
+ allow(third_discussion).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.discussions_to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#discussions_to_be_resolved" do
+ before do
+ allow(first_discussion).to receive(:to_be_resolved?).and_return(true)
+ allow(second_discussion).to receive(:to_be_resolved?).and_return(false)
+ allow(third_discussion).to receive(:to_be_resolved?).and_return(false)
+ end
+
+ it 'includes only discussions that need to be resolved' do
+ expect(subject.discussions_to_be_resolved).to eq([first_discussion])
+ end
+ end
+
+ describe '#discussions_can_be_resolved_by?' do
+ let(:user) { build(:user) }
+
+ context 'all discussions can be resolved by the user' do
+ before do
+ allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(third_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ end
+
+ it 'allows a user to resolve the discussions' do
+ expect(subject.discussions_can_be_resolved_by?(user)).to be(true)
+ end
+ end
+
+ context 'one discussion cannot be resolved by the user' do
+ before do
+ allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
+ allow(third_discussion).to receive(:can_resolve?).with(user).and_return(false)
+ end
+
+ it 'allows a user to resolve the discussions' do
+ expect(subject.discussions_can_be_resolved_by?(user)).to be(false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
index 255b584a85e..494e6f1b6f6 100644
--- a/spec/models/concerns/relative_positioning_spec.rb
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Issue, 'RelativePositioning' do
+describe RelativePositioning do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:issue1) { create(:issue, project: project) }
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
new file mode 100644
index 00000000000..18327fe262d
--- /dev/null
+++ b/spec/models/concerns/resolvable_discussion_spec.rb
@@ -0,0 +1,548 @@
+require 'spec_helper'
+
+describe Discussion, ResolvableDiscussion, models: true do
+ subject { described_class.new([first_note, second_note, third_note]) }
+
+ let(:first_note) { create(:discussion_note_on_merge_request) }
+ let(:merge_request) { first_note.noteable }
+ let(:project) { first_note.project }
+ let(:second_note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, in_reply_to: first_note) }
+ let(:third_note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+
+ describe "#resolvable?" do
+ context "when potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(true)
+ end
+
+ context "when all notes are unresolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(false)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when some notes are unresolvable and some notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+
+ context "when all notes are resolvable" do
+ before do
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(true)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+
+ context "when not potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+ end
+
+ describe "#resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns true" do
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are not resolved" do
+ before do
+ allow(first_note).to receive(:resolved?).and_return(true)
+ allow(third_note).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#can_resolve?" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when not signed in" do
+ let(:current_user) { nil }
+
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+
+ context "when signed in" do
+ context "when the signed in user is the noteable author" do
+ before do
+ subject.noteable.author = current_user
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user can push to the project" do
+ before do
+ subject.project.team << [current_user, :master]
+ end
+
+ it "returns true" do
+ expect(subject.can_resolve?(current_user)).to be true
+ end
+ end
+
+ context "when the signed in user is a random user" do
+ it "returns false" do
+ expect(subject.can_resolve?(current_user)).to be false
+ end
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+ let(:second_note) { create(:diff_note_on_commit) } # unresolvable
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+
+ first_note.reload
+ third_note.reload
+ end
+
+ it "doesn't change resolved_at on the resolved notes" do
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved notes" do
+ expect(first_note.resolved_by).to eq(user)
+ expect(third_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved notes" do
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ expect { subject.resolve!(current_user) }.not_to change { third_note.resolved? }
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved state" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "doesn't change resolved_at on the resolved note" do
+ expect(first_note.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload.resolved_at }
+ end
+
+ it "doesn't change resolved_by on the resolved note" do
+ expect(first_note.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved_by }
+ end
+
+ it "doesn't change the resolved state on the resolved note" do
+ expect(first_note.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved? }
+ end
+
+ it "sets resolved_at on the unresolved note" do
+ subject.resolve!(current_user)
+ third_note.reload
+
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved note" do
+ subject.resolve!(current_user)
+ third_note.reload
+
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved note as resolved" do
+ subject.resolve!(current_user)
+ third_note.reload
+
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+
+ context "when no resolvable notes are resolved" do
+ it "sets resolved_at on the unresolved notes" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_at).not_to be_nil
+ expect(third_note.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by on the unresolved notes" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_by).to eq(current_user)
+ expect(third_note.resolved_by).to eq(current_user)
+ end
+
+ it "marks the unresolved notes as resolved" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved?).to be true
+ expect(third_note.resolved?).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ let(:user) { create(:user) }
+
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+
+ allow(first_note).to receive(:resolvable?).and_return(true)
+ allow(second_note).to receive(:resolvable?).and_return(false)
+ allow(third_note).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when all resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ third_note.resolve!(user)
+ end
+
+ it "unsets resolved_at on the resolved notes" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_at).to be_nil
+ expect(third_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved notes" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved_by).to be_nil
+ expect(third_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved notes as resolved" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(first_note.resolved?).to be false
+ expect(third_note.resolved?).to be false
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+ first_note.reload
+ third_note.reload
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when some resolvable notes are resolved" do
+ before do
+ first_note.resolve!(user)
+ end
+
+ it "unsets resolved_at on the resolved note" do
+ subject.unresolve!
+
+ expect(subject.first_note.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by on the resolved note" do
+ subject.unresolve!
+
+ expect(subject.first_note.resolved_by).to be_nil
+ end
+
+ it "unmarks the resolved note as resolved" do
+ subject.unresolve!
+
+ expect(subject.first_note.resolved?).to be false
+ end
+ end
+ end
+ end
+
+ describe "#first_note_to_resolve" do
+ it "returns the first note that still needs to be resolved" do
+ allow(first_note).to receive(:to_be_resolved?).and_return(false)
+ allow(second_note).to receive(:to_be_resolved?).and_return(true)
+
+ expect(subject.first_note_to_resolve).to eq(second_note)
+ end
+ end
+
+ describe "#last_resolved_note" do
+ let(:current_user) { create(:user) }
+
+ before do
+ first_note.resolve!(current_user)
+ third_note.resolve!(current_user)
+ second_note.resolve!(current_user)
+ end
+
+ it "returns the last note that was resolved" do
+ expect(subject.last_resolved_note).to eq(second_note)
+ end
+ end
+end
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
new file mode 100644
index 00000000000..1503ccdff11
--- /dev/null
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -0,0 +1,329 @@
+require 'spec_helper'
+
+describe Note, ResolvableNote, models: true do
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ subject { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+
+ context 'resolvability scopes' do
+ let!(:note1) { create(:note, project: project) }
+ let!(:note2) { create(:diff_note_on_commit, project: project) }
+ let!(:note3) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+ let!(:note4) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+ let!(:note5) { create(:discussion_note_on_issue, project: project) }
+ let!(:note6) { create(:discussion_note_on_merge_request, :system, noteable: merge_request, project: project) }
+
+ describe '.potentially_resolvable' do
+ it 'includes diff and discussion notes on merge requests' do
+ expect(Note.potentially_resolvable).to match_array([note3, note4, note6])
+ end
+ end
+
+ describe '.resolvable' do
+ it 'includes non-system diff and discussion notes on merge requests' do
+ expect(Note.resolvable).to match_array([note3, note4])
+ end
+ end
+
+ describe '.resolved' do
+ it 'includes resolved non-system diff and discussion notes on merge requests' do
+ expect(Note.resolved).to match_array([note3])
+ end
+ end
+
+ describe '.unresolved' do
+ it 'includes non-resolved non-system diff and discussion notes on merge requests' do
+ expect(Note.unresolved).to match_array([note4])
+ end
+ end
+ end
+
+ describe ".resolve!" do
+ let(:current_user) { create(:user) }
+ let!(:commit_note) { create(:diff_note_on_commit, project: project) }
+ let!(:resolved_note) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+ let!(:unresolved_note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project) }
+
+ before do
+ described_class.resolve!(current_user)
+
+ commit_note.reload
+ resolved_note.reload
+ unresolved_note.reload
+ end
+
+ it 'resolves only the resolvable, not yet resolved notes' do
+ expect(commit_note.resolved_at).to be_nil
+ expect(resolved_note.resolved_by).not_to eq(current_user)
+ expect(unresolved_note.resolved_at).not_to be_nil
+ expect(unresolved_note.resolved_by).to eq(current_user)
+ end
+ end
+
+ describe ".unresolve!" do
+ let!(:resolved_note) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+
+ before do
+ described_class.unresolve!
+
+ resolved_note.reload
+ end
+
+ it 'unresolves the resolved notes' do
+ expect(resolved_note.resolved_by).to be_nil
+ expect(resolved_note.resolved_at).to be_nil
+ end
+ end
+
+ describe '#resolvable?' do
+ context "when potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(true)
+ end
+
+ context "when a system note" do
+ before do
+ subject.system = true
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+
+ context "when a regular note" do
+ it "returns true" do
+ expect(subject.resolvable?).to be true
+ end
+ end
+ end
+
+ context "when not potentially resolvable" do
+ before do
+ allow(subject).to receive(:potentially_resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.resolvable?).to be false
+ end
+ end
+ end
+
+ describe "#to_be_resolved?" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(true)
+ end
+
+ it "returns false" do
+ expect(subject.to_be_resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ before do
+ allow(subject).to receive(:resolved?).and_return(false)
+ end
+
+ it "returns true" do
+ expect(subject.to_be_resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#resolved?" do
+ let(:current_user) { create(:user) }
+
+ context 'when not resolvable' do
+ before do
+ subject.resolve!(current_user)
+
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(subject.resolved?).to be_falsey
+ end
+ end
+
+ context 'when resolvable' do
+ context 'when the note has been resolved' do
+ before do
+ subject.resolve!(current_user)
+ end
+
+ it 'returns true' do
+ expect(subject.resolved?).to be_truthy
+ end
+ end
+
+ context 'when the note has not been resolved' do
+ it 'returns false' do
+ expect(subject.resolved?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe "#resolve!" do
+ let(:current_user) { create(:user) }
+
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't set resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "doesn't set resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "doesn't mark as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when already resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns nil" do
+ expect(subject.resolve!(current_user)).to be_nil
+ end
+
+ it "doesn't change resolved_at" do
+ expect(subject.resolved_at).not_to be_nil
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
+ end
+
+ it "doesn't change resolved_by" do
+ expect(subject.resolved_by).to eq(user)
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
+ end
+
+ it "doesn't change resolved status" do
+ expect(subject.resolved?).to be true
+
+ expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
+ end
+ end
+
+ context "when not yet resolved" do
+ it "returns true" do
+ expect(subject.resolve!(current_user)).to be true
+ end
+
+ it "sets resolved_at" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_at).not_to be_nil
+ end
+
+ it "sets resolved_by" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved_by).to eq(current_user)
+ end
+
+ it "marks as resolved" do
+ subject.resolve!(current_user)
+
+ expect(subject.resolved?).to be true
+ end
+ end
+ end
+ end
+
+ describe "#unresolve!" do
+ context "when not resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+
+ context "when resolvable" do
+ before do
+ allow(subject).to receive(:resolvable?).and_return(true)
+ end
+
+ context "when resolved" do
+ let(:user) { create(:user) }
+
+ before do
+ subject.resolve!(user)
+ end
+
+ it "returns true" do
+ expect(subject.unresolve!).to be true
+ end
+
+ it "unsets resolved_at" do
+ subject.unresolve!
+
+ expect(subject.resolved_at).to be_nil
+ end
+
+ it "unsets resolved_by" do
+ subject.unresolve!
+
+ expect(subject.resolved_by).to be_nil
+ end
+
+ it "unmarks as resolved" do
+ subject.unresolve!
+
+ expect(subject.resolved?).to be false
+ end
+ end
+
+ context "when not resolved" do
+ it "returns nil" do
+ expect(subject.unresolve!).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 677e60e1282..49a4132f763 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Group, 'Routable' do
- let!(:group) { create(:group) }
+ let!(:group) { create(:group, name: 'foo') }
describe 'Validations' do
it { is_expected.to validate_presence_of(:route) }
@@ -9,6 +9,7 @@ describe Group, 'Routable' do
describe 'Associations' do
it { is_expected.to have_one(:route).dependent(:destroy) }
+ it { is_expected.to have_many(:redirect_routes).dependent(:destroy) }
end
describe 'Callbacks' do
@@ -35,10 +36,53 @@ describe Group, 'Routable' do
describe '.find_by_full_path' do
let!(:nested_group) { create(:group, parent: group) }
- it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
- it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
- it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
- it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ context 'without any redirect routes' do
+ it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
+ it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ end
+
+ context 'with redirect routes' do
+ let!(:group_redirect_route) { group.redirect_routes.create!(path: 'bar') }
+ let!(:nested_group_redirect_route) { nested_group.redirect_routes.create!(path: nested_group.path.sub('foo', 'bar')) }
+
+ context 'without follow_redirects option' do
+ context 'with the given path not matching any route' do
+ it { expect(described_class.find_by_full_path('unknown')).to eq(nil) }
+ end
+
+ context 'with the given path matching the canonical route' do
+ it { expect(described_class.find_by_full_path(group.to_param)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param)).to eq(nested_group) }
+ end
+
+ context 'with the given path matching a redirect route' do
+ it { expect(described_class.find_by_full_path(group_redirect_route.path)).to eq(nil) }
+ it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase)).to eq(nil) }
+ it { expect(described_class.find_by_full_path(nested_group_redirect_route.path)).to eq(nil) }
+ end
+ end
+
+ context 'with follow_redirects option set to true' do
+ context 'with the given path not matching any route' do
+ it { expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) }
+ end
+
+ context 'with the given path matching the canonical route' do
+ it { expect(described_class.find_by_full_path(group.to_param, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group.to_param.upcase, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group.to_param, follow_redirects: true)).to eq(nested_group) }
+ end
+
+ context 'with the given path matching a redirect route' do
+ it { expect(described_class.find_by_full_path(group_redirect_route.path, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(group_redirect_route.path.upcase, follow_redirects: true)).to eq(group) }
+ it { expect(described_class.find_by_full_path(nested_group_redirect_route.path, follow_redirects: true)).to eq(nested_group) }
+ end
+ end
+ end
end
describe '.where_full_path_in' do
@@ -81,12 +125,137 @@ describe Group, 'Routable' do
it { is_expected.to eq([nested_group]) }
end
+ describe '.member_self_and_descendants' do
+ let!(:user) { create(:user) }
+ let!(:nested_group) { create(:group, parent: group) }
+
+ before { group.add_owner(user) }
+ subject { described_class.member_self_and_descendants(user.id) }
+
+ it { is_expected.to match_array [group, nested_group] }
+ end
+
+ describe '.member_hierarchy' do
+ # foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz
+ let!(:user) { create(:user) }
+
+ # group
+ # _______ (foo) _______
+ # | |
+ # | |
+ # nested_group_1 nested_group_2
+ # (bar) (barbaz)
+ # | |
+ # | |
+ # nested_group_1_1 nested_group_2_1
+ # (baz) (baz)
+ #
+ let!(:nested_group_1) { create :group, parent: group, name: 'bar' }
+ let!(:nested_group_1_1) { create :group, parent: nested_group_1, name: 'baz' }
+ let!(:nested_group_2) { create :group, parent: group, name: 'barbaz' }
+ let!(:nested_group_2_1) { create :group, parent: nested_group_2, name: 'baz' }
+
+ context 'user is not a member of any group' do
+ subject { described_class.member_hierarchy(user.id) }
+
+ it 'returns an empty array' do
+ is_expected.to eq []
+ end
+ end
+
+ context 'user is member of all groups' do
+ before do
+ group.add_owner(user)
+ nested_group_1.add_owner(user)
+ nested_group_1_1.add_owner(user)
+ nested_group_2.add_owner(user)
+ nested_group_2_1.add_owner(user)
+ end
+ subject { described_class.member_hierarchy(user.id) }
+
+ it 'returns all groups' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1,
+ nested_group_2, nested_group_2_1
+ ]
+ end
+ end
+
+ context 'user is member of the top group' do
+ before { group.add_owner(user) }
+ subject { described_class.member_hierarchy(user.id) }
+
+ it 'returns all groups' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1,
+ nested_group_2, nested_group_2_1
+ ]
+ end
+ end
+
+ context 'user is member of the first child (internal node), branch 1' do
+ before { nested_group_1.add_owner(user) }
+ subject { described_class.member_hierarchy(user.id) }
+
+ it 'returns the groups in the hierarchy' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1
+ ]
+ end
+ end
+
+ context 'user is member of the first child (internal node), branch 2' do
+ before { nested_group_2.add_owner(user) }
+ subject { described_class.member_hierarchy(user.id) }
+
+ it 'returns the groups in the hierarchy' do
+ is_expected.to match_array [
+ group,
+ nested_group_2, nested_group_2_1
+ ]
+ end
+ end
+
+ context 'user is member of the last child (leaf node)' do
+ before { nested_group_1_1.add_owner(user) }
+ subject { described_class.member_hierarchy(user.id) }
+
+ it 'returns the groups in the hierarchy' do
+ is_expected.to match_array [
+ group,
+ nested_group_1, nested_group_1_1
+ ]
+ end
+ end
+ end
+
describe '#full_path' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
it { expect(group.full_path).to eq(group.path) }
it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") }
+
+ context 'with RequestStore active' do
+ before do
+ RequestStore.begin!
+ end
+
+ after do
+ RequestStore.end!
+ RequestStore.clear!
+ end
+
+ it 'does not load the route table more than once' do
+ expect(group).to receive(:uncached_full_path).once.and_call_original
+
+ 3.times { group.full_path }
+ expect(group.full_path).to eq(group.path)
+ end
+ end
end
describe '#full_name' do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index fd3b8307571..e698207166c 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -1,9 +1,11 @@
require 'spec_helper'
-describe Issue, 'Spammable' do
+describe Spammable do
let(:issue) { create(:issue, description: 'Test Desc.') }
describe 'Associations' do
+ subject { build(:issue) }
+
it { is_expected.to have_one(:user_agent_detail).dependent(:destroy) }
end
diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb
index c3af7a0960f..8c945686b66 100644
--- a/spec/models/concerns/strip_attribute_spec.rb
+++ b/spec/models/concerns/strip_attribute_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Milestone, "StripAttribute" do
+describe StripAttribute do
let(:milestone) { create(:milestone) }
describe ".strip_attributes" do
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
new file mode 100644
index 00000000000..eff41d85972
--- /dev/null
+++ b/spec/models/container_repository_spec.rb
@@ -0,0 +1,234 @@
+require 'spec_helper'
+
+describe ContainerRepository do
+ let(:group) { create(:group, name: 'group') }
+ let(:project) { create(:project, path: 'test', group: group) }
+
+ let(:repository) do
+ create(:container_repository, name: 'my_image', project: project)
+ end
+
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab',
+ host_port: 'registry.gitlab')
+
+ stub_request(:get, 'http://registry.gitlab/v2/group/test/my_image/tags/list')
+ .with(headers: { 'Accept' => 'application/vnd.docker.distribution.manifest.v2+json' })
+ .to_return(
+ status: 200,
+ body: JSON.dump(tags: ['test_tag']),
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ describe 'associations' do
+ it 'belongs to the project' do
+ expect(repository).to belong_to(:project)
+ end
+ end
+
+ describe '#tag' do
+ it 'has a test tag' do
+ expect(repository.tag('test')).not_to be_nil
+ end
+ end
+
+ describe '#path' do
+ context 'when project path does not contain uppercase letters' do
+ it 'returns a full path to the repository' do
+ expect(repository.path).to eq('group/test/my_image')
+ end
+ end
+
+ context 'when path contains uppercase letters' do
+ let(:project) { create(:project, path: 'MY_PROJECT', group: group) }
+
+ it 'returns a full path without capital letters' do
+ expect(repository.path).to eq('group/my_project/my_image')
+ end
+ end
+ end
+
+ describe '#manifest' do
+ it 'returns non-empty manifest' do
+ expect(repository.manifest).not_to be_nil
+ end
+ end
+
+ describe '#valid?' do
+ it 'is a valid repository' do
+ expect(repository).to be_valid
+ end
+ end
+
+ describe '#tags' do
+ it 'returns non-empty tags list' do
+ expect(repository.tags).not_to be_empty
+ end
+ end
+
+ describe '#has_tags?' do
+ it 'has tags' do
+ expect(repository).to have_tags
+ end
+ end
+
+ describe '#delete_tags!' do
+ let(:repository) do
+ create(:container_repository, name: 'my_image',
+ tags: %w[latest rc1],
+ project: project)
+ end
+
+ context 'when action succeeds' do
+ it 'returns status that indicates success' do
+ expect(repository.client)
+ .to receive(:delete_repository_tag)
+ .and_return(true)
+
+ expect(repository.delete_tags!).to be_truthy
+ end
+ end
+
+ context 'when action fails' do
+ it 'returns status that indicates failure' do
+ expect(repository.client)
+ .to receive(:delete_repository_tag)
+ .and_return(false)
+
+ expect(repository.delete_tags!).to be_falsey
+ end
+ end
+ end
+
+ describe '#location' do
+ context 'when registry is running on a custom port' do
+ before do
+ stub_container_registry_config(enabled: true,
+ api_url: 'http://registry.gitlab:5000',
+ host_port: 'registry.gitlab:5000')
+ end
+
+ it 'returns a full location of the repository' do
+ expect(repository.location)
+ .to eq 'registry.gitlab:5000/group/test/my_image'
+ end
+ end
+ end
+
+ describe '#root_repository?' do
+ context 'when repository is a root repository' do
+ let(:repository) { create(:container_repository, :root) }
+
+ it 'returns true' do
+ expect(repository).to be_root_repository
+ end
+ end
+
+ context 'when repository is not a root repository' do
+ it 'returns false' do
+ expect(repository).not_to be_root_repository
+ end
+ end
+ end
+
+ describe '.build_from_path' do
+ let(:registry_path) do
+ ContainerRegistry::Path.new(project.full_path + '/some/image')
+ end
+
+ let(:repository) do
+ described_class.build_from_path(registry_path)
+ end
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with a correct name' do
+ expect(repository.name).to eq 'some/image'
+ end
+
+ it 'is not persisted' do
+ expect(repository).not_to be_persisted
+ end
+ end
+
+ describe '.create_from_path!' do
+ let(:repository) do
+ described_class.create_from_path!(ContainerRegistry::Path.new(path))
+ end
+
+ let(:repository_path) { ContainerRegistry::Path.new(path) }
+
+ context 'when received multi-level repository path' do
+ let(:path) { project.full_path + '/some/image' }
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with a correct name' do
+ expect(repository.name).to eq 'some/image'
+ end
+ end
+
+ context 'when path is too long' do
+ let(:path) do
+ project.full_path + '/a/b/c/d/e/f/g/h/i/j/k/l/n/o/p/s/t/u/x/y/z'
+ end
+
+ it 'does not create repository and raises error' do
+ expect { repository }.to raise_error(
+ ContainerRegistry::Path::InvalidRegistryPathError)
+ end
+ end
+
+ context 'when received multi-level repository with nested groups' do
+ let(:group) { create(:group, :nested, name: 'nested') }
+ let(:path) { project.full_path + '/some/image' }
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with a correct name' do
+ expect(repository.name).to eq 'some/image'
+ end
+
+ it 'has path including a nested group' do
+ expect(repository.path).to include 'nested/test/some/image'
+ end
+ end
+
+ context 'when received root repository path' do
+ let(:path) { project.full_path }
+
+ it 'fabricates repository assigned to a correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'fabricates repository with an empty name' do
+ expect(repository.name).to be_empty
+ end
+ end
+ end
+
+ describe '.build_root_repository' do
+ let(:repository) do
+ described_class.build_root_repository(project)
+ end
+
+ it 'fabricates a root repository object' do
+ expect(repository).to be_root_repository
+ end
+
+ it 'assignes it to the correct project' do
+ expect(repository.project).to eq project
+ end
+
+ it 'does not persist it' do
+ expect(repository).not_to be_persisted
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 55483fc876a..4f33f3c6d69 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -13,7 +13,7 @@ describe 'CycleAnalytics#plan', feature: true do
data_fn: -> (context) do
{
issue: context.create(:issue, project: context.project),
- branch_name: context.random_git_name
+ branch_name: context.generate(:branch)
}
end,
start_time_conditions: [["issue associated with a milestone",
@@ -35,7 +35,7 @@ describe 'CycleAnalytics#plan', feature: true do
context "when a regular label (instead of a list label) is added to the issue" do
it "returns nil" do
- branch_name = random_git_name
+ branch_name = generate(:branch)
label = create(:label)
issue = create(:issue, project: project)
issue.update(label_ids: [label.id])
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index e6a826a9418..4744b9e05ea 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -23,7 +23,7 @@ describe 'CycleAnalytics#production', feature: true do
# Make other changes on master
sha = context.project.repository.create_file(
context.user,
- context.random_git_name,
+ context.generate(:branch),
'content',
message: 'commit message',
branch_name: 'master')
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 3a02ed81adb..f78d7a23105 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -28,7 +28,7 @@ describe 'CycleAnalytics#staging', feature: true do
# Make other changes on master
sha = context.project.repository.create_file(
context.user,
- context.random_git_name,
+ context.generate(:branch),
'content',
message: 'commit message',
branch_name: 'master')
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index c2ba012a0e6..d0b919efcf9 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -14,6 +14,7 @@ describe 'CycleAnalytics#test', feature: true do
issue = context.create(:issue, project: context.project)
merge_request = context.create_merge_request_closing_issue(issue)
pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project)
+ merge_request.update(head_pipeline: pipeline)
{ pipeline: pipeline, issue: issue }
end,
start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 080ff2f3f43..4bda7d4314a 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -49,6 +49,34 @@ describe Deployment, models: true do
end
end
+ describe '#metrics' do
+ let(:deployment) { create(:deployment) }
+
+ subject { deployment.metrics }
+
+ context 'metrics are disabled' do
+ it { is_expected.to eq({}) }
+ end
+
+ context 'metrics are enabled' do
+ let(:simple_metrics) do
+ {
+ success: true,
+ metrics: {},
+ last_update: 42,
+ deployment_time: 1494408956
+ }
+ end
+
+ before do
+ allow(deployment.project).to receive_message_chain(:monitoring_service, :deployment_metrics)
+ .with(any_args).and_return(simple_metrics)
+ end
+
+ it { is_expected.to eq(simple_metrics) }
+ end
+ end
+
describe '#stop_action' do
let(:build) { create(:ci_build) }
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
new file mode 100644
index 00000000000..81f338745b1
--- /dev/null
+++ b/spec/models/diff_discussion_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe DiffDiscussion, model: true do
+ include RepoHelpers
+
+ subject { described_class.new([diff_note]) }
+
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
+
+ describe '#reply_attributes' do
+ it 'includes position and original_position' do
+ attributes = subject.reply_attributes
+ expect(attributes[:position]).to eq(diff_note.position.to_json)
+ expect(attributes[:original_position]).to eq(diff_note.original_position.to_json)
+ end
+ end
+
+ describe '#merge_request_version_params' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, importing: true) }
+ let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'when the discussion is active' do
+ it 'returns an empty hash, which will end up showing the latest version' do
+ expect(subject.merge_request_version_params).to eq({})
+ end
+ end
+
+ context 'when the discussion is on an older merge request version' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: nil,
+ new_line: 4,
+ diff_refs: merge_request_diff1.diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns the diff ID for the version to show' do
+ expect(diff_id: merge_request_diff1.id)
+ end
+ end
+
+ context 'when the discussion is on a comparison between merge request versions' do
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: ".gitmodules",
+ new_path: ".gitmodules",
+ old_line: 4,
+ new_line: 4,
+ diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs
+ )
+ end
+
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) }
+
+ it 'returns the diff ID and start sha of the versions to compare' do
+ expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
+ end
+ end
+
+ context 'when the discussion does not have a merge request version' do
+ let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, diff_refs: project.commit(sample_commit.id).diff_refs) }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ it 'returns nil' do
+ expect(subject.merge_request_version_params).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 9ea3a4b7020..ab4c51a87b0 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -31,43 +31,6 @@ describe DiffNote, models: true do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
- describe ".resolve!" do
- let(:current_user) { create(:user) }
- let!(:commit_note) { create(:diff_note_on_commit) }
- let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
- let!(:unresolved_note) { create(:diff_note_on_merge_request) }
-
- before do
- described_class.resolve!(current_user)
-
- commit_note.reload
- resolved_note.reload
- unresolved_note.reload
- end
-
- it 'resolves only the resolvable, not yet resolved notes' do
- expect(commit_note.resolved_at).to be_nil
- expect(resolved_note.resolved_by).not_to eq(current_user)
- expect(unresolved_note.resolved_at).not_to be_nil
- expect(unresolved_note.resolved_by).to eq(current_user)
- end
- end
-
- describe ".unresolve!" do
- let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
-
- before do
- described_class.unresolve!
-
- resolved_note.reload
- end
-
- it 'unresolves the resolved notes' do
- expect(resolved_note.resolved_by).to be_nil
- expect(resolved_note.resolved_at).to be_nil
- end
- end
-
describe "#position=" do
context "when provided a string" do
it "sets the position" do
@@ -94,6 +57,32 @@ describe DiffNote, models: true do
end
end
+ describe "#original_position=" do
+ context "when provided a string" do
+ it "sets the original position" do
+ subject.original_position = new_position.to_json
+
+ expect(subject.original_position).to eq(new_position)
+ end
+ end
+
+ context "when provided a hash" do
+ it "sets the original position" do
+ subject.original_position = new_position.to_h
+
+ expect(subject.original_position).to eq(new_position)
+ end
+ end
+
+ context "when provided a position object" do
+ it "sets the original position" do
+ subject.original_position = new_position
+
+ expect(subject.original_position).to eq(new_position)
+ end
+ end
+ end
+
describe "#diff_file" do
it "returns the correct diff file" do
diff_file = subject.diff_file
@@ -226,252 +215,6 @@ describe DiffNote, models: true do
end
end
- describe "#resolvable?" do
- context "when noteable is a commit" do
- subject { create(:diff_note_on_commit, project: project, position: position) }
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
-
- context "when noteable is a merge request" do
- context "when a system note" do
- before do
- subject.system = true
- end
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
-
- context "when a regular note" do
- it "returns true" do
- expect(subject.resolvable?).to be true
- end
- end
- end
- end
-
- describe "#to_be_resolved?" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when not resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.to_be_resolved?).to be true
- end
- end
- end
- end
-
- describe "#resolve!" do
- let(:current_user) { create(:user) }
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't set resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "doesn't set resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "doesn't mark as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when already resolved" do
- let(:user) { create(:user) }
-
- before do
- subject.resolve!(user)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't change resolved_at" do
- expect(subject.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
- end
-
- it "doesn't change resolved_by" do
- expect(subject.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
- end
-
- it "doesn't change resolved status" do
- expect(subject.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
- end
- end
-
- context "when not yet resolved" do
- it "returns true" do
- expect(subject.resolve!(current_user)).to be true
- end
-
- it "sets resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to eq(current_user)
- end
-
- it "marks as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be true
- end
- end
- end
- end
-
- describe "#unresolve!" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- let(:user) { create(:user) }
-
- before do
- subject.resolve!(user)
- end
-
- it "returns true" do
- expect(subject.unresolve!).to be true
- end
-
- it "unsets resolved_at" do
- subject.unresolve!
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "unsets resolved_by" do
- subject.unresolve!
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "unmarks as resolved" do
- subject.unresolve!
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when not resolved" do
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
- end
- end
-
- describe "#discussion" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.discussion).to be_nil
- end
- end
-
- context "when resolvable" do
- let!(:diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: subject.position) }
- let!(:diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
-
- let(:active_position2) do
- Gitlab::Diff::Position.new(
- old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: 16,
- new_line: 22,
- diff_refs: merge_request.diff_refs
- )
- end
-
- it "returns the discussion this note is in" do
- discussion = subject.discussion
-
- expect(discussion.id).to eq(subject.discussion_id)
- expect(discussion.notes).to eq([subject, diff_note2])
- end
- end
- end
-
describe "#discussion_id" do
let(:note) { create(:diff_note_on_merge_request) }
@@ -497,27 +240,37 @@ describe DiffNote, models: true do
end
end
- describe "#original_discussion_id" do
- let(:note) { create(:diff_note_on_merge_request) }
+ describe '#created_at_diff?' do
+ let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: diff_refs
+ )
+ end
- context "when it is newly created" do
- it "has a discussion id" do
- expect(note.original_discussion_id).not_to be_nil
- expect(note.original_discussion_id).to match(/\A\h{40}\z/)
+ context "when noteable is a commit" do
+ subject { build(:diff_note_on_commit, project: project, position: position) }
+
+ it "returns true" do
+ expect(subject.created_at_diff?(diff_refs)).to be true
end
end
- context "when it didn't store a discussion id before" do
- before do
- note.update_column(:original_discussion_id, nil)
+ context "when noteable is a merge request" do
+ context "when the diff refs match the original one of the diff note" do
+ it "returns true" do
+ expect(subject.created_at_diff?(diff_refs)).to be true
+ end
end
- it "has a discussion id" do
- # The original_discussion_id is set in `after_initialize`, so `reload` won't work
- reloaded_note = Note.find(note.id)
-
- expect(reloaded_note.original_discussion_id).not_to be_nil
- expect(reloaded_note.original_discussion_id).to match(/\A\h{40}\z/)
+ context "when the diff refs don't match the original one of the diff note" do
+ it "returns false" do
+ expect(subject.created_at_diff?(merge_request.diff_refs)).to be false
+ end
end
end
end
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index bc32fadd391..0221e23ced8 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -4,618 +4,27 @@ describe Discussion, model: true do
subject { described_class.new([first_note, second_note, third_note]) }
let(:first_note) { create(:diff_note_on_merge_request) }
- let(:second_note) { create(:diff_note_on_merge_request) }
+ let(:merge_request) { first_note.noteable }
+ let(:second_note) { create(:diff_note_on_merge_request, in_reply_to: first_note) }
let(:third_note) { create(:diff_note_on_merge_request) }
- describe "#resolvable?" do
- context "when a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(true)
- end
-
- context "when all notes are unresolvable" do
- before do
- allow(first_note).to receive(:resolvable?).and_return(false)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
-
- context "when some notes are unresolvable and some notes are resolvable" do
- before do
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.resolvable?).to be true
- end
- end
-
- context "when all notes are resolvable" do
- before do
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(true)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.resolvable?).to be true
- end
- end
- end
-
- context "when not a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolvable?).to be false
- end
- end
- end
-
- describe "#resolved?" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.resolved?).to be true
- end
- end
-
- context "when some resolvable notes are not resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.resolved?).to be false
- end
- end
- end
- end
-
- describe "#to_be_resolved?" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.to_be_resolved?).to be false
- end
- end
-
- context "when some resolvable notes are not resolved" do
- before do
- allow(first_note).to receive(:resolved?).and_return(true)
- allow(third_note).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.to_be_resolved?).to be true
- end
- end
- end
- end
-
- describe "#can_resolve?" do
- let(:current_user) { create(:user) }
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.can_resolve?(current_user)).to be false
- end
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when not signed in" do
- let(:current_user) { nil }
-
- it "returns false" do
- expect(subject.can_resolve?(current_user)).to be false
- end
- end
-
- context "when signed in" do
- context "when the signed in user is the noteable author" do
- before do
- subject.noteable.author = current_user
- end
-
- it "returns true" do
- expect(subject.can_resolve?(current_user)).to be true
- end
- end
-
- context "when the signed in user can push to the project" do
- before do
- subject.project.team << [current_user, :master]
- end
-
- it "returns true" do
- expect(subject.can_resolve?(current_user)).to be true
- end
- end
-
- context "when the signed in user is a random user" do
- it "returns false" do
- expect(subject.can_resolve?(current_user)).to be false
- end
- end
- end
- end
- end
-
- describe "#resolve!" do
- let(:current_user) { create(:user) }
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.resolve!(current_user)).to be_nil
- end
-
- it "doesn't set resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "doesn't set resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "doesn't mark as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when resolvable" do
- let(:user) { create(:user) }
- let(:second_note) { create(:diff_note_on_commit) } # unresolvable
-
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- third_note.resolve!(user)
-
- first_note.reload
- third_note.reload
- end
-
- it "doesn't change resolved_at on the resolved notes" do
- expect(first_note.resolved_at).not_to be_nil
- expect(third_note.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
- expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_at }
- end
-
- it "doesn't change resolved_by on the resolved notes" do
- expect(first_note.resolved_by).to eq(user)
- expect(third_note.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
- expect { subject.resolve!(current_user) }.not_to change { third_note.resolved_by }
- end
-
- it "doesn't change the resolved state on the resolved notes" do
- expect(first_note.resolved?).to be true
- expect(third_note.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
- expect { subject.resolve!(current_user) }.not_to change { third_note.resolved? }
- end
-
- it "doesn't change resolved_at" do
- expect(subject.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_at }
- end
-
- it "doesn't change resolved_by" do
- expect(subject.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved_by }
- end
-
- it "doesn't change resolved state" do
- expect(subject.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.not_to change { subject.resolved? }
- end
- end
-
- context "when some resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- end
-
- it "doesn't change resolved_at on the resolved note" do
- expect(first_note.resolved_at).not_to be_nil
-
- expect { subject.resolve!(current_user) }.
- not_to change { first_note.reload.resolved_at }
- end
-
- it "doesn't change resolved_by on the resolved note" do
- expect(first_note.resolved_by).to eq(user)
-
- expect { subject.resolve!(current_user) }.
- not_to change { first_note.reload && first_note.resolved_by }
- end
-
- it "doesn't change the resolved state on the resolved note" do
- expect(first_note.resolved?).to be true
-
- expect { subject.resolve!(current_user) }.
- not_to change { first_note.reload && first_note.resolved? }
- end
-
- it "sets resolved_at on the unresolved note" do
- subject.resolve!(current_user)
- third_note.reload
-
- expect(third_note.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by on the unresolved note" do
- subject.resolve!(current_user)
- third_note.reload
-
- expect(third_note.resolved_by).to eq(current_user)
- end
-
- it "marks the unresolved note as resolved" do
- subject.resolve!(current_user)
- third_note.reload
-
- expect(third_note.resolved?).to be true
- end
-
- it "sets resolved_at" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by" do
- subject.resolve!(current_user)
-
- expect(subject.resolved_by).to eq(current_user)
- end
-
- it "marks as resolved" do
- subject.resolve!(current_user)
-
- expect(subject.resolved?).to be true
- end
- end
-
- context "when no resolvable notes are resolved" do
- it "sets resolved_at on the unresolved notes" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_at).not_to be_nil
- expect(third_note.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by on the unresolved notes" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_by).to eq(current_user)
- expect(third_note.resolved_by).to eq(current_user)
- end
-
- it "marks the unresolved notes as resolved" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved?).to be true
- expect(third_note.resolved?).to be true
- end
-
- it "sets resolved_at" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_at).not_to be_nil
- end
-
- it "sets resolved_by" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_by).to eq(current_user)
- end
-
- it "marks as resolved" do
- subject.resolve!(current_user)
- first_note.reload
- third_note.reload
-
- expect(subject.resolved?).to be true
- end
- end
- end
- end
-
- describe "#unresolve!" do
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- it "returns nil" do
- expect(subject.unresolve!).to be_nil
- end
- end
-
- context "when resolvable" do
- let(:user) { create(:user) }
-
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- third_note.resolve!(user)
- end
-
- it "unsets resolved_at on the resolved notes" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_at).to be_nil
- expect(third_note.resolved_at).to be_nil
- end
-
- it "unsets resolved_by on the resolved notes" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved_by).to be_nil
- expect(third_note.resolved_by).to be_nil
- end
-
- it "unmarks the resolved notes as resolved" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(first_note.resolved?).to be false
- expect(third_note.resolved?).to be false
- end
-
- it "unsets resolved_at" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_at).to be_nil
- end
-
- it "unsets resolved_by" do
- subject.unresolve!
- first_note.reload
- third_note.reload
-
- expect(subject.resolved_by).to be_nil
- end
-
- it "unmarks as resolved" do
- subject.unresolve!
-
- expect(subject.resolved?).to be false
- end
- end
-
- context "when some resolvable notes are resolved" do
- before do
- first_note.resolve!(user)
- end
-
- it "unsets resolved_at on the resolved note" do
- subject.unresolve!
-
- expect(subject.first_note.resolved_at).to be_nil
- end
-
- it "unsets resolved_by on the resolved note" do
- subject.unresolve!
-
- expect(subject.first_note.resolved_by).to be_nil
- end
-
- it "unmarks the resolved note as resolved" do
- subject.unresolve!
-
- expect(subject.first_note.resolved?).to be false
- end
- end
+ describe '.build' do
+ it 'returns a discussion of the right type' do
+ discussion = described_class.build([first_note, second_note], merge_request)
+ expect(discussion).to be_a(DiffDiscussion)
+ expect(discussion.notes.count).to be(2)
+ expect(discussion.first_note).to be(first_note)
+ expect(discussion.noteable).to be(merge_request)
end
end
- describe "#first_note_to_resolve" do
- it "returns the first not that still needs to be resolved" do
- allow(first_note).to receive(:to_be_resolved?).and_return(false)
- allow(second_note).to receive(:to_be_resolved?).and_return(true)
-
- expect(subject.first_note_to_resolve).to eq(second_note)
- end
- end
-
- describe "#collapsed?" do
- context "when a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(true)
- end
-
- context "when resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(true)
- end
-
- context "when resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.collapsed?).to be true
- end
- end
-
- context "when not resolved" do
- before do
- allow(subject).to receive(:resolved?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.collapsed?).to be false
- end
- end
- end
-
- context "when not resolvable" do
- before do
- allow(subject).to receive(:resolvable?).and_return(false)
- end
-
- context "when active" do
- before do
- allow(subject).to receive(:active?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.collapsed?).to be false
- end
- end
-
- context "when outdated" do
- before do
- allow(subject).to receive(:active?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.collapsed?).to be true
- end
- end
- end
- end
-
- context "when not a diff discussion" do
- before do
- allow(subject).to receive(:diff_discussion?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.collapsed?).to be false
- end
- end
- end
-
- describe "#truncated_diff_lines" do
- let(:truncated_lines) { subject.truncated_diff_lines }
-
- context "when diff is greater than allowed number of truncated diff lines " do
- it "returns fewer lines" do
- expect(subject.diff_lines.count).to be > described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
-
- expect(truncated_lines.count).to be <= described_class::NUMBER_OF_TRUNCATED_DIFF_LINES
- end
- end
-
- context "when some diff lines are meta" do
- it "returns no meta lines" do
- expect(subject.diff_lines).to include(be_meta)
- expect(truncated_lines).not_to include(be_meta)
- end
+ describe '.build_collection' do
+ it 'returns an array of discussions of the right type' do
+ discussions = described_class.build_collection([first_note, second_note, third_note], merge_request)
+ expect(discussions).to eq([
+ DiffDiscussion.new([first_note, second_note], merge_request),
+ DiffDiscussion.new([third_note], merge_request)
+ ])
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 9f0e7fbbe26..12519de8636 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -100,13 +100,28 @@ describe Environment, models: true do
let(:head_commit) { project.commit }
let(:commit) { project.commit.parent }
- it 'returns deployment id for the environment' do
- expect(environment.first_deployment_for(commit)).to eq deployment1
- end
+ context 'Gitaly find_ref_name feature disabled' do
+ it 'returns deployment id for the environment' do
+ expect(environment.first_deployment_for(commit)).to eq deployment1
+ end
- it 'return nil when no deployment is found' do
- expect(environment.first_deployment_for(head_commit)).to eq nil
+ it 'return nil when no deployment is found' do
+ expect(environment.first_deployment_for(head_commit)).to eq nil
+ end
end
+
+ # TODO: Uncomment when feature is reenabled
+ # context 'Gitaly find_ref_name feature enabled' do
+ # before do
+ # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
+ # end
+ #
+ # it 'calls GitalyClient' do
+ # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
+ #
+ # environment.first_deployment_for(commit)
+ # end
+ # end
end
describe '#environment_type' do
@@ -191,25 +206,52 @@ describe Environment, models: true do
end
context 'when matching action is defined' do
- let(:build) { create(:ci_build) }
- let!(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
- context 'when action did not yet finish' do
- let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
+ let!(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ on_stop: 'close_app')
+ end
- it 'returns the same action' do
- expect(subject).to eq(close_action)
- expect(subject.user).to eq(user)
+ context 'when user is not allowed to stop environment' do
+ let!(:close_action) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
+ end
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
- context 'if action did finish' do
- let!(:close_action) { create(:ci_build, :manual, :success, pipeline: build.pipeline, name: 'close_app') }
+ context 'when user is allowed to stop environment' do
+ before do
+ project.add_master(user)
+ end
+
+ context 'when action did not yet finish' do
+ let!(:close_action) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
+ end
+
+ it 'returns the same action' do
+ expect(subject).to eq(close_action)
+ expect(subject.user).to eq(user)
+ end
+ end
- it 'returns a new action of the same type' do
- is_expected.to be_persisted
- expect(subject.name).to eq(close_action.name)
- expect(subject.user).to eq(user)
+ context 'if action did finish' do
+ let!(:close_action) do
+ create(:ci_build, :manual, :success,
+ pipeline: pipeline, name: 'close_app')
+ end
+
+ it 'returns a new action of the same type' do
+ expect(subject).to be_persisted
+ expect(subject.name).to eq(close_action.name)
+ expect(subject.user).to eq(user)
+ end
end
end
end
@@ -351,7 +393,7 @@ describe Environment, models: true do
it 'returns the metrics from the deployment service' do
expect(project.monitoring_service)
- .to receive(:metrics).with(environment)
+ .to receive(:environment_metrics).with(environment)
.and_return(:fake_metrics)
is_expected.to eq(:fake_metrics)
@@ -396,7 +438,7 @@ describe Environment, models: true do
"foo**bar" => "foo-bar" + SUFFIX,
"*-foo" => "env-foo" + SUFFIX,
"staging-12345678-" => "staging-12345678" + SUFFIX,
- "staging-12345678-01234567" => "staging-12345678" + SUFFIX,
+ "staging-12345678-01234567" => "staging-12345678" + SUFFIX
}.each do |name, matcher|
it "returns a slug matching #{matcher}, given #{name}" do
slug = described_class.new(name: name).generate_slug
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 8c90a538f57..b8cb967c4cc 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -15,13 +15,39 @@ describe Event, models: true do
end
describe 'Callbacks' do
- describe 'after_create :reset_project_activity' do
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project) }
+ describe 'after_create :reset_project_activity' do
it 'calls the reset_project_activity method' do
expect_any_instance_of(described_class).to receive(:reset_project_activity)
- create_event(project, project.owner)
+ create_push_event(project, project.owner)
+ end
+ end
+
+ describe 'after_create :set_last_repository_updated_at' do
+ context 'with a push event' do
+ it 'updates the project last_repository_updated_at' do
+ project.update(last_repository_updated_at: 1.year.ago)
+
+ create_push_event(project, project.owner)
+
+ project.reload
+
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
+ end
+ end
+
+ context 'without a push event' do
+ it 'does not update the project last_repository_updated_at' do
+ project.update(last_repository_updated_at: 1.year.ago)
+
+ create(:closed_issue_event, project: project, author: project.owner)
+
+ project.reload
+
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(1.year.ago)
+ end
end
end
end
@@ -29,7 +55,7 @@ describe Event, models: true do
describe "Push event" do
let(:project) { create(:empty_project, :private) }
let(:user) { project.owner }
- let(:event) { create_event(project, user) }
+ let(:event) { create_push_event(project, user) }
it do
expect(event.push?).to be_truthy
@@ -92,8 +118,8 @@ describe Event, models: true do
let(:author) { create(:author) }
let(:assignee) { create(:user) }
let(:admin) { create(:admin) }
- let(:issue) { create(:issue, project: project, author: author, assignee: assignee) }
- let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
+ let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) }
+ let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
let(:note_on_commit) { create(:note_on_commit, project: project) }
let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) }
let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) }
@@ -243,7 +269,7 @@ describe Event, models: true do
expect(project).not_to receive(:update_column).
with(:last_activity_at, a_kind_of(Time))
- create_event(project, project.owner)
+ create_push_event(project, project.owner)
end
end
@@ -251,11 +277,11 @@ describe Event, models: true do
it 'updates the project' do
project.update(last_activity_at: 1.year.ago)
- create_event(project, project.owner)
+ create_push_event(project, project.owner)
project.reload
- project.last_activity_at <= 1.minute.ago
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
end
end
end
@@ -278,7 +304,7 @@ describe Event, models: true do
end
end
- def create_event(project, user, attrs = {})
+ def create_push_event(project, user, attrs = {})
data = {
before: Gitlab::Git::BLANK_SHA,
after: "0220c11b9a3e6c69dc8fd35321254ca9a7b98f7e",
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index 55b87d1c48a..a14efda3eda 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -137,7 +137,7 @@ describe GlobalMilestone, models: true do
[
milestone1_project1,
milestone1_project2,
- milestone1_project3,
+ milestone1_project3
]
milestones_relation = Milestone.where(id: milestones.map(&:id))
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 5d87938235a..6ca1eb0374d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -55,6 +55,34 @@ describe Group, models: true do
it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
it { is_expected.to validate_presence_of :path }
it { is_expected.not_to validate_presence_of :owner }
+ it { is_expected.to validate_presence_of :two_factor_grace_period }
+ it { is_expected.to validate_numericality_of(:two_factor_grace_period).is_greater_than_or_equal_to(0) }
+
+ describe 'path validation' do
+ it 'rejects paths reserved on the root namespace when the group has no parent' do
+ group = build(:group, path: 'api')
+
+ expect(group).not_to be_valid
+ end
+
+ it 'allows root paths when the group has a parent' do
+ group = build(:group, path: 'api', parent: create(:group))
+
+ expect(group).to be_valid
+ end
+
+ it 'rejects any wildcard paths when not a top level group' do
+ group = build(:group, path: 'tree', parent: create(:group))
+
+ expect(group).not_to be_valid
+ end
+
+ it 'rejects reserved group paths' do
+ group = build(:group, path: 'activity', parent: create(:group))
+
+ expect(group).not_to be_valid
+ end
+ end
end
describe '.visible_to_user' do
@@ -147,6 +175,26 @@ describe Group, models: true do
end
end
+ describe '#avatar_url' do
+ let!(:group) { create(:group, :access_requestable, :with_avatar) }
+ let(:user) { create(:user) }
+ let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
+ let(:avatar_path) { "/uploads/group/avatar/#{group.id}/dk.png" }
+
+ context 'when avatar file is uploaded' do
+ before { group.add_master(user) }
+
+ it 'shows correct avatar url' do
+ expect(group.avatar_url).to eq(avatar_path)
+ expect(group.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
+
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+
+ expect(group.avatar_url).to eq([gitlab_host, avatar_path].join)
+ end
+ end
+ end
+
describe '.search' do
it 'returns groups with a matching name' do
expect(described_class.search(group.name)).to eq([group])
@@ -315,4 +363,44 @@ describe Group, models: true do
to include(master.id, developer.id)
end
end
+
+ describe '#update_two_factor_requirement' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_user(user, GroupMember::OWNER)
+ end
+
+ it 'is called when require_two_factor_authentication is changed' do
+ expect_any_instance_of(User).to receive(:update_two_factor_requirement)
+
+ group.update!(require_two_factor_authentication: true)
+ end
+
+ it 'is called when two_factor_grace_period is changed' do
+ expect_any_instance_of(User).to receive(:update_two_factor_requirement)
+
+ group.update!(two_factor_grace_period: 23)
+ end
+
+ it 'is not called when other attributes are changed' do
+ expect_any_instance_of(User).not_to receive(:update_two_factor_requirement)
+
+ group.update!(description: 'foobar')
+ end
+
+ it 'calls #update_two_factor_requirement on each group member' do
+ other_user = create(:user)
+ group.add_user(other_user, GroupMember::OWNER)
+
+ calls = 0
+ allow_any_instance_of(User).to receive(:update_two_factor_requirement) do
+ calls += 1
+ end
+
+ group.update!(require_two_factor_authentication: true, two_factor_grace_period: 23)
+
+ expect(calls).to eq 2
+ end
+ end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 8acec805584..4340170888d 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,6 +1,19 @@
require "spec_helper"
describe SystemHook, models: true do
+ context 'default attributes' do
+ let(:system_hook) { build(:system_hook) }
+
+ it 'sets defined default parameters' do
+ attrs = {
+ push_events: false,
+ repository_update_events: true,
+ enable_ssl_verification: true
+ }
+ expect(system_hook).to have_attributes(attrs)
+ end
+ end
+
describe "execute" do
let(:system_hook) { create(:system_hook) }
let(:user) { create(:user) }
@@ -105,4 +118,12 @@ describe SystemHook, models: true do
).once
end
end
+
+ describe '.repository_update_hooks' do
+ it 'returns hooks for repository update events only' do
+ hook = create(:system_hook, repository_update_events: true)
+ create(:system_hook, repository_update_events: false)
+ expect(SystemHook.repository_update_hooks).to eq([hook])
+ end
+ end
end
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index d8aed25c041..93c2c538e10 100644
--- a/spec/models/issue_collection_spec.rb
+++ b/spec/models/issue_collection_spec.rb
@@ -28,7 +28,7 @@ describe IssueCollection do
end
it 'returns the issues the user is assigned to' do
- issue1.assignee = user
+ issue1.assignees << user
expect(collection.updatable_by_user(user)).to eq([issue1])
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index b8584301baa..bb4e70db2e9 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Issue, models: true do
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
+ it { is_expected.to have_many(:assignees) }
end
describe 'modules' do
@@ -37,6 +38,24 @@ describe Issue, models: true do
end
end
+ describe '#card_attributes' do
+ it 'includes the author name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignees).and_return([])
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => '' })
+ end
+
+ it 'includes the assignee name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignees).and_return([double(name: 'Douwe')])
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+ end
+ end
+
describe '#closed_at' do
after do
Timecop.return
@@ -51,14 +70,6 @@ describe Issue, models: true do
expect(issue.closed_at).to eq(now)
end
-
- it 'sets closed_at to nil when issue is reopened' do
- issue = create(:issue, state: 'closed')
-
- issue.reopen
-
- expect(issue.closed_at).to be_nil
- end
end
describe '#to_reference' do
@@ -132,22 +143,24 @@ describe Issue, models: true do
end
end
- describe '#is_being_reassigned?' do
- it 'returns true if the issue assignee has changed' do
- subject.assignee = create(:user)
- expect(subject.is_being_reassigned?).to be_truthy
- end
- it 'returns false if the issue assignee has not changed' do
- expect(subject.is_being_reassigned?).to be_falsey
+ describe '#assignee_or_author?' do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue) }
+
+ it 'returns true for a user that is assigned to an issue' do
+ issue.assignees << user
+
+ expect(issue.assignee_or_author?(user)).to be_truthy
end
- end
- describe '#is_being_reassigned?' do
- it 'returns issues assigned to user' do
- user = create(:user)
- create_list(:issue, 2, assignee: user)
+ it 'returns true for a user that is the author of an issue' do
+ issue.update(author: user)
- expect(Issue.open_for(user).count).to eq 2
+ expect(issue.assignee_or_author?(user)).to be_truthy
+ end
+
+ it 'returns false for a user that is not the assignee or author' do
+ expect(issue.assignee_or_author?(user)).to be_falsey
end
end
@@ -308,6 +321,27 @@ describe Issue, models: true do
end
end
+ describe '#has_related_branch?' do
+ let(:issue) { create(:issue, title: "Blue Bell Knoll") }
+ subject { issue.has_related_branch? }
+
+ context 'branch found' do
+ before do
+ allow(issue.project.repository).to receive(:branch_names).and_return(["iceblink-luck", issue.to_branch_name])
+ end
+
+ it { is_expected.to eq true }
+ end
+
+ context 'branch not found' do
+ before do
+ allow(issue.project.repository).to receive(:branch_names).and_return(["lazy-calm"])
+ end
+
+ it { is_expected.to eq false }
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
subject { create(:issue, project: create(:project, :repository)) }
@@ -378,12 +412,15 @@ describe Issue, models: true do
it 'updates when assignees change' do
user1 = create(:user)
user2 = create(:user)
- issue = create(:issue, assignee: user1)
+ project = create(:empty_project)
+ issue = create(:issue, assignees: [user1], project: project)
+ project.add_developer(user1)
+ project.add_developer(user2)
expect(user1.assigned_open_issues_count).to eq(1)
expect(user2.assigned_open_issues_count).to eq(0)
- issue.assignee = user2
+ issue.assignees = [user2]
issue.save
expect(user1.assigned_open_issues_count).to eq(0)
@@ -669,6 +706,11 @@ describe Issue, models: true do
expect(attrs_hash).to include(:human_total_time_spent)
expect(attrs_hash).to include('time_estimate')
end
+
+ it 'includes assignee_ids and deprecated assignee_id' do
+ expect(attrs_hash).to include(:assignee_id)
+ expect(attrs_hash).to include(:assignee_ids)
+ end
end
describe '#check_for_spam' do
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index a9139f7d4ab..80ca19acdda 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -42,11 +42,27 @@ describe Label, models: true do
end
end
+ describe '#color' do
+ it 'strips color' do
+ label = described_class.new(color: ' #abcdef ')
+ label.valid?
+
+ expect(label.color).to eq('#abcdef')
+ end
+ end
+
describe '#title' do
it 'sanitizes title' do
label = described_class.new(title: '<b>foo & bar?</b>')
expect(label.title).to eq('foo & bar?')
end
+
+ it 'strips title' do
+ label = described_class.new(title: ' label ')
+ label.valid?
+
+ expect(label.title).to eq('label')
+ end
end
describe 'priorization' do
diff --git a/spec/models/legacy_diff_discussion_spec.rb b/spec/models/legacy_diff_discussion_spec.rb
new file mode 100644
index 00000000000..6eb4a2aaf39
--- /dev/null
+++ b/spec/models/legacy_diff_discussion_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe LegacyDiffDiscussion, models: true do
+ subject { create(:legacy_diff_note_on_merge_request).to_discussion }
+
+ describe '#reply_attributes' do
+ it 'includes line_code' do
+ expect(subject.reply_attributes[:line_code]).to eq(subject.line_code)
+ end
+ end
+
+ describe '#merge_request_version_params' do
+ context 'when the discussion is active' do
+ before do
+ allow(subject).to receive(:active?).and_return(true)
+ end
+
+ it 'returns an empty hash, which will end up showing the latest version' do
+ expect(subject.merge_request_version_params).to eq({})
+ end
+ end
+
+ context 'when the discussion is outdated' do
+ before do
+ allow(subject).to receive(:active?).and_return(false)
+ end
+
+ it 'returns nil' do
+ expect(subject.merge_request_version_params).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/legacy_diff_note_spec.rb b/spec/models/legacy_diff_note_spec.rb
deleted file mode 100644
index 81517a18b74..00000000000
--- a/spec/models/legacy_diff_note_spec.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-require 'spec_helper'
-
-describe LegacyDiffNote, models: true do
- describe "Commit diff line notes" do
- let!(:note) { create(:legacy_diff_note_on_commit, note: "+1 from me") }
- let!(:commit) { note.noteable }
-
- it "saves a valid note" do
- expect(note.commit_id).to eq(commit.id)
- expect(note.noteable.id).to eq(commit.id)
- end
-
- it "is recognized by #legacy_diff_note?" do
- expect(note).to be_legacy_diff_note
- end
- end
-
- describe '#active?' do
- it 'is always true when the note has no associated diff line' do
- note = build(:legacy_diff_note_on_merge_request)
-
- expect(note).to receive(:diff_line).and_return(nil)
-
- expect(note).to be_active
- end
-
- it 'is never true when the note has no noteable associated' do
- note = build(:legacy_diff_note_on_merge_request)
-
- expect(note).to receive(:diff_line).and_return(double)
- expect(note).to receive(:noteable).and_return(nil)
-
- expect(note).not_to be_active
- end
-
- it 'returns the memoized value if defined' do
- note = build(:legacy_diff_note_on_merge_request)
-
- note.instance_variable_set(:@active, 'foo')
- expect(note).not_to receive(:find_noteable_diff)
-
- expect(note.active?).to eq 'foo'
- end
-
- context 'for a merge request noteable' do
- it 'is false when noteable has no matching diff' do
- merge = build_stubbed(:merge_request, :simple)
- note = build(:legacy_diff_note_on_merge_request, noteable: merge)
-
- allow(note).to receive(:diff_line).and_return(double)
- expect(note).to receive(:find_noteable_diff).and_return(nil)
-
- expect(note).not_to be_active
- end
-
- it 'is true when noteable has a matching diff' do
- merge = create(:merge_request, :simple)
-
- # Generate a real line_code value so we know it will match. We use a
- # random line from a random diff just for funsies.
- diff = merge.raw_diffs.to_a.sample
- line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample
- code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos)
-
- # We're persisting in order to trigger the set_diff callback
- note = create(:legacy_diff_note_on_merge_request, noteable: merge,
- line_code: code,
- project: merge.source_project)
-
- # Make sure we don't get a false positive from a guard clause
- expect(note).to receive(:find_noteable_diff).and_call_original
- expect(note).to be_active
- end
- end
- end
-
- describe "#discussion_id" do
- let(:note) { create(:note) }
-
- context "when it is newly created" do
- it "has a discussion id" do
- expect(note.discussion_id).not_to be_nil
- expect(note.discussion_id).to match(/\A\h{40}\z/)
- end
- end
-
- context "when it didn't store a discussion id before" do
- before do
- note.update_column(:discussion_id, nil)
- end
-
- it "has a discussion id" do
- # The discussion_id is set in `after_initialize`, so `reload` won't work
- reloaded_note = Note.find(note.id)
-
- expect(reloaded_note.discussion_id).not_to be_nil
- expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
- end
- end
- end
-end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index c720cc9f2c2..ccc3deac199 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -386,6 +386,33 @@ describe Member, models: true do
end
end
+ describe '.add_users' do
+ %w[project group].each do |source_type|
+ context "when source is a #{source_type}" do
+ let!(:source) { create(source_type, :public, :access_requestable) }
+ let!(:admin) { create(:admin) }
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+
+ it 'returns a <Source>Member objects' do
+ members = described_class.add_users(source, [user1, user2], :master)
+
+ expect(members).to be_a Array
+ expect(members.size).to eq(2)
+ expect(members.first).to be_a "#{source_type.classify}Member".constantize
+ expect(members.first).to be_persisted
+ end
+
+ it 'returns an empty array' do
+ members = described_class.add_users(source, [], :master)
+
+ expect(members).to be_a Array
+ expect(members).to be_empty
+ end
+ end
+ end
+ end
+
describe '#accept_request' do
let(:member) { create(:project_member, requested_at: Time.now.utc) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 370aeb9e0a9..17765b25856 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -13,12 +13,12 @@ describe GroupMember, models: true do
end
end
- describe '.add_users_to_group' do
+ describe '.add_users' do
it 'adds the given users to the given group' do
group = create(:group)
users = create_list(:user, 2)
- described_class.add_users_to_group(
+ described_class.add_users(
group,
[users.first.id, users.second],
described_class::MASTER
@@ -61,7 +61,7 @@ describe GroupMember, models: true do
describe '#after_accept_request' do
it 'calls NotificationService.accept_group_access_request' do
- member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
+ member = create(:group_member, user: build(:user), requested_at: Time.now)
expect_any_instance_of(NotificationService).to receive(:new_group_member)
@@ -75,4 +75,19 @@ describe GroupMember, models: true do
it { is_expected.to eq 'Group' }
end
end
+
+ describe '#update_two_factor_requirement' do
+ let(:user) { build :user }
+ let(:group_member) { build :group_member, user: user }
+
+ it 'is called after creation and deletion' do
+ expect(user).to receive(:update_two_factor_requirement)
+
+ group_member.save
+
+ expect(user).to receive(:update_two_factor_requirement)
+
+ group_member.destroy
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 24e7c1b17d9..ce870fcc1d3 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,6 +9,7 @@ describe MergeRequest, models: true do
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
+ it { is_expected.to belong_to(:assignee) }
it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
end
@@ -86,6 +87,44 @@ describe MergeRequest, models: true do
end
end
+ describe '#card_attributes' do
+ it 'includes the author name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignee).and_return(nil)
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ end
+
+ it 'includes the assignee name' do
+ allow(subject).to receive(:author).and_return(double(name: 'Robert'))
+ allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))
+
+ expect(subject.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+ end
+ end
+
+ describe '#assignee_or_author?' do
+ let(:user) { create(:user) }
+
+ it 'returns true for a user that is assigned to a merge request' do
+ subject.assignee = user
+
+ expect(subject.assignee_or_author?(user)).to eq(true)
+ end
+
+ it 'returns true for a user that is the author of a merge request' do
+ subject.author = user
+
+ expect(subject.assignee_or_author?(user)).to eq(true)
+ end
+
+ it 'returns false for a user that is not the assignee or author' do
+ expect(subject.assignee_or_author?(user)).to eq(false)
+ end
+ end
+
describe '#cache_merge_request_closes_issues!' do
before do
subject.project.team << [subject.author, :developer]
@@ -199,10 +238,10 @@ describe MergeRequest, models: true do
end
context 'when there are no MR diffs' do
- it 'delegates to the compare object' do
+ it 'delegates to the compare object, setting no_collapse: true' do
merge_request.compare = double(:compare)
- expect(merge_request.compare).to receive(:diffs).with(options)
+ expect(merge_request.compare).to receive(:diffs).with(options.merge(no_collapse: true))
merge_request.diffs(options)
end
@@ -215,15 +254,22 @@ describe MergeRequest, models: true do
end
context 'when there are MR diffs' do
- before do
+ it 'returns the correct count' do
merge_request.save
+
+ expect(merge_request.diff_size).to eq('105')
end
- it 'returns the correct count' do
- expect(merge_request.diff_size).to eq(105)
+ it 'returns the correct overflow count' do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
+ merge_request.save
+
+ expect(merge_request.diff_size).to eq('2+')
end
it 'does not perform highlighting' do
+ merge_request.save
+
expect(Gitlab::Diff::Highlight).not_to receive(:new)
merge_request.diff_size
@@ -231,7 +277,7 @@ describe MergeRequest, models: true do
end
context 'when there are no MR diffs' do
- before do
+ def set_compare(merge_request)
merge_request.compare = CompareService.new(
merge_request.source_project,
merge_request.source_branch
@@ -242,10 +288,21 @@ describe MergeRequest, models: true do
end
it 'returns the correct count' do
- expect(merge_request.diff_size).to eq(105)
+ set_compare(merge_request)
+
+ expect(merge_request.diff_size).to eq('105')
+ end
+
+ it 'returns the correct overflow count' do
+ allow(Commit).to receive(:max_diff_options).and_return(max_files: 2)
+ set_compare(merge_request)
+
+ expect(merge_request.diff_size).to eq('2+')
end
it 'does not perform highlighting' do
+ set_compare(merge_request)
+
expect(Gitlab::Diff::Highlight).not_to receive(:new)
merge_request.diff_size
@@ -277,16 +334,6 @@ describe MergeRequest, models: true do
end
end
- describe '#is_being_reassigned?' do
- it 'returns true if the merge_request assignee has changed' do
- subject.assignee = create(:user)
- expect(subject.is_being_reassigned?).to be_truthy
- end
- it 'returns false if the merge request assignee has not changed' do
- expect(subject.is_being_reassigned?).to be_falsey
- end
- end
-
describe '#for_fork?' do
it 'returns true if the merge request is for a fork' do
subject.source_project = build_stubbed(:empty_project, namespace: create(:group))
@@ -441,7 +488,7 @@ describe MergeRequest, models: true do
end
it "can't be removed when its a protected branch" do
- allow(subject.source_project).to receive(:protected_branch?).and_return(true)
+ allow(ProtectedBranch).to receive(:protected?).and_return(true)
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
@@ -671,13 +718,8 @@ describe MergeRequest, models: true do
describe '#head_pipeline' do
describe 'when the source project exists' do
it 'returns the latest pipeline' do
- pipeline = double(:ci_pipeline, ref: 'master')
-
- allow(subject).to receive(:diff_head_sha).and_return('123abc')
-
- expect(subject.source_project).to receive(:pipeline_for).
- with('master', '123abc').
- and_return(pipeline)
+ pipeline = create(:ci_empty_pipeline, project: subject.source_project, ref: 'master', status: 'running', sha: "123abc")
+ subject.update(head_pipeline: pipeline)
expect(subject.head_pipeline).to eq(pipeline)
end
@@ -820,15 +862,17 @@ describe MergeRequest, models: true do
user1 = create(:user)
user2 = create(:user)
mr = create(:merge_request, assignee: user1)
+ mr.project.add_developer(user1)
+ mr.project.add_developer(user2)
- expect(user1.assigned_open_merge_request_count).to eq(1)
- expect(user2.assigned_open_merge_request_count).to eq(0)
+ expect(user1.assigned_open_merge_requests_count).to eq(1)
+ expect(user2.assigned_open_merge_requests_count).to eq(0)
mr.assignee = user2
mr.save
- expect(user1.assigned_open_merge_request_count).to eq(0)
- expect(user2.assigned_open_merge_request_count).to eq(1)
+ expect(user1.assigned_open_merge_requests_count).to eq(0)
+ expect(user2.assigned_open_merge_requests_count).to eq(1)
end
end
@@ -1224,247 +1268,6 @@ describe MergeRequest, models: true do
end
end
- context "discussion status" do
- let(:first_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
- let(:second_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
- let(:third_discussion) { Discussion.new([create(:diff_note_on_merge_request)]) }
-
- before do
- allow(subject).to receive(:diff_discussions).and_return([first_discussion, second_discussion, third_discussion])
- end
-
- describe '#resolvable_discussions' do
- before do
- allow(first_discussion).to receive(:to_be_resolved?).and_return(true)
- allow(second_discussion).to receive(:to_be_resolved?).and_return(false)
- allow(third_discussion).to receive(:to_be_resolved?).and_return(false)
- end
-
- it 'includes only discussions that need to be resolved' do
- expect(subject.resolvable_discussions).to eq([first_discussion])
- end
- end
-
- describe '#discussions_can_be_resolved_by? user' do
- let(:user) { build(:user) }
-
- context 'all discussions can be resolved by the user' do
- before do
- allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(third_discussion).to receive(:can_resolve?).with(user).and_return(true)
- end
-
- it 'allows a user to resolve the discussions' do
- expect(subject.discussions_can_be_resolved_by?(user)).to be(true)
- end
- end
-
- context 'one discussion cannot be resolved by the user' do
- before do
- allow(first_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(second_discussion).to receive(:can_resolve?).with(user).and_return(true)
- allow(third_discussion).to receive(:can_resolve?).with(user).and_return(false)
- end
-
- it 'allows a user to resolve the discussions' do
- expect(subject.discussions_can_be_resolved_by?(user)).to be(false)
- end
- end
- end
-
- describe "#discussions_resolvable?" do
- context "when all discussions are unresolvable" do
- before do
- allow(first_discussion).to receive(:resolvable?).and_return(false)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_resolvable?).to be false
- end
- end
-
- context "when some discussions are unresolvable and some discussions are resolvable" do
- before do
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.discussions_resolvable?).to be true
- end
- end
-
- context "when all discussions are resolvable" do
- before do
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(true)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.discussions_resolvable?).to be true
- end
- end
- end
-
- describe "#discussions_resolved?" do
- context "when discussions are not resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_resolved?).to be false
- end
- end
-
- context "when discussions are resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(true)
-
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable discussions are resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(true)
- end
-
- it "returns true" do
- expect(subject.discussions_resolved?).to be true
- end
- end
-
- context "when some resolvable discussions are not resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_resolved?).to be false
- end
- end
- end
- end
-
- describe "#discussions_to_be_resolved?" do
- context "when discussions are not resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(false)
- end
-
- it "returns false" do
- expect(subject.discussions_to_be_resolved?).to be false
- end
- end
-
- context "when discussions are resolvable" do
- before do
- allow(subject).to receive(:discussions_resolvable?).and_return(true)
-
- allow(first_discussion).to receive(:resolvable?).and_return(true)
- allow(second_discussion).to receive(:resolvable?).and_return(false)
- allow(third_discussion).to receive(:resolvable?).and_return(true)
- end
-
- context "when all resolvable discussions are resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(true)
- end
-
- it "returns false" do
- expect(subject.discussions_to_be_resolved?).to be false
- end
- end
-
- context "when some resolvable discussions are not resolved" do
- before do
- allow(first_discussion).to receive(:resolved?).and_return(true)
- allow(third_discussion).to receive(:resolved?).and_return(false)
- end
-
- it "returns true" do
- expect(subject.discussions_to_be_resolved?).to be true
- end
- end
- end
- end
- end
-
- describe '#conflicts_can_be_resolved_in_ui?' do
- def create_merge_request(source_branch)
- create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr|
- mr.mark_as_unmergeable
- end
- end
-
- it 'returns a falsey value when the MR can be merged without conflicts' do
- merge_request = create_merge_request('master')
- merge_request.mark_as_mergeable
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
- merge_request = create_merge_request('master')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR has a missing ref after a force push' do
- merge_request = create_merge_request('conflict-resolvable')
- allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR does not support new diff notes' do
- merge_request = create_merge_request('conflict-resolvable')
- merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the conflicts contain a large file' do
- merge_request = create_merge_request('conflict-too-large')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the conflicts contain a binary file' do
- merge_request = create_merge_request('conflict-binary-file')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do
- merge_request = create_merge_request('conflict-missing-side')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a truthy value when the conflicts are resolvable in the UI' do
- merge_request = create_merge_request('conflict-resolvable')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
- end
-
- it 'returns a truthy value when the conflicts have to be resolved in an editor' do
- merge_request = create_merge_request('conflict-contains-conflict-markers')
-
- expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
- end
- end
-
describe "#source_project_missing?" do
let(:project) { create(:empty_project) }
let(:fork_project) { create(:empty_project, forked_from_project: project) }
@@ -1589,11 +1392,15 @@ describe MergeRequest, models: true do
describe '#mergeable_with_slash_command?' do
def create_pipeline(status)
- create(:ci_pipeline_with_one_job,
+ pipeline = create(:ci_pipeline_with_one_job,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha,
status: status)
+
+ merge_request.update(head_pipeline: pipeline)
+
+ pipeline
end
let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
@@ -1710,4 +1517,23 @@ describe MergeRequest, models: true do
expect(subject.has_no_commits?).to be_truthy
end
end
+
+ describe '#merge_request_diff_for' do
+ subject { create(:merge_request, importing: true) }
+ let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) }
+ let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+
+ context 'with diff refs' do
+ it 'returns the diffs' do
+ expect(subject.merge_request_diff_for(merge_request_diff1.diff_refs)).to eq(merge_request_diff1)
+ end
+ end
+
+ context 'with a commit SHA' do
+ it 'returns the diffs' do
+ expect(subject.merge_request_diff_for(merge_request_diff3.head_commit_sha)).to eq(merge_request_diff3)
+ end
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index f3f48f951a8..e3e8e6d571c 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -109,18 +109,6 @@ describe Milestone, models: true do
it { expect(milestone.percent_complete(user)).to eq(75) }
end
- describe '#is_empty?' do
- before do
- milestone.issues << create(:issue, project: project)
- milestone.issues << create(:closed_issue, project: project)
- milestone.merge_requests << create(:merge_request)
- end
-
- it { expect(milestone.closed_items_count(user)).to eq(1) }
- it { expect(milestone.total_items_count(user)).to eq(3) }
- it { expect(milestone.is_empty?(user)).to be_falsey }
- end
-
describe '#can_be_closed?' do
it { expect(milestone.can_be_closed?).to be_truthy }
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ccaf0d7abc7..8624616316c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -34,6 +34,13 @@ describe Namespace, models: true do
let(:group) { build(:group, :nested, path: 'tree') }
it { expect(group).not_to be_valid }
+
+ it 'rejects nested paths' do
+ parent = create(:group, :nested, path: 'environments')
+ namespace = build(:project, path: 'folders', namespace: parent)
+
+ expect(namespace).not_to be_valid
+ end
end
context 'top-level group' do
@@ -47,6 +54,7 @@ describe Namespace, models: true do
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
it { is_expected.to respond_to(:to_param) }
+ it { is_expected.to respond_to(:has_parent?) }
end
describe '#to_param' do
@@ -148,42 +156,62 @@ describe Namespace, models: true do
expect(@namespace.move_dir).to be_truthy
end
- context "when any project has container tags" do
+ context "when any project has container images" do
+ let(:container_repository) { create(:container_repository) }
+
before do
stub_container_registry_config(enabled: true)
- stub_container_registry_tags('tag')
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
- create(:empty_project, namespace: @namespace)
+ create(:empty_project, namespace: @namespace, container_repositories: [container_repository])
allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return('new_path')
end
- it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') }
+ it 'raises an error about not movable project' do
+ expect { @namespace.move_dir }.to raise_error(/Namespace cannot be moved/)
+ end
end
- context 'renaming a sub-group' do
+ context 'with subgroups' do
let(:parent) { create(:group, name: 'parent', path: 'parent') }
let(:child) { create(:group, name: 'child', path: 'child', parent: parent) }
let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) }
- let(:uploads_dir) { File.join(CarrierWave.root, 'uploads', 'parent') }
- let(:pages_dir) { File.join(TestEnv.pages_path, 'parent') }
+ let(:uploads_dir) { File.join(CarrierWave.root, 'uploads') }
+ let(:pages_dir) { TestEnv.pages_path }
before do
- FileUtils.mkdir_p(File.join(uploads_dir, 'child', 'the-project'))
- FileUtils.mkdir_p(File.join(pages_dir, 'child', 'the-project'))
+ FileUtils.mkdir_p(File.join(uploads_dir, 'parent', 'child', 'the-project'))
+ FileUtils.mkdir_p(File.join(pages_dir, 'parent', 'child', 'the-project'))
+ end
+
+ context 'renaming child' do
+ it 'correctly moves the repository, uploads and pages' do
+ expected_repository_path = File.join(TestEnv.repos_path, 'parent', 'renamed', 'the-project.git')
+ expected_upload_path = File.join(uploads_dir, 'parent', 'renamed', 'the-project')
+ expected_pages_path = File.join(pages_dir, 'parent', 'renamed', 'the-project')
+
+ child.update_attributes!(path: 'renamed')
+
+ expect(File.directory?(expected_repository_path)).to be(true)
+ expect(File.directory?(expected_upload_path)).to be(true)
+ expect(File.directory?(expected_pages_path)).to be(true)
+ end
end
- it 'correctly moves the repository, uploads and pages' do
- expected_repository_path = File.join(TestEnv.repos_path, 'parent', 'renamed', 'the-project.git')
- expected_upload_path = File.join(uploads_dir, 'renamed', 'the-project')
- expected_pages_path = File.join(pages_dir, 'renamed', 'the-project')
+ context 'renaming parent' do
+ it 'correctly moves the repository, uploads and pages' do
+ expected_repository_path = File.join(TestEnv.repos_path, 'renamed', 'child', 'the-project.git')
+ expected_upload_path = File.join(uploads_dir, 'renamed', 'child', 'the-project')
+ expected_pages_path = File.join(pages_dir, 'renamed', 'child', 'the-project')
- child.update_attributes!(path: 'renamed')
+ parent.update_attributes!(path: 'renamed')
- expect(File.directory?(expected_repository_path)).to be(true)
- expect(File.directory?(expected_upload_path)).to be(true)
- expect(File.directory?(expected_pages_path)).to be(true)
+ expect(File.directory?(expected_repository_path)).to be(true)
+ expect(File.directory?(expected_upload_path)).to be(true)
+ expect(File.directory?(expected_pages_path)).to be(true)
+ end
end
end
end
@@ -295,4 +323,13 @@ describe Namespace, models: true do
to eq([namespace.owner_id])
end
end
+
+ describe '#all_projects' do
+ let(:group) { create(:group) }
+ let(:child) { create(:group, parent: group) }
+ let!(:project1) { create(:project_empty_repo, namespace: group) }
+ let!(:project2) { create(:project_empty_repo, namespace: child) }
+
+ it { expect(group.all_projects.to_a).to eq([project2, project1]) }
+ end
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index 492c4e01bd8..0fe8a591a45 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -9,4 +9,40 @@ describe Network::Graph, models: true do
expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
end
+
+ describe '#commits' do
+ let(:graph) { described_class.new(project, 'refs/heads/master', project.repository.commit, nil) }
+
+ it 'returns a list of commits' do
+ commits = graph.commits
+
+ expect(commits).not_to be_empty
+ expect(commits).to all( be_kind_of(Network::Commit) )
+ end
+
+ it 'it the commits by commit date (descending)' do
+ # Remove duplicate timestamps because they make it harder to
+ # assert that the commits are sorted as expected.
+ commits = graph.commits.uniq(&:date)
+ sorted_commits = commits.sort_by(&:date).reverse
+
+ expect(commits).not_to be_empty
+ expect(commits.map(&:id)).to eq(sorted_commits.map(&:id))
+ end
+
+ it 'sorts children before parents for commits with the same timestamp' do
+ commits_by_time = graph.commits.group_by(&:date)
+
+ commits_by_time.each do |time, commits|
+ commit_ids = commits.map(&:id)
+
+ commits.each_with_index do |commit, index|
+ parent_indexes = commit.parent_ids.map { |parent_id| commit_ids.find_index(parent_id) }.compact
+
+ # All parents of the current commit should appear after it
+ expect(parent_indexes).to all( be > index )
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 33536487c41..7a01cef9b4b 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -245,22 +245,36 @@ describe Note, models: true do
end
end
+ describe '.find_discussion' do
+ let!(:note) { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:discussion_note_on_merge_request, in_reply_to: note) }
+ let(:merge_request) { note.noteable }
+
+ it 'returns a discussion with multiple notes' do
+ discussion = merge_request.notes.find_discussion(note.discussion_id)
+
+ expect(discussion).not_to be_nil
+ expect(discussion.notes).to match_array([note, note2])
+ expect(discussion.first_note.discussion_id).to eq(note.discussion_id)
+ end
+ end
+
describe ".grouped_diff_discussions" do
let!(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let!(:active_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
- let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
+ let!(:active_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, in_reply_to: active_diff_note1) }
let!(:active_diff_note3) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: active_position2) }
let!(:outdated_diff_note1) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
- let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: outdated_position) }
+ let!(:outdated_diff_note2) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, in_reply_to: outdated_diff_note1) }
let(:active_position2) do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
- old_line: 16,
- new_line: 22,
- diff_refs: merge_request.diff_refs
+ old_line: nil,
+ new_line: 13,
+ diff_refs: project.commit(sample_commit.id).diff_refs
)
end
@@ -274,50 +288,77 @@ describe Note, models: true do
)
end
- subject { merge_request.notes.grouped_diff_discussions }
+ context 'active diff discussions' do
+ subject { merge_request.notes.grouped_diff_discussions }
- it "includes active discussions" do
- discussions = subject.values
+ it "includes active discussions" do
+ discussions = subject.values.flatten
- expect(discussions.count).to eq(2)
- expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
- expect(discussions.all?(&:active?)).to be true
+ expect(discussions.count).to eq(2)
+ expect(discussions.map(&:id)).to eq([active_diff_note1.discussion_id, active_diff_note3.discussion_id])
+ expect(discussions.all?(&:active?)).to be true
- expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
- expect(discussions.last.notes).to eq([active_diff_note3])
- end
+ expect(discussions.first.notes).to eq([active_diff_note1, active_diff_note2])
+ expect(discussions.last.notes).to eq([active_diff_note3])
+ end
- it "doesn't include outdated discussions" do
- expect(subject.values.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
- end
+ it "doesn't include outdated discussions" do
+ expect(subject.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id)
+ end
- it "groups the discussions by line code" do
- expect(subject[active_diff_note1.line_code].id).to eq(active_diff_note1.discussion_id)
- expect(subject[active_diff_note3.line_code].id).to eq(active_diff_note3.discussion_id)
+ it "groups the discussions by line code" do
+ expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id)
+ expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
end
- end
- describe "#discussion_id" do
- let(:note) { create(:note) }
+ context 'diff discussions for older diff refs' do
+ subject { merge_request.notes.grouped_diff_discussions(diff_refs) }
- context "when it is newly created" do
- it "has a discussion id" do
- expect(note.discussion_id).not_to be_nil
- expect(note.discussion_id).to match(/\A\h{40}\z/)
- end
- end
+ context 'for diff refs a discussion was created at' do
+ let(:diff_refs) { active_position2.diff_refs }
- context "when it didn't store a discussion id before" do
- before do
- note.update_column(:discussion_id, nil)
+ it "includes discussions that were created then" do
+ discussions = subject.values.flatten
+
+ expect(discussions.count).to eq(1)
+
+ discussion = discussions.first
+
+ expect(discussion.id).to eq(active_diff_note3.discussion_id)
+ expect(discussion.active?).to be true
+ expect(discussion.active?(diff_refs)).to be false
+ expect(discussion.created_at_diff?(diff_refs)).to be true
+
+ expect(discussion.notes).to eq([active_diff_note3])
+ end
+
+ it "groups the discussions by original line code" do
+ expect(subject[active_diff_note3.original_line_code].first.id).to eq(active_diff_note3.discussion_id)
+ end
end
- it "has a discussion id" do
- # The discussion_id is set in `after_initialize`, so `reload` won't work
- reloaded_note = Note.find(note.id)
+ context 'for diff refs a discussion was last active at' do
+ let(:diff_refs) { outdated_position.diff_refs }
- expect(reloaded_note.discussion_id).not_to be_nil
- expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ it "includes discussions that were last active" do
+ discussions = subject.values.flatten
+
+ expect(discussions.count).to eq(1)
+
+ discussion = discussions.first
+
+ expect(discussion.id).to eq(outdated_diff_note1.discussion_id)
+ expect(discussion.active?).to be false
+ expect(discussion.active?(diff_refs)).to be true
+ expect(discussion.created_at_diff?(diff_refs)).to be true
+
+ expect(discussion.notes).to eq([outdated_diff_note1, outdated_diff_note2])
+ end
+
+ it "groups the discussions by line code" do
+ expect(subject[outdated_diff_note1.line_code].first.id).to eq(outdated_diff_note1.discussion_id)
+ end
end
end
end
@@ -388,15 +429,267 @@ describe Note, models: true do
end
end
+ describe '#can_be_discussion_note?' do
+ context 'for a note on a merge request' do
+ it 'returns true' do
+ note = build(:note_on_merge_request)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a note on an issue' do
+ it 'returns true' do
+ note = build(:note_on_issue)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a note on a commit' do
+ it 'returns true' do
+ note = build(:note_on_commit)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a note on a snippet' do
+ it 'returns true' do
+ note = build(:note_on_project_snippet)
+
+ expect(note.can_be_discussion_note?).to be_truthy
+ end
+ end
+
+ context 'for a diff note on merge request' do
+ it 'returns false' do
+ note = build(:diff_note_on_merge_request)
+
+ expect(note.can_be_discussion_note?).to be_falsey
+ end
+ end
+
+ context 'for a diff note on commit' do
+ it 'returns false' do
+ note = build(:diff_note_on_commit)
+
+ expect(note.can_be_discussion_note?).to be_falsey
+ end
+ end
+
+ context 'for a discussion note' do
+ it 'returns false' do
+ note = build(:discussion_note_on_merge_request)
+
+ expect(note.can_be_discussion_note?).to be_falsey
+ end
+ end
+ end
+
+ describe '#discussion_class' do
+ let(:note) { build(:note_on_commit) }
+ let(:merge_request) { create(:merge_request) }
+
+ context 'when the note is displayed out of context' do
+ it 'returns OutOfContextDiscussion' do
+ expect(note.discussion_class(merge_request)).to be(OutOfContextDiscussion)
+ end
+ end
+
+ context 'when the note is displayed in the original context' do
+ it 'returns IndividualNoteDiscussion' do
+ expect(note.discussion_class(note.noteable)).to be(IndividualNoteDiscussion)
+ end
+ end
+ end
+
+ describe "#discussion_id" do
+ let(:note) { create(:note_on_commit) }
+
+ context "when it is newly created" do
+ it "has a discussion id" do
+ expect(note.discussion_id).not_to be_nil
+ expect(note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context "when it didn't store a discussion id before" do
+ before do
+ note.update_column(:discussion_id, nil)
+ end
+
+ it "has a discussion id" do
+ # The discussion_id is set in `after_initialize`, so `reload` won't work
+ reloaded_note = Note.find(note.id)
+
+ expect(reloaded_note.discussion_id).not_to be_nil
+ expect(reloaded_note.discussion_id).to match(/\A\h{40}\z/)
+ end
+ end
+
+ context 'when the note is displayed out of context' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'overrides the discussion id' do
+ expect(note.discussion_id(merge_request)).not_to eq(note.discussion_id)
+ end
+ end
+ end
+
+ describe '#to_discussion' do
+ subject { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:discussion_note_on_merge_request, project: subject.project, noteable: subject.noteable, in_reply_to: subject) }
+
+ it "returns a discussion with just this note" do
+ discussion = subject.to_discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject])
+ end
+ end
+
+ describe "#discussion" do
+ let!(:note1) { create(:discussion_note_on_merge_request) }
+ let!(:note2) { create(:diff_note_on_merge_request, project: note1.project, noteable: note1.noteable) }
+
+ context 'when the note is part of a discussion' do
+ subject { create(:discussion_note_on_merge_request, project: note1.project, noteable: note1.noteable, in_reply_to: note1) }
+
+ it "returns the discussion this note is in" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([note1, subject])
+ end
+ end
+
+ context 'when the note is not part of a discussion' do
+ subject { create(:note) }
+
+ it "returns a discussion with just this note" do
+ discussion = subject.discussion
+
+ expect(discussion.id).to eq(subject.discussion_id)
+ expect(discussion.notes).to eq([subject])
+ end
+ end
+ end
+
+ describe "#part_of_discussion?" do
+ context 'for a regular note' do
+ let(:note) { build(:note) }
+
+ it 'returns false' do
+ expect(note.part_of_discussion?).to be_falsey
+ end
+ end
+
+ context 'for a diff note' do
+ let(:note) { build(:diff_note_on_commit) }
+
+ it 'returns true' do
+ expect(note.part_of_discussion?).to be_truthy
+ end
+ end
+
+ context 'for a discussion note' do
+ let(:note) { build(:discussion_note_on_merge_request) }
+
+ it 'returns true' do
+ expect(note.part_of_discussion?).to be_truthy
+ end
+ end
+ end
+
+ describe '#in_reply_to?' do
+ context 'for a note' do
+ context 'when part of a discussion' do
+ subject { create(:discussion_note_on_issue) }
+ let(:note) { create(:discussion_note_on_issue, in_reply_to: subject) }
+
+ it 'checks if the note is in reply to the other discussion' do
+ expect(subject).to receive(:in_reply_to?).with(note).and_call_original
+ expect(subject).to receive(:in_reply_to?).with(note.noteable).and_call_original
+ expect(subject).to receive(:in_reply_to?).with(note.to_discussion).and_call_original
+
+ subject.in_reply_to?(note)
+ end
+ end
+
+ context 'when not part of a discussion' do
+ subject { create(:note) }
+ let(:note) { create(:note, in_reply_to: subject) }
+
+ it 'checks if the note is in reply to the other noteable' do
+ expect(subject).to receive(:in_reply_to?).with(note).and_call_original
+ expect(subject).to receive(:in_reply_to?).with(note.noteable).and_call_original
+
+ subject.in_reply_to?(note)
+ end
+ end
+ end
+
+ context 'for a discussion' do
+ context 'when part of the same discussion' do
+ subject { create(:diff_note_on_merge_request) }
+ let(:note) { create(:diff_note_on_merge_request, in_reply_to: subject) }
+
+ it 'returns true' do
+ expect(subject.in_reply_to?(note.to_discussion)).to be_truthy
+ end
+ end
+
+ context 'when not part of the same discussion' do
+ subject { create(:diff_note_on_merge_request) }
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ it 'returns false' do
+ expect(subject.in_reply_to?(note.to_discussion)).to be_falsey
+ end
+ end
+ end
+
+ context 'for a noteable' do
+ context 'when a comment on the same noteable' do
+ subject { create(:note) }
+ let(:note) { create(:note, in_reply_to: subject) }
+
+ it 'returns true' do
+ expect(subject.in_reply_to?(note.noteable)).to be_truthy
+ end
+ end
+
+ context 'when not a comment on the same noteable' do
+ subject { create(:note) }
+ let(:note) { create(:note) }
+
+ it 'returns false' do
+ expect(subject.in_reply_to?(note.noteable)).to be_falsey
+ end
+ end
+ end
+ end
+
describe 'expiring ETag cache' do
let(:note) { build(:note_on_issue) }
- it "expires cache for note's issue when note is saved" do
+ def expect_expiration(note)
expect_any_instance_of(Gitlab::EtagCaching::Store)
.to receive(:touch)
.with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+ end
+
+ it "expires cache for note's issue when note is saved" do
+ expect_expiration(note)
note.save!
end
+
+ it "expires cache for note's issue when note is destroyed" do
+ expect_expiration(note)
+
+ note.destroy!
+ end
end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index 33ef67f97a7..cd0a4a94809 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -16,7 +16,7 @@ describe ProjectAuthorization do
it 'inserts rows in batches' do
described_class.insert_authorizations([
[user.id, project1.id, Gitlab::Access::MASTER],
- [user.id, project2.id, Gitlab::Access::MASTER],
+ [user.id, project2.id, Gitlab::Access::MASTER]
], 1)
expect(user.project_authorizations.count).to eq(2)
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 48aef3a93f2..95c35162d96 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -28,7 +28,7 @@ describe AsanaService, models: true do
commits: messages.map do |m|
{
message: m,
- url: 'https://gitlab.com/',
+ url: 'https://gitlab.com/'
}
end
}
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index 190ff4c535d..c159ab00ab1 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -7,7 +7,8 @@ describe ChatMessage::IssueMessage, models: true do
{
user: {
name: 'Test User',
- username: 'test.user'
+ username: 'test.user',
+ avatar_url: 'http://someavatar.com'
},
project_name: 'project_name',
project_url: 'http://somewhere.com',
@@ -25,43 +26,84 @@ describe ChatMessage::IssueMessage, models: true do
}
end
- let(:color) { '#C95823' }
+ context 'without markdown' do
+ let(:color) { '#C95823' }
- context '#initialize' do
- before do
- args[:object_attributes][:description] = nil
+ context '#initialize' do
+ before do
+ args[:object_attributes][:description] = nil
+ end
+
+ it 'returns a non-null description' do
+ expect(subject.description).to eq('')
+ end
end
- it 'returns a non-null description' do
- expect(subject.description).to eq('')
+ context 'open' do
+ it 'returns a message regarding opening of issues' do
+ expect(subject.pretext).to eq(
+ '[<http://somewhere.com|project_name>] Issue opened by test.user')
+ expect(subject.attachments).to eq([
+ {
+ title: "#100 Issue title",
+ title_link: "http://url.com",
+ text: "issue description",
+ color: color
+ }
+ ])
+ end
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 opened by test.user')
- expect(subject.attachments).to eq([
- {
- title: "#100 Issue title",
- title_link: "http://url.com",
- text: "issue description",
- color: color,
- }
- ])
+ 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')
+ expect(subject.attachments).to be_empty
+ end
end
end
- context 'close' do
+ context 'with markdown' do
before do
- args[:object_attributes][:action] = 'close'
- args[:object_attributes][:state] = 'closed'
+ args[:markdown] = true
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')
- expect(subject.attachments).to be_empty
+ context 'open' do
+ it 'returns a message regarding opening of issues' do
+ expect(subject.pretext).to eq(
+ '[[project_name](http://somewhere.com)] Issue opened by test.user')
+ expect(subject.attachments).to eq('issue description')
+ expect(subject.activity).to eq({
+ title: 'Issue opened by 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')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq({
+ title: 'Issue closed by 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/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index cc154112e90..61f17031172 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -7,45 +7,84 @@ describe ChatMessage::MergeMessage, models: true do
{
user: {
name: 'Test User',
- username: 'test.user'
+ username: 'test.user',
+ avatar_url: 'http://someavatar.com'
},
project_name: 'project_name',
project_url: 'http://somewhere.com',
object_attributes: {
- title: "Issue title\nSecond line",
+ title: "Merge Request title\nSecond line",
id: 10,
iid: 100,
assignee_id: 1,
url: 'http://url.com',
state: 'opened',
- description: 'issue description',
+ description: 'merge request description',
source_branch: 'source_branch',
- target_branch: 'target_branch',
+ target_branch: 'target_branch'
}
}
end
- let(:color) { '#345' }
+ 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 opened <http://somewhere.com/merge_requests/100|merge request !100> '\
- 'in <http://somewhere.com|project_name>: *Issue title*')
- expect(subject.attachments).to be_empty
+ context 'open' do
+ it 'returns a message regarding opening of merge requests' do
+ expect(subject.pretext).to eq(
+ 'test.user opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
+ 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 closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*')
+ expect(subject.attachments).to be_empty
+ end
end
end
- context 'close' do
+ context 'with markdown' do
before do
- args[:object_attributes][:state] = 'closed'
+ args[:markdown] = true
+ end
+
+ context 'open' do
+ it 'returns a message regarding opening of merge requests' do
+ expect(subject.pretext).to eq(
+ 'test.user opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq({
+ title: 'Merge Request opened by 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
- it 'returns a message regarding closing of merge requests' do
- expect(subject.pretext).to eq(
- 'test.user closed <http://somewhere.com/merge_requests/100|merge request !100> '\
- 'in <http://somewhere.com|project_name>: *Issue title*')
- expect(subject.attachments).to be_empty
+
+ 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 closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq({
+ title: 'Merge Request closed by 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
end
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index da700a08e57..7996536218a 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -1,130 +1,190 @@
require 'spec_helper'
describe ChatMessage::NoteMessage, models: true do
- let(:color) { '#345' }
+ subject { described_class.new(args) }
- before do
- @args = {
- 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'
- }
+ 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"
+ 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
- it 'returns a message regarding notes on commits' do
- message = described_class.new(@args)
- expect(message.pretext).to eq("test.user <http://url.com|commented on " \
- "commit 5f163b2b> in <http://somewhere.com|project_name>: " \
- "*Added a commit message*")
- expected_attachments = [
- {
- text: "comment on a commit",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
+ context 'without markdown' do
+ it 'returns a message regarding notes on commits' do
+ expect(subject.pretext).to eq("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 [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 [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"
+ 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
- it 'returns a message regarding notes on a merge request' do
- message = described_class.new(@args)
- expect(message.pretext).to eq("test.user <http://url.com|commented on " \
- "merge request !30> in <http://somewhere.com|project_name>: " \
- "*merge request title*")
- expected_attachments = [
- {
- text: "comment on a merge request",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
+ context 'without markdown' do
+ it 'returns a message regarding notes on a merge request' do
+ expect(subject.pretext).to eq("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 [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 [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"
+ 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
- it 'returns a message regarding notes on an issue' do
- message = described_class.new(@args)
- expect(message.pretext).to eq(
- "test.user <http://url.com|commented on " \
- "issue #20> in <http://somewhere.com|project_name>: " \
- "*issue title*")
- expected_attachments = [
- {
- text: "comment on an issue",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
+ context 'without markdown' do
+ it 'returns a message regarding notes on an issue' do
+ expect(subject.pretext).to eq(
+ "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 [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 [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"
+ args[:object_attributes][:note] = 'comment on a snippet'
+ args[:object_attributes][:noteable_type] = 'Snippet'
+ args[:snippet] = {
+ id: 5,
+ title: "snippet title\ndetails\n"
}
end
- it 'returns a message regarding notes on a project snippet' do
- message = described_class.new(@args)
- expect(message.pretext).to eq("test.user <http://url.com|commented on " \
- "snippet #5> in <http://somewhere.com|project_name>: " \
- "*snippet title*")
- expected_attachments = [
- {
- text: "comment on a snippet",
- color: color,
- }
- ]
- expect(message.attachments).to eq(expected_attachments)
+ context 'without markdown' do
+ it 'returns a message regarding notes on a project snippet' do
+ expect(subject.pretext).to eq("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 [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/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index bf2a9616455..7d2599dc703 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -2,8 +2,9 @@ require 'spec_helper'
describe ChatMessage::PipelineMessage do
subject { described_class.new(args) }
- let(:user) { { name: 'hacker' } }
+ let(:user) { { name: 'hacker' } }
+ let(:duration) { 7210 }
let(:args) do
{
object_attributes: {
@@ -14,54 +15,118 @@ describe ChatMessage::PipelineMessage do
status: status,
duration: duration
},
- project: { path_with_namespace: 'project_name',
- web_url: 'http://example.gitlab.com' },
+ project: {
+ path_with_namespace: 'project_name',
+ web_url: 'http://example.gitlab.com'
+ },
user: user
}
end
- let(:message) { build_message }
+ context 'without markdown' do
+ context 'pipeline succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+ let(:message) { build_message('passed') }
+
+ it 'returns a message with information about succeeded build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
- context 'pipeline succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:duration) { 10 }
- let(:message) { build_message('passed') }
+ context 'pipeline failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:message) { build_message }
- it 'returns a message with information about succeeded build' do
- verify_message
+ it 'returns a message with information about failed build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+
+ context 'when triggered by API therefore lacking user' do
+ let(:user) { nil }
+ let(:message) { build_message(status, 'API') }
+
+ it 'returns a message stating it is by API' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
end
- end
- context 'pipeline failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:duration) { 10 }
+ def build_message(status_text = status, name = user[:name])
+ "<http://example.gitlab.com|project_name>:" \
+ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
+ " of branch `<http://example.gitlab.com/commits/develop|develop>`" \
+ " by #{name} #{status_text} in 02:00:10"
+ end
+ end
- it 'returns a message with information about failed build' do
- verify_message
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
end
- context 'when triggered by API therefore lacking user' do
- let(:user) { nil }
- let(:message) { build_message(status, 'API') }
+ context 'pipeline succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+ let(:message) { build_markdown_message('passed') }
- it 'returns a message stating it is by API' do
- verify_message
+ it 'returns a message with information about succeeded build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.attachments).to eq(message)
+ expect(subject.activity).to eq({
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker passed',
+ subtitle: 'in [project_name](http://example.gitlab.com)',
+ text: 'in 02:00:10',
+ image: ''
+ })
end
end
- end
- def verify_message
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
+ context 'pipeline failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:message) { build_markdown_message }
+
+ it 'returns a message with information about failed build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.attachments).to eq(message)
+ expect(subject.activity).to eq({
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by hacker failed',
+ subtitle: 'in [project_name](http://example.gitlab.com)',
+ text: 'in 02:00:10',
+ image: ''
+ })
+ end
- def build_message(status_text = status, name = user[:name])
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ context 'when triggered by API therefore lacking user' do
+ let(:user) { nil }
+ let(:message) { build_markdown_message(status, 'API') }
+
+ it 'returns a message stating it is by API' do
+ expect(subject.pretext).to be_empty
+ expect(subject.attachments).to eq(message)
+ expect(subject.activity).to eq({
+ title: 'Pipeline [#123](http://example.gitlab.com/pipelines/123) of branch `[develop](http://example.gitlab.com/commits/develop)` by API failed',
+ subtitle: 'in [project_name](http://example.gitlab.com)',
+ text: 'in 02:00:10',
+ image: ''
+ })
+ end
+ end
+ end
+
+ def build_markdown_message(status_text = status, name = user[:name])
+ "[project_name](http://example.gitlab.com):" \
+ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch `[develop](http://example.gitlab.com/commits/develop)`" \
+ " by #{name} #{status_text} in 02:00:10"
+ end
end
end
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index 24928873bad..e38117b75f6 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -10,6 +10,7 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name',
ref: 'refs/heads/master',
user_name: 'test.user',
+ user_avatar: 'http://someavatar.com',
project_url: 'http://url.com'
}
end
@@ -20,22 +21,40 @@ describe ChatMessage::PushMessage, models: true do
before do
args[:commits] = [
{ message: 'message1', url: 'http://url1.com', id: 'abcdefghijkl', author: { name: 'author1' } },
- { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } },
+ { message: 'message2', url: 'http://url2.com', id: '123456789012', author: { name: 'author2' } }
]
end
- 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"\
- "<http://url2.com|12345678>: message2 - author2",
- color: color,
- }
- ])
+ 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 - 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 - author2")
+ expect(subject.activity).to eq({
+ title: 'test.user pushed to branch',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/before...after)',
+ image: 'http://someavatar.com'
+ })
+ end
end
end
@@ -47,15 +66,36 @@ describe ChatMessage::PushMessage, models: true do
project_name: 'project_name',
ref: 'refs/tags/new_tag',
user_name: 'test.user',
+ user_avatar: 'http://someavatar.com',
project_url: 'http://url.com'
}
end
- it 'returns a message regarding pushes' do
- expect(subject.pretext).to eq('test.user pushed new tag ' \
- '<http://url.com/commits/new_tag|new_tag> to ' \
- '<http://url.com|project_name>')
- expect(subject.attachments).to be_empty
+ context 'without markdown' do
+ it 'returns a message regarding pushes' do
+ expect(subject.pretext).to eq('test.user pushed new tag ' \
+ '`<http://url.com/commits/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/commits/new_tag)` to [project_name](http://url.com)')
+ expect(subject.attachments).to be_empty
+ expect(subject.activity).to eq({
+ title: 'test.user created tag',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ image: 'http://someavatar.com'
+ })
+ end
end
end
@@ -64,12 +104,31 @@ describe ChatMessage::PushMessage, models: true do
args[:before] = Gitlab::Git::BLANK_SHA
end
- 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
+ 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 created branch',
+ subtitle: 'in [project_name](http://url.com)',
+ text: '[Compare changes](http://url.com/compare/0000000000000000000000000000000000000000...after)',
+ image: 'http://someavatar.com'
+ })
+ end
end
end
@@ -78,11 +137,30 @@ describe ChatMessage::PushMessage, models: true do
args[:after] = Gitlab::Git::BLANK_SHA
end
- 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
+ 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',
+ 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/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index a2ad61e38e7..4ca1b8aa7b7 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -7,7 +7,8 @@ describe ChatMessage::WikiPageMessage, models: true do
{
user: {
name: 'Test User',
- username: 'test.user'
+ username: 'test.user',
+ avatar_url: 'http://someavatar.com'
},
project_name: 'project_name',
project_url: 'http://somewhere.com',
@@ -19,54 +20,128 @@ describe ChatMessage::WikiPageMessage, models: true do
}
end
- describe '#pretext' do
- context 'when :action == "create"' do
- before { args[:object_attributes][:action] = 'create' }
+ context 'without markdown' do
+ describe '#pretext' do
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
- it 'returns a message that a new wiki page was created' do
- expect(subject.pretext).to eq(
- 'test.user created <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
- '*Wiki page title*')
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ '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 { args[:object_attributes][:action] = 'update' }
+
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'test.user edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
end
end
- context 'when :action == "update"' do
- before { args[:object_attributes][:action] = 'update' }
+ describe '#attachments' do
+ let(:color) { '#345' }
- it 'returns a message that a wiki page was updated' do
- expect(subject.pretext).to eq(
- 'test.user edited <http://url.com|wiki page> in <http://somewhere.com|project_name>: '\
- '*Wiki page title*')
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns the attachment for a new wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color
+ }
+ ])
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'returns the attachment for an updated wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color
+ }
+ ])
+ end
end
end
end
- describe '#attachments' do
- let(:color) { '#345' }
+ context 'with markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ describe '#pretext' do
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ 'test.user created [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ end
+ end
- context 'when :action == "create"' do
- before { args[:object_attributes][:action] = 'create' }
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
- it 'returns the attachment for a new wiki page' do
- expect(subject.attachments).to eq([
- {
- text: "Wiki page description",
- color: color,
- }
- ])
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'test.user edited [wiki page](http://url.com) in [project_name](http://somewhere.com): *Wiki page title*')
+ end
end
end
- context 'when :action == "update"' do
- before { args[:object_attributes][:action] = 'update' }
+ describe '#attachments' do
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns the attachment for a new wiki page' do
+ expect(subject.attachments).to eq('Wiki page description')
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'returns the attachment for an updated wiki page' do
+ expect(subject.attachments).to eq('Wiki page description')
+ end
+ end
+ end
+
+ describe '#activity' do
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns the attachment for a new wiki page' do
+ expect(subject.activity).to eq({
+ title: '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 { args[:object_attributes][:action] = 'update' }
- it 'returns the attachment for an updated wiki page' do
- expect(subject.attachments).to eq([
- {
- text: "Wiki page description",
- color: color,
- }
- ])
+ it 'returns the attachment for an updated wiki page' do
+ expect(subject.activity).to eq({
+ title: '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
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index c98e7ee14fd..8fbe42248ae 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -1,11 +1,29 @@
require 'spec_helper'
describe ChatNotificationService, models: true do
- describe "Associations" do
+ describe 'Associations' do
before do
allow(subject).to receive(:activated?).and_return(true)
end
it { is_expected.to validate_presence_of :webhook }
end
+
+ describe '#can_test?' do
+ context 'with empty repository' do
+ it 'returns true' do
+ subject.project = create(:empty_project, :empty_repo)
+
+ expect(subject.can_test?).to be true
+ end
+ end
+
+ context 'with repository' do
+ it 'returns true' do
+ subject.project = create(:project)
+
+ expect(subject.can_test?).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
index fbe6f344a98..869b25b933b 100644
--- a/spec/models/project_services/issue_tracker_service_spec.rb
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -8,7 +8,7 @@ describe IssueTrackerService, models: true do
let(:service) { RedmineService.new(project: project, active: true) }
before do
- create(:service, project: project, active: true, category: 'issue_tracker')
+ create(:custom_issue_tracker_service, project: project)
end
context 'when service is changed manually by user' do
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index bf7950ef1c9..c1c2f2a7219 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -4,7 +4,7 @@ describe KubernetesService, models: true, caching: true do
include KubernetesHelpers
include ReactiveCachingHelpers
- let(:project) { create(:kubernetes_project) }
+ let(:project) { build_stubbed(:kubernetes_project) }
let(:service) { project.kubernetes_service }
# We use Kubeclient to interactive with the Kubernetes API. It will
@@ -32,7 +32,8 @@ describe KubernetesService, models: true, caching: true do
describe 'Validations' do
context 'when service is active' do
before { subject.active = true }
- it { is_expected.to validate_presence_of(:namespace) }
+
+ it { is_expected.not_to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:api_url) }
it { is_expected.to validate_presence_of(:token) }
@@ -53,9 +54,9 @@ describe KubernetesService, models: true, caching: true do
'a' * 63 => true,
'a' * 64 => false,
'a.b' => false,
- 'a*b' => false,
+ 'a*b' => false
}.each do |namespace, validity|
- it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do
+ it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
subject.namespace = namespace
expect(subject.valid?).to eq(validity)
@@ -66,24 +67,40 @@ describe KubernetesService, models: true, caching: true do
context 'when service is inactive' do
before { subject.active = false }
- it { is_expected.not_to validate_presence_of(:namespace) }
+
it { is_expected.not_to validate_presence_of(:api_url) }
it { is_expected.not_to validate_presence_of(:token) }
end
end
describe '#initialize_properties' do
- context 'with a project' do
- let(:namespace_name) { "#{project.path}-#{project.id}" }
+ context 'without a project' do
+ it 'leaves the namespace unset' do
+ expect(described_class.new.namespace).to be_nil
+ end
+ end
+ end
+
+ describe '#fields' do
+ let(:kube_namespace) do
+ subject.fields.find { |h| h[:name] == 'namespace' }
+ end
+
+ context 'as template' do
+ before { subject.template = true }
- it 'defaults to the project name with ID' do
- expect(described_class.new(project: project).namespace).to eq(namespace_name)
+ it 'sets the namespace to the default' do
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:placeholder]).to eq(subject.class::TEMPLATE_PLACEHOLDER)
end
end
- context 'without a project' do
- it 'leaves the namespace unset' do
- expect(described_class.new.namespace).to be_nil
+ context 'with associated project' do
+ before { subject.project = project }
+
+ it 'sets the namespace to the default' do
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
end
end
end
@@ -138,38 +155,40 @@ describe KubernetesService, models: true, caching: true do
before do
subject.api_url = 'https://kube.domain.com'
subject.token = 'token'
- subject.namespace = 'my-project'
subject.ca_pem = 'CA PEM DATA'
+ subject.project = project
end
- it 'sets KUBE_URL' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
- )
- end
+ context 'namespace is provided' do
+ before { subject.namespace = 'my-project' }
- it 'sets KUBE_TOKEN' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_TOKEN', value: 'token', public: false }
- )
+ it 'sets the variables' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
+ { key: 'KUBE_TOKEN', value: 'token', public: false },
+ { key: 'KUBE_NAMESPACE', value: 'my-project', public: true },
+ { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
+ )
+ end
end
- it 'sets KUBE_NAMESPACE' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
- )
- end
+ context 'no namespace provided' do
+ it 'sets the variables' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
+ { key: 'KUBE_TOKEN', value: 'token', public: false },
+ { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
+ )
+ end
- it 'sets KUBE_CA_PEM' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
- )
- end
+ it 'sets the KUBE_NAMESPACE' do
+ kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
- it 'sets KUBE_CA_PEM_FILE' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
- )
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
+ end
end
end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
new file mode 100644
index 00000000000..facc034f69c
--- /dev/null
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -0,0 +1,277 @@
+require 'spec_helper'
+
+describe MicrosoftTeamsService, models: true do
+ let(:chat_service) { described_class.new }
+ let(:webhook_url) { 'https://example.gitlab.com/' }
+
+ 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 { subject.active = true }
+
+ it { is_expected.to validate_presence_of(:webhook) }
+ it_behaves_like 'issue tracker service URL attribute', :webhook
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+
+ it { is_expected.not_to validate_presence_of(:webhook) }
+ end
+ end
+
+ describe "#execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ context 'with push events' do
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ it "calls Microsoft Teams API for push events" do
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
+ it 'specifies the webhook when it is configured' do
+ expect(MicrosoftTeams::Notifier).to receive(:new).with(webhook_url).and_return(double(:microsoft_teams_service).as_null_object)
+
+ chat_service.execute(push_sample_data)
+ end
+ end
+
+ context 'with issue events' do
+ let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
+ let(:issues_sample_data) do
+ service = Issues::CreateService.new(project, user, opts)
+ issue = service.execute
+ service.hook_data(issue, 'open')
+ end
+
+ it "calls Microsoft Teams API" do
+ chat_service.execute(issues_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with merge events' do
+ let(:opts) do
+ {
+ title: 'Awesome merge_request',
+ description: 'please fix',
+ source_branch: 'feature',
+ target_branch: 'master'
+ }
+ end
+
+ let(:merge_sample_data) do
+ service = MergeRequests::CreateService.new(project, user, opts)
+ merge_request = service.execute
+ service.hook_data(merge_request, 'open')
+ end
+
+ it "calls Microsoft Teams API" do
+ chat_service.execute(merge_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with wiki page events' do
+ let(:opts) do
+ {
+ title: "Awesome wiki_page",
+ content: "Some text describing some thing or another",
+ format: "md",
+ message: "user created page: Awesome wiki_page"
+ }
+ end
+
+ let(:wiki_page_sample_data) do
+ service = WikiPages::CreateService.new(project, user, opts)
+ wiki_page = service.execute
+ service.hook_data(wiki_page, 'create')
+ end
+
+ it "calls Microsoft Teams API" do
+ chat_service.execute(wiki_page_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+ end
+
+ describe "Note events" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, creator: user) }
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ project_id: project.id,
+ service_hook: true,
+ webhook: webhook_url
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ context 'when commit comment event executed' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user,
+ project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "calls Microsoft Teams API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
+
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when merge request comment event executed' do
+ let(:merge_request_note) do
+ create(:note_on_merge_request, project: project,
+ note: "merge request note")
+ end
+
+ it "calls Microsoft Teams API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
+
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when issue comment event executed' do
+ let(:issue_note) do
+ create(:note_on_issue, project: project, note: "issue note")
+ end
+
+ it "calls Microsoft Teams API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'when snippet comment event executed' do
+ let(:snippet_note) do
+ create(:note_on_project_snippet, project: project,
+ note: "snippet note")
+ end
+
+ it "calls Microsoft Teams API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
+
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+ end
+
+ describe 'Pipeline events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: status,
+ sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ before do
+ allow(chat_service).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+ end
+
+ shared_examples 'call Microsoft Teams API' do
+ before do
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it 'calls Microsoft Teams API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+
+ chat_service.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'call Microsoft Teams API'
+ end
+
+ context 'with succeeded pipeline' do
+ let(:status) { 'success' }
+
+ context 'with default to notify_only_broken_pipelines' do
+ it 'does not call Microsoft Teams API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = chat_service.execute(data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context 'with setting notify_only_broken_pipelines to false' do
+ before do
+ chat_service.notify_only_broken_pipelines = false
+ end
+
+ it_behaves_like 'call Microsoft Teams API'
+ end
+ end
+
+ context 'only notify for the default branch' do
+ context 'when enabled' do
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch')
+ end
+
+ before do
+ chat_service.notify_only_default_branch = true
+ end
+
+ it 'does not call the Microsoft Teams API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = chat_service.execute(data)
+
+ expect(result).to be_falsy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/pipeline_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index 03932895b0e..03932895b0e 100644
--- a/spec/models/project_services/pipeline_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index 45b2f1068bf..a76e909d04d 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -40,7 +40,7 @@ describe PivotaltrackerService, models: true do
name: 'Some User'
},
url: 'https://example.com/commit',
- message: 'commit message',
+ message: 'commit message'
}
]
}
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index d15079b686b..1f9d3c07b51 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -6,6 +6,7 @@ describe PrometheusService, models: true, caching: true do
let(:project) { create(:prometheus_project) }
let(:service) { project.prometheus_service }
+ let(:environment_query) { Gitlab::Prometheus::Queries::EnvironmentQuery }
describe "Associations" do
it { is_expected.to belong_to :project }
@@ -45,17 +46,18 @@ describe PrometheusService, models: true, caching: true do
end
end
- describe '#metrics' do
+ describe '#environment_metrics' do
let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
- subject { service.metrics(environment) }
around do |example|
Timecop.freeze { example.run }
end
context 'with valid data' do
+ subject { service.environment_metrics(environment) }
+
before do
- stub_reactive_cache(service, prometheus_data, 'env-slug')
+ stub_reactive_cache(service, prometheus_data, environment_query, environment.id)
end
it 'returns reactive data' do
@@ -64,15 +66,36 @@ describe PrometheusService, models: true, caching: true do
end
end
+ describe '#deployment_metrics' do
+ let(:deployment) { build_stubbed(:deployment)}
+ let(:deployment_query) { Gitlab::Prometheus::Queries::DeploymentQuery }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with valid data' do
+ subject { service.deployment_metrics(deployment) }
+
+ before do
+ stub_reactive_cache(service, prometheus_data, deployment_query, deployment.id)
+ end
+
+ it 'returns reactive data' do
+ is_expected.to eq(prometheus_data.merge(deployment_time: deployment.created_at.to_i))
+ end
+ end
+ end
+
describe '#calculate_reactive_cache' do
- let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+ let(:environment) { create(:environment, slug: 'env-slug') }
around do |example|
Timecop.freeze { example.run }
end
subject do
- service.calculate_reactive_cache(environment.slug)
+ service.calculate_reactive_cache(environment_query.to_s, environment.id)
end
context 'when service is inactive' do
@@ -94,7 +117,7 @@ describe PrometheusService, models: true, caching: true do
[404, 500].each do |status|
context "when Prometheus responds with #{status}" do
before do
- stub_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!')
+ stub_all_prometheus_requests(environment.slug, status: status, body: "QUERY FAILED!")
end
it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index d9d7c0b0aaa..5fe4885eeb4 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -5,9 +5,6 @@ describe ProjectSnippet, models: true do
it { is_expected.to belong_to(:project) }
end
- describe "Mass assignment" do
- end
-
describe "Validation" do
it { is_expected.to validate_presence_of(:project) }
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 59a2560ca06..f2b4e9070b4 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -22,6 +22,7 @@ describe Project, models: true do
it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
+ it { is_expected.to have_one(:microsoft_teams_service).dependent(:destroy) }
it { is_expected.to have_one(:mattermost_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
@@ -57,6 +58,7 @@ describe Project, models: true do
it { is_expected.to have_many(:builds) }
it { is_expected.to have_many(:runner_projects) }
it { is_expected.to have_many(:runners) }
+ it { is_expected.to have_many(:active_runners) }
it { is_expected.to have_many(:variables) }
it { is_expected.to have_many(:triggers) }
it { is_expected.to have_many(:pages_domains) }
@@ -71,6 +73,7 @@ describe Project, models: true do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:pipeline_schedules).dependent(:destroy) }
context 'after initialized' do
it "has a project_feature" do
@@ -251,6 +254,34 @@ describe Project, models: true do
expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.')
end
end
+
+ describe 'path validation' do
+ it 'allows paths reserved on the root namespace' do
+ project = build(:project, path: 'api')
+
+ expect(project).to be_valid
+ end
+
+ it 'rejects paths reserved on another level' do
+ project = build(:project, path: 'tree')
+
+ expect(project).not_to be_valid
+ end
+
+ it 'rejects nested paths' do
+ parent = create(:group, :nested, path: 'environments')
+ project = build(:project, path: 'folders', namespace: parent)
+
+ expect(project).not_to be_valid
+ end
+
+ it 'allows a reserved group name' do
+ parent = create(:group)
+ project = build(:project, path: 'avatar', namespace: parent)
+
+ expect(project).to be_valid
+ end
+ end
end
describe 'default_scope' do
@@ -702,25 +733,6 @@ describe Project, models: true do
end
end
- describe '#open_branches' do
- let(:project) { create(:project, :repository) }
-
- before do
- project.protected_branches.create(name: 'master')
- end
-
- it { expect(project.open_branches.map(&:name)).to include('feature') }
- it { expect(project.open_branches.map(&:name)).not_to include('master') }
-
- it "includes branches matching a protected branch wildcard" do
- expect(project.open_branches.map(&:name)).to include('feature')
-
- create(:protected_branch, name: 'feat*', project: project)
-
- expect(Project.find(project.id).open_branches.map(&:name)).to include('feature')
- end
- end
-
describe '#star_count' do
it 'counts stars from multiple users' do
user1 = create :user
@@ -798,17 +810,19 @@ describe Project, models: true do
let(:project) { create(:empty_project) }
- context 'When avatar file is uploaded' do
- before do
- project.update_columns(avatar: 'uploads/avatar.png')
- allow(project.avatar).to receive(:present?) { true }
- end
+ context 'when avatar file is uploaded' do
+ let(:project) { create(:empty_project, :with_avatar) }
+ let(:avatar_path) { "/uploads/project/avatar/#{project.id}/dk.png" }
+ let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
- let(:avatar_path) do
- "/uploads/project/avatar/#{project.id}/uploads/avatar.png"
- end
+ it 'shows correct url' do
+ expect(project.avatar_url).to eq(avatar_path)
+ expect(project.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
- it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+
+ expect(project.avatar_url).to eq([gitlab_host, avatar_path].join)
+ end
end
context 'When avatar file in git' do
@@ -816,9 +830,7 @@ describe Project, models: true do
allow(project).to receive(:avatar_in_git) { true }
end
- let(:avatar_path) do
- "/#{project.full_path}/avatar"
- end
+ let(:avatar_path) { "/#{project.full_path}/avatar" }
it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
@@ -961,7 +973,7 @@ describe Project, models: true do
before do
storages = {
'default' => { 'path' => 'tmp/tests/repositories' },
- 'picked' => { 'path' => 'tmp/tests/repositories' },
+ 'picked' => { 'path' => 'tmp/tests/repositories' }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -1157,11 +1169,12 @@ describe Project, models: true do
# Project#gitlab_shell returns a new instance of Gitlab::Shell on every
# call. This makes testing a bit easier.
allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)
-
allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
end
it 'renames a repository' do
+ stub_container_registry_config(enabled: false)
+
expect(gitlab_shell).to receive(:mv_repository).
ordered.
with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}").
@@ -1185,10 +1198,13 @@ describe Project, models: true do
project.rename_repo
end
- context 'container registry with tags' do
+ context 'container registry with images' do
+ let(:container_repository) { create(:container_repository) }
+
before do
stub_container_registry_config(enabled: true)
- stub_container_registry_tags('tag')
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+ project.container_repositories << container_repository
end
subject { project.rename_repo }
@@ -1291,62 +1307,6 @@ describe Project, models: true do
end
end
- describe '#protected_branch?' do
- context 'existing project' do
- let(:project) { create(:project, :repository) }
-
- it 'returns true when the branch matches a protected branch via direct match' do
- create(:protected_branch, project: project, name: "foo")
-
- expect(project.protected_branch?('foo')).to eq(true)
- end
-
- it 'returns true when the branch matches a protected branch via wildcard match' do
- create(:protected_branch, project: project, name: "production/*")
-
- expect(project.protected_branch?('production/some-branch')).to eq(true)
- end
-
- it 'returns false when the branch does not match a protected branch via direct match' do
- expect(project.protected_branch?('foo')).to eq(false)
- end
-
- it 'returns false when the branch does not match a protected branch via wildcard match' do
- create(:protected_branch, project: project, name: "production/*")
-
- expect(project.protected_branch?('staging/some-branch')).to eq(false)
- end
- end
-
- context "new project" do
- let(:project) { create(:empty_project) }
-
- it 'returns false when default_protected_branch is unprotected' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
-
- expect(project.protected_branch?('master')).to be false
- end
-
- it 'returns false when default_protected_branch lets developers push' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
-
- expect(project.protected_branch?('master')).to be false
- end
-
- it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
-
- expect(project.protected_branch?('master')).to be true
- end
-
- it 'returns true when default_branch_protection is in full protection' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
-
- expect(project.protected_branch?('master')).to be true
- end
- end
- end
-
describe '#user_can_push_to_empty_repo?' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
@@ -1386,38 +1346,17 @@ describe Project, models: true do
end
end
- describe '#container_registry_path_with_namespace' do
- let(:project) { create(:empty_project, path: 'PROJECT') }
-
- subject { project.container_registry_path_with_namespace }
-
- it { is_expected.not_to eq(project.path_with_namespace) }
- it { is_expected.to eq(project.path_with_namespace.downcase) }
- end
-
- describe '#container_registry_repository' do
+ describe '#container_registry_url' do
let(:project) { create(:empty_project) }
- before { stub_container_registry_config(enabled: true) }
-
- subject { project.container_registry_repository }
-
- it { is_expected.not_to be_nil }
- end
-
- describe '#container_registry_repository_url' do
- let(:project) { create(:empty_project) }
-
- subject { project.container_registry_repository_url }
+ subject { project.container_registry_url }
before { stub_container_registry_config(**registry_settings) }
context 'for enabled registry' do
let(:registry_settings) do
- {
- enabled: true,
- host_port: 'example.com',
- }
+ { enabled: true,
+ host_port: 'example.com' }
end
it { is_expected.not_to be_nil }
@@ -1425,9 +1364,7 @@ describe Project, models: true do
context 'for disabled registry' do
let(:registry_settings) do
- {
- enabled: false
- }
+ { enabled: false }
end
it { is_expected.to be_nil }
@@ -1437,28 +1374,60 @@ describe Project, models: true do
describe '#has_container_registry_tags?' do
let(:project) { create(:empty_project) }
- subject { project.has_container_registry_tags? }
-
- context 'for enabled registry' do
+ context 'when container registry is enabled' do
before { stub_container_registry_config(enabled: true) }
- context 'with tags' do
- before { stub_container_registry_tags('test', 'test2') }
+ context 'when tags are present for multi-level registries' do
+ before do
+ create(:container_repository, project: project, name: 'image')
+
+ stub_container_registry_tags(repository: /image/,
+ tags: %w[latest rc1])
+ end
- it { is_expected.to be_truthy }
+ it 'should have image tags' do
+ expect(project).to have_container_registry_tags
+ end
end
- context 'when no tags' do
- before { stub_container_registry_tags }
+ context 'when tags are present for root repository' do
+ before do
+ stub_container_registry_tags(repository: project.full_path,
+ tags: %w[latest rc1 pre1])
+ end
- it { is_expected.to be_falsey }
+ it 'should have image tags' do
+ expect(project).to have_container_registry_tags
+ end
+ end
+
+ context 'when there are no tags at all' do
+ before do
+ stub_container_registry_tags(repository: :any, tags: [])
+ end
+
+ it 'should not have image tags' do
+ expect(project).not_to have_container_registry_tags
+ end
end
end
- context 'for disabled registry' do
+ context 'when container registry is disabled' do
before { stub_container_registry_config(enabled: false) }
- it { is_expected.to be_falsey }
+ it 'should not have image tags' do
+ expect(project).not_to have_container_registry_tags
+ end
+
+ it 'should not check root repository tags' do
+ expect(project).not_to receive(:full_path)
+ expect(project).not_to have_container_registry_tags
+ end
+
+ it 'should iterate through container repositories' do
+ expect(project).to receive(:container_repositories)
+ expect(project).not_to have_container_registry_tags
+ end
end
end
@@ -1934,11 +1903,38 @@ describe Project, models: true do
describe '#pipeline_status' do
let(:project) { create(:project) }
it 'builds a pipeline status' do
- expect(project.pipeline_status).to be_a(Ci::PipelineStatus)
+ expect(project.pipeline_status).to be_a(Gitlab::Cache::Ci::ProjectPipelineStatus)
end
it 'hase a loaded pipeline status' do
expect(project.pipeline_status).to be_loaded
end
end
+
+ describe '#append_or_update_attribute' do
+ let(:project) { create(:project) }
+
+ it 'shows full error updating an invalid MR' do
+ error_message = 'Failed to replace merge_requests because one or more of the new records could not be saved.'\
+ ' Validate fork Source project is not a fork of the target project'
+
+ expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) }.
+ to raise_error(ActiveRecord::RecordNotSaved, error_message)
+ end
+
+ it 'updates the project succesfully' do
+ merge_request = create(:merge_request, target_project: project, source_project: project)
+
+ expect { project.append_or_update_attribute(:merge_requests, [merge_request]) }.
+ not_to raise_error
+ end
+ end
+
+ describe '#last_repository_updated_at' do
+ it 'sets to created_at upon creation' do
+ project = create(:empty_project, created_at: 2.hours.ago)
+
+ expect(project.last_repository_updated_at.to_i).to eq(project.created_at.to_i)
+ end
+ end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index ff29f6f66ba..c5ffbda9821 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -35,7 +35,7 @@ describe ProjectStatistics, models: true do
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 4.exabytes - 1,
+ build_artifacts_size: 4.exabytes - 1
)
statistics.reload
@@ -149,7 +149,7 @@ describe ProjectStatistics, models: true do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
- lfs_objects_size: 3,
+ lfs_objects_size: 3
)
statistics.reload
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index b5b9cd024b0..969e9f7a130 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -213,9 +213,12 @@ describe ProjectWiki, models: true do
end
it 'updates project activity' do
- expect(subject).to receive(:update_project_activity)
-
subject.create_page('Test Page', 'This is content')
+
+ project.reload
+
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
@@ -240,9 +243,12 @@ describe ProjectWiki, models: true do
end
it 'updates project activity' do
- expect(subject).to receive(:update_project_activity)
-
subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again')
+
+ project.reload
+
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
@@ -258,9 +264,12 @@ describe ProjectWiki, models: true do
end
it 'updates project activity' do
- expect(subject).to receive(:update_project_activity)
-
subject.delete_page(@page)
+
+ project.reload
+
+ expect(project.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(project.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
end
diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb
new file mode 100644
index 00000000000..4c9bade592b
--- /dev/null
+++ b/spec/models/protectable_dropdown_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ProtectableDropdown, models: true do
+ let(:project) { create(:project, :repository) }
+ let(:subject) { described_class.new(project, :branches) }
+
+ describe '#protectable_ref_names' do
+ before do
+ project.protected_branches.create(name: 'master')
+ end
+
+ it { expect(subject.protectable_ref_names).to include('feature') }
+ it { expect(subject.protectable_ref_names).not_to include('master') }
+
+ it "includes branches matching a protected branch wildcard" do
+ expect(subject.protectable_ref_names).to include('feature')
+
+ create(:protected_branch, name: 'feat*', project: project)
+
+ subject = described_class.new(project.reload, :branches)
+
+ expect(subject.protectable_ref_names).to include('feature')
+ end
+ end
+end
diff --git a/spec/models/protected_branch/merge_access_level_spec.rb b/spec/models/protected_branch/merge_access_level_spec.rb
new file mode 100644
index 00000000000..1e7242e9fa8
--- /dev/null
+++ b/spec/models/protected_branch/merge_access_level_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe ProtectedBranch::MergeAccessLevel, :models do
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+end
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
new file mode 100644
index 00000000000..de68351198c
--- /dev/null
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe ProtectedBranch::PushAccessLevel, :models do
+ it { is_expected.to validate_inclusion_of(:access_level).in_array([Gitlab::Access::MASTER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS]) }
+end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 8bf0d24a128..ca347cf92c9 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -7,9 +7,6 @@ describe ProtectedBranch, models: true do
it { is_expected.to belong_to(:project) }
end
- describe "Mass assignment" do
- end
-
describe 'Validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:name) }
@@ -113,8 +110,8 @@ describe ProtectedBranch, models: true do
staging = build(:protected_branch, name: "staging")
expect(ProtectedBranch.matching("production")).to be_empty
- expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).to include(production)
- expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).not_to include(staging)
+ expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).to include(production)
+ expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).not_to include(staging)
end
end
@@ -132,8 +129,64 @@ describe ProtectedBranch, models: true do
staging = build(:protected_branch, name: "staging/*")
expect(ProtectedBranch.matching("production/some-branch")).to be_empty
- expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).to include(production)
- expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).not_to include(staging)
+ expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).to include(production)
+ expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).not_to include(staging)
+ end
+ end
+ end
+
+ describe '#protected?' do
+ context 'existing project' do
+ let(:project) { create(:project, :repository) }
+
+ it 'returns true when the branch matches a protected branch via direct match' do
+ create(:protected_branch, project: project, name: "foo")
+
+ expect(ProtectedBranch.protected?(project, 'foo')).to eq(true)
+ end
+
+ it 'returns true when the branch matches a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
+
+ expect(ProtectedBranch.protected?(project, 'production/some-branch')).to eq(true)
+ end
+
+ it 'returns false when the branch does not match a protected branch via direct match' do
+ expect(ProtectedBranch.protected?(project, 'foo')).to eq(false)
+ end
+
+ it 'returns false when the branch does not match a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
+
+ expect(ProtectedBranch.protected?(project, 'staging/some-branch')).to eq(false)
+ end
+ end
+
+ context "new project" do
+ let(:project) { create(:empty_project) }
+
+ it 'returns false when default_protected_branch is unprotected' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be false
+ end
+
+ it 'returns false when default_protected_branch lets developers push' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be false
+ end
+
+ it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be true
+ end
+
+ it 'returns true when default_branch_protection is in full protection' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be true
end
end
end
diff --git a/spec/models/protected_tag_spec.rb b/spec/models/protected_tag_spec.rb
new file mode 100644
index 00000000000..51353852a93
--- /dev/null
+++ b/spec/models/protected_tag_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe ProtectedTag, models: true do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validation' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:name) }
+ end
+end
diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb
new file mode 100644
index 00000000000..71827421dd7
--- /dev/null
+++ b/spec/models/redirect_route_spec.rb
@@ -0,0 +1,27 @@
+require 'rails_helper'
+
+describe RedirectRoute, models: true do
+ let(:group) { create(:group) }
+ let!(:redirect_route) { group.redirect_routes.create(path: 'gitlabb') }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:source) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:source) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_uniqueness_of(:path) }
+ end
+
+ describe '.matching_path_and_descendants' do
+ let!(:redirect2) { group.redirect_routes.create(path: 'gitlabb/test') }
+ let!(:redirect3) { group.redirect_routes.create(path: 'gitlabb/test/foo') }
+ let!(:redirect4) { group.redirect_routes.create(path: 'gitlabb/test/foo/bar') }
+ let!(:redirect5) { group.redirect_routes.create(path: 'gitlabb/test/baz') }
+
+ it 'returns correct routes' do
+ expect(RedirectRoute.matching_path_and_descendants('gitlabb/test')).to match_array([redirect2, redirect3, redirect4, redirect5])
+ end
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 585b87b828d..718b7d5e86b 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Repository, models: true do
include RepoHelpers
- TestBlob = Struct.new(:name)
+ TestBlob = Struct.new(:path)
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
@@ -24,21 +24,8 @@ describe Repository, models: true do
repository.commit(merge_commit_id)
end
- let(:author_email) { FFaker::Internet.email }
-
- # I have to remove periods from the end of the name
- # This happened when the user's name had a suffix (i.e. "Sr.")
- # This seems to be what git does under the hood. For example, this commit:
- #
- # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
- #
- # results in this:
- #
- # $ git show --pretty
- # ...
- # Author: Foo Sr <foo@example.com>
- # ...
- let(:author_name) { FFaker::Name.name.chomp("\.") }
+ let(:author_email) { 'user@example.org' }
+ let(:author_name) { 'John Doe' }
describe '#branch_names_contains' do
subject { repository.branch_names_contains(sample_commit.id) }
@@ -123,22 +110,11 @@ describe Repository, models: true do
end
describe '#ref_name_for_sha' do
- context 'ref found' do
- it 'returns the ref' do
- allow_any_instance_of(Gitlab::Popen).to receive(:popen).
- and_return(["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0])
-
- expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
- end
- end
-
- context 'ref not found' do
- it 'returns nil' do
- allow_any_instance_of(Gitlab::Popen).to receive(:popen).
- and_return(["", 0])
+ it 'returns the ref' do
+ allow(repository.raw_repository).to receive(:ref_name_for_sha).
+ and_return('refs/environments/production/77')
- expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq nil
- end
+ expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
end
end
@@ -184,6 +160,27 @@ describe Repository, models: true do
end
end
+ describe '#commits' do
+ it 'sets follow when path is a single path' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
+
+ repository.commits('master', path: 'README.md')
+ repository.commits('master', path: ['README.md'])
+ end
+
+ it 'does not set follow when path is multiple paths' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
+
+ repository.commits('master', path: ['README.md', 'CHANGELOG'])
+ end
+
+ it 'does not set follow when there are no paths' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
+
+ repository.commits('master')
+ end
+ end
+
describe '#find_commits_by_message' do
it 'returns commits with messages containing a given string' do
commit_ids = repository.find_commits_by_message('submodule').map(&:id)
@@ -557,31 +554,31 @@ describe Repository, models: true do
it 'accepts changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
- expect(repository.changelog.name).to eq('changelog')
+ expect(repository.changelog.path).to eq('changelog')
end
it 'accepts news instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')])
- expect(repository.changelog.name).to eq('news')
+ expect(repository.changelog.path).to eq('news')
end
it 'accepts history instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')])
- expect(repository.changelog.name).to eq('history')
+ expect(repository.changelog.path).to eq('history')
end
it 'accepts changes instead of changelog' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')])
- expect(repository.changelog.name).to eq('changes')
+ expect(repository.changelog.path).to eq('changes')
end
it 'is case-insensitive' do
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')])
- expect(repository.changelog.name).to eq('CHANGELOG')
+ expect(repository.changelog.path).to eq('CHANGELOG')
end
end
@@ -616,7 +613,7 @@ describe Repository, models: true do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
- expect(repository.license_blob.name).to eq('LICENSE')
+ expect(repository.license_blob.path).to eq('LICENSE')
end
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
@@ -646,7 +643,7 @@ describe Repository, models: true do
expect(repository.license_key).to be_nil
end
- it 'detects license file with no recognizable open-source license content' do
+ it 'returns nil when the content is not recognizable' do
repository.create_file(user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
@@ -662,12 +659,45 @@ describe Repository, models: true do
end
end
+ describe '#license' do
+ before do
+ repository.delete_file(user, 'LICENSE',
+ message: 'Remove LICENSE', branch_name: 'master')
+ end
+
+ it 'returns nil when no license is detected' do
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns nil when the repository does not exist' do
+ expect(repository).to receive(:exists?).and_return(false)
+
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns nil when the content is not recognizable' do
+ repository.create_file(user, 'LICENSE', 'Copyright!',
+ message: 'Add LICENSE', branch_name: 'master')
+
+ expect(repository.license).to be_nil
+ end
+
+ it 'returns the license' do
+ license = Licensee::License.new('mit')
+ repository.create_file(user, 'LICENSE',
+ license.content,
+ message: 'Add LICENSE', branch_name: 'master')
+
+ expect(repository.license).to eq(license)
+ end
+ end
+
describe "#gitlab_ci_yml", caching: true do
it 'returns valid file' do
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
expect(repository.tree).to receive(:blobs).and_return(files)
- expect(repository.gitlab_ci_yml.name).to eq('.gitlab-ci.yml')
+ expect(repository.gitlab_ci_yml.path).to eq('.gitlab-ci.yml')
end
it 'returns nil if not exists' do
@@ -1090,21 +1120,33 @@ describe Repository, models: true do
end
describe '#merge' do
- it 'merges the code and return the commit id' do
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
+
+ let(:commit_options) do
+ author = repository.user_to_committer(user)
+ { message: 'Test \r\n\r\n message', committer: author, author: author }
+ end
+
+ it 'merges the code and returns the commit id' do
expect(merge_commit).to be_present
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
-
- merge_commit_id = repository.merge(user,
- merge_request.diff_head_sha,
- merge_request,
- commit_options)
+ merge_commit_id = merge(repository, user, merge_request, commit_options)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
+
+ it 'removes carriage returns from commit message' do
+ merge_commit_id = merge(repository, user, merge_request, commit_options)
+
+ expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r"))
+ end
+
+ def merge(repository, user, merge_request, options = {})
+ repository.merge(user, merge_request.diff_head_sha, merge_request, options)
+ end
end
describe '#revert' do
@@ -1272,7 +1314,6 @@ describe Repository, models: true do
:changelog,
:license,
:contributing,
- :version,
:gitignore,
:koding,
:gitlab_ci,
@@ -1293,19 +1334,9 @@ describe Repository, models: true do
end
end
- describe '#before_import' do
- it 'flushes the repository caches' do
- expect(repository).to receive(:expire_content_cache)
-
- repository.before_import
- end
- end
-
describe '#after_import' do
it 'flushes and builds the cache' do
expect(repository).to receive(:expire_content_cache)
- expect(repository).to receive(:expire_tags_cache)
- expect(repository).to receive(:expire_branches_cache)
repository.after_import
end
@@ -1382,12 +1413,22 @@ describe Repository, models: true do
describe '#branch_count' do
it 'returns the number of branches' do
expect(repository.branch_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.branches.count
+
+ expect(repository.branch_count).to eq(rugged_count)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
expect(repository.tag_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.tags.count
+
+ expect(repository.tag_count).to eq(rugged_count)
end
end
@@ -1607,15 +1648,25 @@ describe Repository, models: true do
describe '#readme', caching: true do
context 'with a non-existing repository' do
it 'returns nil' do
- expect(repository).to receive(:tree).with(:head).and_return(nil)
+ allow(repository).to receive(:tree).with(:head).and_return(nil)
expect(repository.readme).to be_nil
end
end
context 'with an existing repository' do
- it 'returns the README' do
- expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob)
+ context 'when no README exists' do
+ it 'returns nil' do
+ allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
+
+ expect(repository.readme).to be_nil
+ end
+ end
+
+ context 'when a README exists' do
+ it 'returns the README' do
+ expect(repository.readme).to be_an_instance_of(ReadmeBlob)
+ end
end
end
end
@@ -1806,11 +1857,12 @@ describe Repository, models: true do
describe '#refresh_method_caches' do
it 'refreshes the caches of the given types' do
expect(repository).to receive(:expire_method_caches).
- with(%i(readme license_blob license_key))
+ with(%i(rendered_readme license_blob license_key license))
- expect(repository).to receive(:readme)
+ expect(repository).to receive(:rendered_readme)
expect(repository).to receive(:license_blob)
expect(repository).to receive(:license_key)
+ expect(repository).to receive(:license)
repository.refresh_method_caches(%i(readme license))
end
@@ -1851,4 +1903,22 @@ describe Repository, models: true do
end
end
end
+
+ describe '#is_ancestor?' do
+ context 'Gitaly is_ancestor feature enabled' do
+ let(:commit) { repository.commit }
+ let(:ancestor) { commit.parents.first }
+
+ before do
+ allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(true)
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+ end
+
+ it "asks Gitaly server if it's an ancestor" do
+ expect_any_instance_of(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).with(ancestor.id, commit.id)
+
+ repository.is_ancestor?(ancestor.id, commit.id)
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 171a51fcc5b..c1fe1b06c52 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -1,19 +1,43 @@
require 'spec_helper'
describe Route, models: true do
- let!(:group) { create(:group, path: 'git_lab', name: 'git_lab') }
- let!(:route) { group.route }
+ let(:group) { create(:group, path: 'git_lab', name: 'git_lab') }
+ let(:route) { group.route }
describe 'relationships' do
it { is_expected.to belong_to(:source) }
end
describe 'validations' do
+ before { route }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
it { is_expected.to validate_uniqueness_of(:path) }
end
+ describe 'callbacks' do
+ context 'after update' do
+ it 'calls #create_redirect_for_old_path' do
+ expect(route).to receive(:create_redirect_for_old_path)
+ route.update_attributes(path: 'foo')
+ end
+
+ it 'calls #delete_conflicting_redirects' do
+ expect(route).to receive(:delete_conflicting_redirects)
+ route.update_attributes(path: 'foo')
+ end
+ end
+
+ context 'after create' do
+ it 'calls #delete_conflicting_redirects' do
+ route.destroy
+ new_route = Route.new(source: group, path: group.path)
+ expect(new_route).to receive(:delete_conflicting_redirects)
+ new_route.save!
+ end
+ end
+ end
+
describe '.inside_path' do
let!(:nested_group) { create(:group, path: 'test', name: 'test', parent: group) }
let!(:deep_nested_group) { create(:group, path: 'foo', name: 'foo', parent: nested_group) }
@@ -37,7 +61,7 @@ describe Route, models: true do
context 'when route name is set' do
before { route.update_attributes(path: 'bar') }
- it "updates children routes with new path" do
+ it 'updates children routes with new path' do
expect(described_class.exists?(path: 'bar')).to be_truthy
expect(described_class.exists?(path: 'bar/test')).to be_truthy
expect(described_class.exists?(path: 'bar/test/foo')).to be_truthy
@@ -56,10 +80,24 @@ describe Route, models: true do
expect(route.update_attributes(path: 'bar')).to be_truthy
end
end
+
+ context 'when conflicting redirects exist' do
+ let!(:conflicting_redirect1) { route.create_redirect('bar/test') }
+ let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') }
+ let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
+
+ it 'deletes the conflicting redirects' do
+ route.update_attributes(path: 'bar')
+
+ expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey
+ expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey
+ expect(RedirectRoute.exists?(path: 'gitlab-org')).to be_truthy
+ end
+ end
end
context 'name update' do
- it "updates children routes with new path" do
+ it 'updates children routes with new path' do
route.update_attributes(name: 'bar')
expect(described_class.exists?(name: 'bar')).to be_truthy
@@ -77,4 +115,72 @@ describe Route, models: true do
end
end
end
+
+ describe '#create_redirect_for_old_path' do
+ context 'if the path changed' do
+ it 'creates a RedirectRoute for the old path' do
+ redirect_scope = route.source.redirect_routes.where(path: 'git_lab')
+ expect(redirect_scope.exists?).to be_falsey
+ route.path = 'new-path'
+ route.save!
+ expect(redirect_scope.exists?).to be_truthy
+ end
+ end
+ end
+
+ describe '#create_redirect' do
+ it 'creates a RedirectRoute with the same source' do
+ redirect_route = route.create_redirect('foo')
+ expect(redirect_route).to be_a(RedirectRoute)
+ expect(redirect_route).to be_persisted
+ expect(redirect_route.source).to eq(route.source)
+ expect(redirect_route.path).to eq('foo')
+ end
+ end
+
+ describe '#delete_conflicting_redirects' do
+ context 'when a redirect route with the same path exists' do
+ let!(:redirect1) { route.create_redirect(route.path) }
+
+ it 'deletes the redirect' do
+ route.delete_conflicting_redirects
+ expect(route.conflicting_redirects).to be_empty
+ end
+
+ context 'when redirect routes with paths descending from the route path exists' do
+ let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
+ let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
+ let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
+ let!(:other_redirect) { route.create_redirect("other") }
+
+ it 'deletes all redirects with paths that descend from the route path' do
+ route.delete_conflicting_redirects
+ expect(route.conflicting_redirects).to be_empty
+ end
+ end
+ end
+ end
+
+ describe '#conflicting_redirects' do
+ context 'when a redirect route with the same path exists' do
+ let!(:redirect1) { route.create_redirect(route.path) }
+
+ it 'returns the redirect route' do
+ expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
+ expect(route.conflicting_redirects).to match_array([redirect1])
+ end
+
+ context 'when redirect routes with paths descending from the route path exists' do
+ let!(:redirect2) { route.create_redirect("#{route.path}/foo") }
+ let!(:redirect3) { route.create_redirect("#{route.path}/foo/bar") }
+ let!(:redirect4) { route.create_redirect("#{route.path}/baz/quz") }
+ let!(:other_redirect) { route.create_redirect("other") }
+
+ it 'returns the redirect routes' do
+ expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3, redirect4])
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
new file mode 100644
index 00000000000..5710edbc9e0
--- /dev/null
+++ b/spec/models/sent_notification_spec.rb
@@ -0,0 +1,174 @@
+require 'spec_helper'
+
+describe SentNotification, model: true do
+ describe 'validation' do
+ describe 'note validity' do
+ context "when the project doesn't match the noteable's project" do
+ subject { build(:sent_notification, noteable: create(:issue)) }
+
+ it "is invalid" do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context "when the project doesn't match the discussion project" do
+ let(:discussion_id) { create(:note).discussion_id }
+ subject { build(:sent_notification, in_reply_to_discussion_id: discussion_id) }
+
+ it "is invalid" do
+ expect(subject).not_to be_valid
+ end
+ end
+
+ context "when the noteable project and discussion project match" do
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:discussion_id) { create(:note, project: project, noteable: issue).discussion_id }
+ subject { build(:sent_notification, project: project, noteable: issue, in_reply_to_discussion_id: discussion_id) }
+
+ it "is valid" do
+ expect(subject).to be_valid
+ end
+ end
+ end
+ end
+
+ describe '.record' do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue) }
+
+ it 'creates a new SentNotification' do
+ expect { described_class.record(issue, user.id) }.to change { SentNotification.count }.by(1)
+ end
+ end
+
+ describe '.record_note' do
+ let(:user) { create(:user) }
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ it 'creates a new SentNotification' do
+ expect { described_class.record_note(note, user.id) }.to change { SentNotification.count }.by(1)
+ end
+ end
+
+ describe '#create_reply' do
+ context 'for issue' do
+ let(:issue) { create(:issue) }
+ subject { described_class.record(issue, issue.author.id) }
+
+ it 'creates a comment on the issue' do
+ note = subject.create_reply('Test')
+ expect(note.in_reply_to?(issue)).to be_truthy
+ end
+ end
+
+ context 'for issue comment' do
+ let(:note) { create(:note_on_issue) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a comment on the issue' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
+ end
+ end
+
+ context 'for issue discussion' do
+ let(:note) { create(:discussion_note_on_issue) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
+ end
+ end
+
+ context 'for merge request' do
+ let(:merge_request) { create(:merge_request) }
+ subject { described_class.record(merge_request, merge_request.author.id) }
+
+ it 'creates a comment on the merge_request' do
+ note = subject.create_reply('Test')
+ expect(note.in_reply_to?(merge_request)).to be_truthy
+ end
+ end
+
+ context 'for merge request comment' do
+ let(:note) { create(:note_on_merge_request) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a comment on the merge request' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
+ end
+ end
+
+ context 'for merge request diff discussion' do
+ let(:note) { create(:diff_note_on_merge_request) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
+ end
+ end
+
+ context 'for merge request non-diff discussion' do
+ let(:note) { create(:discussion_note_on_merge_request) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
+ end
+ end
+
+ context 'for commit' do
+ let(:project) { create(:project) }
+ let(:commit) { project.commit }
+ subject { described_class.record(commit, project.creator.id) }
+
+ it 'creates a comment on the commit' do
+ note = subject.create_reply('Test')
+ expect(note.in_reply_to?(commit)).to be_truthy
+ end
+ end
+
+ context 'for commit comment' do
+ let(:note) { create(:note_on_commit) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a comment on the commit' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).not_to eq(note.discussion_id)
+ end
+ end
+
+ context 'for commit diff discussion' do
+ let(:note) { create(:diff_note_on_commit) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
+ end
+ end
+
+ context 'for commit non-diff discussion' do
+ let(:note) { create(:discussion_note_on_commit) }
+ subject { described_class.record_note(note, note.author.id) }
+
+ it 'creates a reply on the discussion' do
+ new_note = subject.create_reply('Test')
+ expect(new_note.in_reply_to?(note)).to be_truthy
+ expect(new_note.discussion_id).to eq(note.discussion_id)
+ end
+ end
+ end
+end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 0e2f07e945f..134882648b9 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -6,44 +6,53 @@ describe Service, models: true do
it { is_expected.to have_one :service_hook }
end
+ describe 'Validations' do
+ it { is_expected.to validate_presence_of(:type) }
+ end
+
describe "Test Button" do
- before do
- @service = Service.new
- end
+ describe '#can_test?' do
+ let(:service) { create(:service, project: project) }
- describe "Testable" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
+
+ it 'returns true' do
+ expect(service.can_test?).to be true
+ end
end
+ end
+
+ describe '#test' do
+ let(:data) { 'test' }
+ let(:service) { create(:service, project: project) }
- describe '#test' do
- let(:data) { 'test' }
+ context 'when repository is not empty' do
+ let(:project) { create(:project, :repository) }
it 'test runs execute' do
- expect(@service).to receive(:execute).with(data)
+ expect(service).to receive(:execute).with(data)
- @service.test(data)
+ service.test(data)
end
end
- end
- describe "With commits" do
- let(:project) { create(:project, :repository) }
+ context 'when repository is empty' do
+ let(:project) { create(:empty_project) }
- before do
- allow(@service).to receive(:project).and_return(project)
- @testable = @service.can_test?
- end
+ it 'test runs execute' do
+ expect(service).to receive(:execute).with(data)
- describe '#can_test?' do
- it { expect(@testable).to eq(true) }
+ service.test(data)
+ end
end
end
end
diff --git a/spec/models/snippet_blob_spec.rb b/spec/models/snippet_blob_spec.rb
new file mode 100644
index 00000000000..120b390586b
--- /dev/null
+++ b/spec/models/snippet_blob_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe SnippetBlob, models: true do
+ let(:snippet) { create(:snippet) }
+
+ subject { described_class.new(snippet) }
+
+ describe '#id' do
+ it 'returns the snippet ID' do
+ expect(subject.id).to eq(snippet.id)
+ end
+ end
+
+ describe '#name' do
+ it 'returns the snippet file name' do
+ expect(subject.name).to eq(snippet.file_name)
+ end
+ end
+
+ describe '#size' do
+ it 'returns the data size' do
+ expect(subject.size).to eq(subject.data.bytesize)
+ end
+ end
+
+ describe '#data' do
+ it 'returns the snippet content' do
+ expect(subject.data).to eq(snippet.content)
+ end
+ end
+
+ describe '#rendered_markup' do
+ context 'when the content is GFM' do
+ let(:snippet) { create(:snippet, file_name: 'file.md') }
+
+ it 'returns the rendered GFM' do
+ expect(subject.rendered_markup).to eq(snippet.content_html)
+ end
+ end
+
+ context 'when the content is not GFM' do
+ it 'returns nil' do
+ expect(subject.rendered_markup).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 8095d01b69e..1e5c96fe593 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -5,7 +5,6 @@ describe Snippet, models: true do
subject { described_class }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
- it { is_expected.to include_module(Linguist::BlobHelper) }
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
@@ -132,46 +131,6 @@ describe Snippet, models: true do
end
end
- describe '.accessible_to' do
- let(:author) { create(:author) }
- let(:project) { create(:empty_project) }
-
- let!(:public_snippet) { create(:snippet, :public) }
- let!(:internal_snippet) { create(:snippet, :internal) }
- let!(:private_snippet) { create(:snippet, :private, author: author) }
-
- let!(:project_public_snippet) { create(:snippet, :public, project: project) }
- let!(:project_internal_snippet) { create(:snippet, :internal, project: project) }
- let!(:project_private_snippet) { create(:snippet, :private, project: project) }
-
- it 'returns only public snippets when user is blank' do
- expect(described_class.accessible_to(nil)).to match_array [public_snippet, project_public_snippet]
- end
-
- it 'returns only public, and internal snippets for regular users' do
- user = create(:user)
-
- expect(described_class.accessible_to(user)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns public, internal snippets and project private snippets for project members' do
- member = create(:user)
- project.team << [member, :developer]
-
- expect(described_class.accessible_to(member)).to match_array [public_snippet, internal_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
-
- it 'returns private snippets where the user is the author' do
- expect(described_class.accessible_to(author)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet]
- end
-
- it 'returns all snippets when for admins' do
- admin = create(:admin)
-
- expect(described_class.accessible_to(admin)).to match_array [public_snippet, internal_snippet, private_snippet, project_public_snippet, project_internal_snippet, project_private_snippet]
- end
- end
-
describe '#participants' do
let(:project) { create(:empty_project, :public) }
let(:snippet) { create(:snippet, content: 'foo', project: project) }
@@ -241,4 +200,16 @@ describe Snippet, models: true do
end
end
end
+
+ describe '#blob' do
+ let(:snippet) { create(:snippet) }
+
+ it 'returns a blob representing the snippet data' do
+ blob = snippet.blob
+
+ expect(blob).to be_a(Blob)
+ expect(blob.path).to eq(snippet.file_name)
+ expect(blob.data).to eq(snippet.content)
+ end
+ end
end
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index c4ec7625cb0..838fba6c92d 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe SpamLog, models: true do
+ let(:admin) { create(:admin) }
+
describe 'associations' do
it { is_expected.to belong_to(:user) }
end
@@ -13,13 +15,18 @@ describe SpamLog, models: true do
it 'blocks the user' do
spam_log = build(:spam_log)
- expect { spam_log.remove_user }.to change { spam_log.user.blocked? }.to(true)
+ expect { spam_log.remove_user(deleted_by: admin) }.to change { spam_log.user.blocked? }.to(true)
end
it 'removes the user' do
spam_log = build(:spam_log)
+ user = spam_log.user
+
+ Sidekiq::Testing.inline! do
+ spam_log.remove_user(deleted_by: admin)
+ end
- expect { spam_log.remove_user }.to change { User.count }.by(-1)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 581305ad39f..3f80e1ac534 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -125,4 +125,50 @@ describe Todo, models: true do
expect(subject.target_reference).to eq issue.to_reference(full: true)
end
end
+
+ describe '#self_added?' do
+ let(:user_1) { build(:user) }
+
+ before do
+ subject.user = user_1
+ end
+
+ it 'is true when the user is the author' do
+ subject.author = user_1
+
+ expect(subject).to be_self_added
+ end
+
+ it 'is false when the user is not the author' do
+ subject.author = build(:user)
+
+ expect(subject).not_to be_self_added
+ end
+ end
+
+ describe '#self_assigned?' do
+ let(:user_1) { build(:user) }
+
+ before do
+ subject.user = user_1
+ subject.author = user_1
+ subject.action = Todo::ASSIGNED
+ end
+
+ it 'is true when todo is ASSIGNED and self_added' do
+ expect(subject).to be_self_assigned
+ end
+
+ it 'is false when the todo is not ASSIGNED' do
+ subject.action = Todo::MENTIONED
+
+ expect(subject).not_to be_self_assigned
+ end
+
+ it 'is false when todo is not self_added' do
+ subject.author = build(:user)
+
+ expect(subject).not_to be_self_assigned
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a9e37be1157..6a15830a15c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -24,11 +24,8 @@ describe User, models: true do
it { is_expected.to have_many(:recent_events).class_name('Event') }
it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_issues).dependent(:nullify) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
- it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
- it { is_expected.to have_one(:abuse_report) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
it { is_expected.to have_many(:todos).dependent(:destroy) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
@@ -37,6 +34,34 @@ describe User, models: true do
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
+
+ describe "#abuse_report" do
+ let(:current_user) { create(:user) }
+ let(:other_user) { create(:user) }
+
+ it { is_expected.to have_one(:abuse_report) }
+
+ it "refers to the abuse report whose user_id is the current user" do
+ abuse_report = create(:abuse_report, reporter: other_user, user: current_user)
+
+ expect(current_user.abuse_report).to eq(abuse_report)
+ end
+
+ it "does not refer to the abuse report whose reporter_id is the current user" do
+ create(:abuse_report, reporter: current_user, user: other_user)
+
+ expect(current_user.abuse_report).to be_nil
+ end
+
+ it "does not update the user_id of an abuse report when the user is updated" do
+ abuse_report = create(:abuse_report, reporter: current_user, user: other_user)
+
+ current_user.block
+
+ expect(abuse_report.reload.user).to eq(other_user)
+ end
+ end
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
@@ -72,6 +97,18 @@ describe User, models: true do
expect(user.errors.values).to eq [['dashboard is a reserved name']]
end
+ it 'allows child names' do
+ user = build(:user, username: 'avatar')
+
+ expect(user).to be_valid
+ end
+
+ it 'allows wildcard names' do
+ user = build(:user, username: 'blob')
+
+ expect(user).to be_valid
+ end
+
it 'validates uniqueness' do
expect(subject).to validate_uniqueness_of(:username).case_insensitive
end
@@ -288,7 +325,7 @@ describe User, models: true do
end
describe "Respond to" do
- it { is_expected.to respond_to(:is_admin?) }
+ it { is_expected.to respond_to(:admin?) }
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:private_token) }
it { is_expected.to respond_to(:external?) }
@@ -307,6 +344,35 @@ describe User, models: true do
end
end
+ describe '#update_tracked_fields!', :redis do
+ let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") }
+ let(:user) { create(:user) }
+
+ it 'writes trackable attributes' do
+ expect do
+ user.update_tracked_fields!(request)
+ end.to change { user.reload.current_sign_in_at }
+ end
+
+ it 'does not write trackable attributes when called a second time within the hour' do
+ user.update_tracked_fields!(request)
+
+ expect do
+ user.update_tracked_fields!(request)
+ end.not_to change { user.reload.current_sign_in_at }
+ end
+
+ it 'writes trackable attributes for a different user' do
+ user2 = create(:user)
+
+ user.update_tracked_fields!(request)
+
+ expect do
+ user2.update_tracked_fields!(request)
+ end.to change { user2.reload.current_sign_in_at }
+ end
+ end
+
shared_context 'user keys' do
let(:user) { create(:user) }
let!(:key) { create(:key, user: user) }
@@ -559,7 +625,7 @@ describe User, models: true do
describe 'normal user' do
let(:user) { create(:user, name: 'John Smith') }
- it { expect(user.is_admin?).to be_falsey }
+ it { expect(user.admin?).to be_falsey }
it { expect(user.require_ssh_key?).to be_truthy }
it { expect(user.can_create_group?).to be_truthy }
it { expect(user.can_create_project?).to be_truthy }
@@ -610,7 +676,7 @@ describe User, models: true do
protocol_and_expectation = {
'http' => false,
'ssh' => true,
- '' => true,
+ '' => true
}
protocol_and_expectation.each do |protocol, expected|
@@ -812,6 +878,75 @@ describe User, models: true do
end
end
+ describe '.find_by_full_path' do
+ let!(:user) { create(:user) }
+
+ context 'with a route matching the given path' do
+ let!(:route) { user.namespace.route }
+
+ it 'returns the user' do
+ expect(User.find_by_full_path(route.path)).to eq(user)
+ end
+
+ it 'is case-insensitive' do
+ expect(User.find_by_full_path(route.path.upcase)).to eq(user)
+ expect(User.find_by_full_path(route.path.downcase)).to eq(user)
+ end
+ end
+
+ context 'with a redirect route matching the given path' do
+ let!(:redirect_route) { user.namespace.redirect_routes.create(path: 'foo') }
+
+ context 'without the follow_redirects option' do
+ it 'returns nil' do
+ expect(User.find_by_full_path(redirect_route.path)).to eq(nil)
+ end
+ end
+
+ context 'with the follow_redirects option set to true' do
+ it 'returns the user' do
+ expect(User.find_by_full_path(redirect_route.path, follow_redirects: true)).to eq(user)
+ end
+
+ it 'is case-insensitive' do
+ expect(User.find_by_full_path(redirect_route.path.upcase, follow_redirects: true)).to eq(user)
+ expect(User.find_by_full_path(redirect_route.path.downcase, follow_redirects: true)).to eq(user)
+ end
+ end
+ end
+
+ context 'without a route or a redirect route matching the given path' do
+ context 'without the follow_redirects option' do
+ it 'returns nil' do
+ expect(User.find_by_full_path('unknown')).to eq(nil)
+ end
+ end
+ context 'with the follow_redirects option set to true' do
+ it 'returns nil' do
+ expect(User.find_by_full_path('unknown', follow_redirects: true)).to eq(nil)
+ end
+ end
+ end
+
+ context 'with a group route matching the given path' do
+ context 'when the group namespace has an owner_id (legacy data)' do
+ let!(:group) { create(:group, path: 'group_path', owner: user) }
+
+ it 'returns nil' do
+ expect(User.find_by_full_path('group_path')).to eq(nil)
+ end
+ end
+
+ context 'when the group namespace does not have an owner_id' do
+ let!(:group) { create(:group, path: 'group_path') }
+
+ it 'returns nil' do
+ expect(User.find_by_full_path('group_path')).to eq(nil)
+ end
+ end
+ end
+ end
+
describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) }
@@ -837,6 +972,24 @@ describe User, models: true do
end
end
+ describe '#avatar_url' do
+ let(:user) { create(:user, :with_avatar) }
+
+ context 'when avatar file is uploaded' do
+ let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" }
+ let(:avatar_path) { "/uploads/user/avatar/#{user.id}/dk.png" }
+
+ it 'shows correct avatar url' do
+ expect(user.avatar_url).to eq(avatar_path)
+ expect(user.avatar_url(only_path: false)).to eq([gitlab_host, avatar_path].join)
+
+ allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
+
+ expect(user.avatar_url).to eq([gitlab_host, avatar_path].join)
+ end
+ end
+ end
+
describe '#requires_ldap_check?' do
let(:user) { User.new }
@@ -1407,6 +1560,17 @@ describe User, models: true do
it { expect(user.nested_groups).to eq([nested_group]) }
end
+ describe '#all_expanded_groups' do
+ let!(:user) { create(:user) }
+ let!(:group) { create(:group) }
+ let!(:nested_group_1) { create(:group, parent: group) }
+ let!(:nested_group_2) { create(:group, parent: group) }
+
+ before { nested_group_1.add_owner(user) }
+
+ it { expect(user.all_expanded_groups).to match_array [group, nested_group_1] }
+ end
+
describe '#nested_groups_projects' do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
@@ -1520,5 +1684,135 @@ describe User, models: true do
expect(ghost.email).to eq('ghost1@example.com')
end
end
+
+ context 'when a domain whitelist is in place' do
+ before do
+ stub_application_setting(domain_whitelist: ['gitlab.com'])
+ end
+
+ it 'creates a ghost user' do
+ expect(User.ghost).to be_persisted
+ end
+ end
+ end
+
+ describe '#update_two_factor_requirement' do
+ let(:user) { create :user }
+
+ context 'with 2FA requirement on groups' do
+ let(:group1) { create :group, require_two_factor_authentication: true, two_factor_grace_period: 23 }
+ let(:group2) { create :group, require_two_factor_authentication: true, two_factor_grace_period: 32 }
+
+ before do
+ group1.add_user(user, GroupMember::OWNER)
+ group2.add_user(user, GroupMember::OWNER)
+
+ user.update_two_factor_requirement
+ end
+
+ it 'requires 2FA' do
+ expect(user.require_two_factor_authentication_from_group).to be true
+ end
+
+ it 'uses the shortest grace period' do
+ expect(user.two_factor_grace_period).to be 23
+ end
+ end
+
+ context 'with 2FA requirement on nested parent group' do
+ let!(:group1) { create :group, require_two_factor_authentication: true }
+ let!(:group1a) { create :group, require_two_factor_authentication: false, parent: group1 }
+
+ before do
+ group1a.add_user(user, GroupMember::OWNER)
+
+ user.update_two_factor_requirement
+ end
+
+ it 'requires 2FA' do
+ expect(user.require_two_factor_authentication_from_group).to be true
+ end
+ end
+
+ context 'with 2FA requirement on nested child group' do
+ let!(:group1) { create :group, require_two_factor_authentication: false }
+ let!(:group1a) { create :group, require_two_factor_authentication: true, parent: group1 }
+
+ before do
+ group1.add_user(user, GroupMember::OWNER)
+
+ user.update_two_factor_requirement
+ end
+
+ it 'requires 2FA' do
+ expect(user.require_two_factor_authentication_from_group).to be true
+ end
+ end
+
+ context 'without 2FA requirement on groups' do
+ let(:group) { create :group }
+
+ before do
+ group.add_user(user, GroupMember::OWNER)
+
+ user.update_two_factor_requirement
+ end
+
+ it 'does not require 2FA' do
+ expect(user.require_two_factor_authentication_from_group).to be false
+ end
+
+ it 'falls back to the default grace period' do
+ expect(user.two_factor_grace_period).to be 48
+ end
+ end
+ end
+
+ context '.active' do
+ before do
+ User.ghost
+ create(:user, name: 'user', state: 'active')
+ create(:user, name: 'user', state: 'blocked')
+ end
+
+ it 'only counts active and non internal users' do
+ expect(User.active.count).to eq(1)
+ end
+ end
+
+ describe 'preferred language' do
+ it 'is English by default' do
+ user = create(:user)
+
+ expect(user.preferred_language).to eq('en')
+ end
+ end
+
+ context '#invalidate_issue_cache_counts' do
+ let(:user) { build_stubbed(:user) }
+
+ it 'invalidates cache for issue counter' do
+ cache_mock = double
+
+ expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count'])
+
+ allow(Rails).to receive(:cache).and_return(cache_mock)
+
+ user.invalidate_issue_cache_counts
+ end
+ end
+
+ context '#invalidate_merge_request_cache_counts' do
+ let(:user) { build_stubbed(:user) }
+
+ it 'invalidates cache for Merge Request counter' do
+ cache_mock = double
+
+ expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_merge_requests_count'])
+
+ allow(Rails).to receive(:cache).and_return(cache_mock)
+
+ user.invalidate_merge_request_cache_counts
+ end
end
end