summaryrefslogtreecommitdiff
path: root/spec/models
diff options
context:
space:
mode:
authorHiroyuki Sato <sathiroyuki@gmail.com>2017-07-23 21:26:02 +0900
committerHiroyuki Sato <sathiroyuki@gmail.com>2017-07-23 21:26:02 +0900
commit9b25bbc45d4e6602452e230506601ff0ed8ba84a (patch)
tree95f7326e4162a325a5e7c7304c33f18194b52cbe /spec/models
parent839018d2d4e3e899b1fa06a43d093a0fdceced42 (diff)
parentb92d5135d8522e1370636799d74b51f9a37d0b2f (diff)
downloadgitlab-ce-9b25bbc45d4e6602452e230506601ff0ed8ba84a.tar.gz
Merge branch 'master' into 1827-prevent-concurrent-editing-wiki
Conflicts: app/controllers/projects/wikis_controller.rb app/views/projects/wikis/edit.html.haml spec/features/projects/wiki/user_updates_wiki_page_spec.rb
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/ability_spec.rb69
-rw-r--r--spec/models/abuse_report_spec.rb7
-rw-r--r--spec/models/application_setting_spec.rb235
-rw-r--r--spec/models/award_emoji_spec.rb14
-rw-r--r--spec/models/blob_spec.rb346
-rw-r--r--spec/models/blob_viewer/base_spec.rb149
-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/readme_spec.rb49
-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/broadcast_message_spec.rb19
-rw-r--r--spec/models/ci/artifact_blob_spec.rb44
-rw-r--r--spec/models/ci/build_spec.rb739
-rw-r--r--spec/models/ci/group_spec.rb44
-rw-r--r--spec/models/ci/group_variable_spec.rb31
-rw-r--r--spec/models/ci/legacy_stage_spec.rb (renamed from spec/models/ci/stage_spec.rb)67
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb137
-rw-r--r--spec/models/ci/pipeline_schedule_variable_spec.rb7
-rw-r--r--spec/models/ci/pipeline_spec.rb344
-rw-r--r--spec/models/ci/runner_spec.rb14
-rw-r--r--spec/models/ci/trigger_spec.rb4
-rw-r--r--spec/models/ci/variable_spec.rb46
-rw-r--r--spec/models/commit_range_spec.rb6
-rw-r--r--spec/models/commit_spec.rb65
-rw-r--r--spec/models/commit_status_spec.rb174
-rw-r--r--spec/models/concerns/access_requestable_spec.rb8
-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/case_sensitivity_spec.rb148
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb50
-rw-r--r--spec/models/concerns/each_batch_spec.rb53
-rw-r--r--spec/models/concerns/editable_spec.rb11
-rw-r--r--spec/models/concerns/feature_gate_spec.rb19
-rw-r--r--spec/models/concerns/has_status_spec.rb36
-rw-r--r--spec/models/concerns/has_variable_spec.rb43
-rw-r--r--spec/models/concerns/ignorable_column_spec.rb38
-rw-r--r--spec/models/concerns/issuable_spec.rb206
-rw-r--r--spec/models/concerns/mentionable_spec.rb53
-rw-r--r--spec/models/concerns/milestoneish_spec.rb63
-rw-r--r--spec/models/concerns/noteable_spec.rb261
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb25
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb122
-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.rb86
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb46
-rw-r--r--spec/models/concerns/spammable_spec.rb4
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb2
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb5
-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.rb2
-rw-r--r--spec/models/deployment_spec.rb77
-rw-r--r--spec/models/diff_discussion_spec.rb91
-rw-r--r--spec/models/diff_note_spec.rb380
-rw-r--r--spec/models/diff_viewer/base_spec.rb150
-rw-r--r--spec/models/diff_viewer/server_side_spec.rb36
-rw-r--r--spec/models/discussion_spec.rb623
-rw-r--r--spec/models/environment_spec.rb183
-rw-r--r--spec/models/event_spec.rb50
-rw-r--r--spec/models/forked_project_link_spec.rb80
-rw-r--r--spec/models/generic_commit_status_spec.rb9
-rw-r--r--spec/models/global_milestone_spec.rb65
-rw-r--r--spec/models/group_spec.rb185
-rw-r--r--spec/models/hooks/project_hook_spec.rb6
-rw-r--r--spec/models/hooks/service_hook_spec.rb37
-rw-r--r--spec/models/hooks/system_hook_spec.rb47
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb30
-rw-r--r--spec/models/hooks/web_hook_spec.rb93
-rw-r--r--spec/models/issue_collection_spec.rb6
-rw-r--r--spec/models/issue_spec.rb185
-rw-r--r--spec/models/key_spec.rb18
-rw-r--r--spec/models/label_spec.rb33
-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/list_spec.rb22
-rw-r--r--spec/models/member_spec.rb35
-rw-r--r--spec/models/members/group_member_spec.rb29
-rw-r--r--spec/models/members/project_member_spec.rb13
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb15
-rw-r--r--spec/models/merge_request_diff_file_spec.rb11
-rw-r--r--spec/models/merge_request_diff_spec.rb26
-rw-r--r--spec/models/merge_request_spec.rb578
-rw-r--r--spec/models/milestone_spec.rb106
-rw-r--r--spec/models/namespace_spec.rb192
-rw-r--r--spec/models/network/graph_spec.rb36
-rw-r--r--spec/models/note_spec.rb393
-rw-r--r--spec/models/notification_setting_spec.rb30
-rw-r--r--spec/models/pages_domain_spec.rb61
-rw-r--r--spec/models/personal_access_token_spec.rb20
-rw-r--r--spec/models/project_authorization_spec.rb6
-rw-r--r--spec/models/project_feature_spec.rb12
-rw-r--r--spec/models/project_group_link_spec.rb14
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb14
-rw-r--r--spec/models/project_services/bugzilla_service_spec.rb8
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb10
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb111
-rw-r--r--spec/models/project_services/campfire_service_spec.rb19
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb77
-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.rb163
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb20
-rw-r--r--spec/models/project_services/custom_issue_tracker_service_spec.rb8
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb10
-rw-r--r--spec/models/project_services/emails_on_push_service_spec.rb8
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb12
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb8
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb8
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb27
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb50
-rw-r--r--spec/models/project_services/irker_service_spec.rb8
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb32
-rw-r--r--spec/models/project_services/jira_service_spec.rb212
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb228
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb24
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb281
-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.rb10
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb51
-rw-r--r--spec/models/project_services/pushover_service_spec.rb8
-rw-r--r--spec/models/project_services/redmine_service_spec.rb12
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb16
-rw-r--r--spec/models/project_snippet_spec.rb3
-rw-r--r--spec/models/project_spec.rb758
-rw-r--r--spec/models/project_statistics_spec.rb4
-rw-r--r--spec/models/project_team_spec.rb179
-rw-r--r--spec/models/project_wiki_spec.rb79
-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.rb418
-rw-r--r--spec/models/route_spec.rb147
-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.rb96
-rw-r--r--spec/models/spam_log_spec.rb11
-rw-r--r--spec/models/system_note_metadata_spec.rb27
-rw-r--r--spec/models/todo_spec.rb46
-rw-r--r--spec/models/upload_spec.rb4
-rw-r--r--spec/models/user_spec.rb651
-rw-r--r--spec/models/wiki_page_spec.rb4
159 files changed, 10710 insertions, 3961 deletions
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 30f8fdf91b2..dc7a0d80752 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -1,6 +1,12 @@
require 'spec_helper'
describe Ability, lib: true do
+ context 'using a nil subject' do
+ it 'has no permissions' do
+ expect(Ability.policy_for(nil, nil)).to be_banned
+ end
+ end
+
describe '.can_edit_note?' do
let(:project) { create(:empty_project) }
let(:note) { create(:note_on_issue, project: project) }
@@ -63,8 +69,8 @@ describe Ability, lib: true do
project = create(:empty_project, :public)
user = build(:user)
- expect(described_class.users_that_can_read_project([user], project)).
- to eq([user])
+ expect(described_class.users_that_can_read_project([user], project))
+ .to eq([user])
end
end
@@ -74,8 +80,8 @@ describe Ability, lib: true do
it 'returns users that are administrators' do
user = build(:user, admin: true)
- expect(described_class.users_that_can_read_project([user], project)).
- to eq([user])
+ expect(described_class.users_that_can_read_project([user], project))
+ .to eq([user])
end
it 'returns internal users while skipping external users' do
@@ -83,8 +89,8 @@ describe Ability, lib: true do
user2 = build(:user, external: true)
users = [user1, user2]
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([user1])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([user1])
end
it 'returns external users if they are the project owner' do
@@ -94,8 +100,8 @@ describe Ability, lib: true do
expect(project).to receive(:owner).twice.and_return(user1)
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([user1])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([user1])
end
it 'returns external users if they are project members' do
@@ -105,8 +111,8 @@ describe Ability, lib: true do
expect(project.team).to receive(:members).twice.and_return([user1])
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([user1])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([user1])
end
it 'returns an empty Array if all users are external users without access' do
@@ -114,8 +120,8 @@ describe Ability, lib: true do
user2 = build(:user, external: true)
users = [user1, user2]
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([])
end
end
@@ -125,8 +131,8 @@ describe Ability, lib: true do
it 'returns users that are administrators' do
user = build(:user, admin: true)
- expect(described_class.users_that_can_read_project([user], project)).
- to eq([user])
+ expect(described_class.users_that_can_read_project([user], project))
+ .to eq([user])
end
it 'returns external users if they are the project owner' do
@@ -136,8 +142,8 @@ describe Ability, lib: true do
expect(project).to receive(:owner).twice.and_return(user1)
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([user1])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([user1])
end
it 'returns external users if they are project members' do
@@ -147,8 +153,8 @@ describe Ability, lib: true do
expect(project.team).to receive(:members).twice.and_return([user1])
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([user1])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([user1])
end
it 'returns an empty Array if all users are internal users without access' do
@@ -156,8 +162,8 @@ describe Ability, lib: true do
user2 = build(:user)
users = [user1, user2]
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([])
end
it 'returns an empty Array if all users are external users without access' do
@@ -165,8 +171,8 @@ describe Ability, lib: true do
user2 = build(:user, external: true)
users = [user1, user2]
- expect(described_class.users_that_can_read_project(users, project)).
- to eq([])
+ expect(described_class.users_that_can_read_project(users, project))
+ .to eq([])
end
end
end
@@ -204,8 +210,8 @@ describe Ability, lib: true do
user = build(:user, admin: true)
issue = build(:issue)
- expect(described_class.issues_readable_by_user([issue], user)).
- to eq([issue])
+ expect(described_class.issues_readable_by_user([issue], user))
+ .to eq([issue])
end
end
@@ -216,8 +222,8 @@ describe Ability, lib: true do
expect(issue).to receive(:readable_by?).with(user).and_return(true)
- expect(described_class.issues_readable_by_user([issue], user)).
- to eq([issue])
+ expect(described_class.issues_readable_by_user([issue], user))
+ .to eq([issue])
end
it 'returns an empty Array when no issues are readable' do
@@ -238,8 +244,8 @@ describe Ability, lib: true do
expect(hidden_issue).to receive(:publicly_visible?).and_return(false)
expect(visible_issue).to receive(:publicly_visible?).and_return(true)
- issues = described_class.
- issues_readable_by_user([hidden_issue, visible_issue])
+ issues = described_class
+ .issues_readable_by_user([hidden_issue, visible_issue])
expect(issues).to eq([visible_issue])
end
@@ -249,12 +255,15 @@ describe Ability, lib: true do
describe '.project_disabled_features_rules' do
let(:project) { create(:empty_project, :wiki_disabled) }
- subject { described_class.allowed(project.owner, project) }
+ subject { described_class.policy_for(project.owner, project) }
context 'wiki named abilities' do
it 'disables wiki abilities if the project has no wiki' do
expect(project).to receive(:has_external_wiki?).and_return(false)
- expect(subject).not_to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki)
+ expect(subject).not_to be_allowed(:read_wiki)
+ expect(subject).not_to be_allowed(:create_wiki)
+ expect(subject).not_to be_allowed(:update_wiki)
+ expect(subject).not_to be_allowed(:admin_wiki)
end
end
end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 4e71597521d..c1bf5551fe0 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -28,8 +28,7 @@ RSpec.describe AbuseReport, type: :model do
end
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)
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, hard_delete: true)
subject.remove_user(deleted_by: user)
end
@@ -37,8 +36,8 @@ RSpec.describe AbuseReport, type: :model do
describe '#notify' do
it 'delivers' do
- expect(AbuseReportMailer).to receive(:notify).with(subject.id).
- and_return(spy)
+ expect(AbuseReportMailer).to receive(:notify).with(subject.id)
+ .and_return(spy)
subject.notify
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 01ca1584ed2..e600eab6565 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' }
@@ -77,7 +78,9 @@ describe ApplicationSetting, models: true do
# Upgraded databases will have this sort of content
context 'repository_storages is a String, not an Array' do
- before { setting.__send__(:raw_write_attribute, :repository_storages, 'default') }
+ before do
+ setting.__send__(:raw_write_attribute, :repository_storages, 'default')
+ end
it { expect(setting.repository_storages_before_type_cast).to eq('default') }
it { expect(setting.repository_storages).to eq(['default']) }
@@ -88,7 +91,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)
@@ -152,6 +155,18 @@ describe ApplicationSetting, models: true do
end
end
+ describe '.current' do
+ context 'redis unavailable' do
+ it 'returns an ApplicationSetting' do
+ allow(Rails.cache).to receive(:fetch).and_call_original
+ allow(ApplicationSetting).to receive(:last).and_return(:last)
+ expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(ArgumentError)
+
+ expect(ApplicationSetting.current).to eq(:last)
+ end
+ end
+ end
+
context 'restricted signup domains' do
it 'sets single domain' do
setting.domain_whitelist_raw = 'example.com'
@@ -210,4 +225,220 @@ describe ApplicationSetting, models: true do
expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
end
end
+
+ describe 'performance bar settings' do
+ describe 'performance_bar_allowed_group_id=' do
+ context 'with a blank path' do
+ before do
+ setting.performance_bar_allowed_group_id = create(:group).full_path
+ end
+
+ it 'persists nil for a "" path and clears allowed user IDs cache' do
+ expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_allowed_group_id = ''
+
+ expect(setting.performance_bar_allowed_group_id).to be_nil
+ end
+ end
+
+ context 'with an invalid path' do
+ it 'does not persist an invalid group path' do
+ setting.performance_bar_allowed_group_id = 'foo'
+
+ expect(setting.performance_bar_allowed_group_id).to be_nil
+ end
+ end
+
+ context 'with a path to an existing group' do
+ let(:group) { create(:group) }
+
+ it 'persists a valid group path and clears allowed user IDs cache' do
+ expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_allowed_group_id = group.full_path
+
+ expect(setting.performance_bar_allowed_group_id).to eq(group.id)
+ end
+
+ context 'when the given path is the same' do
+ context 'with a blank path' do
+ before do
+ setting.performance_bar_allowed_group_id = nil
+ end
+
+ it 'clears the cached allowed user IDs' do
+ expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_allowed_group_id = ''
+ end
+ end
+
+ context 'with a valid path' do
+ before do
+ setting.performance_bar_allowed_group_id = group.full_path
+ end
+
+ it 'clears the cached allowed user IDs' do
+ expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_allowed_group_id = group.full_path
+ end
+ end
+ end
+ end
+ end
+
+ describe 'performance_bar_allowed_group' do
+ context 'with no performance_bar_allowed_group_id saved' do
+ it 'returns nil' do
+ expect(setting.performance_bar_allowed_group).to be_nil
+ end
+ end
+
+ context 'with a performance_bar_allowed_group_id saved' do
+ let(:group) { create(:group) }
+
+ before do
+ setting.performance_bar_allowed_group_id = group.full_path
+ end
+
+ it 'returns the group' do
+ expect(setting.performance_bar_allowed_group).to eq(group)
+ end
+ end
+ end
+
+ describe 'performance_bar_enabled' do
+ context 'with the Performance Bar is enabled' do
+ let(:group) { create(:group) }
+
+ before do
+ setting.performance_bar_allowed_group_id = group.full_path
+ end
+
+ it 'returns true' do
+ expect(setting.performance_bar_enabled).to be_truthy
+ end
+ end
+ end
+
+ describe 'performance_bar_enabled=' do
+ context 'when the performance bar is enabled' do
+ let(:group) { create(:group) }
+
+ before do
+ setting.performance_bar_allowed_group_id = group.full_path
+ end
+
+ context 'when passing true' do
+ it 'does not clear allowed user IDs cache' do
+ expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_enabled = true
+
+ expect(setting.performance_bar_allowed_group_id).to eq(group.id)
+ expect(setting.performance_bar_enabled).to be_truthy
+ end
+ end
+
+ context 'when passing false' do
+ it 'disables the performance bar and clears allowed user IDs cache' do
+ expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_enabled = false
+
+ expect(setting.performance_bar_allowed_group_id).to be_nil
+ expect(setting.performance_bar_enabled).to be_falsey
+ end
+ end
+ end
+
+ context 'when the performance bar is disabled' do
+ context 'when passing true' do
+ it 'does nothing and does not clear allowed user IDs cache' do
+ expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_enabled = true
+
+ expect(setting.performance_bar_allowed_group_id).to be_nil
+ expect(setting.performance_bar_enabled).to be_falsey
+ end
+ end
+
+ context 'when passing false' do
+ it 'does nothing and does not clear allowed user IDs cache' do
+ expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
+
+ setting.performance_bar_enabled = false
+
+ expect(setting.performance_bar_allowed_group_id).to be_nil
+ expect(setting.performance_bar_enabled).to be_falsey
+ end
+ end
+ end
+ 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 03d02b4d382..e1193e0d19a 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,120 +28,338 @@ 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) }
+
+ 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
- expect(described_class.decorate(git_blob)).not_to be_svg
+ 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
- it' is falsey when language is not SVG' do
- git_blob = double(text?: true, language: double(name: 'XML'))
+ describe '#stored_externally?' 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 true' do
+ expect(blob.stored_externally?).to be_truthy
+ end
+ end
+
+ context 'when the project does not have LFS enabled' do
+ before do
+ project.lfs_enabled = false
+ end
+
+ 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 '#video?' do
- it 'is falsey with image extension' do
- git_blob = Gitlab::Git::Blob.new(name: 'image.png')
+ 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(blob.raw_binary?).to be_falsey
+ end
+ end
+ end
- expect(described_class.decorate(git_blob)).not_to be_video
+ 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
- UploaderHelper::VIDEO_EXT.each do |ext|
- it "is truthy when extension is .#{ext}" do
- git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}")
+ 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(described_class.decorate(git_blob)).to be_video
+ 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
+
+ describe '#extension' do
+ it 'returns the extension' do
+ blob = fake_blob(path: 'file.md')
+
+ expect(blob.extension).to eq('md')
+ end
+ end
+
+ describe '#file_type' do
+ it 'returns the file type' do
+ blob = fake_blob(path: 'README.md')
+
+ expect(blob.file_type).to eq(:readme)
end
end
- describe '#to_partial_path' do
- def stubbed_blob(overrides = {})
- overrides.reverse_merge!(
- image?: false,
- language: nil,
- lfs_pointer?: false,
- svg?: false,
- text?: false
- )
+ 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
+
+ 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
+
+ context 'when the file represented by the blob is text-based' do
+ it 'returns a text viewer' do
+ blob = fake_blob
- described_class.decorate(double).tap do |blob|
- allow(blob).to receive_messages(overrides)
+ expect(blob.simple_viewer).to be_a(BlobViewer::Text)
end
end
+ end
+
+ describe '#rich_viewer' do
+ context 'when the blob has an external storage error' do
+ before do
+ project.lfs_enabled = false
+ end
- it 'handles LFS pointers' do
- blob = stubbed_blob(lfs_pointer?: true)
+ it 'returns nil' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- expect(blob.to_partial_path).to eq 'download'
+ expect(blob.rich_viewer).to be_nil
+ end
end
- it 'handles SVGs' do
- blob = stubbed_blob(text?: true, svg?: true)
+ context 'when the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
- expect(blob.to_partial_path).to eq 'image'
+ expect(blob.rich_viewer).to be_nil
+ end
end
- it 'handles images' do
- blob = stubbed_blob(image?: true)
+ context 'when the blob is stored externally' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'file.pdf', lfs: true)
- expect(blob.to_partial_path).to eq 'image'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'handles text' do
- blob = stubbed_blob(text?: true)
+ context 'when the blob is binary' do
+ it 'returns a matching binary viewer' do
+ blob = fake_blob(path: 'file.pdf', binary: true)
- expect(blob.to_partial_path).to eq 'text'
+ expect(blob.rich_viewer).to be_a(BlobViewer::PDF)
+ end
end
- it 'defaults to download' do
- blob = stubbed_blob
+ context 'when the blob is text-based' do
+ it 'returns a matching text-based viewer' do
+ blob = fake_blob(path: 'file.md')
- expect(blob.to_partial_path).to eq 'download'
+ expect(blob.rich_viewer).to be_a(BlobViewer::Markup)
+ end
end
end
- describe '#size_within_svg_limits?' do
- let(:blob) { described_class.decorate(double(:blob)) }
+ describe '#auxiliary_viewer' do
+ context 'when the blob has an external storage error' do
+ before do
+ project.lfs_enabled = false
+ end
- it 'returns true when the blob size is smaller than the SVG limit' do
- expect(blob).to receive(:size).and_return(42)
+ it 'returns nil' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
- expect(blob.size_within_svg_limits?).to eq(true)
+ expect(blob.auxiliary_viewer).to be_nil
+ 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 the blob is empty' do
+ it 'returns nil' do
+ blob = fake_blob(data: '')
- expect(blob.size_within_svg_limits?).to eq(true)
+ expect(blob.auxiliary_viewer).to be_nil
+ 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 blob is stored externally' do
+ it 'returns a matching viewer' do
+ blob = fake_blob(path: 'LICENSE', lfs: true)
- expect(blob.size_within_svg_limits?).to eq(false)
+ expect(blob.auxiliary_viewer).to be_a(BlobViewer::License)
+ end
+ end
+
+ 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
+
+ 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 '#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
+
+ 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.rendered_as_text?).to be_falsey
+ end
+ end
+ end
+
+ 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.rendered_as_text?(ignore_errors: false)).to be_falsey
+ end
+ end
+
+ context "when the viewer doesn't have render errors" do
+ it 'returns true' do
+ blob = fake_blob(path: 'file.md')
+
+ 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..574438838d8
--- /dev/null
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -0,0 +1,149 @@
+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.collapse_limit = 1.megabyte
+ self.size_limit = 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 '#collapsed?' do
+ context 'when the blob size is larger than the collapse limit' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.collapsed?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the collapse limit' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
+
+ it 'returns false' do
+ expect(viewer.collapsed?).to be_falsey
+ end
+ end
+ end
+
+ describe '#too_large?' do
+ context 'when the blob size is larger than the size limit' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
+
+ it 'returns true' do
+ expect(viewer.too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the size limit' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns false' do
+ expect(viewer.too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the blob is expanded' do
+ before do
+ blob.expand!
+ end
+
+ context 'when the blob size is larger than the size limit' 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 size limit' 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 not expanded' do
+ context 'when the blob size is larger than the collapse limit' do
+ let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
+
+ it 'returns :collapsed' do
+ expect(viewer.render_error).to eq(:collapsed)
+ end
+ end
+
+ context 'when the blob size is smaller than the collapse limit' 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/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
new file mode 100644
index 00000000000..02679dbb544
--- /dev/null
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe BlobViewer::Readme, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: 'README.md') }
+ subject { described_class.new(blob) }
+
+ describe '#render_error' do
+ context 'when there is no wiki' do
+ it 'returns :no_wiki' do
+ expect(subject.render_error).to eq(:no_wiki)
+ end
+ end
+
+ context 'when there is an external wiki' do
+ before do
+ project.has_external_wiki = true
+ end
+
+ it 'returns nil' do
+ expect(subject.render_error).to be_nil
+ end
+ end
+
+ context 'when there is a local wiki' do
+ before do
+ project.wiki_enabled = true
+ end
+
+ context 'when the wiki is empty' do
+ it 'returns :no_wiki' do
+ expect(subject.render_error).to eq(:no_wiki)
+ end
+ end
+
+ context 'when the wiki is not empty' do
+ before do
+ WikiPages::CreateService.new(project, project.owner, title: 'home', content: 'Home page').execute
+ end
+
+ it 'returns nil' do
+ expect(subject.render_error).to be_nil
+ end
+ end
+ 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/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 219db365a91..333f4139a96 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -21,22 +21,29 @@ describe BroadcastMessage, models: true do
end
describe '.current' do
- it "returns last message if time match" do
+ it 'returns message if time match' do
message = create(:broadcast_message)
- expect(BroadcastMessage.current).to eq message
+ expect(BroadcastMessage.current).to include(message)
end
- it "returns nil if time not come" do
+ it 'returns multiple messages if time match' do
+ message1 = create(:broadcast_message)
+ message2 = create(:broadcast_message)
+
+ expect(BroadcastMessage.current).to contain_exactly(message1, message2)
+ end
+
+ it 'returns empty list if time not come' do
create(:broadcast_message, :future)
- expect(BroadcastMessage.current).to be_nil
+ expect(BroadcastMessage.current).to be_empty
end
- it "returns nil if time has passed" do
+ it 'returns empty list if time has passed' do
create(:broadcast_message, :expired)
- expect(BroadcastMessage.current).to be_nil
+ expect(BroadcastMessage.current).to be_empty
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 fd6ea2d6722..0b521d720f3 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -17,8 +17,21 @@ 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 '.manual_actions' do
+ let!(:manual_but_created) { create(:ci_build, :manual, status: :created, pipeline: pipeline) }
+ let!(:manual_but_succeeded) { create(:ci_build, :manual, status: :success, pipeline: pipeline) }
+ let!(:manual_action) { create(:ci_build, :manual, pipeline: pipeline) }
+
+ subject { described_class.manual_actions }
+
+ it { is_expected.to include(manual_action) }
+ it { is_expected.to include(manual_but_succeeded) }
+ it { is_expected.not_to include(manual_but_created) }
+ end
describe '#actionize' do
context 'when build is a created' do
@@ -78,32 +91,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? }
@@ -120,12 +107,18 @@ describe Ci::Build, :models do
it { is_expected.to be_truthy }
context 'is expired' do
- before { build.update(artifacts_expire_at: Time.now - 7.days) }
+ before do
+ build.update(artifacts_expire_at: Time.now - 7.days)
+ end
+
it { is_expected.to be_falsy }
end
context 'is not expired' do
- before { build.update(artifacts_expire_at: Time.now + 7.days) }
+ before do
+ build.update(artifacts_expire_at: Time.now + 7.days)
+ end
+
it { is_expected.to be_truthy }
end
end
@@ -135,13 +128,17 @@ describe Ci::Build, :models do
subject { build.artifacts_expired? }
context 'is expired' do
- before { build.update(artifacts_expire_at: Time.now - 7.days) }
+ before do
+ build.update(artifacts_expire_at: Time.now - 7.days)
+ end
it { is_expected.to be_truthy }
end
context 'is not expired' do
- before { build.update(artifacts_expire_at: Time.now + 7.days) }
+ before do
+ build.update(artifacts_expire_at: Time.now + 7.days)
+ end
it { is_expected.to be_falsey }
end
@@ -166,7 +163,9 @@ describe Ci::Build, :models do
context 'when artifacts_expire_at is specified' do
let(:expire_at) { Time.now + 7.days }
- before { build.artifacts_expire_at = expire_at }
+ before do
+ build.artifacts_expire_at = expire_at
+ end
it { is_expected.to be_within(5).of(expire_at - Time.now) }
end
@@ -272,12 +271,98 @@ 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
@@ -438,7 +523,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 +617,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,51 +788,58 @@ 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
+ describe '#has_tags?' do
+ context 'when build has tags' do
+ subject { create(:ci_build, tag_list: ['tag']) }
- it 'has commands' do
- expect(build).to have_commands
- end
+ it { is_expected.to have_tags }
end
- context 'when does not have commands' do
- context 'when commands are an empty string' do
- let(:build) do
- create(:ci_build, commands: '')
- end
+ context 'when build does not have tags' do
+ subject { create(:ci_build, tag_list: []) }
- it 'has no commands' do
- expect(build).not_to have_commands
+ it { is_expected.not_to have_tags }
+ end
+ end
+
+ describe 'build auto retry feature' do
+ describe '#retries_count' do
+ subject { create(:ci_build, name: 'test', pipeline: pipeline) }
+
+ context 'when build has been retried several times' do
+ before do
+ create(:ci_build, :retried, name: 'test', pipeline: pipeline)
+ create(:ci_build, :retried, name: 'test', pipeline: pipeline)
end
- end
- context 'when commands are not set at all' do
- let(:build) do
- create(:ci_build, commands: nil)
+ it 'reports a correct retry count value' do
+ expect(subject.retries_count).to eq 2
end
+ end
- it 'has no commands' do
- expect(build).not_to have_commands
+ context 'when build has not been retried' do
+ it 'returns zero' do
+ expect(subject.retries_count).to eq 0
end
end
end
- end
- describe '#has_tags?' do
- context 'when build has tags' do
- subject { create(:ci_build, tag_list: ['tag']) }
+ describe '#retries_max' do
+ context 'when max retries value is defined' do
+ subject { create(:ci_build, options: { retry: 1 }) }
- it { is_expected.to have_tags }
- end
+ it 'returns a number of configured max retries' do
+ expect(subject.retries_max).to eq 1
+ end
+ end
- context 'when build does not have tags' do
- subject { create(:ci_build, tag_list: []) }
+ context 'when max retries value is not defined' do
+ subject { create(:ci_build) }
- it { is_expected.not_to have_tags }
+ it 'returns zero' do
+ expect(subject.retries_max).to eq 0
+ end
+ end
end
end
@@ -795,8 +855,8 @@ describe Ci::Build, :models do
describe '#merge_request' do
def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
- create(factory, source_project_id: pipeline.gl_project_id,
- target_project_id: pipeline.gl_project_id,
+ create(factory, source_project: pipeline.project,
+ target_project: pipeline.project,
source_branch: build.ref,
created_at: created_at)
end
@@ -844,8 +904,8 @@ describe Ci::Build, :models do
pipeline2 = create(:ci_pipeline, project: project)
@build2 = create(:ci_build, pipeline: pipeline2)
- allow(@merge_request).to receive(:commits_sha).
- and_return([pipeline.sha, pipeline2.sha])
+ allow(@merge_request).to receive(:commit_shas)
+ .and_return([pipeline.sha, pipeline2.sha])
allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
end
@@ -895,6 +955,10 @@ describe Ci::Build, :models do
context 'when other build is retried' do
let!(:retried_build) { Ci::Build.retry(other_build, user) }
+ before do
+ retried_build.success
+ end
+
it 'returns a retried build' do
is_expected.to contain_exactly(retried_build)
end
@@ -902,22 +966,30 @@ 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
+
+ context 'when there is no environment' do
+ it { is_expected.to be_nil }
end
end
@@ -928,26 +1000,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,41 +1037,19 @@ 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',
- '1-foo' => '1-foo',
- 'fix/1-foo' => 'fix-1-foo',
- 'fix-1-foo' => 'fix-1-foo',
- 'a' * 63 => 'a' * 63,
- 'a' * 64 => 'a' * 63,
- 'FOO' => 'foo',
+ 'master' => 'master',
+ '1-foo' => '1-foo',
+ 'fix/1-foo' => 'fix-1-foo',
+ 'fix-1-foo' => 'fix-1-foo',
+ 'a' * 63 => 'a' * 63,
+ 'a' * 64 => 'a' * 63,
+ 'FOO' => 'foo',
+ '-' + 'a' * 61 + '-' => 'a' * 61,
+ '-' + 'a' * 62 + '-' => 'a' * 62,
+ '-' + 'a' * 63 + '-' => 'a' * 62,
+ 'a' * 62 + ' ' => 'a' * 62
}.each do |ref, slug|
it "transforms #{ref} to #{slug}" do
build.ref = ref
@@ -1074,64 +1106,11 @@ 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) }
+ before do
+ build.update(artifacts_expire_at: 1.day.from_now)
+ end
it 'has expiring artifacts' do
expect(build).to have_expiring_artifacts
@@ -1139,71 +1118,13 @@ describe Ci::Build, :models do
end
context 'when artifacts do not have expiration date set' do
- before { build.update(artifacts_expire_at: nil) }
-
- it 'does not have expiring artifacts' do
- expect(build).not_to have_expiring_artifacts
- end
- 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)
+ build.update(artifacts_expire_at: nil)
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)
+ it 'does not have expiring artifacts' do
+ expect(build).not_to have_expiring_artifacts
end
-
- it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
end
end
@@ -1299,12 +1220,14 @@ describe Ci::Build, :models do
{ key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
{ key: 'CI_PROJECT_NAME', value: project.path, public: true },
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
+ { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path.parameterize, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
{ key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
+ { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, 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
@@ -1336,11 +1259,6 @@ describe Ci::Build, :models do
end
context 'when build has an environment' do
- before do
- build.update(environment: 'production')
- create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
- end
-
let(:environment_variables) do
[
{ key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
@@ -1348,7 +1266,66 @@ describe Ci::Build, :models do
]
end
- it { environment_variables.each { |v| is_expected.to include(v) } }
+ let!(:environment) do
+ create(:environment,
+ project: build.project,
+ name: 'production',
+ slug: 'prod-slug',
+ external_url: '')
+ end
+
+ before do
+ build.update(environment: 'production')
+ end
+
+ shared_examples 'containing environment variables' do
+ it { environment_variables.each { |v| is_expected.to include(v) } }
+ end
+
+ context 'when no URL was set' do
+ it_behaves_like 'containing environment variables'
+
+ it 'does not have CI_ENVIRONMENT_URL' do
+ keys = subject.map { |var| var[:key] }
+
+ expect(keys).not_to include('CI_ENVIRONMENT_URL')
+ end
+ end
+
+ context 'when an URL was set' do
+ let(:url) { 'http://host/test' }
+
+ before do
+ environment_variables <<
+ { key: 'CI_ENVIRONMENT_URL', value: url, public: true }
+ end
+
+ context 'when the URL was set from the job' do
+ before do
+ build.update(options: { environment: { url: url } })
+ end
+
+ it_behaves_like 'containing environment variables'
+
+ context 'when variables are used in the URL, it does not expand' do
+ let(:url) { 'http://$CI_PROJECT_NAME-$CI_ENVIRONMENT_SLUG' }
+
+ it_behaves_like 'containing environment variables'
+
+ it 'puts $CI_ENVIRONMENT_URL in the last so all other variables are available to be used when runners are trying to expand it' do
+ expect(subject.last).to eq(environment_variables.last)
+ end
+ end
+ end
+
+ context 'when the URL was not set from the job, but environment' do
+ before do
+ environment.update(external_url: url)
+ end
+
+ it_behaves_like 'containing environment variables'
+ end
+ end
end
context 'when build started manually' do
@@ -1375,16 +1352,102 @@ describe Ci::Build, :models do
it { is_expected.to include(tag_variable) }
end
- context 'when secure variable is defined' do
- let(:secure_variable) do
+ context 'when secret variable is defined' do
+ let(:secret_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
- build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ create(:ci_variable,
+ secret_variable.slice(:key, :value).merge(project: project))
end
- it { is_expected.to include(secure_variable) }
+ it { is_expected.to include(secret_variable) }
+ end
+
+ context 'when protected variable is defined' do
+ let(:protected_variable) do
+ { key: 'PROTECTED_KEY', value: 'protected_value', public: false }
+ end
+
+ before do
+ create(:ci_variable,
+ :protected,
+ protected_variable.slice(:key, :value).merge(project: project))
+ end
+
+ context 'when the branch is protected' do
+ before do
+ create(:protected_branch, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the tag is protected' do
+ before do
+ create(:protected_tag, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the ref is not protected' do
+ it { is_expected.not_to include(protected_variable) }
+ end
+ end
+
+ context 'when group secret variable is defined' do
+ let(:secret_variable) do
+ { key: 'SECRET_KEY', value: 'secret_value', public: false }
+ end
+
+ let(:group) { create(:group, :access_requestable) }
+
+ before do
+ build.project.update(group: group)
+
+ create(:ci_group_variable,
+ secret_variable.slice(:key, :value).merge(group: group))
+ end
+
+ it { is_expected.to include(secret_variable) }
+ end
+
+ context 'when group protected variable is defined' do
+ let(:protected_variable) do
+ { key: 'PROTECTED_KEY', value: 'protected_value', public: false }
+ end
+
+ let(:group) { create(:group, :access_requestable) }
+
+ before do
+ build.project.update(group: group)
+
+ create(:ci_group_variable,
+ :protected,
+ protected_variable.slice(:key, :value).merge(group: group))
+ end
+
+ context 'when the branch is protected' do
+ before do
+ create(:protected_branch, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the tag is protected' do
+ before do
+ create(:protected_tag, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the ref is not protected' do
+ it { is_expected.not_to include(protected_variable) }
+ end
end
context 'when build is for triggers' do
@@ -1405,6 +1468,23 @@ describe Ci::Build, :models do
it { is_expected.to include(predefined_trigger_variable) }
end
+ context 'when a job was triggered by a pipeline schedule' do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+
+ let!(:pipeline_schedule_variable) do
+ create(:ci_pipeline_schedule_variable,
+ key: 'SCHEDULE_VARIABLE_KEY',
+ pipeline_schedule: pipeline_schedule)
+ end
+
+ before do
+ pipeline_schedule.pipelines << pipeline
+ pipeline_schedule.reload
+ end
+
+ it { is_expected.to include(pipeline_schedule_variable.to_runner_variable) }
+ end
+
context 'when yaml_variables are undefined' do
before do
build.yaml_variables = nil
@@ -1460,7 +1540,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
@@ -1505,20 +1585,46 @@ describe Ci::Build, :models do
it { is_expected.to include(deployment_variable) }
end
+ context 'when project has custom CI config path' do
+ let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true } }
+
+ before do
+ project.update(ci_config_path: 'custom')
+ end
+
+ it { is_expected.to include(ci_config_path) }
+ end
+
context 'returns variables in valid order' do
+ let(:build_pre_var) { { key: 'build', value: 'value' } }
+ let(:project_pre_var) { { key: 'project', value: 'value' } }
+ let(:pipeline_pre_var) { { key: 'pipeline', value: 'value' } }
+ let(:build_yaml_var) { { key: 'yaml', value: 'value' } }
+
before do
- allow(build).to receive(:predefined_variables) { ['predefined'] }
- allow(project).to receive(:predefined_variables) { ['project'] }
- allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
- allow(build).to receive(:yaml_variables) { ['yaml'] }
- allow(project).to receive(:secret_variables) { ['secret'] }
+ allow(build).to receive(:predefined_variables) { [build_pre_var] }
+ allow(project).to receive(:predefined_variables) { [project_pre_var] }
+ allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] }
+ allow(build).to receive(:yaml_variables) { [build_yaml_var] }
+
+ allow(project).to receive(:secret_variables_for)
+ .with(ref: 'master', environment: nil) do
+ [create(:ci_variable, key: 'secret', value: 'value')]
+ end
end
- it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
+ it do
+ is_expected.to eq(
+ [build_pre_var,
+ project_pre_var,
+ pipeline_pre_var,
+ build_yaml_var,
+ { key: 'secret', value: 'value', public: false }])
+ end
end
end
- describe 'State transition: any => [:pending]' do
+ describe 'state transition: any => [:pending]' do
let(:build) { create(:ci_build, :created) }
it 'queues BuildQueueWorker' do
@@ -1527,4 +1633,35 @@ describe Ci::Build, :models do
build.enqueue
end
end
+
+ describe 'state transition when build fails' do
+ context 'when build is configured to be retried' do
+ subject { create(:ci_build, :running, options: { retry: 3 }) }
+
+ it 'retries builds and assigns a same user to it' do
+ expect(described_class).to receive(:retry)
+ .with(subject, subject.user)
+
+ subject.drop!
+ end
+ end
+
+ context 'when build is not configured to be retried' do
+ subject { create(:ci_build, :running) }
+
+ it 'does not retry build' do
+ expect(described_class).not_to receive(:retry)
+
+ subject.drop!
+ end
+
+ it 'does not count retries when not necessary' do
+ expect(described_class).not_to receive(:retry)
+ expect_any_instance_of(described_class)
+ .not_to receive(:retries_count)
+
+ subject.drop!
+ end
+ end
+ end
end
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/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
new file mode 100644
index 00000000000..24b914face9
--- /dev/null
+++ b/spec/models/ci/group_variable_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Ci::GroupVariable, models: true do
+ subject { build(:ci_group_variable) }
+
+ it { is_expected.to include_module(HasVariable) }
+ it { is_expected.to include_module(Presentable) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id) }
+
+ describe '.unprotected' do
+ subject { described_class.unprotected }
+
+ context 'when variable is protected' do
+ before do
+ create(:ci_group_variable, :protected)
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when variable is not protected' do
+ let(:variable) { create(:ci_group_variable, protected: false) }
+
+ it 'returns the variable' do
+ is_expected.to contain_exactly(variable)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb
index c4a9743a4e2..d43c33d3807 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/legacy_stage_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::Stage, models: true do
+describe Ci::LegacyStage, :models do
let(:stage) { build(:ci_stage) }
let(:pipeline) { stage.pipeline }
let(:stage_name) { stage.name }
@@ -28,6 +28,46 @@ 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
+
+ context 'when a name is nil on legacy pipelines' do
+ before do
+ pipeline.builds.first.update_attribute(:name, nil)
+ end
+
+ it 'returns an array of three groups' do
+ expect(stage.groups.map(&:name))
+ .to eq ['', 'aaaaa', 'rspec', 'spinach']
+ end
+ end
+ end
+
describe '#statuses_count' do
before do
create_job(:ci_build)
@@ -73,6 +113,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
@@ -170,22 +214,31 @@ describe Ci::Stage, models: true do
context 'when stage has warnings' do
context 'when using memoized warnings flag' do
context 'when there are warnings' do
- let(:stage) { build(:ci_stage, warnings: true) }
+ let(:stage) { build(:ci_stage, warnings: 2) }
- it 'has memoized warnings' do
+ it 'returns true using memoized value' do
expect(stage).not_to receive(:statuses)
expect(stage).to have_warnings
end
end
context 'when there are no warnings' do
- let(:stage) { build(:ci_stage, warnings: false) }
+ let(:stage) { build(:ci_stage, warnings: 0) }
- it 'has memoized warnings' do
+ it 'returns false using memoized value' do
expect(stage).not_to receive(:statuses)
expect(stage).not_to have_warnings
end
end
+
+ context 'when number of warnings is not a valid value' do
+ let(:stage) { build(:ci_stage, warnings: true) }
+
+ it 'calculates statuses using database queries' do
+ expect(stage).to receive(:statuses).and_call_original
+ expect(stage).not_to have_warnings
+ end
+ end
end
context 'when calculating warnings from statuses' do
@@ -214,7 +267,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/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
new file mode 100644
index 00000000000..6427deda31e
--- /dev/null
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -0,0 +1,137 @@
+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 have_many(:variables) }
+
+ 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
+
+ context 'when active is false' do
+ it 'does not allow nullified ref' do
+ pipeline_schedule = build(:ci_pipeline_schedule, :inactive, ref: nil)
+
+ expect(pipeline_schedule).not_to be_valid
+ end
+ 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
+
+ describe '#job_variables' do
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule) }
+
+ let!(:pipeline_schedule_variables) do
+ create_list(:ci_pipeline_schedule_variable, 2, pipeline_schedule: pipeline_schedule)
+ end
+
+ subject { pipeline_schedule.job_variables }
+
+ before do
+ pipeline_schedule.reload
+ end
+
+ it { is_expected.to contain_exactly(*pipeline_schedule_variables.map(&:to_runner_variable)) }
+ end
+end
diff --git a/spec/models/ci/pipeline_schedule_variable_spec.rb b/spec/models/ci/pipeline_schedule_variable_spec.rb
new file mode 100644
index 00000000000..0de76a57b7f
--- /dev/null
+++ b/spec/models/ci/pipeline_schedule_variable_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+
+describe Ci::PipelineScheduleVariable, models: true do
+ subject { build(:ci_pipeline_schedule_variable) }
+
+ it { is_expected.to include_module(HasVariable) }
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index dd5f7098d06..ba0696fa210 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -12,18 +12,44 @@ 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 }
+ it { is_expected.to validate_presence_of(:sha) }
+ it { is_expected.to validate_presence_of(:status) }
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
+ describe '#source' do
+ context 'when creating new pipeline' do
+ let(:pipeline) do
+ build(:ci_empty_pipeline, status: :created, project: project, source: nil)
+ end
+
+ it "prevents from creating an object" do
+ expect(pipeline).not_to be_valid
+ end
+ end
+
+ context 'when updating existing pipeline' do
+ before do
+ pipeline.update_attribute(:source, nil)
+ end
+
+ it "object is valid" do
+ expect(pipeline).to be_valid
+ end
+ end
+ end
+
describe '#block' do
it 'changes pipeline status to manual' do
expect(pipeline.block).to be true
@@ -56,8 +82,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 +92,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 +160,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,
@@ -161,8 +224,19 @@ describe Ci::Pipeline, models: true do
status: 'success')
end
- describe '#stages' do
- subject { pipeline.stages }
+ describe '#stage_seeds' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: { rspec: { script: 'rake' } })
+ end
+
+ it 'returns preseeded stage seeds object' do
+ expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed)
+ expect(pipeline.stage_seeds.count).to eq 1
+ end
+ end
+
+ describe '#legacy_stages' do
+ subject { pipeline.legacy_stages }
context 'stages list' do
it 'returns ordered list of stages' do
@@ -181,13 +255,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
@@ -197,6 +273,24 @@ describe Ci::Pipeline, models: true do
end
end
end
+
+ context 'when there is a stage with warnings' do
+ before do
+ create(:commit_status, pipeline: pipeline,
+ stage: 'deploy',
+ name: 'prod:2',
+ stage_idx: 2,
+ status: 'failed',
+ allow_failure: true)
+ end
+
+ it 'populates stage with correct number of warnings' do
+ deploy_stage = pipeline.legacy_stages.third
+
+ expect(deploy_stage).not_to receive(:statuses)
+ expect(deploy_stage).to have_warnings
+ end
+ end
end
describe '#stages_count' do
@@ -205,22 +299,22 @@ describe Ci::Pipeline, models: true do
end
end
- describe '#stages_name' do
+ describe '#stages_names' do
it 'returns a valid names of stages' do
- expect(pipeline.stages_name).to eq(%w(build test deploy))
+ expect(pipeline.stages_names).to eq(%w(build test deploy))
end
end
end
- describe '#stage' do
- subject { pipeline.stage('test') }
+ describe '#legacy_stage' do
+ subject { pipeline.legacy_stage('test') }
context 'with status in stage' do
before do
create(:commit_status, pipeline: pipeline, stage: 'test')
end
- it { expect(subject).to be_a Ci::Stage }
+ it { expect(subject).to be_a Ci::LegacyStage }
it { expect(subject.name).to eq 'test' }
it { expect(subject.statuses).not_to be_empty }
end
@@ -238,32 +332,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) }
- expect(pipeline.duration).to eq(40)
+ 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).to be_manual
+ expect(pipeline.duration).to eq 4.minutes
+ end
end
end
@@ -317,12 +435,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
@@ -397,6 +524,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
@@ -404,6 +535,20 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#has_stage_seeds?' do
+ context 'when pipeline has stage seeds' do
+ subject { build(:ci_pipeline_with_one_job) }
+
+ it { is_expected.to have_stage_seeds }
+ end
+
+ context 'when pipeline does not have stage seeds' do
+ subject { create(:ci_pipeline_without_jobs) }
+
+ it { is_expected.not_to have_stage_seeds }
+ end
+ end
+
describe '#has_warnings?' do
subject { pipeline.has_warnings? }
@@ -463,8 +608,8 @@ describe Ci::Pipeline, models: true do
it 'returns the latest pipeline for the same ref and different sha' do
expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
- expect(pipelines.map(&:status)).
- to contain_exactly('success', 'failed', 'skipped')
+ expect(pipelines.map(&:status))
+ .to contain_exactly('success', 'failed', 'skipped')
end
end
@@ -473,8 +618,8 @@ describe Ci::Pipeline, models: true do
it 'returns the latest pipeline for ref and different sha' do
expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
- expect(pipelines.map(&:status)).
- to contain_exactly('success', 'failed')
+ expect(pipelines.map(&:status))
+ .to contain_exactly('success', 'failed')
end
end
end
@@ -509,11 +654,30 @@ describe Ci::Pipeline, models: true do
end
it 'returns the latest successful pipeline' do
- expect(described_class.latest_successful_for('ref')).
- to eq(latest_successful_pipeline)
+ expect(described_class.latest_successful_for('ref'))
+ .to eq(latest_successful_pipeline)
end
end
+ describe '.latest_successful_for_refs' do
+ include_context 'with some outdated pipelines'
+
+ let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') }
+ let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') }
+
+ it 'returns the latest successful pipeline for both refs' do
+ refs = %w(ref1 ref2 ref3)
+
+ expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 })
+ end
+ end
+
+ describe '.internal_sources' do
+ subject { described_class.internal_sources }
+
+ it { is_expected.to be_an(Array) }
+ end
+
describe '#status' do
let(:build) do
create(:ci_build, :created, pipeline: pipeline, name: 'test')
@@ -584,6 +748,39 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#ci_yaml_file_path' do
+ subject { pipeline.ci_yaml_file_path }
+
+ it 'returns the path from project' do
+ allow(pipeline.project).to receive(:ci_config_path) { 'custom/path' }
+
+ is_expected.to eq('custom/path')
+ end
+
+ it 'returns default when custom path is nil' do
+ allow(pipeline.project).to receive(:ci_config_path) { nil }
+
+ is_expected.to eq('.gitlab-ci.yml')
+ end
+
+ it 'returns default when custom path is empty' do
+ allow(pipeline.project).to receive(:ci_config_path) { '' }
+
+ is_expected.to eq('.gitlab-ci.yml')
+ end
+ end
+
+ describe '#ci_yaml_file' do
+ it 'reports error if the file is not found' do
+ allow(pipeline.project).to receive(:ci_config_path) { 'custom' }
+
+ pipeline.ci_yaml_file
+
+ expect(pipeline.yaml_errors)
+ .to eq('Failed to load CI/CD config file at custom')
+ end
+ end
+
describe '#detailed_status' do
subject { pipeline.detailed_status(user) }
@@ -647,7 +844,7 @@ describe Ci::Pipeline, models: true do
let(:pipeline) { create(:ci_pipeline, status: :manual) }
it 'returns detailed status for blocked pipeline' do
- expect(subject.text).to eq 'manual'
+ expect(subject.text).to eq 'blocked'
end
end
@@ -743,6 +940,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
@@ -844,7 +1051,7 @@ describe Ci::Pipeline, models: true do
end
before do
- ProjectWebHookWorker.drain
+ WebHookWorker.drain
end
context 'with pipeline hooks enabled' do
@@ -935,11 +1142,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
@@ -958,6 +1166,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)
@@ -970,7 +1195,9 @@ describe Ci::Pipeline, models: true do
end
context 'when pipeline is not stuck' do
- before { create(:ci_runner, :shared, :online) }
+ before do
+ create(:ci_runner, :shared, :online)
+ end
it 'is not stuck' do
expect(pipeline).not_to be_stuck
@@ -1011,10 +1238,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/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 76ce558eea0..4b9cce28e0e 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -276,14 +276,14 @@ describe Ci::Runner, models: true do
it 'sets a new last_update value when it is called the first time' do
last_update = runner.ensure_runner_queue_value
- expect_value_in_redis.to eq(last_update)
+ expect_value_in_queues.to eq(last_update)
end
it 'does not change if it is not expired and called again' do
last_update = runner.ensure_runner_queue_value
expect(runner.ensure_runner_queue_value).to eq(last_update)
- expect_value_in_redis.to eq(last_update)
+ expect_value_in_queues.to eq(last_update)
end
context 'updates runner queue after changing editable value' do
@@ -294,7 +294,7 @@ describe Ci::Runner, models: true do
end
it 'sets a new last_update value' do
- expect_value_in_redis.not_to eq(last_update)
+ expect_value_in_queues.not_to eq(last_update)
end
end
@@ -306,12 +306,12 @@ describe Ci::Runner, models: true do
end
it 'has an old last_update value' do
- expect_value_in_redis.to eq(last_update)
+ expect_value_in_queues.to eq(last_update)
end
end
- def expect_value_in_redis
- Gitlab::Redis.with do |redis|
+ def expect_value_in_queues
+ Gitlab::Redis::Queues.with do |redis|
runner_queue_key = runner.send(:runner_queue_key)
expect(redis.get(runner_queue_key))
end
@@ -330,7 +330,7 @@ describe Ci::Runner, models: true do
end
it 'cleans up the queue' do
- Gitlab::Redis.with do |redis|
+ Gitlab::Redis::Queues.with do |redis|
expect(redis.get(queue_key)).to be_nil
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 bee9f714849..890ffaae494 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -1,39 +1,33 @@
require 'spec_helper'
describe Ci::Variable, models: true do
- subject { Ci::Variable.new }
+ subject { build(:ci_variable) }
- let(:secret_value) { 'secret' }
-
- it { is_expected.to validate_presence_of(:key) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) }
- it { is_expected.to validate_length_of(:key).is_at_most(255) }
- it { is_expected.to allow_value('foo').for(:key) }
- it { is_expected.not_to allow_value('foo bar').for(:key) }
- it { is_expected.not_to allow_value('foo/bar').for(:key) }
-
- before :each do
- subject.value = secret_value
+ describe 'validations' do
+ it { is_expected.to include_module(HasVariable) }
+ it { is_expected.to include_module(Presentable) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) }
end
- describe '#value' do
- it 'stores the encrypted value' do
- expect(subject.encrypted_value).not_to be_nil
- end
+ describe '.unprotected' do
+ subject { described_class.unprotected }
- it 'stores an iv for value' do
- expect(subject.encrypted_value_iv).not_to be_nil
- end
+ context 'when variable is protected' do
+ before do
+ create(:ci_variable, :protected)
+ end
- it 'stores a salt for value' do
- expect(subject.encrypted_value_salt).not_to be_nil
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
end
- it 'fails to decrypt if iv is incorrect' do
- subject.encrypted_value_iv = SecureRandom.hex
- subject.instance_variable_set(:@value, nil)
- expect { subject.value }.
- to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
+ context 'when variable is not protected' do
+ let(:variable) { create(:ci_variable, protected: false) }
+
+ it 'returns the variable' do
+ is_expected.to contain_exactly(variable)
+ end
end
end
end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index e4bddf67096..ba9c3f66d21 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -147,9 +147,9 @@ describe CommitRange, models: true do
note: commit1.revert_description(user),
project: issue.project)
- expect_any_instance_of(Commit).to receive(:reverts_commit?).
- with(commit1, user).
- and_return(true)
+ expect_any_instance_of(Commit).to receive(:reverts_commit?)
+ .with(commit1, user)
+ .and_return(true)
expect(commit1.has_been_reverted?(user, issue)).to eq(true)
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 32f9366a14c..528b211c9d6 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -19,17 +19,15 @@ describe Commit, models: true do
expect(commit.author).to eq(user)
end
- it 'caches the author' do
+ it 'caches the author', :request_store do
user = create(:user, email: commit.author_email)
- expect(RequestStore).to receive(:active?).twice.and_return(true)
- expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original
+ expect(User).to receive(:find_by_any_email).and_call_original
expect(commit.author).to eq(user)
- key = "commit_author:#{commit.author_email}"
+ key = "Commit:author:#{commit.author_email.downcase}"
expect(RequestStore.store[key]).to eq(user)
expect(commit.author).to eq(user)
- RequestStore.store.clear
end
end
@@ -67,11 +65,11 @@ describe Commit, models: true do
expect(commit.title).to eq("--no commit message")
end
- it "truncates a message without a newline at 80 characters" do
+ it 'truncates a message without a newline at natural break to 80 characters' do
message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
allow(commit).to receive(:safe_message).and_return(message)
- expect(commit.title).to eq("#{message[0..79]}…")
+ expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…')
end
it "truncates a message with a newline before 80 characters at the newline" do
@@ -113,6 +111,28 @@ eos
end
end
+ describe 'description' do
+ it 'returns description of commit message if title less than 100 characters' do
+ message = <<eos
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit.
+Vivamus egestas lacinia lacus, sed rutrum mauris.
+eos
+
+ allow(commit).to receive(:safe_message).and_return(message)
+ expect(commit.description).to eq('Vivamus egestas lacinia lacus, sed rutrum mauris.')
+ end
+
+ it 'returns full commit message if commit title more than 100 characters' do
+ message = <<eos
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.
+Vivamus egestas lacinia lacus, sed rutrum mauris.
+eos
+
+ allow(commit).to receive(:safe_message).and_return(message)
+ expect(commit.description).to eq(message)
+ end
+ end
+
describe "delegation" do
subject { commit }
@@ -184,19 +204,25 @@ eos
it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
context 'commit has no description' do
- before { allow(commit).to receive(:description?).and_return(false) }
+ before do
+ allow(commit).to receive(:description?).and_return(false)
+ end
it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
end
context "another_commit's description does not revert commit" do
- before { allow(commit).to receive(:description).and_return("Foo Bar") }
+ before do
+ allow(commit).to receive(:description).and_return("Foo Bar")
+ end
it { expect(commit.reverts_commit?(another_commit, user)).to be_falsy }
end
context "another_commit's description reverts commit" do
- before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") }
+ before do
+ allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar")
+ end
it { expect(commit.reverts_commit?(another_commit, user)).to be_truthy }
end
@@ -212,6 +238,25 @@ eos
end
end
+ describe '#last_pipeline' do
+ let!(:first_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+ let!(:second_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+
+ it 'returns last pipeline' do
+ expect(commit.last_pipeline).to eq second_pipeline
+ end
+ end
+
describe '#status' do
context 'without ref argument' do
before do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index ea5e4e21039..1e074c7ad26 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)) }
@@ -30,23 +31,40 @@ describe CommitStatus, :models do
describe '#author' do
subject { commit_status.author }
- before { commit_status.author = User.new }
+
+ before do
+ commit_status.author = User.new
+ end
it { is_expected.to eq(commit_status.user) }
end
+ describe 'status state machine' do
+ let!(:commit_status) { create(:commit_status, :running, project: project) }
+
+ it 'invalidates the cache after a transition' do
+ expect(ExpireJobCacheWorker).to receive(:perform_async).with(commit_status.id)
+
+ commit_status.success!
+ end
+ end
+
describe '#started?' do
subject { commit_status.started? }
context 'without started_at' do
- before { commit_status.started_at = nil }
+ before do
+ commit_status.started_at = nil
+ end
it { is_expected.to be_falsey }
end
%w[running success failed].each do |status|
context "if commit status is #{status}" do
- before { commit_status.status = status }
+ before do
+ commit_status.status = status
+ end
it { is_expected.to be_truthy }
end
@@ -54,7 +72,9 @@ describe CommitStatus, :models do
%w[pending canceled].each do |status|
context "if commit status is #{status}" do
- before { commit_status.status = status }
+ before do
+ commit_status.status = status
+ end
it { is_expected.to be_falsey }
end
@@ -66,7 +86,9 @@ describe CommitStatus, :models do
%w[pending running].each do |state|
context "if commit_status.status is #{state}" do
- before { commit_status.status = state }
+ before do
+ commit_status.status = state
+ end
it { is_expected.to be_truthy }
end
@@ -74,7 +96,9 @@ describe CommitStatus, :models do
%w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
- before { commit_status.status = state }
+ before do
+ commit_status.status = state
+ end
it { is_expected.to be_falsey }
end
@@ -86,7 +110,9 @@ describe CommitStatus, :models do
%w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
- before { commit_status.status = state }
+ before do
+ commit_status.status = state
+ end
it { is_expected.to be_truthy }
end
@@ -94,13 +120,41 @@ describe CommitStatus, :models do
%w[pending running].each do |state|
context "if commit_status.status is #{state}" do
- before { commit_status.status = state }
+ before do
+ commit_status.status = state
+ end
it { is_expected.to be_falsey }
end
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 +184,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 +196,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 +224,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
@@ -214,11 +284,48 @@ describe CommitStatus, :models do
end
end
+ describe '.status' do
+ context 'when there are multiple statuses present' do
+ before do
+ create_status(status: 'running')
+ create_status(status: 'success')
+ create_status(allow_failure: true, status: 'failed')
+ end
+
+ it 'returns a correct compound status' do
+ expect(described_class.all.status).to eq 'running'
+ end
+ end
+
+ context 'when there are only allowed to fail commit statuses present' do
+ before do
+ create_status(allow_failure: true, status: 'failed')
+ end
+
+ it 'returns status that indicates success' do
+ expect(described_class.all.status).to eq 'success'
+ end
+ end
+
+ context 'when using a scope to select latest statuses' do
+ before do
+ create_status(name: 'test', retried: true, status: 'failed')
+ create_status(allow_failure: true, name: 'test', status: 'failed')
+ end
+
+ it 'returns status according to the scope' do
+ expect(described_class.latest.status).to eq 'success'
+ end
+ end
+ end
+
describe '#before_sha' do
subject { commit_status.before_sha }
context 'when no before_sha is set for pipeline' do
- before { pipeline.before_sha = nil }
+ before do
+ pipeline.before_sha = nil
+ end
it 'returns blank sha' do
is_expected.to eq(Gitlab::Git::BLANK_SHA)
@@ -227,7 +334,10 @@ describe CommitStatus, :models do
context 'for before_sha set for pipeline' do
let(:value) { '1234' }
- before { pipeline.before_sha = value }
+
+ before do
+ pipeline.before_sha = value
+ end
it 'returns the set value' do
is_expected.to eq(value)
@@ -297,4 +407,40 @@ describe CommitStatus, :models do
end
end
end
+
+ describe '#locking_enabled?' do
+ before do
+ commit_status.lock_version = 100
+ end
+
+ subject { commit_status.locking_enabled? }
+
+ context "when changing status" do
+ before do
+ commit_status.status = "running"
+ end
+
+ it "lock" do
+ is_expected.to be true
+ end
+
+ it "raise exception when trying to update" do
+ expect{ commit_status.save }.to raise_error(ActiveRecord::StaleObjectError)
+ end
+ end
+
+ context "when changing description" do
+ before do
+ commit_status.description = "test"
+ end
+
+ it "do not lock" do
+ is_expected.to be false
+ end
+
+ it "save correctly" do
+ expect(commit_status.save).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
index 4829ef17a20..97b7e48bb3c 100644
--- a/spec/models/concerns/access_requestable_spec.rb
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -14,7 +14,9 @@ describe AccessRequestable do
let(:group) { create(:group, :public, :access_requestable) }
let(:user) { create(:user) }
- before { group.request_access(user) }
+ before do
+ group.request_access(user)
+ end
it { expect(group.requesters.exists?(user_id: user)).to be_truthy }
end
@@ -32,7 +34,9 @@ describe AccessRequestable do
let(:project) { create(:empty_project, :public, :access_requestable) }
let(:user) { create(:user) }
- before { project.request_access(user) }
+ before do
+ project.request_access(user)
+ end
it { expect(project.requesters.exists?(user_id: user)).to be_truthy }
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/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
index 92fdc5cd65d..a6fccb668e3 100644
--- a/spec/models/concerns/case_sensitivity_spec.rb
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -15,13 +15,13 @@ describe CaseSensitivity, models: true do
it 'returns the criteria for a column and a value' do
criteria = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:foo).
- and_return('"foo"')
+ expect(connection).to receive(:quote_table_name)
+ .with(:foo)
+ .and_return('"foo"')
- expect(model).to receive(:where).
- with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar').
- and_return(criteria)
+ expect(model).to receive(:where)
+ .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar')
+ .and_return(criteria)
expect(model.iwhere(foo: 'bar')).to eq(criteria)
end
@@ -29,13 +29,13 @@ describe CaseSensitivity, models: true do
it 'returns the criteria for a column with a table, and a value' do
criteria = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:'foo.bar').
- and_return('"foo"."bar"')
+ expect(connection).to receive(:quote_table_name)
+ .with(:'foo.bar')
+ .and_return('"foo"."bar"')
- expect(model).to receive(:where).
- with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar').
- and_return(criteria)
+ expect(model).to receive(:where)
+ .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar')
+ .and_return(criteria)
expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria)
end
@@ -46,21 +46,21 @@ describe CaseSensitivity, models: true do
initial = double(:criteria)
final = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:foo).
- and_return('"foo"')
+ expect(connection).to receive(:quote_table_name)
+ .with(:foo)
+ .and_return('"foo"')
- expect(connection).to receive(:quote_table_name).
- with(:bar).
- and_return('"bar"')
+ expect(connection).to receive(:quote_table_name)
+ .with(:bar)
+ .and_return('"bar"')
- expect(model).to receive(:where).
- with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar').
- and_return(initial)
+ expect(model).to receive(:where)
+ .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar')
+ .and_return(initial)
- expect(initial).to receive(:where).
- with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz').
- and_return(final)
+ expect(initial).to receive(:where)
+ .with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz')
+ .and_return(final)
got = model.iwhere(foo: 'bar', bar: 'baz')
@@ -71,21 +71,21 @@ describe CaseSensitivity, models: true do
initial = double(:criteria)
final = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:'foo.bar').
- and_return('"foo"."bar"')
+ expect(connection).to receive(:quote_table_name)
+ .with(:'foo.bar')
+ .and_return('"foo"."bar"')
- expect(connection).to receive(:quote_table_name).
- with(:'foo.baz').
- and_return('"foo"."baz"')
+ expect(connection).to receive(:quote_table_name)
+ .with(:'foo.baz')
+ .and_return('"foo"."baz"')
- expect(model).to receive(:where).
- with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar').
- and_return(initial)
+ expect(model).to receive(:where)
+ .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar')
+ .and_return(initial)
- expect(initial).to receive(:where).
- with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz').
- and_return(final)
+ expect(initial).to receive(:where)
+ .with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz')
+ .and_return(final)
got = model.iwhere('foo.bar'.to_sym => 'bar',
'foo.baz'.to_sym => 'baz')
@@ -105,13 +105,13 @@ describe CaseSensitivity, models: true do
it 'returns the criteria for a column and a value' do
criteria = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:foo).
- and_return('`foo`')
+ expect(connection).to receive(:quote_table_name)
+ .with(:foo)
+ .and_return('`foo`')
- expect(model).to receive(:where).
- with(%q{`foo` = :value}, value: 'bar').
- and_return(criteria)
+ expect(model).to receive(:where)
+ .with(%q{`foo` = :value}, value: 'bar')
+ .and_return(criteria)
expect(model.iwhere(foo: 'bar')).to eq(criteria)
end
@@ -119,16 +119,16 @@ describe CaseSensitivity, models: true do
it 'returns the criteria for a column with a table, and a value' do
criteria = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:'foo.bar').
- and_return('`foo`.`bar`')
+ expect(connection).to receive(:quote_table_name)
+ .with(:'foo.bar')
+ .and_return('`foo`.`bar`')
- expect(model).to receive(:where).
- with(%q{`foo`.`bar` = :value}, value: 'bar').
- and_return(criteria)
+ expect(model).to receive(:where)
+ .with(%q{`foo`.`bar` = :value}, value: 'bar')
+ .and_return(criteria)
- expect(model.iwhere('foo.bar'.to_sym => 'bar')).
- to eq(criteria)
+ expect(model.iwhere('foo.bar'.to_sym => 'bar'))
+ .to eq(criteria)
end
end
@@ -137,21 +137,21 @@ describe CaseSensitivity, models: true do
initial = double(:criteria)
final = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:foo).
- and_return('`foo`')
+ expect(connection).to receive(:quote_table_name)
+ .with(:foo)
+ .and_return('`foo`')
- expect(connection).to receive(:quote_table_name).
- with(:bar).
- and_return('`bar`')
+ expect(connection).to receive(:quote_table_name)
+ .with(:bar)
+ .and_return('`bar`')
- expect(model).to receive(:where).
- with(%q{`foo` = :value}, value: 'bar').
- and_return(initial)
+ expect(model).to receive(:where)
+ .with(%q{`foo` = :value}, value: 'bar')
+ .and_return(initial)
- expect(initial).to receive(:where).
- with(%q{`bar` = :value}, value: 'baz').
- and_return(final)
+ expect(initial).to receive(:where)
+ .with(%q{`bar` = :value}, value: 'baz')
+ .and_return(final)
got = model.iwhere(foo: 'bar', bar: 'baz')
@@ -162,21 +162,21 @@ describe CaseSensitivity, models: true do
initial = double(:criteria)
final = double(:criteria)
- expect(connection).to receive(:quote_table_name).
- with(:'foo.bar').
- and_return('`foo`.`bar`')
+ expect(connection).to receive(:quote_table_name)
+ .with(:'foo.bar')
+ .and_return('`foo`.`bar`')
- expect(connection).to receive(:quote_table_name).
- with(:'foo.baz').
- and_return('`foo`.`baz`')
+ expect(connection).to receive(:quote_table_name)
+ .with(:'foo.baz')
+ .and_return('`foo`.`baz`')
- expect(model).to receive(:where).
- with(%q{`foo`.`bar` = :value}, value: 'bar').
- and_return(initial)
+ expect(model).to receive(:where)
+ .with(%q{`foo`.`bar` = :value}, value: 'bar')
+ .and_return(initial)
- expect(initial).to receive(:where).
- with(%q{`foo`.`baz` = :value}, value: 'baz').
- and_return(final)
+ expect(initial).to receive(:where)
+ .with(%q{`foo`.`baz` = :value}, value: 'baz')
+ .and_return(final)
got = model.iwhere('foo.bar'.to_sym => 'bar',
'foo.baz'.to_sym => 'baz')
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..f3e148f95f0
--- /dev/null
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -0,0 +1,50 @@
+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
+
+ describe '#line_code_in_diffs' do
+ context 'when the discussion is active in the diff' do
+ let(:diff_refs) { subject.position.diff_refs }
+
+ it 'returns the current line code' do
+ expect(subject.line_code_in_diffs(diff_refs)).to eq(subject.line_code)
+ end
+ end
+
+ context 'when the discussion was created in the diff' do
+ let(:diff_refs) { subject.original_position.diff_refs }
+
+ it 'returns the original line code' do
+ expect(subject.line_code_in_diffs(diff_refs)).to eq(subject.original_line_code)
+ end
+ end
+
+ context 'when the discussion is unrelated to the diff' do
+ let(:diff_refs) { subject.project.commit(RepoHelpers.sample_commit.id).diff_refs }
+
+ it 'returns nil' do
+ expect(subject.line_code_in_diffs(diff_refs)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb
new file mode 100644
index 00000000000..951690a217b
--- /dev/null
+++ b/spec/models/concerns/each_batch_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe EachBatch do
+ describe '.each_batch' do
+ let(:model) do
+ Class.new(ActiveRecord::Base) do
+ include EachBatch
+
+ self.table_name = 'users'
+ end
+ end
+
+ before do
+ 5.times { create(:user, updated_at: 1.day.ago) }
+ end
+
+ it 'yields an ActiveRecord::Relation when a block is given' do
+ model.each_batch do |relation|
+ expect(relation).to be_a_kind_of(ActiveRecord::Relation)
+ end
+ end
+
+ it 'yields a batch index as the second argument' do
+ model.each_batch do |_, index|
+ expect(index).to eq(1)
+ end
+ end
+
+ it 'accepts a custom batch size' do
+ amount = 0
+
+ model.each_batch(of: 1) { amount += 1 }
+
+ expect(amount).to eq(5)
+ end
+
+ it 'does not include ORDER BYs in the yielded relations' do
+ model.each_batch do |relation|
+ expect(relation.to_sql).not_to include('ORDER BY')
+ end
+ end
+
+ it 'allows updating of the yielded relations' do
+ time = Time.now
+
+ model.each_batch do |relation|
+ relation.update_all(updated_at: time)
+ end
+
+ expect(model.where(updated_at: time).count).to eq(5)
+ end
+ end
+end
diff --git a/spec/models/concerns/editable_spec.rb b/spec/models/concerns/editable_spec.rb
new file mode 100644
index 00000000000..cd73af3b480
--- /dev/null
+++ b/spec/models/concerns/editable_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Editable do
+ describe '#is_edited?' do
+ let(:issue) { create(:issue, last_edited_at: nil) }
+ let(:edited_issue) { create(:issue, created_at: 3.days.ago, last_edited_at: 2.days.ago) }
+
+ it { expect(issue.is_edited?).to eq(false) }
+ it { expect(edited_issue.is_edited?).to eq(true) }
+ end
+end
diff --git a/spec/models/concerns/feature_gate_spec.rb b/spec/models/concerns/feature_gate_spec.rb
new file mode 100644
index 00000000000..3f601243245
--- /dev/null
+++ b/spec/models/concerns/feature_gate_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe FeatureGate do
+ describe 'User' do
+ describe '#flipper_id' do
+ context 'when user is not persisted' do
+ let(:user) { build(:user) }
+
+ it { expect(user.flipper_id).to be_nil }
+ end
+
+ context 'when user is persisted' do
+ let(:user) { create(:user) }
+
+ it { expect(user.flipper_id).to eq "User:#{user.id}" }
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index f134da441c2..a38f2553eb1 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -48,7 +48,7 @@ describe HasStatus do
[create(type, status: :failed, allow_failure: true)]
end
- it { is_expected.to eq 'skipped' }
+ it { is_expected.to eq 'success' }
end
context 'success and canceled' do
@@ -110,6 +110,24 @@ describe HasStatus do
it { is_expected.to eq 'running' }
end
+ context 'when one status finished and second is still created' do
+ let!(:statuses) do
+ [create(type, status: :success), create(type, status: :created)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'when there is a manual status before created status' do
+ let!(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :manual, allow_failure: false),
+ create(type, status: :created)]
+ end
+
+ it { is_expected.to eq 'manual' }
+ end
+
context 'when one status is a blocking manual action' do
let!(:statuses) do
[create(type, status: :failed),
@@ -150,8 +168,8 @@ describe HasStatus do
describe ".#{status}" do
it 'contains the job' do
- expect(CommitStatus.public_send(status).all).
- to contain_exactly(job)
+ expect(CommitStatus.public_send(status).all)
+ .to contain_exactly(job)
end
end
@@ -213,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/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb
new file mode 100644
index 00000000000..f4b24e6d1d9
--- /dev/null
+++ b/spec/models/concerns/has_variable_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe HasVariable do
+ subject { build(:ci_variable) }
+
+ it { is_expected.to validate_presence_of(:key) }
+ it { is_expected.to validate_length_of(:key).is_at_most(255) }
+ it { is_expected.to allow_value('foo').for(:key) }
+ it { is_expected.not_to allow_value('foo bar').for(:key) }
+ it { is_expected.not_to allow_value('foo/bar').for(:key) }
+
+ describe '#value' do
+ before do
+ subject.value = 'secret'
+ end
+
+ it 'stores the encrypted value' do
+ expect(subject.encrypted_value).not_to be_nil
+ end
+
+ it 'stores an iv for value' do
+ expect(subject.encrypted_value_iv).not_to be_nil
+ end
+
+ it 'stores a salt for value' do
+ expect(subject.encrypted_value_salt).not_to be_nil
+ end
+
+ it 'fails to decrypt if iv is incorrect' do
+ subject.encrypted_value_iv = SecureRandom.hex
+ subject.instance_variable_set(:@value, nil)
+ expect { subject.value }
+ .to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
+ end
+ end
+
+ describe '#to_runner_variable' do
+ it 'returns a hash for the runner' do
+ expect(subject.to_runner_variable)
+ .to eq(key: subject.key, value: subject.value, public: false)
+ end
+ end
+end
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 545a11912e3..505039c9d88 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,47 +45,23 @@ 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) }
- 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)
+ subject { build(:issue) }
- 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
+ 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
- context "when previous assignee does not exist" do
- before{ issue.update(assignee: nil) }
+ describe 'author_name' do
+ it 'is delegated to author' do
+ expect(issue.author_name).to eq issue.author.name
+ end
- it "updates cache count for the new assignee" do
- expect_any_instance_of(User).to receive(:update_cache_counts)
+ it 'returns nil when author is nil' do
+ issue.author_id = nil
+ issue.save(validate: false)
- issue.update(assignee: user)
- end
- end
+ expect(issue.author_name).to eq nil
end
end
@@ -87,17 +69,17 @@ describe Issue, "Issuable" do
let!(:searchable_issue) { create(:issue, title: "Searchable issue") }
it 'returns notes with a matching title' do
- expect(described_class.search(searchable_issue.title)).
- to eq([searchable_issue])
+ 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)).
- to eq([searchable_issue])
+ expect(issuable_class.search(searchable_issue.title.upcase))
+ .to eq([searchable_issue])
end
end
@@ -107,32 +89,32 @@ describe Issue, "Issuable" do
end
it 'returns notes with a matching title' do
- expect(described_class.full_search(searchable_issue.title)).
- to eq([searchable_issue])
+ 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)).
- to eq([searchable_issue])
+ 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)).
- to eq([searchable_issue])
+ 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)).
- to eq([searchable_issue])
+ 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)).
- to eq([searchable_issue])
+ expect(issuable_class.full_search(searchable_issue.description.upcase))
+ .to eq([searchable_issue])
end
end
@@ -173,7 +155,7 @@ describe Issue, "Issuable" do
end
describe "#sort" do
- let(:project) { build_stubbed(:empty_project) }
+ let(:project) { create(:empty_project) }
context "by milestone due date" do
# Correct order is:
@@ -218,7 +200,9 @@ describe Issue, "Issuable" do
let(:project) { issue.project }
context 'user is not a participant in the issue' do
- before { allow(issue).to receive(:participants).with(user).and_return([]) }
+ before do
+ allow(issue).to receive(:participants).with(user).and_return([])
+ end
it 'returns false when no subcription exists' do
expect(issue.subscribed?(user, project)).to be_falsey
@@ -238,7 +222,9 @@ describe Issue, "Issuable" do
end
context 'user is a participant in the issue' do
- before { allow(issue).to receive(:participants).with(user).and_return([user]) }
+ before do
+ allow(issue).to receive(:participants).with(user).and_return([user])
+ end
it 'returns false when no subcription exists' do
expect(issue.subscribed?(user, project)).to be_truthy
@@ -270,34 +256,43 @@ describe Issue, "Issuable" do
end
context "issue is assigned" do
- before { issue.update_attribute(:assignee, user) }
+ before do
+ issue.assignees << user
+ end
it "returns correct hook data" do
- expect(data[:object_attributes]['assignee_id']).to eq(user.id)
- expect(data[:assignee]).to eq(user.hook_attrs)
+ expect(data[:assignees].first).to eq(user.hook_attrs)
end
end
- include_examples 'project hook data'
- include_examples 'deprecated repository hook data'
- end
+ context "merge_request is assigned" do
+ let(:merge_request) { create(:merge_request) }
+ let(:data) { merge_request.to_hook_data(user) }
- 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)
+ before do
+ merge_request.update_attribute(:assignee, user)
+ end
- expect(issue.card_attributes).
- to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ it "returns correct hook data" do
+ expect(data[:object_attributes]['assignee_id']).to eq(user.id)
+ expect(data[:assignee]).to eq(user.hook_attrs)
+ end
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'))
+ context 'issue has labels' do
+ let(:labels) { [create(:label), create(:label)] }
- expect(issue.card_attributes).
- to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+ before do
+ issue.update_attribute(:labels, labels)
+ end
+
+ it 'includes labels in the hook data' do
+ expect(data[:labels]).to eq(labels.map(&:hook_attrs))
+ end
end
+
+ include_examples 'project hook data'
+ include_examples 'deprecated repository hook data'
end
describe '#labels_array' do
@@ -344,6 +339,46 @@ describe Issue, "Issuable" do
end
end
+ describe '.order_due_date_and_labels_priority' do
+ let(:project) { create(:empty_project) }
+
+ def create_issue(milestone, labels)
+ create(:labeled_issue, milestone: milestone, labels: labels, project: project)
+ end
+
+ it 'sorts issues in order of milestone due date, then label priority' do
+ first_priority = create(:label, project: project, priority: 1)
+ second_priority = create(:label, project: project, priority: 2)
+ no_priority = create(:label, project: project)
+
+ first_milestone = create(:milestone, project: project, due_date: Time.now)
+ second_milestone = create(:milestone, project: project, due_date: Time.now + 1.month)
+ third_milestone = create(:milestone, project: project)
+
+ # The issues here are ordered by label priority, to ensure that we don't
+ # accidentally just sort by creation date.
+ second_milestone_first_priority = create_issue(second_milestone, [first_priority, second_priority, no_priority])
+ third_milestone_first_priority = create_issue(third_milestone, [first_priority, second_priority, no_priority])
+ first_milestone_second_priority = create_issue(first_milestone, [second_priority, no_priority])
+ second_milestone_second_priority = create_issue(second_milestone, [second_priority, no_priority])
+ no_milestone_second_priority = create_issue(nil, [second_priority, no_priority])
+ first_milestone_no_priority = create_issue(first_milestone, [no_priority])
+ second_milestone_no_labels = create_issue(second_milestone, [])
+ third_milestone_no_priority = create_issue(third_milestone, [no_priority])
+
+ result = Issue.order_due_date_and_labels_priority
+
+ expect(result).to eq([first_milestone_second_priority,
+ first_milestone_no_priority,
+ second_milestone_first_priority,
+ second_milestone_second_priority,
+ second_milestone_no_labels,
+ third_milestone_first_priority,
+ no_milestone_second_priority,
+ third_milestone_no_priority])
+ end
+ end
+
describe '.order_labels_priority' do
let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) }
let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) }
@@ -388,27 +423,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..e2a29e0ae70 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -61,7 +61,9 @@ describe Issue, "Mentionable" do
end
context 'when the current user can see the issue' do
- before { private_project.team << [user, Gitlab::Access::DEVELOPER] }
+ before do
+ private_project.team << [user, Gitlab::Access::DEVELOPER]
+ end
it 'includes the reference' do
expect(referenced_issues(user)).to contain_exactly(private_issue, public_issue)
@@ -163,3 +165,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 ad703a6c8bb..cefe7fb6fea 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -11,20 +11,51 @@ 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) }
+ let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+ let(:label_3) { create(:label, title: 'label_3', project: project) }
before do
project.team << [member, :developer]
project.team << [guest, :guest]
end
+ describe '#sorted_issues' do
+ it 'sorts issues by label priority' do
+ issue.labels << label_1
+ security_issue_1.labels << label_2
+ closed_issue_1.labels << label_3
+
+ issues = milestone.sorted_issues(member)
+
+ expect(issues.first).to eq(issue)
+ expect(issues.second).to eq(security_issue_1)
+ expect(issues.third).not_to eq(closed_issue_1)
+ end
+ end
+
+ describe '#sorted_merge_requests' do
+ it 'sorts merge requests by label priority' do
+ merge_request_1 = create(:labeled_merge_request, labels: [label_2], source_project: project, source_branch: 'branch_1', milestone: milestone)
+ merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone)
+ merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone)
+
+ merge_requests = milestone.sorted_merge_requests
+
+ expect(merge_requests.first).to eq(merge_request_2)
+ expect(merge_requests.second).to eq(merge_request_1)
+ expect(merge_requests.third).to eq(merge_request_3)
+ end
+ end
+
describe '#closed_items_count' do
it 'does not count confidential issues for non project members' do
expect(milestone.closed_items_count(non_member)).to eq 2
@@ -116,21 +147,41 @@ describe Milestone, 'Milestoneish' do
end
end
+ describe '#remaining_days' do
+ it 'shows 0 if no due date' do
+ milestone = build_stubbed(:milestone)
+
+ expect(milestone.remaining_days).to eq(0)
+ end
+
+ it 'shows 0 if expired' do
+ milestone = build_stubbed(:milestone, due_date: 2.days.ago)
+
+ expect(milestone.remaining_days).to eq(0)
+ end
+
+ it 'shows correct remaining days' do
+ milestone = build_stubbed(:milestone, due_date: 2.days.from_now)
+
+ expect(milestone.remaining_days).to eq(2)
+ end
+ end
+
describe '#elapsed_days' do
it 'shows 0 if no start_date set' do
- milestone = build(:milestone)
+ milestone = build_stubbed(:milestone)
expect(milestone.elapsed_days).to eq(0)
end
it 'shows 0 if start_date is a future' do
- milestone = build(:milestone, start_date: Time.now + 2.days)
+ milestone = build_stubbed(:milestone, start_date: Time.now + 2.days)
expect(milestone.elapsed_days).to eq(0)
end
it 'shows correct amount of days' do
- milestone = build(:milestone, start_date: Time.now - 2.days)
+ milestone = build_stubbed(:milestone, start_date: Time.now - 2.days)
expect(milestone.elapsed_days).to eq(2)
end
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/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index a0765a264cf..5f9b7e0a367 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe ReactiveCaching, caching: true do
+describe ReactiveCaching, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
class CacheTest
@@ -40,7 +40,10 @@ describe ReactiveCaching, caching: true do
let(:instance) { CacheTest.new(666, &calculation) }
describe '#with_reactive_cache' do
- before { stub_reactive_cache }
+ before do
+ stub_reactive_cache
+ end
+
subject(:go!) { instance.result }
context 'when cache is empty' do
@@ -60,12 +63,17 @@ describe ReactiveCaching, caching: true do
end
context 'when the cache is full' do
- before { stub_reactive_cache(instance, 4) }
+ before do
+ stub_reactive_cache(instance, 4)
+ end
it { is_expected.to eq(2) }
context 'and expired' do
- before { invalidate_reactive_cache(instance) }
+ before do
+ invalidate_reactive_cache(instance)
+ end
+
it { is_expected.to be_nil }
end
end
@@ -84,7 +92,9 @@ describe ReactiveCaching, caching: true do
subject(:go!) { instance.exclusively_update_reactive_cache! }
context 'when the lease is free and lifetime is not exceeded' do
- before { stub_reactive_cache(instance, "preexisting") }
+ before do
+ stub_reactive_cache(instance, "preexisting")
+ end
it 'takes and releases the lease' do
expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000")
@@ -106,7 +116,10 @@ describe ReactiveCaching, caching: true do
end
context 'and #calculate_reactive_cache raises an exception' do
- before { stub_reactive_cache(instance, "preexisting") }
+ before do
+ stub_reactive_cache(instance, "preexisting")
+ end
+
let(:calculation) { -> { raise "foo"} }
it 'leaves the cache untouched' do
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
index 69906382545..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) }
@@ -12,12 +12,6 @@ describe Issue, 'RelativePositioning' do
end
end
- describe '#min_relative_position' do
- it 'returns maximum position' do
- expect(issue.min_relative_position).to eq issue.relative_position
- end
- end
-
describe '#max_relative_position' do
it 'returns maximum position' do
expect(issue.max_relative_position).to eq issue1.relative_position
@@ -29,8 +23,8 @@ describe Issue, 'RelativePositioning' do
expect(issue1.prev_relative_position).to eq issue.relative_position
end
- it 'returns minimum position if there is no issue above' do
- expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION
+ it 'returns nil if there is no issue above' do
+ expect(issue.prev_relative_position).to eq nil
end
end
@@ -39,8 +33,8 @@ describe Issue, 'RelativePositioning' do
expect(issue.next_relative_position).to eq issue1.relative_position
end
- it 'returns next position if there is no issue below' do
- expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION
+ it 'returns nil if there is no issue below' do
+ expect(issue1.next_relative_position).to eq nil
end
end
@@ -72,6 +66,34 @@ describe Issue, 'RelativePositioning' do
end
end
+ describe '#shift_after?' do
+ it 'returns true' do
+ issue.update(relative_position: issue1.relative_position - 1)
+
+ expect(issue.shift_after?).to be_truthy
+ end
+
+ it 'returns false' do
+ issue.update(relative_position: issue1.relative_position - 2)
+
+ expect(issue.shift_after?).to be_falsey
+ end
+ end
+
+ describe '#shift_before?' do
+ it 'returns true' do
+ issue.update(relative_position: issue1.relative_position + 1)
+
+ expect(issue.shift_before?).to be_truthy
+ end
+
+ it 'returns false' do
+ issue.update(relative_position: issue1.relative_position + 2)
+
+ expect(issue.shift_before?).to be_falsey
+ end
+ end
+
describe '#move_between' do
it 'positions issue between two other' do
new_issue.move_between(issue, issue1)
@@ -100,5 +122,83 @@ describe Issue, 'RelativePositioning' do
expect(new_issue.relative_position).to be > issue.relative_position
expect(issue.relative_position).to be < issue1.relative_position
end
+
+ it 'positions issues between other two if distance is 1' do
+ issue1.update relative_position: issue.relative_position + 1
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be > issue.relative_position
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+
+ it 'positions issue in the middle of other two if distance is big enough' do
+ issue.update relative_position: 6000
+ issue1.update relative_position: 10000
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to eq(8000)
+ end
+
+ it 'positions issue closer to the middle if we are at the very top' do
+ issue1.update relative_position: 6000
+
+ new_issue.move_between(nil, issue1)
+
+ expect(new_issue.relative_position).to eq(6000 - RelativePositioning::IDEAL_DISTANCE)
+ end
+
+ it 'positions issue closer to the middle if we are at the very bottom' do
+ issue.update relative_position: 6000
+ issue1.update relative_position: nil
+
+ new_issue.move_between(issue, nil)
+
+ expect(new_issue.relative_position).to eq(6000 + RelativePositioning::IDEAL_DISTANCE)
+ end
+
+ it 'positions issue in the middle of other two if distance is not big enough' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 400
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to eq(250)
+ end
+
+ it 'positions issue in the middle of other two is there is no place' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 101
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be_between(issue.relative_position, issue1.relative_position)
+ end
+
+ it 'uses rebalancing if there is no place' do
+ issue.update relative_position: 100
+ issue1.update relative_position: 101
+ issue2 = create(:issue, relative_position: 102, project: project)
+ new_issue.update relative_position: 103
+
+ new_issue.move_between(issue1, issue2)
+ new_issue.save!
+
+ expect(new_issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ expect(issue.reload.relative_position).not_to eq(100)
+ end
+
+ it 'positions issue right if we pass none-sequential parameters' do
+ issue.update relative_position: 99
+ issue1.update relative_position: 101
+ issue2 = create(:issue, relative_position: 102, project: project)
+ new_issue.update relative_position: 103
+
+ new_issue.move_between(issue, issue2)
+ new_issue.save!
+
+ expect(new_issue.relative_position).to be(100)
+ end
end
end
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
new file mode 100644
index 00000000000..3934992c143
--- /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..36aedd2f701 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
@@ -71,22 +115,34 @@ describe Group, 'Routable' do
end
end
- describe '.member_descendants' do
- let!(:user) { create(:user) }
- let!(:nested_group) { create(:group, parent: group) }
-
- before { group.add_owner(user) }
- subject { described_class.member_descendants(user.id) }
-
- it { is_expected.to eq([nested_group]) }
- 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', :request_store do
+ 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 '#expires_full_path_cache' do
+ context 'with RequestStore active', :request_store do
+ it 'expires the full_path cache' do
+ expect(group.full_path).to eq('foo')
+
+ group.route.update(path: 'bar', name: 'bar')
+ group.expires_full_path_cache
+
+ expect(group.full_path).to eq('bar')
+ end
+ end
end
describe '#full_name' do
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
new file mode 100644
index 00000000000..21893e0cbaa
--- /dev/null
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe ShaAttribute do
+ let(:model) { Class.new { include ShaAttribute } }
+
+ before do
+ columns = [
+ double(:column, name: 'name', type: :text),
+ double(:column, name: 'sha1', type: :binary)
+ ]
+
+ allow(model).to receive(:columns).and_return(columns)
+ end
+
+ describe '#sha_attribute' do
+ context 'when the table exists' do
+ before do
+ allow(model).to receive(:table_exists?).and_return(true)
+ end
+
+ it 'defines a SHA attribute for a binary column' do
+ expect(model).to receive(:attribute)
+ .with(:sha1, an_instance_of(Gitlab::Database::ShaAttribute))
+
+ model.sha_attribute(:sha1)
+ end
+
+ it 'raises ArgumentError when the column type is not :binary' do
+ expect { model.sha_attribute(:name) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'when the table does not exist' do
+ before do
+ allow(model).to receive(:table_exists?).and_return(false)
+ end
+
+ it 'does nothing' do
+ expect(model).not_to receive(:columns)
+ expect(model).not_to receive(:attribute)
+
+ model.sha_attribute(:name)
+ end
+ end
+ end
+end
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/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 4b0bfa43abf..882afeccfc6 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -49,7 +49,10 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
end
context 'token is generated' do
- before { subject.send("reset_#{token_field}!") }
+ before do
+ subject.send("reset_#{token_field}!")
+ end
+
it 'persists a new token' do
expect(subject.send(:read_attribute, token_field)).to be_a String
end
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..fd58bd1d6ad 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -13,7 +13,7 @@ describe 'CycleAnalytics#test', feature: true do
data_fn: lambda do |context|
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)
+ pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project, head_pipeline_of: merge_request)
{ 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..bb84d3fc13d 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,8 +16,21 @@ describe Deployment, models: true do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe 'after_create callbacks' do
+ let(:environment) { create(:environment) }
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'invalidates the environment etag cache' do
+ old_value = store.get(environment.etag_cache_key)
+
+ create(:deployment, environment: environment)
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#includes_commit?' do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
let(:deployment) do
create(:deployment, environment: environment, sha: project.commit.id)
@@ -49,6 +62,64 @@ 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 '#additional_metrics' do
+ let(:project) { create(:project) }
+ let(:deployment) { create(:deployment, project: project) }
+
+ subject { deployment.additional_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
+ }
+ end
+
+ let(:prometheus_service) { double('prometheus_service') }
+
+ before do
+ allow(project).to receive(:prometheus_service).and_return(prometheus_service)
+ allow(prometheus_service).to receive(:additional_deployment_metrics).and_return(simple_metrics)
+ end
+
+ it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) }
+ end
+ end
+
describe '#stop_action' do
let(:build) { create(:ci_build) }
@@ -61,7 +132,7 @@ describe Deployment, models: true do
end
context 'with other actions' do
- let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+ let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
context 'when matching action is defined' do
let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') }
@@ -89,7 +160,7 @@ describe Deployment, models: true do
context 'when matching action is defined' do
let(:build) { create(:ci_build) }
let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') }
- let!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+ let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
it { is_expected.to be_truthy }
end
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
new file mode 100644
index 00000000000..45b2f6e4beb
--- /dev/null
+++ b/spec/models/diff_discussion_spec.rb
@@ -0,0 +1,91 @@
+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(subject.merge_request_version_params).to eq(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) }
+
+ before do
+ diff_note.position = diff_note.original_position
+ diff_note.save!
+ end
+
+ 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..297c2108dc2 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
@@ -156,7 +145,7 @@ describe DiffNote, models: true do
context "when the merge request's diff refs don't match that of the diff note" do
before do
- allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs)
+ allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs)
end
it "returns false" do
@@ -171,12 +160,6 @@ describe DiffNote, models: true do
context "when noteable is a commit" do
let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) }
- it "doesn't use the DiffPositionUpdateService" do
- expect(Notes::DiffPositionUpdateService).not_to receive(:new)
-
- diff_note
- end
-
it "doesn't update the position" do
diff_note
@@ -189,12 +172,6 @@ describe DiffNote, models: true do
let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
context "when the note is active" do
- it "doesn't use the DiffPositionUpdateService" do
- expect(Notes::DiffPositionUpdateService).not_to receive(:new)
-
- diff_note
- end
-
it "doesn't update the position" do
diff_note
@@ -205,273 +182,20 @@ describe DiffNote, models: true do
context "when the note is outdated" do
before do
- allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs)
+ allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
end
- it "uses the DiffPositionUpdateService" do
- service = instance_double("Notes::DiffPositionUpdateService")
- expect(Notes::DiffPositionUpdateService).to receive(:new).with(
- project,
- nil,
- old_diff_refs: position.diff_refs,
- new_diff_refs: commit.diff_refs,
- paths: [path]
- ).and_return(service)
- expect(service).to receive(:execute)
-
+ it "updates the position" do
diff_note
- end
- end
- end
- 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
+ expect(diff_note.original_position).to eq(position)
+ expect(diff_note.position).not_to eq(position)
+ end
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 +221,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/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb
new file mode 100644
index 00000000000..3755f4a56f3
--- /dev/null
+++ b/spec/models/diff_viewer/base_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+describe DiffViewer::Base, model: true do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') }
+
+ let(:viewer_class) do
+ Class.new(described_class) do
+ include DiffViewer::ServerSide
+
+ self.extensions = %w(jpg)
+ self.binary = true
+ self.collapse_limit = 1.megabyte
+ self.size_limit = 5.megabytes
+ end
+ end
+
+ let(:viewer) { viewer_class.new(diff_file) }
+
+ describe '.can_render?' do
+ context 'when the extension is supported' do
+ let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
+
+ context 'when the binaryness matches' do
+ it 'returns true' do
+ expect(viewer_class.can_render?(diff_file)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ before do
+ allow(diff_file.old_blob).to receive(:binary?).and_return(false)
+ allow(diff_file.new_blob).to receive(:binary?).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(diff_file)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the file type is supported' do
+ let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('LICENSE') }
+
+ before do
+ viewer_class.file_types = %i(license)
+ viewer_class.binary = false
+ end
+
+ context 'when the binaryness matches' do
+ it 'returns true' do
+ expect(viewer_class.can_render?(diff_file)).to be_truthy
+ end
+ end
+
+ context 'when the binaryness does not match' do
+ before do
+ allow(diff_file.old_blob).to receive(:binary?).and_return(true)
+ allow(diff_file.new_blob).to receive(:binary?).and_return(true)
+ end
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(diff_file)).to be_falsey
+ end
+ end
+ end
+
+ context 'when the extension and file type are not supported' do
+ it 'returns false' do
+ expect(viewer_class.can_render?(diff_file)).to be_falsey
+ end
+ end
+
+ context 'when the file was renamed and only the old blob is supported' do
+ let(:commit) { project.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('files/images/6049019_460s.jpg') }
+
+ before do
+ allow(diff_file).to receive(:renamed_file?).and_return(true)
+ allow(diff_file.new_blob).to receive(:extension).and_return('jpeg')
+ end
+
+ it 'returns false' do
+ expect(viewer_class.can_render?(diff_file)).to be_falsey
+ end
+ end
+ end
+
+ describe '#collapsed?' do
+ context 'when the combined blob size is larger than the collapse limit' do
+ before do
+ allow(diff_file.old_blob).to receive(:raw_size).and_return(512.kilobytes)
+ allow(diff_file.new_blob).to receive(:raw_size).and_return(513.kilobytes)
+ end
+
+ it 'returns true' do
+ expect(viewer.collapsed?).to be_truthy
+ end
+ end
+
+ context 'when the combined blob size is smaller than the collapse limit' do
+ it 'returns false' do
+ expect(viewer.collapsed?).to be_falsey
+ end
+ end
+ end
+
+ describe '#too_large?' do
+ context 'when the combined blob size is larger than the size limit' do
+ before do
+ allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes)
+ allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes)
+ end
+
+ it 'returns true' do
+ expect(viewer.too_large?).to be_truthy
+ end
+ end
+
+ context 'when the blob size is smaller than the size limit' do
+ it 'returns false' do
+ expect(viewer.too_large?).to be_falsey
+ end
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the combined blob size is larger than the size limit' do
+ before do
+ allow(diff_file.old_blob).to receive(:raw_size).and_return(2.megabytes)
+ allow(diff_file.new_blob).to receive(:raw_size).and_return(4.megabytes)
+ end
+
+ it 'returns :too_large' do
+ expect(viewer.render_error).to eq(:too_large)
+ end
+ end
+
+ context 'when the combined blob size is smaller than the size limit' do
+ it 'returns nil' do
+ expect(viewer.render_error).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb
new file mode 100644
index 00000000000..2d926e06936
--- /dev/null
+++ b/spec/models/diff_viewer/server_side_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe DiffViewer::ServerSide, model: true do
+ let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+ let(:diff_file) { commit.diffs.diff_file_with_new_path('files/ruby/popen.rb') }
+
+ let(:viewer_class) do
+ Class.new(DiffViewer::Base) do
+ include DiffViewer::ServerSide
+ end
+ end
+
+ subject { viewer_class.new(diff_file) }
+
+ describe '#prepare!' do
+ it 'loads all diff file data' do
+ expect(diff_file.old_blob).to receive(:load_all_data!)
+ expect(diff_file.new_blob).to receive(:load_all_data!)
+
+ subject.prepare!
+ end
+ end
+
+ describe '#render_error' do
+ context 'when the diff file is stored externally' do
+ before do
+ allow(diff_file).to receive(:stored_externally?).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/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 b4305e92812..0a2cd8c2957 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment, models: true do
- let(:project) { create(:empty_project) }
+ set(:project) { create(:empty_project) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
@@ -34,6 +34,26 @@ describe Environment, models: true do
end
end
+ describe 'state machine' do
+ it 'invalidates the cache after a change' do
+ expect(environment).to receive(:expire_etag_cache)
+
+ environment.stop
+ end
+ end
+
+ describe '#expire_etag_cache' do
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'changes the cached value' do
+ old_value = store.get(environment.etag_cache_key)
+
+ environment.stop
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
@@ -107,6 +127,10 @@ describe Environment, models: true do
it 'return nil when no deployment is found' do
expect(environment.first_deployment_for(head_commit)).to eq nil
end
+
+ it 'returns a UTF-8 ref' do
+ expect(environment.first_deployment_for(commit).ref).to be_utf8
+ end
end
describe '#environment_type' do
@@ -135,7 +159,7 @@ describe Environment, models: true do
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!(:close_action) { create(:ci_build, pipeline: build.pipeline, name: 'close_app', when: :manual) }
+ let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') }
context 'when environment is available' do
before do
@@ -191,25 +215,55 @@ 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
+
+ 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 'returns the same action' do
- expect(subject).to eq(close_action)
- expect(subject.user).to eq(user)
+ 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_developer(user)
- 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)
+ create(:protected_branch, :developers_can_merge,
+ name: 'master', project: project)
+ 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
+
+ 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
@@ -239,7 +293,7 @@ describe Environment, models: true do
describe '#actions_for' do
let(:deployment) { create(:deployment, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline }
- let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
+ let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )}
let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
it 'returns a list of actions with matching environment' do
@@ -351,7 +405,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)
@@ -367,6 +421,99 @@ describe Environment, models: true do
end
end
+ describe '#has_metrics?' do
+ subject { environment.has_metrics? }
+
+ context 'when the enviroment is available' do
+ context 'with a deployment service' do
+ let(:project) { create(:prometheus_project) }
+
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'without a monitoring service' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when the environment is unavailable' do
+ let(:project) { create(:prometheus_project) }
+
+ before do
+ environment.stop
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#additional_metrics' do
+ let(:project) { create(:prometheus_project) }
+ subject { environment.additional_metrics }
+
+ context 'when the environment has additional metrics' do
+ before do
+ allow(environment).to receive(:has_additional_metrics?).and_return(true)
+ end
+
+ it 'returns the additional metrics from the deployment service' do
+ expect(project.prometheus_service).to receive(:additional_environment_metrics)
+ .with(environment)
+ .and_return(:fake_metrics)
+
+ is_expected.to eq(:fake_metrics)
+ end
+ end
+
+ context 'when the environment does not have metrics' do
+ before do
+ allow(environment).to receive(:has_additional_metrics?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#has_additional_metrics??' do
+ subject { environment.has_additional_metrics? }
+
+ context 'when the enviroment is available' do
+ context 'with a deployment service' do
+ let(:project) { create(:prometheus_project) }
+
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'without a monitoring service' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when the environment is unavailable' do
+ let(:project) { create(:prometheus_project) }
+
+ before do
+ environment.stop
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '#slug' do
it "is automatically generated" do
expect(environment.slug).not_to be_nil
@@ -396,7 +543,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..10b9bf9f43a 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) }
@@ -240,10 +266,10 @@ describe Event, models: true do
it 'does not update the project' do
project.update(last_activity_at: Time.now)
- expect(project).not_to receive(:update_column).
- with(:last_activity_at, a_kind_of(Time))
+ 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/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 454550c9710..38fbdd2536a 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -2,53 +2,75 @@ require 'spec_helper'
describe ForkedProjectLink, "add link on fork" do
let(:project_from) { create(:project, :repository) }
- let(:namespace) { create(:namespace) }
- let(:user) { create(:user, namespace: namespace) }
+ let(:project_to) { fork_project(project_from, user) }
+ let(:user) { create(:user) }
+ let(:namespace) { user.namespace }
before do
- create(:project_member, :reporter, user: user, project: project_from)
- @project_to = fork_project(project_from, user)
+ project_from.add_reporter(user)
+ end
+
+ it 'project_from knows its forks' do
+ _ = project_to
+
+ expect(project_from.forks.count).to eq(1)
end
it "project_to knows it is forked" do
- expect(@project_to.forked?).to be_truthy
+ expect(project_to.forked?).to be_truthy
end
it "project knows who it is forked from" do
- expect(@project_to.forked_from_project).to eq(project_from)
+ expect(project_to.forked_from_project).to eq(project_from)
end
-end
-describe '#forked?' do
- let(:forked_project_link) { build(:forked_project_link) }
- let(:project_from) { create(:project, :repository) }
- let(:project_to) { create(:project, forked_project_link: forked_project_link) }
+ context 'project_to is pending_delete' do
+ before do
+ project_to.update!(pending_delete: true)
+ end
- before :each do
- forked_project_link.forked_from_project = project_from
- forked_project_link.forked_to_project = project_to
- forked_project_link.save!
+ it { expect(project_from.forks.count).to eq(0) }
end
- it "project_to knows it is forked" do
- expect(project_to.forked?).to be_truthy
- end
+ context 'project_from is pending_delete' do
+ before do
+ project_from.update!(pending_delete: true)
+ end
- it "project_from is not forked" do
- expect(project_from.forked?).to be_falsey
+ it { expect(project_to.forked_from_project).to be_nil }
end
- it "project_to.destroy destroys fork_link" do
- expect(forked_project_link).to receive(:destroy)
- project_to.destroy
+ describe '#forked?' do
+ let(:project_to) { create(:project, forked_project_link: forked_project_link) }
+ let(:forked_project_link) { create(:forked_project_link) }
+
+ before do
+ forked_project_link.forked_from_project = project_from
+ forked_project_link.forked_to_project = project_to
+ forked_project_link.save!
+ end
+
+ it "project_to knows it is forked" do
+ expect(project_to.forked?).to be_truthy
+ end
+
+ it "project_from is not forked" do
+ expect(project_from.forked?).to be_falsey
+ end
+
+ it "project_to.destroy destroys fork_link" do
+ project_to.destroy
+
+ expect(ForkedProjectLink.exists?(id: forked_project_link.id)).to eq(false)
+ end
end
-end
-def fork_project(from_project, user)
- shell = double('gitlab_shell', fork_repository: true)
+ def fork_project(from_project, user)
+ service = Projects::ForkService.new(from_project, user)
+ shell = double('gitlab_shell', fork_repository: true)
- service = Projects::ForkService.new(from_project, user)
- allow(service).to receive(:gitlab_shell).and_return(shell)
+ allow(service).to receive(:gitlab_shell).and_return(shell)
- service.execute
+ service.execute
+ end
end
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index f4c3e6d503f..152e97e09bf 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -19,7 +19,10 @@ describe GenericCommitStatus, models: true do
describe '#context' do
subject { generic_commit_status.context }
- before { generic_commit_status.context = 'my_context' }
+
+ before do
+ generic_commit_status.context = 'my_context'
+ end
it { is_expected.to eq(generic_commit_status.name) }
end
@@ -39,7 +42,9 @@ describe GenericCommitStatus, models: true do
end
context 'when user has ability to see datails' do
- before { project.team << [user, :developer] }
+ before do
+ project.team << [user, :developer]
+ end
it 'details path points to an external URL' do
expect(status).to have_details
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index cacbab8bcb1..a14efda3eda 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -92,6 +92,41 @@ describe GlobalMilestone, models: true do
end
end
+ describe '.states_count' do
+ context 'when the projects have milestones' do
+ before do
+ create(:closed_milestone, title: 'Active Group Milestone', project: project3)
+ create(:active_milestone, title: 'Active Group Milestone', project: project1)
+ create(:active_milestone, title: 'Active Group Milestone', project: project2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project1)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project2)
+ create(:closed_milestone, title: 'Closed Group Milestone', project: project3)
+ end
+
+ it 'returns the quantity of global milestones in each possible state' do
+ expected_count = { opened: 1, closed: 2, all: 2 }
+
+ count = GlobalMilestone.states_count(Project.all)
+
+ expect(count).to eq(expected_count)
+ end
+ end
+
+ context 'when the projects do not have milestones' do
+ before do
+ project1
+ end
+
+ it 'returns 0 as the quantity of global milestones in each state' do
+ expected_count = { opened: 0, closed: 0, all: 0 }
+
+ count = GlobalMilestone.states_count(Project.all)
+
+ expect(count).to eq(expected_count)
+ end
+ end
+ end
+
describe '#initialize' do
let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
@@ -102,7 +137,7 @@ describe GlobalMilestone, models: true do
[
milestone1_project1,
milestone1_project2,
- milestone1_project3,
+ milestone1_project3
]
milestones_relation = Milestone.where(id: milestones.map(&:id))
@@ -127,4 +162,32 @@ describe GlobalMilestone, models: true do
expect(global_milestone.safe_title).to eq('git-test')
end
end
+
+ describe '#state' do
+ context 'when at least one milestone is active' do
+ it 'returns active' do
+ title = 'Active Group Milestone'
+ milestones = [
+ create(:active_milestone, title: title),
+ create(:closed_milestone, title: title)
+ ]
+ global_milestone = GlobalMilestone.new(title, milestones)
+
+ expect(global_milestone.state).to eq('active')
+ end
+ end
+
+ context 'when all milestones are closed' do
+ it 'returns closed' do
+ title = 'Closed Group Milestone'
+ milestones = [
+ create(:closed_milestone, title: title),
+ create(:closed_milestone, title: title)
+ ]
+ global_milestone = GlobalMilestone.new(title, milestones)
+
+ expect(global_milestone.state).to eq('closed')
+ end
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 5d87938235a..770176451fe 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -13,6 +13,7 @@ describe Group, models: true do
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') }
+ it { is_expected.to have_many(:variables).class_name('Ci::GroupVariable') }
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_one(:chat_team) }
@@ -55,6 +56,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
@@ -115,14 +144,20 @@ describe Group, models: true do
describe '#add_user' do
let(:user) { create(:user) }
- before { group.add_user(user, GroupMember::MASTER) }
+
+ before do
+ group.add_user(user, GroupMember::MASTER)
+ end
it { expect(group.group_members.masters.map(&:user)).to include(user) }
end
describe '#add_users' do
let(:user) { create(:user) }
- before { group.add_users([user.id], GroupMember::GUEST) }
+
+ before do
+ group.add_users([user.id], GroupMember::GUEST)
+ end
it "updates the group permission" do
expect(group.group_members.guests.map(&:user)).to include(user)
@@ -134,7 +169,10 @@ describe Group, models: true do
describe '#avatar_type' do
let(:user) { create(:user) }
- before { group.add_user(user, GroupMember::MASTER) }
+
+ before do
+ group.add_user(user, GroupMember::MASTER)
+ end
it "is true if avatar is image" do
group.update_attribute(:avatar, 'uploads/avatar.png')
@@ -147,6 +185,28 @@ 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/-/system/group/avatar/#{group.id}/dk.png" }
+
+ context 'when avatar file is uploaded' do
+ before do
+ group.add_master(user)
+ end
+
+ 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])
@@ -174,7 +234,9 @@ describe Group, models: true do
end
describe '#has_owner?' do
- before { @members = setup_group_members(group) }
+ before do
+ @members = setup_group_members(group)
+ end
it { expect(group.has_owner?(@members[:owner])).to be_truthy }
it { expect(group.has_owner?(@members[:master])).to be_falsey }
@@ -185,7 +247,9 @@ describe Group, models: true do
end
describe '#has_master?' do
- before { @members = setup_group_members(group) }
+ before do
+ @members = setup_group_members(group)
+ end
it { expect(group.has_master?(@members[:owner])).to be_falsey }
it { expect(group.has_master?(@members[:master])).to be_truthy }
@@ -292,7 +356,7 @@ describe Group, models: true do
it { expect(subject.parent).to be_kind_of(Group) }
end
- describe '#members_with_parents' do
+ describe '#members_with_parents', :nested_groups do
let!(:group) { create(:group, :nested) }
let!(:master) { group.parent.add_user(create(:user), GroupMember::MASTER) }
let!(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) }
@@ -311,8 +375,113 @@ describe Group, models: true do
group.add_user(master, GroupMember::MASTER)
group.add_user(developer, GroupMember::DEVELOPER)
- expect(group.user_ids_for_project_authorizations).
- to include(master.id, developer.id)
+ expect(group.user_ids_for_project_authorizations)
+ .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
+
+ describe '#secret_variables_for' do
+ let(:project) { create(:empty_project, group: group) }
+
+ let!(:secret_variable) do
+ create(:ci_group_variable, value: 'secret', group: group)
+ end
+
+ let!(:protected_variable) do
+ create(:ci_group_variable, :protected, value: 'protected', group: group)
+ end
+
+ subject { group.secret_variables_for('ref', project) }
+
+ shared_examples 'ref is protected' do
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(secret_variable, protected_variable)
+ end
+ end
+
+ context 'when the ref is not protected' do
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ it 'contains only the secret variables' do
+ is_expected.to contain_exactly(secret_variable)
+ end
+ end
+
+ context 'when the ref is a protected branch' do
+ before do
+ create(:protected_branch, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+
+ context 'when the ref is a protected tag' do
+ before do
+ create(:protected_tag, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+
+ context 'when group has children', :postgresql do
+ let(:group_child) { create(:group, parent: group) }
+ let(:group_child_2) { create(:group, parent: group_child) }
+ let(:group_child_3) { create(:group, parent: group_child_2) }
+ let(:variable_child) { create(:ci_group_variable, group: group_child) }
+ let(:variable_child_2) { create(:ci_group_variable, group: group_child_2) }
+ let(:variable_child_3) { create(:ci_group_variable, group: group_child_3) }
+
+ it 'returns all variables belong to the group and parent groups' do
+ expected_array1 = [protected_variable, secret_variable]
+ expected_array2 = [variable_child, variable_child_2, variable_child_3]
+ got_array = group_child_3.secret_variables_for('ref', project).to_a
+
+ expect(got_array.shift(2)).to contain_exactly(*expected_array1)
+ expect(got_array).to eq(expected_array2)
+ end
end
end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 474ae62ccec..0af270014b5 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -1,10 +1,14 @@
require 'spec_helper'
describe ProjectHook, models: true do
- describe "Associations" do
+ describe 'associations' do
it { is_expected.to belong_to :project }
end
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+
describe '.push_hooks' do
it 'returns hooks for push events only' do
hook = create(:project_hook, push_events: true)
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 1a83c836652..8e871a41a8c 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -1,36 +1,23 @@
-require "spec_helper"
+require 'spec_helper'
describe ServiceHook, models: true do
- describe "Associations" do
+ describe 'associations' do
it { is_expected.to belong_to :service }
end
- describe "execute" do
- before(:each) do
- @service_hook = create(:service_hook)
- @data = { project_id: 1, data: {} }
-
- WebMock.stub_request(:post, @service_hook.url)
- end
-
- it "POSTs to the webhook URL" do
- @service_hook.execute(@data)
- expect(WebMock).to have_requested(:post, @service_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' }
- ).once
- end
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:service) }
+ end
- it "POSTs the data as JSON" do
- @service_hook.execute(@data)
- expect(WebMock).to have_requested(:post, @service_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Service Hook' }
- ).once
- end
+ describe 'execute' do
+ let(:hook) { build(:service_hook) }
+ let(:data) { { key: 'value' } }
- it "catches exceptions" do
- expect(WebHook).to receive(:post).and_raise("Some HTTP Post error")
+ it '#execute' do
+ expect(WebHookService).to receive(:new).with(hook, data, 'service_hook').and_call_original
+ expect_any_instance_of(WebHookService).to receive(:execute)
- expect { @service_hook.execute(@data) }.to raise_error(RuntimeError)
+ hook.execute(data)
end
end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index e8caad00c44..559778257fa 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,11 +1,26 @@
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
+ }
+ expect(system_hook).to have_attributes(attrs)
+ end
+ end
+
describe "execute" do
let(:system_hook) { create(:system_hook) }
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
let(:group) { create(:group) }
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: 'mydummypass' }
+ end
before do
WebMock.stub_request(:post, system_hook.url)
@@ -29,7 +44,7 @@ describe SystemHook, models: true do
end
it "user_create hook" do
- create(:user)
+ Users::CreateService.new(nil, params).execute
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
@@ -102,4 +117,34 @@ 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
+
+ describe 'execute WebHookService' do
+ let(:hook) { build(:system_hook) }
+ let(:data) { { key: 'value' } }
+ let(:hook_name) { 'system_hook' }
+
+ before do
+ expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
+ end
+
+ it '#execute' do
+ expect_any_instance_of(WebHookService).to receive(:execute)
+
+ hook.execute(data, hook_name)
+ end
+
+ it '#async_execute' do
+ expect_any_instance_of(WebHookService).to receive(:async_execute)
+
+ hook.async_execute(data, hook_name)
+ end
+ end
end
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
new file mode 100644
index 00000000000..c649cf3b589
--- /dev/null
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -0,0 +1,30 @@
+require 'rails_helper'
+
+describe WebHookLog, models: true do
+ it { is_expected.to belong_to(:web_hook) }
+
+ it { is_expected.to serialize(:request_headers).as(Hash) }
+ it { is_expected.to serialize(:request_data).as(Hash) }
+ it { is_expected.to serialize(:response_headers).as(Hash) }
+
+ it { is_expected.to validate_presence_of(:web_hook) }
+
+ describe '#success?' do
+ let(:web_hook_log) { build(:web_hook_log, response_status: status) }
+
+ describe '2xx' do
+ let(:status) { '200' }
+ it { expect(web_hook_log.success?).to be_truthy }
+ end
+
+ describe 'not 2xx' do
+ let(:status) { '500' }
+ it { expect(web_hook_log.success?).to be_falsey }
+ end
+
+ describe 'internal erorr' do
+ let(:status) { 'internal error' }
+ it { expect(web_hook_log.success?).to be_falsey }
+ end
+ end
+end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 9d4db1bfb52..53157c24477 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -1,89 +1,54 @@
require 'spec_helper'
describe WebHook, models: true do
- describe "Validations" do
+ let(:hook) { build(:project_hook) }
+
+ describe 'associations' do
+ it { is_expected.to have_many(:web_hook_logs).dependent(:destroy) }
+ end
+
+ describe 'validations' do
it { is_expected.to validate_presence_of(:url) }
describe 'url' do
- it { is_expected.to allow_value("http://example.com").for(:url) }
- it { is_expected.to allow_value("https://example.com").for(:url) }
- it { is_expected.to allow_value(" https://example.com ").for(:url) }
- it { is_expected.to allow_value("http://test.com/api").for(:url) }
- it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
- it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
+ it { is_expected.to allow_value('http://example.com').for(:url) }
+ it { is_expected.to allow_value('https://example.com').for(:url) }
+ it { is_expected.to allow_value(' https://example.com ').for(:url) }
+ it { is_expected.to allow_value('http://test.com/api').for(:url) }
+ it { is_expected.to allow_value('http://test.com/api?key=abc').for(:url) }
+ it { is_expected.to allow_value('http://test.com/api?key=abc&type=def').for(:url) }
- it { is_expected.not_to allow_value("example.com").for(:url) }
- it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
- it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
+ it { is_expected.not_to allow_value('example.com').for(:url) }
+ it { is_expected.not_to allow_value('ftp://example.com').for(:url) }
+ it { is_expected.not_to allow_value('herp-and-derp').for(:url) }
it 'strips :url before saving it' do
- hook = create(:project_hook, url: ' https://example.com ')
+ hook.url = ' https://example.com '
+ hook.save
expect(hook.url).to eq('https://example.com')
end
end
end
- describe "execute" do
- let(:project) { create(:empty_project) }
- let(:project_hook) { create(:project_hook) }
-
- before(:each) do
- project.hooks << [project_hook]
- @data = { before: 'oldrev', after: 'newrev', ref: 'ref' }
-
- WebMock.stub_request(:post, project_hook.url)
- end
-
- context 'when token is defined' do
- let(:project_hook) { create(:project_hook, :token) }
-
- it 'POSTs to the webhook URL' do
- project_hook.execute(@data, 'push_hooks')
- expect(WebMock).to have_requested(:post, project_hook.url).with(
- headers: { 'Content-Type' => 'application/json',
- 'X-Gitlab-Event' => 'Push Hook',
- 'X-Gitlab-Token' => project_hook.token }
- ).once
- end
- end
-
- it "POSTs to the webhook URL" do
- project_hook.execute(@data, 'push_hooks')
- expect(WebMock).to have_requested(:post, project_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' }
- ).once
- end
-
- it "POSTs the data as JSON" do
- project_hook.execute(@data, 'push_hooks')
- expect(WebMock).to have_requested(:post, project_hook.url).with(
- headers: { 'Content-Type' => 'application/json', 'X-Gitlab-Event' => 'Push Hook' }
- ).once
- end
-
- it "catches exceptions" do
- expect(WebHook).to receive(:post).and_raise("Some HTTP Post error")
-
- expect { project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError)
- end
-
- it "handles SSL exceptions" do
- expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error'))
+ describe 'execute' do
+ let(:data) { { key: 'value' } }
+ let(:hook_name) { 'project hook' }
- expect(project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
+ before do
+ expect(WebHookService).to receive(:new).with(hook, data, hook_name).and_call_original
end
- it "handles 200 status code" do
- WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "Success")
+ it '#execute' do
+ expect_any_instance_of(WebHookService).to receive(:execute)
- expect(project_hook.execute(@data, 'push_hooks')).to eq([200, 'Success'])
+ hook.execute(data, hook_name)
end
- it "handles 2xx status codes" do
- WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: "Success")
+ it '#async_execute' do
+ expect_any_instance_of(WebHookService).to receive(:async_execute)
- expect(project_hook.execute(@data, 'push_hooks')).to eq([201, 'Success'])
+ hook.async_execute(data, hook_name)
end
end
end
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index d8aed25c041..04d23d4c4fd 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
@@ -50,8 +50,8 @@ describe IssueCollection do
context 'using a user that is the owner of a project' do
it 'returns the issues of the project' do
- expect(collection.updatable_by_user(project.namespace.owner)).
- to eq([issue1, issue2])
+ expect(collection.updatable_by_user(project.namespace.owner))
+ .to eq([issue1, issue2])
end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index bba9058f394..bf97c6ececd 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
@@ -22,6 +23,55 @@ describe Issue, models: true do
it { is_expected.to have_db_index(:deleted_at) }
end
+ describe '#order_by_position_and_priority' do
+ let(:project) { create :empty_project }
+ let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
+ let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
+ let!(:issue1) { create(:labeled_issue, project: project, labels: [p1]) }
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [p2]) }
+ let!(:issue3) { create(:issue, project: project, relative_position: 100) }
+ let!(:issue4) { create(:issue, project: project, relative_position: 200) }
+
+ it 'returns ordered list' do
+ expect(project.issues.order_by_position_and_priority)
+ .to match [issue3, issue4, issue1, issue2]
+ 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
+ end
+
+ let!(:now) { Timecop.freeze(Time.now) }
+
+ it 'sets closed_at to Time.now when issue is closed' do
+ issue = create(:issue, state: 'opened')
+
+ issue.close
+
+ expect(issue.closed_at).to eq(now)
+ end
+ end
+
describe '#to_reference' do
let(:namespace) { build(:namespace, path: 'sample-namespace') }
let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) }
@@ -93,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.assignee_or_author?(user)).to be_truthy
+ end
- expect(Issue.open_for(user).count).to eq 2
+ 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
@@ -193,7 +245,9 @@ describe Issue, models: true do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
- before { project.team << [user, :reporter] }
+ before do
+ project.team << [user, :reporter]
+ end
it { is_expected.to eq true }
@@ -207,12 +261,18 @@ describe Issue, models: true do
let(:to_project) { create(:empty_project) }
context 'destination project allowed' do
- before { to_project.team << [user, :reporter] }
+ before do
+ to_project.team << [user, :reporter]
+ end
+
it { is_expected.to eq true }
end
context 'destination project not allowed' do
- before { to_project.team << [user, :guest] }
+ before do
+ to_project.team << [user, :guest]
+ end
+
it { is_expected.to eq false }
end
end
@@ -239,8 +299,8 @@ describe Issue, models: true do
let(:user) { build(:admin) }
before do
- allow(subject.project.repository).to receive(:branch_names).
- and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"])
+ allow(subject.project.repository).to receive(:branch_names)
+ .and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"])
# Without this stub, the `create(:merge_request)` above fails because it can't find
# the source branch. This seems like a reasonable compromise, in comparison with
@@ -262,13 +322,34 @@ describe Issue, models: true do
end
it 'excludes stable branches from the related branches' do
- allow(subject.project.repository).to receive(:branch_names).
- and_return(["#{subject.iid}-0-stable"])
+ allow(subject.project.repository).to receive(:branch_names)
+ .and_return(["#{subject.iid}-0-stable"])
expect(subject.related_branches(user)).to eq []
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)) }
@@ -339,12 +420,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)
@@ -473,7 +557,9 @@ describe Issue, models: true do
end
context 'when the user is the project owner' do
- before { project.team << [user, :master] }
+ before do
+ project.team << [user, :master]
+ end
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
@@ -620,4 +706,57 @@ describe Issue, models: true do
end
end
end
+
+ describe '#hook_attrs' do
+ let(:attrs_hash) { subject.hook_attrs }
+
+ it 'includes time tracking attrs' do
+ expect(attrs_hash).to include(:total_time_spent)
+ expect(attrs_hash).to include(:human_time_estimate)
+ 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
+ let(:project) { create :project, visibility_level: visibility_level }
+ let(:issue) { create :issue, project: project }
+
+ subject do
+ issue.assign_attributes(description: description)
+ issue.check_for_spam?
+ end
+
+ context 'when project is public and spammable attributes changed' do
+ let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
+ let(:description) { 'woo' }
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when project is private' do
+ let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
+ let(:description) { issue.description }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'when spammable attributes have not changed' do
+ let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC }
+ let(:description) { issue.description }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 7c40cfd8253..f27920f9feb 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -34,8 +34,8 @@ describe Key, models: true do
context 'when key was not updated during the last day' do
before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
- and_return('000000')
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
+ .and_return('000000')
end
it 'enqueues a UseKeyWorker job' do
@@ -46,8 +46,8 @@ describe Key, models: true do
context 'when key was updated during the last day' do
before do
- allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).
- and_return(false)
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain)
+ .and_return(false)
end
it 'does not enqueue a UseKeyWorker job' do
@@ -66,14 +66,16 @@ describe Key, models: true do
end
it "does not accept the exact same key twice" do
- create(:key, user: user)
- expect(build(:key, user: user)).not_to be_valid
+ first_key = create(:key, user: user)
+
+ expect(build(:key, user: user, key: first_key.key)).not_to be_valid
end
it "does not accept a duplicate key with a different comment" do
- create(:key, user: user)
- duplicate = build(:key, user: user)
+ first_key = create(:key, user: user)
+ duplicate = build(:key, user: user, key: first_key.key)
duplicate.key << ' extra comment'
+
expect(duplicate).not_to be_valid
end
end
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index a9139f7d4ab..31190fe5685 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -42,11 +42,44 @@ 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
+
+ it 'uses default color if color is missing' do
+ label = described_class.new(color: nil)
+
+ expect(label.color).to be(Label::DEFAULT_COLOR)
+ end
+ end
+
+ describe '#text_color' do
+ it 'uses default color if color is missing' do
+ expect(LabelsHelper).to receive(:text_color_for_bg).with(Label::DEFAULT_COLOR)
+ .and_return(spy)
+
+ label = described_class.new(color: nil)
+
+ label.text_color
+ 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/list_spec.rb b/spec/models/list_spec.rb
index e6ca4853873..db2c2619968 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -19,8 +19,8 @@ describe List do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
end
- context 'when list_type is set to done' do
- subject { described_class.new(list_type: :done) }
+ context 'when list_type is set to closed' do
+ subject { described_class.new(list_type: :closed) }
it { is_expected.not_to validate_presence_of(:label) }
it { is_expected.not_to validate_presence_of(:position) }
@@ -34,8 +34,8 @@ describe List do
expect(subject.destroy).to be_truthy
end
- it 'can not be destroyed when when list_type is set to done' do
- subject = create(:done_list)
+ it 'can not be destroyed when when list_type is set to closed' do
+ subject = create(:closed_list)
expect(subject.destroy).to be_falsey
end
@@ -48,8 +48,8 @@ describe List do
expect(subject).to be_destroyable
end
- it 'returns false when list_type is set to done' do
- subject.list_type = :done
+ it 'returns false when list_type is set to closed' do
+ subject.list_type = :closed
expect(subject).not_to be_destroyable
end
@@ -62,8 +62,8 @@ describe List do
expect(subject).to be_movable
end
- it 'returns false when list_type is set to done' do
- subject.list_type = :done
+ it 'returns false when list_type is set to closed' do
+ subject.list_type = :closed
expect(subject).not_to be_movable
end
@@ -77,10 +77,10 @@ describe List do
expect(subject.title).to eq 'Development'
end
- it 'returns Done when list_type is set to done' do
- subject.list_type = :done
+ it 'returns Closed when list_type is set to closed' do
+ subject.list_type = :closed
- expect(subject.title).to eq 'Done'
+ expect(subject.title).to eq 'Closed'
end
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index c720cc9f2c2..494a88368ba 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -83,8 +83,8 @@ describe Member, models: true do
@accepted_invite_member = create(:project_member, :developer,
project: project,
invite_token: '1234',
- invite_email: 'toto2@example.com').
- tap { |u| u.accept_invite!(accepted_invite_user) }
+ invite_email: 'toto2@example.com')
+ .tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@requested_member = project.requesters.find_by(user_id: requested_user.id)
@@ -265,8 +265,8 @@ describe Member, models: true do
expect(source.users).not_to include(user)
expect(source.requesters.exists?(user_id: user)).to be_truthy
- expect { described_class.add_user(source, user, :master) }.
- to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.add_user(source, user, :master) }
+ .to raise_error(Gitlab::Access::AccessDeniedError)
expect(source.users.reload).not_to include(user)
expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
@@ -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..37014268a70 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
@@ -33,8 +33,8 @@ describe GroupMember, models: true do
it "sends email to user" do
membership = build(:group_member)
- allow(membership).to receive(:notification_service).
- and_return(double('NotificationService').as_null_object)
+ allow(membership).to receive(:notification_service)
+ .and_return(double('NotificationService').as_null_object)
expect(membership).to receive(:notification_service)
membership.save
@@ -44,8 +44,8 @@ describe GroupMember, models: true do
describe "#after_update" do
before do
@group_member = create :group_member
- allow(@group_member).to receive(:notification_service).
- and_return(double('NotificationService').as_null_object)
+ allow(@group_member).to receive(:notification_service)
+ .and_return(double('NotificationService').as_null_object)
end
it "sends email to user" do
@@ -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/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 87ea2e70680..cf9c701e8c5 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -22,16 +22,15 @@ describe ProjectMember, models: true do
end
describe '.add_user' do
- context 'when called with the project owner' do
- it 'adds the user as a member' do
- project = create(:empty_project)
+ it 'adds the user as a member' do
+ user = create(:user)
+ project = create(:empty_project)
- expect(project.users).not_to include(project.owner)
+ expect(project.users).not_to include(user)
- described_class.add_user(project, project.owner, :master, current_user: project.owner)
+ described_class.add_user(project, user, :master, current_user: project.owner)
- expect(project.users.reload).to include(project.owner)
- end
+ expect(project.users.reload).to include(user)
end
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
new file mode 100644
index 00000000000..dbfd1526518
--- /dev/null
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+describe MergeRequestDiffCommit, type: :model do
+ let(:merge_request) { create(:merge_request) }
+ subject { merge_request.commits.first }
+
+ describe '#to_hash' do
+ it 'returns the same results as Commit#to_hash, except for parent_ids' do
+ commit_from_repo = merge_request.project.repository.commit(subject.sha)
+ commit_from_repo_hash = commit_from_repo.to_hash.merge(parent_ids: [])
+
+ expect(subject.to_hash).to eq(commit_from_repo_hash)
+ end
+ end
+end
diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb
new file mode 100644
index 00000000000..7276f5b5061
--- /dev/null
+++ b/spec/models/merge_request_diff_file_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+describe MergeRequestDiffFile, type: :model do
+ describe '#utf8_diff' do
+ it 'does not raise error when a hash value is in binary' do
+ subject.diff = "\x05\x00\x68\x65\x6c\x6c\x6f"
+
+ expect { subject.utf8_diff }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 0a10ee01506..edc2f4bb9f0 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -36,7 +36,9 @@ describe MergeRequestDiff, models: true do
end
context 'when the raw diffs are empty' do
- before { mr_diff.update_attributes(st_diffs: '') }
+ before do
+ MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id)
+ end
it 'returns an empty DiffCollection' do
expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
@@ -45,7 +47,10 @@ describe MergeRequestDiff, models: true do
end
context 'when the raw diffs have invalid content' do
- before { mr_diff.update_attributes(st_diffs: ["--broken-diff"]) }
+ before do
+ MergeRequestDiffFile.delete_all(merge_request_diff_id: mr_diff.id)
+ mr_diff.update_attributes(st_diffs: ["--broken-diff"])
+ end
it 'returns an empty DiffCollection' do
expect(mr_diff.raw_diffs.to_a).to be_empty
@@ -93,7 +98,7 @@ describe MergeRequestDiff, models: true do
end
it 'saves empty state' do
- allow_any_instance_of(MergeRequestDiff).to receive(:commits)
+ allow_any_instance_of(MergeRequestDiff).to receive_message_chain(:compare, :commits)
.and_return([])
mr_diff = create(:merge_request).merge_request_diff
@@ -102,14 +107,14 @@ describe MergeRequestDiff, models: true do
end
end
- describe '#commits_sha' do
+ describe '#commit_shas' do
it 'returns all commits SHA using serialized commits' do
subject.st_commits = [
{ id: 'sha1' },
{ id: 'sha2' }
]
- expect(subject.commits_sha).to eq(%w(sha1 sha2))
+ expect(subject.commit_shas).to eq(%w(sha1 sha2))
end
end
@@ -139,4 +144,15 @@ describe MergeRequestDiff, models: true do
expect(subject.commits_count).to eq 2
end
end
+
+ describe '#utf8_st_diffs' do
+ it 'does not raise error when a hash value is in binary' do
+ subject.st_diffs = [
+ { diff: "\0" },
+ { diff: "\x05\x00\x68\x65\x6c\x6c\x6f" }
+ ]
+
+ expect { subject.utf8_st_diffs }.not_to raise_error
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index fcaf4c71182..1eadc28869f 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,7 +9,8 @@ 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 have_many(:merge_request_diffs).dependent(:destroy) }
+ it { is_expected.to belong_to(:assignee) }
+ it { is_expected.to have_many(:merge_request_diffs) }
end
describe 'modules' do
@@ -86,6 +87,60 @@ 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_ids' do
+ it 'returns an array of the assigned user id' do
+ subject.assignee_id = 123
+
+ expect(subject.assignee_ids).to eq([123])
+ end
+ end
+
+ describe '#assignee_ids=' do
+ it 'sets assignee_id to the last id in the array' do
+ subject.assignee_ids = [123, 456]
+
+ expect(subject.assignee_id).to eq(456)
+ 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 +254,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 expanded: 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(expanded: true))
merge_request.diffs(options)
end
@@ -215,15 +270,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 +293,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 +304,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 +350,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))
@@ -314,8 +377,8 @@ describe MergeRequest, models: true do
end
it 'accesses the set of issues that will be closed on acceptance' do
- allow(subject.project).to receive(:default_branch).
- and_return(subject.target_branch)
+ allow(subject.project).to receive(:default_branch)
+ .and_return(subject.target_branch)
closed = subject.closes_issues
@@ -341,8 +404,8 @@ describe MergeRequest, models: true do
subject.description = "Is related to #{mentioned_issue.to_reference} and #{closing_issue.to_reference}"
allow(subject).to receive(:commits).and_return([commit])
- allow(subject.project).to receive(:default_branch).
- and_return(subject.target_branch)
+ allow(subject.project).to receive(:default_branch)
+ .and_return(subject.target_branch)
expect(subject.issues_mentioned_but_not_closing(subject.author)).to match_array([mentioned_issue])
end
@@ -441,7 +504,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
@@ -490,8 +553,8 @@ describe MergeRequest, models: true do
subject.project.team << [subject.author, :developer]
subject.description = "This issue Closes #{issue.to_reference}"
- allow(subject.project).to receive(:default_branch).
- and_return(subject.target_branch)
+ allow(subject.project).to receive(:default_branch)
+ .and_return(subject.target_branch)
expect(subject.merge_commit_message)
.to match("Closes #{issue.to_reference}")
@@ -542,7 +605,7 @@ describe MergeRequest, models: true do
end
describe "#hook_attrs" do
- let(:attrs_hash) { subject.hook_attrs.to_h }
+ let(:attrs_hash) { subject.hook_attrs }
[:source, :target].each do |key|
describe "#{key} key" do
@@ -558,6 +621,10 @@ describe MergeRequest, models: true do
expect(attrs_hash).to include(:target)
expect(attrs_hash).to include(:last_commit)
expect(attrs_hash).to include(:work_in_progress)
+ expect(attrs_hash).to include(:total_time_spent)
+ expect(attrs_hash).to include(:human_time_estimate)
+ expect(attrs_hash).to include(:human_total_time_spent)
+ expect(attrs_hash).to include('time_estimate')
end
end
@@ -612,18 +679,18 @@ describe MergeRequest, models: true do
end
it 'caches the output' do
- expect(subject).to receive(:compute_diverged_commits_count).
- once.
- and_return(2)
+ expect(subject).to receive(:compute_diverged_commits_count)
+ .once
+ .and_return(2)
subject.diverged_commits_count
subject.diverged_commits_count
end
it 'invalidates the cache when the source sha changes' do
- expect(subject).to receive(:compute_diverged_commits_count).
- twice.
- and_return(2)
+ expect(subject).to receive(:compute_diverged_commits_count)
+ .twice
+ .and_return(2)
subject.diverged_commits_count
allow(subject).to receive(:source_branch_sha).and_return('123abc')
@@ -631,9 +698,9 @@ describe MergeRequest, models: true do
end
it 'invalidates the cache when the target sha changes' do
- expect(subject).to receive(:compute_diverged_commits_count).
- twice.
- and_return(2)
+ expect(subject).to receive(:compute_diverged_commits_count)
+ .twice
+ .and_return(2)
subject.diverged_commits_count
allow(subject).to receive(:target_branch_sha).and_return('123abc')
@@ -653,27 +720,21 @@ describe MergeRequest, models: true do
subject { create :merge_request, :simple }
end
- describe '#commits_sha' do
+ describe '#commit_shas' do
before do
- allow(subject.merge_request_diff).to receive(:commits_sha).
- and_return(['sha1'])
+ allow(subject.merge_request_diff).to receive(:commit_shas)
+ .and_return(['sha1'])
end
it 'delegates to merge request diff' do
- expect(subject.commits_sha).to eq ['sha1']
+ expect(subject.commit_shas).to eq ['sha1']
end
end
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", head_pipeline_of: subject)
expect(subject.head_pipeline).to eq(pipeline)
end
@@ -691,7 +752,7 @@ describe MergeRequest, models: true do
describe '#all_pipelines' do
shared_examples 'returning pipelines with proper ordering' do
let!(:all_pipelines) do
- subject.all_commits_sha.map do |sha|
+ subject.all_commit_shas.map do |sha|
create(:ci_empty_pipeline,
project: subject.source_project,
sha: sha,
@@ -733,16 +794,16 @@ describe MergeRequest, models: true do
end
end
- describe '#all_commits_sha' do
+ describe '#all_commit_shas' do
context 'when merge request is persisted' do
- let(:all_commits_sha) do
+ let(:all_commit_shas) do
subject.merge_request_diffs.flat_map(&:commits).map(&:sha).uniq
end
shared_examples 'returning all SHA' do
it 'returns all SHA from all merge_request_diffs' do
expect(subject.merge_request_diffs.size).to eq(2)
- expect(subject.all_commits_sha).to eq(all_commits_sha)
+ expect(subject.all_commit_shas).to match_array(all_commit_shas)
end
end
@@ -773,7 +834,7 @@ describe MergeRequest, models: true do
end
it 'returns commits from compare commits temporary data' do
- expect(subject.all_commits_sha).to eq [commit, commit]
+ expect(subject.all_commit_shas).to eq [commit, commit]
end
end
@@ -781,7 +842,7 @@ describe MergeRequest, models: true do
subject { build(:merge_request) }
it 'returns array with diff head sha element only' do
- expect(subject.all_commits_sha).to eq [subject.diff_head_sha]
+ expect(subject.all_commit_shas).to eq [subject.diff_head_sha]
end
end
end
@@ -816,15 +877,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
@@ -845,7 +908,9 @@ describe MergeRequest, models: true do
end
context 'when broken' do
- before { allow(subject).to receive(:broken?) { true } }
+ before do
+ allow(subject).to receive(:broken?) { true }
+ end
it 'becomes unmergeable' do
expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
@@ -897,7 +962,9 @@ describe MergeRequest, models: true do
end
context 'when not open' do
- before { subject.close }
+ before do
+ subject.close
+ end
it 'returns false' do
expect(subject.mergeable_state?).to be_falsey
@@ -905,7 +972,9 @@ describe MergeRequest, models: true do
end
context 'when working in progress' do
- before { subject.title = 'WIP MR' }
+ before do
+ subject.title = 'WIP MR'
+ end
it 'returns false' do
expect(subject.mergeable_state?).to be_falsey
@@ -913,7 +982,9 @@ describe MergeRequest, models: true do
end
context 'when broken' do
- before { allow(subject).to receive(:broken?) { true } }
+ before do
+ allow(subject).to receive(:broken?) { true }
+ end
it 'returns false' do
expect(subject.mergeable_state?).to be_falsey
@@ -1131,7 +1202,7 @@ describe MergeRequest, models: true do
end
describe "#reload_diff" do
- let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }
+ let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
let(:commit) { subject.project.commit(sample_commit.id) }
@@ -1150,7 +1221,7 @@ describe MergeRequest, models: true do
subject.reload_diff
end
- it "updates diff note positions" do
+ it "updates diff discussion positions" do
old_diff_refs = subject.diff_refs
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
@@ -1164,18 +1235,18 @@ describe MergeRequest, models: true do
subject.merge_request_diff(true)
end
- expect(Notes::DiffPositionUpdateService).to receive(:new).with(
+ expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
subject.project,
- nil,
+ subject.author,
old_diff_refs: old_diff_refs,
new_diff_refs: commit.diff_refs,
- paths: note.position.paths
+ paths: discussion.position.paths
).and_call_original
- expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+ expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
expect_any_instance_of(DiffNote).to receive(:save).once
- subject.reload_diff
+ subject.reload_diff(subject.author)
end
end
@@ -1196,7 +1267,7 @@ describe MergeRequest, models: true do
end
end
- describe "#diff_sha_refs" do
+ describe "#diff_refs" do
context "with diffs" do
subject { create(:merge_request, :with_diffs) }
@@ -1205,7 +1276,7 @@ describe MergeRequest, models: true do
expect_any_instance_of(Repository).not_to receive(:commit)
- subject.diff_sha_refs
+ subject.diff_refs
end
it "returns expected diff_refs" do
@@ -1215,252 +1286,11 @@ describe MergeRequest, models: true do
head_sha: subject.merge_request_diff.head_commit_sha
)
- expect(subject.diff_sha_refs).to eq(expected_diff_refs)
+ expect(subject.diff_refs).to eq(expected_diff_refs)
end
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) }
@@ -1583,13 +1413,16 @@ describe MergeRequest, models: true do
end
end
- describe '#mergeable_with_slash_command?' do
+ describe '#mergeable_with_quick_action?' 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)
+ status: status,
+ head_pipeline_of: merge_request)
+
+ pipeline
end
let(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) }
@@ -1604,21 +1437,21 @@ describe MergeRequest, models: true do
context 'when autocomplete_precheck is set to true' do
it 'is mergeable by developer' do
- expect(merge_request.mergeable_with_slash_command?(developer, autocomplete_precheck: true)).to be_truthy
+ expect(merge_request.mergeable_with_quick_action?(developer, autocomplete_precheck: true)).to be_truthy
end
it 'is not mergeable by normal user' do
- expect(merge_request.mergeable_with_slash_command?(user, autocomplete_precheck: true)).to be_falsey
+ expect(merge_request.mergeable_with_quick_action?(user, autocomplete_precheck: true)).to be_falsey
end
end
context 'when autocomplete_precheck is set to false' do
it 'is mergeable by developer' do
- expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
end
it 'is not mergeable by normal user' do
- expect(merge_request.mergeable_with_slash_command?(user, last_diff_sha: mr_sha)).to be_falsey
+ expect(merge_request.mergeable_with_quick_action?(user, last_diff_sha: mr_sha)).to be_falsey
end
context 'closed MR' do
@@ -1627,7 +1460,7 @@ describe MergeRequest, models: true do
end
it 'is not mergeable' do
- expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
end
end
@@ -1637,19 +1470,19 @@ describe MergeRequest, models: true do
end
it 'is not mergeable' do
- expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
end
end
context 'sha differs from the MR diff_head_sha' do
it 'is not mergeable' do
- expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: 'some other sha')).to be_falsey
+ expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: 'some other sha')).to be_falsey
end
end
context 'sha is not provided' do
it 'is not mergeable' do
- expect(merge_request.mergeable_with_slash_command?(developer)).to be_falsey
+ expect(merge_request.mergeable_with_quick_action?(developer)).to be_falsey
end
end
@@ -1659,7 +1492,7 @@ describe MergeRequest, models: true do
end
it 'is mergeable' do
- expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
end
end
@@ -1669,7 +1502,7 @@ describe MergeRequest, models: true do
end
it 'is not mergeable' do
- expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_falsey
+ expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey
end
end
@@ -1679,7 +1512,7 @@ describe MergeRequest, models: true do
end
it 'is mergeable' do
- expect(merge_request.mergeable_with_slash_command?(developer, last_diff_sha: mr_sha)).to be_truthy
+ expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy
end
end
end
@@ -1687,8 +1520,8 @@ describe MergeRequest, models: true do
describe '#has_commits?' do
before do
- allow(subject.merge_request_diff).to receive(:commits_count).
- and_return(2)
+ allow(subject.merge_request_diff).to receive(:commits_count)
+ .and_return(2)
end
it 'returns true when merge request diff has commits' do
@@ -1698,12 +1531,99 @@ describe MergeRequest, models: true do
describe '#has_no_commits?' do
before do
- allow(subject.merge_request_diff).to receive(:commits_count).
- and_return(0)
+ allow(subject.merge_request_diff).to receive(:commits_count)
+ .and_return(0)
end
it 'returns true when merge request diff has 0 commits' 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
+
+ describe '#version_params_for' do
+ subject { create(:merge_request, importing: true) }
+ let(:project) { subject.project }
+ 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 'when the diff refs are for an older merge request version' do
+ let(:diff_refs) { merge_request_diff1.diff_refs }
+
+ it 'returns the diff ID for the version to show' do
+ expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff1.id)
+ end
+ end
+
+ context 'when the diff refs are for a comparison between merge request versions' do
+ let(:diff_refs) { merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs }
+
+ it 'returns the diff ID and start sha of the versions to compare' do
+ expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha)
+ end
+ end
+
+ context 'when the diff refs are not for a merge request version' do
+ let(:diff_refs) { project.commit(sample_commit.id).diff_refs }
+
+ it 'returns nil' do
+ expect(subject.version_params_for(diff_refs)).to be_nil
+ end
+ end
+ end
+
+ describe '#fetch_ref' do
+ it 'sets "ref_fetched" flag to true' do
+ subject.update!(ref_fetched: nil)
+
+ subject.fetch_ref
+
+ expect(subject.reload.ref_fetched).to be_truthy
+ end
+ end
+
+ describe '#ref_fetched?' do
+ it 'does not perform git operation when value is cached' do
+ subject.ref_fetched = true
+
+ expect_any_instance_of(Repository).not_to receive(:ref_exists?)
+ expect(subject.ref_fetched?).to be_truthy
+ end
+
+ it 'caches the value when ref exists but value is not cached' do
+ subject.update!(ref_fetched: nil)
+ allow_any_instance_of(Repository).to receive(:ref_exists?)
+ .and_return(true)
+
+ expect(subject.ref_fetched?).to be_truthy
+ expect(subject.reload.ref_fetched).to be_truthy
+ end
+
+ it 'returns false when ref does not exist' do
+ subject.update!(ref_fetched: nil)
+ allow_any_instance_of(Repository).to receive(:ref_exists?)
+ .and_return(false)
+
+ expect(subject.ref_fetched?).to be_falsey
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 3cee2b7714f..2649d04bee3 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -6,9 +6,6 @@ describe Milestone, models: true do
allow(subject).to receive(:set_iid).and_return(false)
end
- it { is_expected.to validate_presence_of(:title) }
- it { is_expected.to validate_presence_of(:project) }
-
describe 'start_date' do
it 'adds an error when start_date is greated then due_date' do
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
@@ -37,17 +34,42 @@ describe Milestone, models: true do
end
end
- describe "unique milestone title per project" do
- it "does not accept the same title in a project twice" do
- new_milestone = Milestone.new(project: milestone.project, title: milestone.title)
- expect(new_milestone).not_to be_valid
+ describe "unique milestone title" do
+ context "per project" do
+ it "does not accept the same title in a project twice" do
+ new_milestone = Milestone.new(project: milestone.project, title: milestone.title)
+ expect(new_milestone).not_to be_valid
+ end
+
+ it "accepts the same title in another project" do
+ project = create(:empty_project)
+ new_milestone = Milestone.new(project: project, title: milestone.title)
+
+ expect(new_milestone).to be_valid
+ end
end
- it "accepts the same title in another project" do
- project = build(:empty_project)
- new_milestone = Milestone.new(project: project, title: milestone.title)
+ context "per group" do
+ let(:group) { create(:group) }
+ let(:milestone) { create(:milestone, group: group) }
- expect(new_milestone).to be_valid
+ before do
+ project.update(group: group)
+ end
+
+ it "does not accept the same title in a group twice" do
+ new_milestone = Milestone.new(group: group, title: milestone.title)
+
+ expect(new_milestone).not_to be_valid
+ end
+
+ it "does not accept the same title of a child project milestone" do
+ create(:milestone, project: group.projects.first)
+
+ new_milestone = Milestone.new(group: group, title: milestone.title)
+
+ expect(new_milestone).not_to be_valid
+ end
end
end
@@ -109,18 +131,6 @@ describe Milestone, models: true do
it { expect(milestone.percent_complete(user)).to eq(75) }
end
- describe :items_count 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
@@ -156,35 +166,6 @@ describe Milestone, models: true do
end
end
- describe '#sort_issues' do
- let(:milestone) { create(:milestone) }
-
- let(:issue1) { create(:issue, milestone: milestone, position: 1) }
- let(:issue2) { create(:issue, milestone: milestone, position: 2) }
- let(:issue3) { create(:issue, milestone: milestone, position: 3) }
- let(:issue4) { create(:issue, position: 42) }
-
- it 'sorts the given issues' do
- milestone.sort_issues([issue3.id, issue2.id, issue1.id])
-
- issue1.reload
- issue2.reload
- issue3.reload
-
- expect(issue1.position).to eq(3)
- expect(issue2.position).to eq(2)
- expect(issue3.position).to eq(1)
- end
-
- it 'ignores issues not part of the milestone' do
- milestone.sort_issues([issue3.id, issue2.id, issue1.id, issue4.id])
-
- issue4.reload
-
- expect(issue4.position).to eq(42)
- end
- end
-
describe '.search' do
let(:milestone) { create(:milestone, title: 'foo', description: 'bar') }
@@ -205,13 +186,13 @@ describe Milestone, models: true do
end
it 'returns milestones with a partially matching description' do
- expect(described_class.search(milestone.description[0..2])).
- to eq([milestone])
+ expect(described_class.search(milestone.description[0..2]))
+ .to eq([milestone])
end
it 'returns milestones with a matching description regardless of the casing' do
- expect(described_class.search(milestone.description.upcase)).
- to eq([milestone])
+ expect(described_class.search(milestone.description.upcase))
+ .to eq([milestone])
end
end
@@ -261,4 +242,17 @@ describe Milestone, models: true do
expect(milestone.to_reference(another_project)).to eq "sample-project%1"
end
end
+
+ describe '#participants' do
+ let(:project) { build(:empty_project, name: 'sample-project') }
+ let(:milestone) { build(:milestone, iid: 1, project: project) }
+
+ it 'returns participants without duplicates' do
+ user = create :user
+ create :issue, project: project, milestone: milestone, assignees: [user]
+ create :issue, project: project, milestone: milestone, assignees: [user]
+
+ expect(milestone.participants).to eq [user]
+ end
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 757f3921450..a4090b37f65 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -34,6 +34,19 @@ 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(:group, path: 'folders', parent: parent)
+
+ expect(namespace).not_to be_valid
+ end
+ end
+
+ context "is case insensitive" do
+ let(:group) { build(:group, path: "Groups") }
+
+ it { expect(group).not_to be_valid }
end
context 'top-level group' do
@@ -47,6 +60,15 @@ 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 'inclusions' do
+ it { is_expected.to include_module(Gitlab::VisibilityLevel) }
+ end
+
+ describe '#visibility_level_field' do
+ it { expect(namespace.visibility_level_field).to eq(:visibility_level) }
end
describe '#to_param' do
@@ -129,10 +151,10 @@ describe Namespace, models: true do
end
end
- describe '#move_dir' do
+ describe '#move_dir', repository: true do
before do
@namespace = create :namespace
- @project = create(:empty_project, namespace: @namespace)
+ @project = create(:project_empty_repo, namespace: @namespace)
allow(@namespace).to receive(:path_changed?).and_return(true)
end
@@ -141,36 +163,115 @@ describe Namespace, models: true do
end
it "moves dir if path changed" do
- new_path = @namespace.path + "_new"
- allow(@namespace).to receive(:path_was).and_return(@namespace.path)
- allow(@namespace).to receive(:path).and_return(new_path)
+ new_path = @namespace.full_path + "_new"
+ allow(@namespace).to receive(:full_path_was).and_return(@namespace.full_path)
+ allow(@namespace).to receive(:full_path).and_return(new_path)
expect(@namespace).to receive(:remove_exports!)
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 '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, FileUploader.base_dir) }
+ let(:pages_dir) { File.join(TestEnv.pages_path) }
+
+ before do
+ 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
+
+ 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')
+
+ 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)
+ end
+ end
end
end
- describe :rm_dir do
- let!(:project) { create(:empty_project, namespace: namespace) }
- let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) }
+ describe '#rm_dir', 'callback', repository: true do
+ let!(:project) { create(:project_empty_repo, namespace: namespace) }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages.default['path'] }
+ let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) }
+ let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") }
+ let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
+
+ it 'renames its dirs when deleted' do
+ allow(GitlabShellWorker).to receive(:perform_in)
- it "removes its dirs when deleted" do
namespace.destroy
- expect(File.exist?(path)).to be(false)
+ expect(File.exist?(deleted_path_in_dir)).to be(true)
+ end
+
+ it 'schedules the namespace for deletion' do
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+
+ namespace.destroy
+ end
+
+ context 'in sub-groups' do
+ let(:parent) { create(:group, path: 'parent') }
+ let(:child) { create(:group, parent: parent, path: 'child') }
+ let!(:project) { create(:project_empty_repo, namespace: child) }
+ let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') }
+ let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") }
+ let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
+
+ it 'renames its dirs when deleted' do
+ allow(GitlabShellWorker).to receive(:perform_in)
+
+ child.destroy
+
+ expect(File.exist?(deleted_path_in_dir)).to be(true)
+ end
+
+ it 'schedules the namespace for deletion' do
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+
+ child.destroy
+ end
end
it 'removes the exports folder' do
@@ -200,38 +301,79 @@ describe Namespace, models: true do
end
end
- describe '#ancestors' do
+ describe '#ancestors', :nested_groups do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
let(:deep_nested_group) { create(:group, parent: nested_group) }
let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
it 'returns the correct ancestors' do
- expect(very_deep_nested_group.ancestors).to eq([group, nested_group, deep_nested_group])
- expect(deep_nested_group.ancestors).to eq([group, nested_group])
- expect(nested_group.ancestors).to eq([group])
+ expect(very_deep_nested_group.ancestors).to include(group, nested_group, deep_nested_group)
+ expect(deep_nested_group.ancestors).to include(group, nested_group)
+ expect(nested_group.ancestors).to include(group)
expect(group.ancestors).to eq([])
end
end
- describe '#descendants' do
- let!(:group) { create(:group) }
+ describe '#descendants', :nested_groups do
+ let!(:group) { create(:group, path: 'git_lab') }
let!(:nested_group) { create(:group, parent: group) }
let!(:deep_nested_group) { create(:group, parent: nested_group) }
let!(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+ let!(:another_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'foo', parent: another_group) }
it 'returns the correct descendants' do
expect(very_deep_nested_group.descendants.to_a).to eq([])
- expect(deep_nested_group.descendants.to_a).to eq([very_deep_nested_group])
- expect(nested_group.descendants.to_a).to eq([deep_nested_group, very_deep_nested_group])
- expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
+ expect(deep_nested_group.descendants.to_a).to include(very_deep_nested_group)
+ expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group)
+ expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group)
+ end
+ end
+
+ describe '#users_with_descendants', :nested_groups do
+ let(:user_a) { create(:user) }
+ let(:user_b) { create(:user) }
+
+ let(:group) { create(:group) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:deep_nested_group) { create(:group, parent: nested_group) }
+
+ it 'returns member users on every nest level without duplication' do
+ group.add_developer(user_a)
+ nested_group.add_developer(user_b)
+ deep_nested_group.add_developer(user_a)
+
+ expect(group.users_with_descendants).to contain_exactly(user_a, user_b)
+ expect(nested_group.users_with_descendants).to contain_exactly(user_a, user_b)
+ expect(deep_nested_group.users_with_descendants).to contain_exactly(user_a)
+ end
+ end
+
+ describe '#soft_delete_without_removing_associations' do
+ let(:project1) { create(:project_empty_repo, namespace: namespace) }
+
+ it 'updates the deleted_at timestamp but preserves projects' do
+ namespace.soft_delete_without_removing_associations
+
+ expect(Project.all).to include(project1)
+ expect(namespace.deleted_at).not_to be_nil
end
end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
- expect(namespace.user_ids_for_project_authorizations).
- to eq([namespace.owner_id])
+ expect(namespace.user_ids_for_project_authorizations)
+ .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..e2b80cb6e61 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -26,14 +26,18 @@ describe Note, models: true do
it { is_expected.to validate_presence_of(:project) }
context 'when note is on commit' do
- before { allow(subject).to receive(:for_commit?).and_return(true) }
+ before do
+ allow(subject).to receive(:for_commit?).and_return(true)
+ end
it { is_expected.to validate_presence_of(:commit_id) }
it { is_expected.not_to validate_presence_of(:noteable_id) }
end
context 'when note is not on commit' do
- before { allow(subject).to receive(:for_commit?).and_return(false) }
+ before do
+ allow(subject).to receive(:for_commit?).and_return(false)
+ end
it { is_expected.not_to validate_presence_of(:commit_id) }
it { is_expected.to validate_presence_of(:noteable_id) }
@@ -148,8 +152,8 @@ describe Note, models: true do
let!(:note2) { create(:note_on_issue) }
it "reads the rendered note body from the cache" do
- expect(Banzai::Renderer).to receive(:cache_collection_render).
- with([{
+ expect(Banzai::Renderer).to receive(:cache_collection_render)
+ .with([{
text: note1.note,
context: {
skip_project_check: false,
@@ -160,8 +164,8 @@ describe Note, models: true do
}
}]).and_call_original
- expect(Banzai::Renderer).to receive(:cache_collection_render).
- with([{
+ expect(Banzai::Renderer).to receive(:cache_collection_render)
+ .with([{
text: note2.note,
context: {
skip_project_check: false,
@@ -245,22 +249,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 +292,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
@@ -361,8 +406,8 @@ describe Note, models: true do
let(:note) { build(:note_on_project_snippet) }
before do
- expect(Banzai::Renderer).to receive(:cacheless_render_field).
- with(note, :note, { skip_project_check: false }).and_return(html)
+ expect(Banzai::Renderer).to receive(:cacheless_render_field)
+ .with(note, :note, { skip_project_check: false }).and_return(html)
note.save
end
@@ -376,8 +421,8 @@ describe Note, models: true do
let(:note) { build(:note_on_personal_snippet) }
before do
- expect(Banzai::Renderer).to receive(:cacheless_render_field).
- with(note, :note, { skip_project_check: true }).and_return(html)
+ expect(Banzai::Renderer).to receive(:cacheless_render_field)
+ .with(note, :note, { skip_project_check: true }).and_return(html)
note.save
end
@@ -388,15 +433,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/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index d58673413c8..cc235ad467e 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -55,4 +55,34 @@ RSpec.describe NotificationSetting, type: :model do
expect(user.notification_settings.for_projects.map(&:project)).to all(have_attributes(pending_delete: false))
end
end
+
+ describe 'event_enabled?' do
+ before do
+ subject.update!(user: create(:user))
+ end
+
+ context 'for an event with a matching column name' do
+ before do
+ subject.update!(events: { new_note: true }.to_json)
+ end
+
+ it 'returns the value of the column' do
+ subject.update!(new_note: false)
+
+ expect(subject.event_enabled?(:new_note)).to be(false)
+ end
+
+ context 'when the column has a nil value' do
+ it 'returns the value from the events hash' do
+ expect(subject.event_enabled?(:new_note)).to be(false)
+ end
+ end
+ end
+
+ context 'for an event without a matching column name' do
+ it 'returns false' do
+ expect(subject.event_enabled?(:foo_event)).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index e6a4583a8fb..f9d060d4e0e 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -5,8 +5,8 @@ describe PagesDomain, models: true do
it { is_expected.to belong_to(:project) }
end
- describe :validate_domain do
- subject { build(:pages_domain, domain: domain) }
+ describe 'validate domain' do
+ subject(:pages_domain) { build(:pages_domain, domain: domain) }
context 'is unique' do
let(:domain) { 'my.domain.com' }
@@ -14,36 +14,25 @@ describe PagesDomain, models: true do
it { is_expected.to validate_uniqueness_of(:domain) }
end
- context 'valid domain' do
- let(:domain) { 'my.domain.com' }
-
- it { is_expected.to be_valid }
- end
-
- context 'valid hexadecimal-looking domain' do
- let(:domain) { '0x12345.com'}
+ {
+ 'my.domain.com' => true,
+ '123.456.789' => true,
+ '0x12345.com' => true,
+ '0123123' => true,
+ '_foo.com' => false,
+ 'reserved.com' => false,
+ 'a.reserved.com' => false,
+ nil => false
+ }.each do |value, validity|
+ context "domain #{value.inspect} validity" do
+ before do
+ allow(Settings.pages).to receive(:host).and_return('reserved.com')
+ end
- it { is_expected.to be_valid }
- end
-
- context 'no domain' do
- let(:domain) { nil }
+ let(:domain) { value }
- it { is_expected.not_to be_valid }
- end
-
- context 'invalid domain' do
- let(:domain) { '0123123' }
-
- it { is_expected.not_to be_valid }
- end
-
- context 'domain from .example.com' do
- let(:domain) { 'my.domain.com' }
-
- before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
-
- it { is_expected.not_to be_valid }
+ it { expect(pages_domain.valid?).to eq(validity) }
+ end
end
end
@@ -75,7 +64,7 @@ describe PagesDomain, models: true do
end
end
- describe :url do
+ describe '#url' do
subject { domain.url }
context 'without the certificate' do
@@ -91,7 +80,7 @@ describe PagesDomain, models: true do
end
end
- describe :has_matching_key? do
+ describe '#has_matching_key?' do
subject { domain.has_matching_key? }
context 'for matching key' do
@@ -107,7 +96,7 @@ describe PagesDomain, models: true do
end
end
- describe :has_intermediates? do
+ describe '#has_intermediates?' do
subject { domain.has_intermediates? }
context 'for self signed' do
@@ -133,7 +122,7 @@ describe PagesDomain, models: true do
end
end
- describe :expired? do
+ describe '#expired?' do
subject { domain.expired? }
context 'for valid' do
@@ -149,7 +138,7 @@ describe PagesDomain, models: true do
end
end
- describe :subject do
+ describe '#subject' do
let(:domain) { build(:pages_domain, :with_certificate) }
subject { domain.subject }
@@ -157,7 +146,7 @@ describe PagesDomain, models: true do
it { is_expected.to eq('/CN=test-certificate') }
end
- describe :certificate_text do
+ describe '#certificate_text' do
let(:domain) { build(:pages_domain, :with_certificate) }
subject { domain.certificate_text }
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 823623d96fa..fa781195608 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -35,6 +35,16 @@ describe PersonalAccessToken, models: true do
end
end
+ describe 'revoke!' do
+ let(:active_personal_access_token) { create(:personal_access_token) }
+
+ it 'revokes the token' do
+ active_personal_access_token.revoke!
+
+ expect(active_personal_access_token.revoked?).to be true
+ end
+ end
+
context "validations" do
let(:personal_access_token) { build(:personal_access_token) }
@@ -51,11 +61,17 @@ describe PersonalAccessToken, models: true do
expect(personal_access_token).to be_valid
end
- it "rejects creating a token with non-API scopes" do
+ it "allows creating a token with read_registry scope" do
+ personal_access_token.scopes = [:read_registry]
+
+ expect(personal_access_token).to be_valid
+ end
+
+ it "rejects creating a token with unavailable scopes" do
personal_access_token.scopes = [:openid, :api]
expect(personal_access_token).not_to be_valid
- expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes"
+ expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes"
end
end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index 33ef67f97a7..ee6bdc39c8c 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -7,8 +7,8 @@ describe ProjectAuthorization do
describe '.insert_authorizations' do
it 'inserts the authorizations' do
- described_class.
- insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]])
+ described_class
+ .insert_authorizations([[user.id, project1.id, Gitlab::Access::MASTER]])
expect(user.project_authorizations.count).to eq(1)
end
@@ -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_feature_spec.rb b/spec/models/project_feature_spec.rb
index 09a4448d387..580c83c12c0 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -4,6 +4,18 @@ describe ProjectFeature do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
+ describe '.quoted_access_level_column' do
+ it 'returns the table name and quoted column name for a feature' do
+ expected = if Gitlab::Database.postgresql?
+ '"project_features"."issues_access_level"'
+ else
+ '`project_features`.`issues_access_level`'
+ end
+
+ expect(described_class.quoted_access_level_column(:issues)).to eq(expected)
+ end
+ end
+
describe '#feature_available?' do
let(:features) { %w(issues wiki builds merge_requests snippets repository) }
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 9b711bfc007..d68d8b719cd 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe ProjectGroupLink do
describe "Associations" do
- it { should belong_to(:group) }
- it { should belong_to(:project) }
+ it { is_expected.to belong_to(:group) }
+ it { is_expected.to belong_to(:project) }
end
describe "Validation" do
@@ -12,10 +12,10 @@ describe ProjectGroupLink do
let(:project) { create(:project, group: group) }
let!(:project_group_link) { create(:project_group_link, project: project) }
- it { should validate_presence_of(:project_id) }
- it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
- it { should validate_presence_of(:group) }
- it { should validate_presence_of(:group_access) }
+ it { is_expected.to validate_presence_of(:project_id) }
+ it { is_expected.to validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) }
+ it { is_expected.to validate_presence_of(:group) }
+ it { is_expected.to validate_presence_of(:group_access) }
it "doesn't allow a project to be shared with the group it is in" do
project_group_link.group = group
@@ -23,7 +23,7 @@ describe ProjectGroupLink do
expect(project_group_link).not_to be_valid
end
- it "doesn't allow a project to be shared with an ancestor of the group it is in" do
+ it "doesn't allow a project to be shared with an ancestor of the group it is in", :nested_groups do
project_group_link.group = parent_group
expect(project_group_link).not_to be_valid
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/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index 4014d6129ee..99190d763f2 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe BambooService, models: true, caching: true do
+describe BambooService, :use_clean_rails_memory_store_caching, models: true do
include ReactiveCachingHelpers
let(:bamboo_url) { 'http://gitlab.com/bamboo' }
@@ -24,7 +24,9 @@ describe BambooService, models: true, caching: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:build_key) }
it { is_expected.to validate_presence_of(:bamboo_url) }
@@ -60,7 +62,9 @@ describe BambooService, models: true, caching: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:build_key) }
it { is_expected.not_to validate_presence_of(:bamboo_url) }
@@ -213,13 +217,13 @@ describe BambooService, models: true, caching: true do
end
def stub_request(status: 200, body: nil)
- bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
+ bamboo_full_url = 'http://gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic'
WebMock.stub_request(:get, bamboo_full_url).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
- )
+ ).with(basic_auth: %w(mic password))
end
def bamboo_response(result_key: 42, build_state: 'success', size: 1)
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
index 739cc72b2ff..5f17bbde390 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -8,7 +8,9 @@ describe BugzillaService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:project_url) }
it { is_expected.to validate_presence_of(:issues_url) }
@@ -19,7 +21,9 @@ describe BugzillaService, models: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:project_url) }
it { is_expected.not_to validate_presence_of(:issues_url) }
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 05b602d8106..b4ee6691e67 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe BuildkiteService, models: true, caching: true do
+describe BuildkiteService, :use_clean_rails_memory_store_caching, models: true do
include ReactiveCachingHelpers
let(:project) { create(:empty_project) }
@@ -23,7 +23,9 @@ describe BuildkiteService, models: true, caching: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:project_url) }
it { is_expected.to validate_presence_of(:token) }
@@ -31,7 +33,9 @@ describe BuildkiteService, models: true, caching: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:project_url) }
it { is_expected.not_to validate_presence_of(:token) }
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
deleted file mode 100644
index 0194f9e2563..00000000000
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'spec_helper'
-
-describe BuildsEmailService do
- let(:data) do
- Gitlab::DataBuilder::Build.build(create(:ci_build))
- end
-
- describe 'Validations' do
- context 'when service is active' do
- before { subject.active = true }
-
- it { is_expected.to validate_presence_of(:recipients) }
-
- context 'when pusher is added' do
- before { subject.add_pusher = true }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- context 'when service is inactive' do
- before { subject.active = false }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- describe '#test_data' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
- let(:user) { create(:user) }
-
- before { project.team << [user, :developer] }
-
- it 'builds test data' do
- data = subject.test_data(project)
-
- expect(data[:object_kind]).to eq("build")
- end
- end
-
- describe '#test' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- subject.recipients = 'test@gitlab.com'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
-
- context 'notify only failed builds is true' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- data[:build_status] = "success"
- subject.recipients = 'test@gitlab.com'
-
- expect(subject).not_to receive(:notify_only_broken_builds)
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
- end
- end
-
- describe '#execute' do
- it 'sends email' do
- subject.recipients = 'test@gitlab.com'
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with succeeded build and notify_only_broken_builds on' do
- expect(subject).to receive(:notify_only_broken_builds).and_return(true)
- data[:build_status] = 'success'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with failed build and build_allow_failure is true' do
- data[:build_status] = 'failed'
- data[:build_allow_failure] = true
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with unknown build status' do
- data[:build_status] = 'foo'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email when recipients list is empty' do
- subject.recipients = ' ,, '
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
- end
-end
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index 953e664fb66..56ff3596190 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -8,13 +8,17 @@ describe CampfireService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:token) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:token) }
end
@@ -35,21 +39,22 @@ describe CampfireService, models: true do
room: 'test-room'
)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
- @rooms_url = 'https://verySecret:X@project-name.campfirenow.com/rooms.json'
+ @rooms_url = 'https://project-name.campfirenow.com/rooms.json'
+ @auth = %w(verySecret X)
@headers = { 'Content-Type' => 'application/json; charset=utf-8' }
end
it "calls Campfire API to get a list of rooms and speak in a room" do
# make sure a valid list of rooms is returned
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
- WebMock.stub_request(:get, @rooms_url).to_return(
+ WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
)
# stub the speak request with the room id found in the previous request's response
- speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/123/speak.json'
- WebMock.stub_request(:post, speak_url)
+ speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
+ WebMock.stub_request(:post, speak_url).with(basic_auth: @auth)
@campfire_service.execute(@sample_data)
@@ -62,7 +67,7 @@ describe CampfireService, models: true do
it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
# return a list of rooms that do not contain a room named 'test-room'
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
- WebMock.stub_request(:get, @rooms_url).to_return(
+ WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
deleted file mode 100644
index 3bd7ec18ae0..00000000000
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe ChatMessage::BuildMessage do
- subject { described_class.new(args) }
-
- let(:args) do
- {
- sha: '97de212e80737a608d939f648d959671fb0a0142',
- ref: 'develop',
- tag: false,
-
- project_name: 'project_name',
- project_url: 'http://example.gitlab.com',
- build_id: 1,
- build_name: build_name,
- build_stage: stage,
-
- commit: {
- status: status,
- author_name: 'hacker',
- author_url: 'http://example.gitlab.com/hacker',
- duration: duration,
- },
- }
- end
-
- let(:message) { build_message }
- let(:stage) { 'test' }
- let(:status) { 'success' }
- let(:build_name) { 'rspec' }
- let(:duration) { 10 }
-
- context 'build 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 'build failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
-
- 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
- end
-
- it 'returns a message with information on build' do
- expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>")
- end
-
- it 'returns a message with stage name' do
- expect(subject.fallback).to include("of stage #{stage}")
- end
-
- it 'returns a message with link to author' do
- expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>")
- end
-
- def build_message(status_text = status, stage_text = stage, build_text = build_name)
- "<http://example.gitlab.com|project_name>:" \
- " Commit <http://example.gitlab.com/commit/" \
- "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
- " of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \
- " on build <http://example.gitlab.com/builds/1|#{build_text}>" \
- " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}"
- end
-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..43b02568cb9 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..c794f659c41 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..17355c1e6f1 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,148 @@ 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 do
+ args[:object_attributes][:action] = 'create'
+ end
- it 'returns a message that a new wiki page was created' do
- expect(subject.pretext).to eq(
- 'test.user 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 do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'test.user 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 do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ 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 do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ 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 do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ 'test.user 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 do
+ args[:object_attributes][:action] = 'update'
+ end
- 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 do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ 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 do
+ args[:object_attributes][:action] = 'update'
+ end
+
+ 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 do
+ args[:object_attributes][:action] = 'create'
+ end
+
+ it 'returns the attachment for a new wiki page' do
+ expect(subject.activity).to eq({
+ title: 'test.user created [wiki page](http://url.com)',
+ subtitle: 'in [project_name](http://somewhere.com)',
+ text: 'Wiki page title',
+ image: 'http://someavatar.com'
+ })
+ end
+ end
+
+ context 'when :action == "update"' do
+ before do
+ args[:object_attributes][:action] = 'update'
+ end
- it 'returns the attachment for an updated wiki page' do
- expect(subject.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/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index 63320931e76..9e574762232 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -8,7 +8,9 @@ describe CustomIssueTrackerService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:project_url) }
it { is_expected.to validate_presence_of(:issues_url) }
@@ -19,7 +21,9 @@ describe CustomIssueTrackerService, models: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:project_url) }
it { is_expected.not_to validate_presence_of(:issues_url) }
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 044737c6026..c9ac256ff38 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe DroneCiService, models: true, caching: true do
+describe DroneCiService, :use_clean_rails_memory_store_caching, models: true do
include ReactiveCachingHelpers
describe 'associations' do
@@ -10,7 +10,9 @@ describe DroneCiService, models: true, caching: true do
describe 'validations' do
context 'active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:token) }
it { is_expected.to validate_presence_of(:drone_url) }
@@ -18,7 +20,9 @@ describe DroneCiService, models: true, caching: true do
end
context 'inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:token) }
it { is_expected.not_to validate_presence_of(:drone_url) }
diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb
index e6f78898c82..d9b7010e5e5 100644
--- a/spec/models/project_services/emails_on_push_service_spec.rb
+++ b/spec/models/project_services/emails_on_push_service_spec.rb
@@ -3,13 +3,17 @@ require 'spec_helper'
describe EmailsOnPushService do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:recipients) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:recipients) }
end
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index bdeea1db1e3..ef10df9e092 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -3,20 +3,24 @@ require 'spec_helper'
describe ExternalWikiService, models: true do
include ExternalWikiHelper
describe "Associations" do
- it { should belong_to :project }
- it { should have_one :service_hook }
+ 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 }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:external_wiki_url) }
it_behaves_like 'issue tracker service URL attribute', :external_wiki_url
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:external_wiki_url) }
end
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index a97e8c6e4ce..56ace04dd58 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -8,13 +8,17 @@ describe FlowdockService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:token) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:token) }
end
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index a13fbae03eb..65c9e714bd1 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -8,14 +8,18 @@ describe GemnasiumService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:token) }
it { is_expected.to validate_presence_of(:api_key) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:token) }
it { is_expected.not_to validate_presence_of(:api_key) }
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index dcb70ee28a8..d45e0a441d4 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -23,38 +23,29 @@ describe GitlabIssueTrackerService, models: true do
describe 'project and issue urls' do
let(:project) { create(:empty_project) }
+ let(:service) { project.create_gitlab_issue_tracker_service(active: true) }
context 'with absolute urls' do
before do
- GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root"
- @service = project.create_gitlab_issue_tracker_service(active: true)
- end
-
- after do
- @service.destroy!
+ allow(GitlabIssueTrackerService).to receive(:default_url_options).and_return(script_name: "/gitlab/root")
end
it 'gives the correct path' do
- expect(@service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues")
- expect(@service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/new")
- expect(@service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/432")
+ expect(service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues")
+ expect(service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/new")
+ expect(service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/432")
end
end
context 'with relative urls' do
before do
- GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root"
- @service = project.create_gitlab_issue_tracker_service(active: true)
- end
-
- after do
- @service.destroy!
+ allow(GitlabIssueTrackerService).to receive(:default_url_options).and_return(script_name: "/gitlab/root")
end
it 'gives the correct path' do
- expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
- expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
- expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
+ expect(service.issue_tracker_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
+ expect(service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
+ expect(service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
end
end
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index bf422ac7ce1..c7c8e9651ab 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -8,13 +8,17 @@ describe HipchatService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:token) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:token) }
end
@@ -280,13 +284,14 @@ describe HipchatService, models: true do
end
end
- context 'build events' do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
+ context 'pipeline events' do
+ let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'for failed' do
- before { build.drop }
+ before do
+ pipeline.drop
+ end
it "calls Hipchat API" do
hipchat.execute(data)
@@ -295,35 +300,36 @@ describe HipchatService, models: true do
end
it "creates a build message" do
- message = hipchat.send(:create_build_message, data)
+ message = hipchat.__send__(:create_pipeline_message, data)
project_url = project.web_url
project_name = project.name_with_namespace.gsub(/\s/, '')
- sha = data[:sha]
- ref = data[:ref]
- ref_type = data[:tag] ? 'tag' : 'branch'
- duration = data[:commit][:duration]
+ pipeline_attributes = data[:object_attributes]
+ ref = pipeline_attributes[:ref]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ duration = pipeline_attributes[:duration]
+ user_name = data[:user][:name]
expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
- "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
+ "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
"of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
- "by #{data[:commit][:author_name]} failed in #{duration} second(s)")
+ "by #{user_name} failed in #{duration} second(s)")
end
end
context 'for succeeded' do
before do
- build.success
+ pipeline.succeed
end
it "calls Hipchat API" do
- hipchat.notify_only_broken_builds = false
+ hipchat.notify_only_broken_pipelines = false
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
it "notifies only broken" do
- hipchat.notify_only_broken_builds = true
+ hipchat.notify_only_broken_pipelines = true
hipchat.execute(data)
expect(WebMock).not_to have_requested(:post, api_url).once
end
@@ -349,17 +355,19 @@ describe HipchatService, models: true do
context 'with a successful build' do
it 'uses the green color' do
- build_data = { object_kind: 'build', commit: { status: 'success' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'success' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
end
end
context 'with a failed build' do
it 'uses the red color' do
- build_data = { object_kind: 'build', commit: { status: 'failed' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'failed' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
end
end
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index d5a16226d9d..a5c4938b54e 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -10,13 +10,17 @@ describe IrkerService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:recipients) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:recipients) }
end
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
new file mode 100644
index 00000000000..869b25b933b
--- /dev/null
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe IssueTrackerService, models: true do
+ describe 'Validations' do
+ let(:project) { create :project }
+
+ describe 'only one issue tracker per project' do
+ let(:service) { RedmineService.new(project: project, active: true) }
+
+ before do
+ create(:custom_issue_tracker_service, project: project)
+ end
+
+ context 'when service is changed manually by user' do
+ it 'executes the validation' do
+ valid = service.valid?(:manual_change)
+
+ expect(valid).to be_falsey
+ expect(service.errors[:base]).to include(
+ 'Another issue tracker is already in use. Only one issue tracker service can be active at a time'
+ )
+ end
+ end
+
+ context 'when service is changed internally' do
+ it 'does not execute the validation' do
+ expect(service.valid?).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 4bca0229e7a..105afed1337 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe JiraService, models: true do
- include Gitlab::Routing.url_helpers
+ include Gitlab::Routing
describe "Associations" do
it { is_expected.to belong_to :project }
@@ -10,7 +10,9 @@ describe JiraService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_presence_of(:project_key) }
@@ -18,53 +20,56 @@ describe JiraService, models: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:url) }
end
- end
- describe '#reference_pattern' do
- it_behaves_like 'allows project key on reference pattern'
+ context 'validating urls' do
+ let(:service) do
+ described_class.new(
+ project: create(:empty_project),
+ active: true,
+ username: 'username',
+ password: 'test',
+ project_key: 'TEST',
+ jira_issue_transition_id: 24,
+ url: 'http://jira.test.com'
+ )
+ end
- it 'does not allow # on the code' do
- expect(subject.reference_pattern.match('#123')).to be_nil
- expect(subject.reference_pattern.match('1#23#12')).to be_nil
- end
- end
+ it 'is valid when all fields have required values' do
+ expect(service).to be_valid
+ end
- describe '#can_test?' do
- let(:jira_service) { described_class.new }
+ it 'is not valid when url is not a valid url' do
+ service.url = 'not valid'
- it 'returns false if username is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: '',
- password: '12345678'
- )
+ expect(service).not_to be_valid
+ end
- expect(jira_service.can_test?).to be_falsy
- end
+ it 'is not valid when api url is not a valid url' do
+ service.api_url = 'not valid'
- it 'returns false if password is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: ''
- )
+ expect(service).not_to be_valid
+ end
+
+ it 'is valid when api url is a valid url' do
+ service.api_url = 'http://jira.test.com/api'
- expect(jira_service.can_test?).to be_falsy
+ expect(service).to be_valid
+ end
end
+ end
- it 'returns true if password and username are present' do
- jira_service = described_class.new
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: '12345678'
- )
+ describe '.reference_pattern' do
+ it_behaves_like 'allows project key on reference pattern'
- expect(jira_service.can_test?).to be_truthy
+ it 'does not allow # on the code' do
+ expect(described_class.reference_pattern.match('#123')).to be_nil
+ expect(described_class.reference_pattern.match('1#23#12')).to be_nil
end
end
@@ -97,18 +102,19 @@ describe JiraService, models: true do
allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123")
+ allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
@jira_service.save
- project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123'
- @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
- @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
- @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
+ project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123'
+ @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions'
+ @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment'
+ @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
- WebMock.stub_request(:get, project_issues_url)
- WebMock.stub_request(:post, @transitions_url)
- WebMock.stub_request(:post, @comment_url)
- WebMock.stub_request(:post, @remote_link_url)
+ WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
+ WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
+ WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
+ WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
end
it "calls JIRA API" do
@@ -187,22 +193,29 @@ describe JiraService, models: true do
describe '#test_settings' do
let(:jira_service) do
described_class.new(
+ project: create(:project),
url: 'http://jira.example.com',
- username: 'gitlab_jira_username',
- password: 'gitlab_jira_password',
+ username: 'jira_username',
+ password: 'jira_password',
project_key: 'GitLabProject'
)
end
- let(:project_url) { 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' }
- before do
- WebMock.stub_request(:get, project_url)
- end
+ def test_settings(api_url)
+ project_url = "http://#{api_url}/rest/api/2/project/GitLabProject"
+
+ WebMock.stub_request(:get, project_url).with(basic_auth: %w(jira_username jira_password))
- it 'tries to get JIRA project' do
jira_service.test_settings
+ end
+
+ it 'tries to get JIRA project with URL when API URL not set' do
+ test_settings('jira.example.com')
+ end
- expect(WebMock).to have_requested(:get, project_url)
+ it 'tries to get JIRA project with API URL if set' do
+ jira_service.update(api_url: 'http://jira.api.com')
+ test_settings('jira.api.com')
end
end
@@ -214,34 +227,75 @@ describe JiraService, models: true do
@jira_service = JiraService.create!(
project: project,
properties: {
- url: 'http://jira.example.com/rest/api/2',
+ url: 'http://jira.example.com/web',
username: 'mic',
password: "password"
}
)
end
- it "reset password if url changed" do
- @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
- @jira_service.save
- expect(@jira_service.password).to be_nil
+ context 'when only web url present' do
+ it 'reset password if url changed' do
+ @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
+
+ it 'reset password if url not changed but api url added' do
+ @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
end
- it "does not reset password if username changed" do
- @jira_service.username = "some_name"
+ context 'when both web and api url present' do
+ before do
+ @jira_service.api_url = 'http://jira.example.com/rest/api/2'
+ @jira_service.password = 'password'
+
+ @jira_service.save
+ end
+ it 'reset password if api url changed' do
+ @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
+
+ it 'does not reset password if url changed' do
+ @jira_service.url = 'http://jira_edited.example.com/rweb'
+ @jira_service.save
+
+ expect(@jira_service.password).to eq("password")
+ end
+
+ it 'reset password if api url set to ""' do
+ @jira_service.api_url = ''
+ @jira_service.save
+
+ expect(@jira_service.password).to be_nil
+ end
+ end
+
+ it 'does not reset password if username changed' do
+ @jira_service.username = 'some_name'
@jira_service.save
- expect(@jira_service.password).to eq("password")
+
+ expect(@jira_service.password).to eq('password')
end
- it "does not reset password if new url is set together with password, even if it's the same password" do
+ it 'does not reset password if new url is set together with password, even if it\'s the same password' do
@jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
- expect(@jira_service.password).to eq("password")
- expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
+
+ expect(@jira_service.password).to eq('password')
+ expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
end
- it "resets password if url changed, even if setter called multiple times" do
+ it 'resets password if url changed, even if setter called multiple times' do
@jira_service.url = 'http://jira1.example.com/rest/api/2'
@jira_service.url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
@@ -249,7 +303,7 @@ describe JiraService, models: true do
end
end
- context "when no password was previously set" do
+ context 'when no password was previously set' do
before do
@jira_service = JiraService.create(
project: project,
@@ -260,26 +314,16 @@ describe JiraService, models: true do
)
end
- it "saves password if new url is set together with password" do
+ it 'saves password if new url is set together with password' do
@jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
- expect(@jira_service.password).to eq("password")
- expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
+ expect(@jira_service.password).to eq('password')
+ expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
end
end
end
- describe "Validations" do
- context "active" do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of :url }
- end
- end
-
describe 'description and title' do
let(:project) { create(:empty_project) }
@@ -321,9 +365,10 @@ describe JiraService, models: true do
context 'when gitlab.yml was initialized' do
before do
settings = {
- "jira" => {
- "title" => "Jira",
- "url" => "http://jira.sample/projects/project_a"
+ 'jira' => {
+ 'title' => 'Jira',
+ 'url' => 'http://jira.sample/projects/project_a',
+ 'api_url' => 'http://jira.sample/api'
}
}
allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
@@ -335,8 +380,9 @@ describe JiraService, models: true do
end
it 'is prepopulated with the settings' do
- expect(@service.properties["title"]).to eq('Jira')
- expect(@service.properties["url"]).to eq('http://jira.sample/projects/project_a')
+ expect(@service.properties['title']).to eq('Jira')
+ expect(@service.properties['url']).to eq('http://jira.sample/projects/project_a')
+ expect(@service.properties['api_url']).to eq('http://jira.sample/api')
end
end
end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index bf7950ef1c9..b66bb5321ab 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -1,38 +1,23 @@
require 'spec_helper'
-describe KubernetesService, models: true, caching: true do
+describe KubernetesService, :use_clean_rails_memory_store_caching, models: 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
- # GET /api/v1 for a list of resources the API supports. This must be stubbed
- # in addition to any other HTTP requests we expect it to perform.
- let(:discovery_url) { service.api_url + '/api/v1' }
- let(:discovery_response) { { body: kube_discovery_body.to_json } }
-
- let(:pods_url) { service.api_url + "/api/v1/namespaces/#{service.namespace}/pods" }
- let(:pods_response) { { body: kube_pods_body(kube_pod).to_json } }
-
- def stub_kubeclient_discover
- WebMock.stub_request(:get, discovery_url).to_return(discovery_response)
- end
-
- def stub_kubeclient_pods
- stub_kubeclient_discover
- WebMock.stub_request(:get, pods_url).to_return(pods_response)
- end
-
describe "Associations" do
it { is_expected.to belong_to :project }
end
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
- it { is_expected.to validate_presence_of(:namespace) }
+ before do
+ subject.active = true
+ end
+
+ 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 +38,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)
@@ -65,30 +50,110 @@ describe KubernetesService, models: true, caching: true do
end
context 'when service is inactive' do
- before { subject.active = false }
- it { is_expected.not_to validate_presence_of(:namespace) }
+ before do
+ subject.active = false
+ end
+
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 do
+ subject.template = true
+ end
- 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 do
+ subject.project = project
+ end
+
+ it 'sets the namespace to the default' do
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
+ end
+ end
+ end
+
+ describe '#actual_namespace' do
+ subject { service.actual_namespace }
+
+ it "returns the default namespace" do
+ is_expected.to eq(service.send(:default_namespace))
+ end
+
+ context 'when namespace is specified' do
+ before do
+ service.namespace = 'my-namespace'
+ end
+
+ it "returns the user-namespace" do
+ is_expected.to eq('my-namespace')
+ end
+ end
+
+ context 'when service is not assigned to project' do
+ before do
+ service.project = nil
+ end
+
+ it "does not return namespace" do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#actual_namespace' do
+ subject { service.actual_namespace }
+
+ it "returns the default namespace" do
+ is_expected.to eq(service.send(:default_namespace))
+ end
+
+ context 'when namespace is specified' do
+ before do
+ service.namespace = 'my-namespace'
+ end
+
+ it "returns the user-namespace" do
+ is_expected.to eq('my-namespace')
+ end
+ end
+
+ context 'when service is not assigned to project' do
+ before do
+ service.project = nil
+ end
+
+ it "does not return namespace" do
+ is_expected.to be_nil
end
end
end
describe '#test' do
+ let(:discovery_url) { 'https://kubernetes.example.com/api/v1' }
+
before do
stub_kubeclient_discover
end
@@ -97,7 +162,8 @@ describe KubernetesService, models: true, caching: true do
let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
it 'tests with the prefix' do
- service.api_url = 'https://kubernetes.example.com/prefix/'
+ service.api_url = 'https://kubernetes.example.com/prefix'
+ stub_kubeclient_discover
expect(service.test[:success]).to be_truthy
expect(WebMock).to have_requested(:get, discovery_url).once
@@ -125,9 +191,9 @@ describe KubernetesService, models: true, caching: true do
end
context 'failure' do
- let(:discovery_response) { { status: 404 } }
-
it 'fails to read the discovery endpoint' do
+ WebMock.stub_request(:get, service.api_url + '/api/v1').to_return(status: 404)
+
expect(service.test[:success]).to be_falsy
expect(WebMock).to have_requested(:get, discovery_url).once
end
@@ -135,46 +201,69 @@ describe KubernetesService, models: true, caching: true do
end
describe '#predefined_variables' do
+ let(:kubeconfig) do
+ config =
+ YAML.load(File.read(expand_fixture_path('config/kubeconfig.yml')))
+
+ config.dig('users', 0, 'user')['token'] =
+ 'token'
+
+ config.dig('clusters', 0, 'cluster')['certificate-authority-data'] =
+ Base64.encode64('CA PEM DATA')
+
+ config.dig('contexts', 0, 'context')['namespace'] =
+ namespace
+
+ YAML.dump(config)
+ end
+
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 }
- )
+ shared_examples 'setting variables' 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_NAMESPACE', value: namespace, public: true },
+ { key: 'KUBECONFIG', value: kubeconfig, public: false, file: 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_TOKEN' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_TOKEN', value: 'token', public: false }
- )
- end
+ context 'namespace is provided' do
+ let(:namespace) { 'my-project' }
- it 'sets KUBE_NAMESPACE' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
- )
- end
+ before do
+ subject.namespace = namespace
+ end
- it 'sets KUBE_CA_PEM' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
- )
+ it_behaves_like 'setting variables'
end
- 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 }
- )
+ context 'no namespace provided' do
+ let(:namespace) { subject.actual_namespace }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets the KUBE_NAMESPACE' do
+ kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
+
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:value]).to match(/\A#{Gitlab::PathRegex::PATH_REGEX_STR}-\d+\z/)
+ end
end
end
describe '#terminals' do
let(:environment) { build(:environment, project: project, name: "env", slug: "env-000000") }
+
subject { service.terminals(environment) }
context 'with invalid pods' do
@@ -210,27 +299,36 @@ describe KubernetesService, models: true, caching: true do
end
describe '#calculate_reactive_cache' do
- before { stub_kubeclient_pods }
subject { service.calculate_reactive_cache }
context 'when service is inactive' do
- before { service.active = false }
+ before do
+ service.active = false
+ end
it { is_expected.to be_nil }
end
context 'when kubernetes responds with valid pods' do
+ before do
+ stub_kubeclient_pods
+ end
+
it { is_expected.to eq(pods: [kube_pod]) }
end
- context 'when kubernetes responds with 500' do
- let(:pods_response) { { status: 500 } }
+ context 'when kubernetes responds with 500s' do
+ before do
+ stub_kubeclient_pods(status: 500)
+ end
it { expect { subject }.to raise_error(KubeException) }
end
- context 'when kubernetes responds with 404' do
- let(:pods_response) { { status: 404 } }
+ context 'when kubernetes responds with 404s' do
+ before do
+ stub_kubeclient_pods(status: 404)
+ end
it { is_expected.to eq(pods: []) }
end
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index f9531be5d25..fa38d23e82f 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -11,8 +11,8 @@ describe MattermostSlashCommandsService, :models do
before do
Mattermost::Session.base_uri("http://mattermost.example.com")
- allow_any_instance_of(Mattermost::Client).to receive(:with_session).
- and_yield(Mattermost::Session.new(nil))
+ allow_any_instance_of(Mattermost::Client).to receive(:with_session)
+ .and_yield(Mattermost::Session.new(nil))
end
describe '#configure' do
@@ -24,8 +24,8 @@ describe MattermostSlashCommandsService, :models do
context 'the requests succeeds' do
before do
- stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
- with(body: {
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create')
+ .with(body: {
team_id: 'abc',
trigger: 'gitlab',
url: 'http://trigger.url',
@@ -37,8 +37,8 @@ describe MattermostSlashCommandsService, :models do
display_name: "GitLab / #{project.name_with_namespace}",
method: 'P',
username: 'GitLab'
- }.to_json).
- to_return(
+ }.to_json)
+ .to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { token: 'token' }.to_json
@@ -58,8 +58,8 @@ describe MattermostSlashCommandsService, :models do
context 'an error is received' do
before do
- stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
- to_return(
+ stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create')
+ .to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
@@ -88,8 +88,8 @@ describe MattermostSlashCommandsService, :models do
context 'the requests succeeds' do
before do
- stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
- to_return(
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all')
+ .to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { 'list' => true }.to_json
@@ -103,8 +103,8 @@ describe MattermostSlashCommandsService, :models do
context 'an error is received' do
before do
- stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
- to_return(
+ stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all')
+ .to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
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..fb95c4cda35
--- /dev/null
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -0,0 +1,281 @@
+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 do
+ subject.active = true
+ end
+
+ 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 do
+ subject.active = false
+ end
+
+ 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
+ Gitlab::DataBuilder::WikiPage.build(wiki_page, user, '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..f4c1a9c94b6 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -8,13 +8,17 @@ describe PivotaltrackerService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:token) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:token) }
end
@@ -40,7 +44,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..3fb134ec3b7 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
-describe PrometheusService, models: true, caching: true do
+describe PrometheusService, :use_clean_rails_memory_store_caching, models: true do
include PrometheusHelpers
include ReactiveCachingHelpers
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 }
@@ -13,13 +14,17 @@ describe PrometheusService, models: true, caching: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:api_url) }
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:api_url) }
end
@@ -45,34 +50,59 @@ 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
- is_expected.to eq(prometheus_data)
+ is_expected.to eq(prometheus_metrics_data)
+ end
+ 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) }
+ let(:fake_deployment_time) { 10 }
+
+ before do
+ stub_reactive_cache(service, prometheus_data, deployment_query, deployment.id)
+ end
+
+ it 'returns reactive data' do
+ expect(deployment).to receive(:created_at).and_return(fake_deployment_time)
+
+ expect(subject).to eq(prometheus_metrics_data.merge(deployment_time: fake_deployment_time))
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
@@ -89,12 +119,13 @@ describe PrometheusService, models: true, caching: true do
end
it { expect(subject.to_json).to eq(prometheus_data.to_json) }
+ it { expect(subject.to_json).to eq(prometheus_data.to_json) }
end
[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_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index a7e7594a7d5..9171d9604ee 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -8,7 +8,9 @@ describe PushoverService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:api_key) }
it { is_expected.to validate_presence_of(:user_key) }
@@ -16,7 +18,9 @@ describe PushoverService, models: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:api_key) }
it { is_expected.not_to validate_presence_of(:user_key) }
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index 0a7b237a051..441b3f896ca 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -8,7 +8,9 @@ describe RedmineService, models: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:project_url) }
it { is_expected.to validate_presence_of(:issues_url) }
@@ -19,7 +21,9 @@ describe RedmineService, models: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:project_url) }
it { is_expected.not_to validate_presence_of(:issues_url) }
@@ -27,11 +31,11 @@ describe RedmineService, models: true do
end
end
- describe '#reference_pattern' do
+ describe '.reference_pattern' do
it_behaves_like 'allows project key on reference pattern'
it 'does allow # on the reference' do
- expect(subject.reference_pattern.match('#123')[:issue]).to eq('123')
+ expect(described_class.reference_pattern.match('#123')[:issue]).to eq('123')
end
end
end
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index 77b18e1c7d0..3f3a74d0f96 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe TeamcityService, models: true, caching: true do
+describe TeamcityService, :use_clean_rails_memory_store_caching, models: true do
include ReactiveCachingHelpers
let(:teamcity_url) { 'http://gitlab.com/teamcity' }
@@ -24,7 +24,9 @@ describe TeamcityService, models: true, caching: true do
describe 'Validations' do
context 'when service is active' do
- before { subject.active = true }
+ before do
+ subject.active = true
+ end
it { is_expected.to validate_presence_of(:build_type) }
it { is_expected.to validate_presence_of(:teamcity_url) }
@@ -60,7 +62,9 @@ describe TeamcityService, models: true, caching: true do
end
context 'when service is inactive' do
- before { subject.active = false }
+ before do
+ subject.active = false
+ end
it { is_expected.not_to validate_presence_of(:build_type) }
it { is_expected.not_to validate_presence_of(:teamcity_url) }
@@ -201,10 +205,12 @@ describe TeamcityService, models: true, caching: true do
end
def stub_request(status: 200, body: nil, build_status: 'success')
- teamcity_full_url = 'http://mic:password@gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
+ teamcity_full_url = 'http://gitlab.com/teamcity/httpAuth/app/rest/builds/branch:unspecified:any,number:123'
+ auth = %w(mic password)
+
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
- WebMock.stub_request(:get, teamcity_full_url).to_return(
+ WebMock.stub_request(:get, teamcity_full_url).with(basic_auth: auth).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
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 e120e21af06..fdcb011d685 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -7,50 +7,50 @@ describe Project, models: true do
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to have_many(:users) }
it { is_expected.to have_many(:services) }
- it { is_expected.to have_many(:events).dependent(:destroy) }
- it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
- it { is_expected.to have_many(:issues).dependent(:destroy) }
- it { is_expected.to have_many(:milestones).dependent(:destroy) }
- it { is_expected.to have_many(:project_members).dependent(:destroy) }
+ it { is_expected.to have_many(:events) }
+ it { is_expected.to have_many(:merge_requests) }
+ it { is_expected.to have_many(:issues) }
+ it { is_expected.to have_many(:milestones) }
+ it { is_expected.to have_many(:project_members).dependent(:delete_all) }
it { is_expected.to have_many(:users).through(:project_members) }
- it { is_expected.to have_many(:requesters).dependent(:destroy) }
- it { is_expected.to have_many(:notes).dependent(:destroy) }
- it { is_expected.to have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) }
- it { is_expected.to have_many(:deploy_keys_projects).dependent(:destroy) }
+ it { is_expected.to have_many(:requesters).dependent(:delete_all) }
+ it { is_expected.to have_many(:notes) }
+ it { is_expected.to have_many(:snippets).class_name('ProjectSnippet') }
+ it { is_expected.to have_many(:deploy_keys_projects) }
it { is_expected.to have_many(:deploy_keys) }
- it { is_expected.to have_many(:hooks).dependent(:destroy) }
- 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(:mattermost_service).dependent(:destroy) }
- it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
- it { is_expected.to have_one(:asana_service).dependent(:destroy) }
- it { is_expected.to have_many(:boards).dependent(:destroy) }
- it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
- it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
- it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
- it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
- it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
- it { is_expected.to have_one(:irker_service).dependent(:destroy) }
- it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
- it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
- it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
- it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
- it { is_expected.to have_one(:slack_slash_commands_service).dependent(:destroy) }
- it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) }
- it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
- it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
- it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
- it { is_expected.to have_one(:teamcity_service).dependent(:destroy) }
- it { is_expected.to have_one(:jira_service).dependent(:destroy) }
- it { is_expected.to have_one(:redmine_service).dependent(:destroy) }
- it { is_expected.to have_one(:custom_issue_tracker_service).dependent(:destroy) }
- it { is_expected.to have_one(:bugzilla_service).dependent(:destroy) }
- it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) }
- it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
- it { is_expected.to have_one(:project_feature).dependent(:destroy) }
- it { is_expected.to have_one(:statistics).class_name('ProjectStatistics').dependent(:delete) }
- it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
+ it { is_expected.to have_many(:hooks) }
+ it { is_expected.to have_many(:protected_branches) }
+ it { is_expected.to have_one(:forked_project_link) }
+ it { is_expected.to have_one(:slack_service) }
+ it { is_expected.to have_one(:microsoft_teams_service) }
+ it { is_expected.to have_one(:mattermost_service) }
+ it { is_expected.to have_one(:pushover_service) }
+ it { is_expected.to have_one(:asana_service) }
+ it { is_expected.to have_many(:boards) }
+ it { is_expected.to have_one(:campfire_service) }
+ it { is_expected.to have_one(:drone_ci_service) }
+ it { is_expected.to have_one(:emails_on_push_service) }
+ it { is_expected.to have_one(:pipelines_email_service) }
+ it { is_expected.to have_one(:irker_service) }
+ it { is_expected.to have_one(:pivotaltracker_service) }
+ it { is_expected.to have_one(:hipchat_service) }
+ it { is_expected.to have_one(:flowdock_service) }
+ it { is_expected.to have_one(:assembla_service) }
+ it { is_expected.to have_one(:slack_slash_commands_service) }
+ it { is_expected.to have_one(:mattermost_slash_commands_service) }
+ it { is_expected.to have_one(:gemnasium_service) }
+ it { is_expected.to have_one(:buildkite_service) }
+ it { is_expected.to have_one(:bamboo_service) }
+ it { is_expected.to have_one(:teamcity_service) }
+ it { is_expected.to have_one(:jira_service) }
+ it { is_expected.to have_one(:redmine_service) }
+ it { is_expected.to have_one(:custom_issue_tracker_service) }
+ it { is_expected.to have_one(:bugzilla_service) }
+ it { is_expected.to have_one(:gitlab_issue_tracker_service) }
+ it { is_expected.to have_one(:external_wiki_service) }
+ it { is_expected.to have_one(:project_feature) }
+ it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') }
+ it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
it { is_expected.to have_many(:commit_statuses) }
@@ -58,20 +58,22 @@ 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) }
- it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
- it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
- it { is_expected.to have_many(:environments).dependent(:destroy) }
- it { is_expected.to have_many(:deployments).dependent(:destroy) }
- it { is_expected.to have_many(:todos).dependent(:destroy) }
- it { is_expected.to have_many(:releases).dependent(:destroy) }
- it { is_expected.to have_many(:lfs_objects_projects).dependent(:destroy) }
- it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
- it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
+ it { is_expected.to have_many(:labels).class_name('ProjectLabel') }
+ it { is_expected.to have_many(:users_star_projects) }
+ it { is_expected.to have_many(:environments) }
+ it { is_expected.to have_many(:deployments) }
+ it { is_expected.to have_many(:todos) }
+ it { is_expected.to have_many(:releases) }
+ it { is_expected.to have_many(:lfs_objects_projects) }
+ it { is_expected.to have_many(:project_group_links) }
+ it { is_expected.to have_many(:notification_settings).dependent(:delete_all) }
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) }
context 'after initialized' do
it "has a project_feature" do
@@ -141,6 +143,10 @@ describe Project, models: true do
it { is_expected.to validate_length_of(:description).is_at_most(2000) }
+ it { is_expected.to validate_length_of(:ci_config_path).is_at_most(255) }
+ it { is_expected.to allow_value('').for(:ci_config_path) }
+ it { is_expected.not_to allow_value('test/../foo').for(:ci_config_path) }
+
it { is_expected.to validate_presence_of(:creator) }
it { is_expected.to validate_presence_of(:namespace) }
@@ -219,6 +225,20 @@ describe Project, models: true do
expect(project2.import_data).to be_nil
end
+ it "does not allow blocked import_url localhost" do
+ project2 = build(:empty_project, import_url: 'http://localhost:9000/t.git')
+
+ expect(project2).to be_invalid
+ expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ end
+
+ it "does not allow blocked import_url port" do
+ project2 = build(:empty_project, import_url: 'http://github.com:25/t.git')
+
+ expect(project2).to be_invalid
+ expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ end
+
describe 'project pending deletion' do
let!(:project_pending_deletion) do
create(:empty_project,
@@ -238,14 +258,33 @@ 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
- end
- describe 'default_scope' do
- it 'excludes projects pending deletion from the results' do
- project = create(:empty_project)
- create(:empty_project, pending_delete: true)
+ 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.all).to eq [project]
+ 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
@@ -270,10 +309,14 @@ describe Project, models: true do
end
describe 'delegation' do
- it { is_expected.to delegate_method(:add_guest).to(:team) }
- it { is_expected.to delegate_method(:add_reporter).to(:team) }
- it { is_expected.to delegate_method(:add_developer).to(:team) }
- it { is_expected.to delegate_method(:add_master).to(:team) }
+ [:add_guest, :add_reporter, :add_developer, :add_master, :add_user, :add_users].each do |method|
+ it { is_expected.to delegate_method(method).to(:team) }
+ end
+
+ it { is_expected.to delegate_method(:empty_repo?).to(:repository) }
+ it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) }
+ it { is_expected.to delegate_method(:count).to(:forks).with_prefix(true) }
+ it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
end
describe '#to_reference' do
@@ -689,25 +732,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
@@ -785,17 +809,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/-/system/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
@@ -803,17 +829,15 @@ 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}" }
+ it { is_expected.to eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
context 'when git repo is empty' do
let(:project) { create(:empty_project) }
- it { should eq nil }
+ it { is_expected.to eq nil }
end
end
@@ -879,7 +903,7 @@ describe Project, models: true do
end
end
- describe '.cached_count', caching: true do
+ describe '.cached_count', :use_clean_rails_memory_store_caching do
let(:group) { create(:group, :public) }
let!(:project1) { create(:empty_project, :public, group: group) }
let!(:project2) { create(:empty_project, :public, group: group) }
@@ -923,6 +947,20 @@ describe Project, models: true do
end
end
+ describe '.starred_by' do
+ it 'returns only projects starred by the given user' do
+ user1 = create(:user)
+ user2 = create(:user)
+ project1 = create(:empty_project)
+ project2 = create(:empty_project)
+ create(:empty_project)
+ user1.toggle_star(project1)
+ user2.toggle_star(project2)
+
+ expect(Project.starred_by(user1)).to contain_exactly(project1)
+ end
+ end
+
describe '.visible_to_user' do
let!(:project) { create(:empty_project, :private) }
let!(:user) { create(:user) }
@@ -948,7 +986,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
@@ -966,13 +1004,17 @@ describe Project, models: true do
subject { project.shared_runners_enabled }
context 'are enabled' do
- before { stub_application_setting(shared_runners_enabled: true) }
+ before do
+ stub_application_setting(shared_runners_enabled: true)
+ end
it { is_expected.to be_truthy }
end
context 'are disabled' do
- before { stub_application_setting(shared_runners_enabled: false) }
+ before do
+ stub_application_setting(shared_runners_enabled: false)
+ end
it { is_expected.to be_falsey }
end
@@ -1068,7 +1110,9 @@ describe Project, models: true do
subject { project.pages_deployed? }
context 'if public folder does exist' do
- before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) }
+ before do
+ allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true)
+ end
it { is_expected.to be_truthy }
end
@@ -1134,6 +1178,16 @@ describe Project, models: true do
expect(relation.search(project.namespace.name)).to eq([project])
end
+
+ describe 'with pending_delete project' do
+ let(:pending_delete_project) { create(:empty_project, pending_delete: true) }
+
+ it 'shows pending deletion project' do
+ search_result = described_class.search(pending_delete_project.name)
+
+ expect(search_result).to eq([pending_delete_project])
+ end
+ end
end
describe '#rename_repo' do
@@ -1144,43 +1198,49 @@ 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
- expect(gitlab_shell).to receive(:mv_repository).
- ordered.
- with(project.repository_storage_path, "#{project.namespace.full_path}/foo", "#{project.full_path}").
- and_return(true)
+ 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}")
+ .and_return(true)
- expect(gitlab_shell).to receive(:mv_repository).
- ordered.
- with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki").
- and_return(true)
+ expect(gitlab_shell).to receive(:mv_repository)
+ .ordered
+ .with(project.repository_storage_path, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki")
+ .and_return(true)
- expect_any_instance_of(SystemHooksService).
- to receive(:execute_hooks_for).
- with(project, :rename)
+ expect_any_instance_of(SystemHooksService)
+ .to receive(:execute_hooks_for)
+ .with(project, :rename)
- expect_any_instance_of(Gitlab::UploadsTransfer).
- to receive(:rename_project).
- with('foo', project.path, project.namespace.full_path)
+ expect_any_instance_of(Gitlab::UploadsTransfer)
+ .to receive(:rename_project)
+ .with('foo', project.path, project.namespace.full_path)
expect(project).to receive(:expire_caches_before_rename)
+ expect(project).to receive(:expires_full_path_cache)
+
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 }
- it { expect{subject}.to raise_error(Exception) }
+ it { expect{subject}.to raise_error(StandardError) }
end
end
@@ -1190,13 +1250,13 @@ describe Project, models: true do
let(:wiki) { double(:wiki, exists?: true) }
it 'expires the caches of the repository and wiki' do
- allow(Repository).to receive(:new).
- with('foo', project).
- and_return(repo)
+ allow(Repository).to receive(:new)
+ .with('foo', project)
+ .and_return(repo)
- allow(Repository).to receive(:new).
- with('foo.wiki', project).
- and_return(wiki)
+ allow(Repository).to receive(:new)
+ .with('foo.wiki', project)
+ .and_return(wiki)
expect(repo).to receive(:before_delete)
expect(wiki).to receive(:before_delete)
@@ -1247,9 +1307,9 @@ describe Project, models: true do
context 'using a regular repository' do
it 'creates the repository' do
- expect(shell).to receive(:add_repository).
- with(project.repository_storage_path, project.path_with_namespace).
- and_return(true)
+ expect(shell).to receive(:add_repository)
+ .with(project.repository_storage_path, project.path_with_namespace)
+ .and_return(true)
expect(project.repository).to receive(:after_create)
@@ -1257,9 +1317,9 @@ describe Project, models: true do
end
it 'adds an error if the repository could not be created' do
- expect(shell).to receive(:add_repository).
- with(project.repository_storage_path, project.path_with_namespace).
- and_return(false)
+ expect(shell).to receive(:add_repository)
+ .with(project.repository_storage_path, project.path_with_namespace)
+ .and_return(false)
expect(project.repository).not_to receive(:after_create)
@@ -1278,59 +1338,47 @@ 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
+ describe '#ensure_repository' do
+ let(:project) { create(:project, :repository) }
+ let(:shell) { Gitlab::Shell.new }
- it 'returns true when the branch matches a protected branch via wildcard match' do
- create(:protected_branch, project: project, name: "production/*")
+ before do
+ allow(project).to receive(:gitlab_shell).and_return(shell)
+ end
- expect(project.protected_branch?('production/some-branch')).to eq(true)
- end
+ it 'creates the repository if it not exist' do
+ allow(project).to receive(:repository_exists?)
+ .and_return(false)
- 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
+ allow(shell).to receive(:add_repository)
+ .with(project.repository_storage_path, project.path_with_namespace)
+ .and_return(true)
- 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).to receive(:create_repository).with(force: true)
- expect(project.protected_branch?('staging/some-branch')).to eq(false)
- end
+ project.ensure_repository
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 'does not create the repository if it exists' do
+ allow(project).to receive(:repository_exists?)
+ .and_return(true)
- 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).not_to receive(:create_repository)
- expect(project.protected_branch?('master')).to be false
- end
+ project.ensure_repository
+ 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)
+ it 'creates the repository if it is a fork' do
+ expect(project).to receive(:forked?).and_return(true)
- expect(project.protected_branch?('master')).to be true
- end
+ allow(project).to receive(:repository_exists?)
+ .and_return(false)
- it 'returns true when default_branch_protection is in full protection' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+ expect(shell).to receive(:add_repository)
+ .with(project.repository_storage_path, project.path_with_namespace)
+ .and_return(true)
- expect(project.protected_branch?('master')).to be true
- end
+ project.ensure_repository
end
end
@@ -1373,38 +1421,19 @@ 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_url }
- 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 }
-
- before { stub_container_registry_config(**registry_settings) }
+ before do
+ stub_container_registry_config(**registry_settings)
+ end
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 }
@@ -1412,9 +1441,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 }
@@ -1424,28 +1451,141 @@ describe Project, models: true do
describe '#has_container_registry_tags?' do
let(:project) { create(:empty_project) }
- subject { project.has_container_registry_tags? }
+ context 'when container registry is enabled' do
+ before do
+ stub_container_registry_config(enabled: true)
+ end
+
+ context 'when tags are present for multi-level registries' do
+ before do
+ create(:container_repository, project: project, name: 'image')
- context 'for enabled registry' do
- before { stub_container_registry_config(enabled: true) }
+ stub_container_registry_tags(repository: /image/,
+ tags: %w[latest rc1])
+ end
- context 'with tags' do
- before { stub_container_registry_tags('test', 'test2') }
+ it 'should have image tags' do
+ expect(project).to have_container_registry_tags
+ end
+ end
+
+ 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_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 there are no tags at all' do
+ before do
+ stub_container_registry_tags(repository: :any, tags: [])
+ end
- it { is_expected.to be_falsey }
+ it 'should not have image tags' do
+ expect(project).not_to have_container_registry_tags
+ end
end
end
- context 'for disabled registry' do
- before { stub_container_registry_config(enabled: false) }
+ context 'when container registry is disabled' do
+ before do
+ stub_container_registry_config(enabled: false)
+ end
- 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
+
+ describe '#ci_config_path=' do
+ let(:project) { create(:empty_project) }
+
+ it 'sets nil' do
+ project.update!(ci_config_path: nil)
+
+ expect(project.ci_config_path).to be_nil
+ end
+
+ it 'sets a string' do
+ project.update!(ci_config_path: 'foo/.gitlab_ci.yml')
+
+ expect(project.ci_config_path).to eq('foo/.gitlab_ci.yml')
+ end
+
+ it 'sets a string but removes all leading slashes and null characters' do
+ project.update!(ci_config_path: "///f\0oo/\0/.gitlab_ci.yml")
+
+ expect(project.ci_config_path).to eq('foo//.gitlab_ci.yml')
+ end
+ end
+
+ describe 'Project import job' do
+ let(:project) { create(:empty_project, import_url: generate(:url)) }
+
+ before do
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository)
+ .with(project.repository_storage_path, project.path_with_namespace, project.import_url)
+ .and_return(true)
+
+ expect_any_instance_of(Repository).to receive(:after_import)
+ .and_call_original
+ end
+
+ it 'imports a project' do
+ expect_any_instance_of(RepositoryImportWorker).to receive(:perform).and_call_original
+
+ project.import_schedule
+
+ expect(project.reload.import_status).to eq('finished')
+ end
+ end
+
+ describe 'project import state transitions' do
+ context 'state transition: [:started] => [:finished]' do
+ let(:housekeeping_service) { spy }
+
+ before do
+ allow(Projects::HousekeepingService).to receive(:new) { housekeeping_service }
+ end
+
+ it 'performs housekeeping when an import of a fresh project is completed' do
+ project = create(:project_empty_repo, :import_started, import_type: :github)
+
+ project.import_finish
+
+ expect(housekeeping_service).to have_received(:execute)
+ end
+
+ it 'does not perform housekeeping when project repository does not exist' do
+ project = create(:empty_project, :import_started, import_type: :github)
+
+ project.import_finish
+
+ expect(housekeeping_service).not_to have_received(:execute)
+ end
+
+ it 'does not perform housekeeping when project does not have a valid import type' do
+ project = create(:empty_project, :import_started, import_type: nil)
+
+ project.import_finish
+
+ expect(housekeeping_service).not_to have_received(:execute)
+ end
end
end
@@ -1530,13 +1670,13 @@ describe Project, models: true do
describe '#add_import_job' do
context 'forked' do
- let(:forked_project_link) { create(:forked_project_link) }
+ let(:forked_project_link) { create(:forked_project_link, :forked_to_empty_project) }
let(:forked_from_project) { forked_project_link.forked_from_project }
let(:project) { forked_project_link.forked_to_project }
it 'schedules a RepositoryForkWorker job' do
- expect(RepositoryForkWorker).to receive(:perform_async).
- with(project.id, forked_from_project.repository_storage_path,
+ expect(RepositoryForkWorker).to receive(:perform_async)
+ .with(project.id, forked_from_project.repository_storage_path,
forked_from_project.path_with_namespace, project.namespace.full_path)
project.add_import_job
@@ -1544,9 +1684,9 @@ describe Project, models: true do
end
context 'not forked' do
- let(:project) { create(:empty_project) }
-
it 'schedules a RepositoryImportWorker job' do
+ project = create(:empty_project, import_url: generate(:url))
+
expect(RepositoryImportWorker).to receive(:perform_async).with(project.id)
project.add_import_job
@@ -1728,6 +1868,90 @@ describe Project, models: true do
end
end
+ describe '#secret_variables_for' do
+ let(:project) { create(:empty_project) }
+
+ let!(:secret_variable) do
+ create(:ci_variable, value: 'secret', project: project)
+ end
+
+ let!(:protected_variable) do
+ create(:ci_variable, :protected, value: 'protected', project: project)
+ end
+
+ subject { project.secret_variables_for(ref: 'ref') }
+
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ shared_examples 'ref is protected' do
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(secret_variable, protected_variable)
+ end
+ end
+
+ context 'when the ref is not protected' do
+ it 'contains only the secret variables' do
+ is_expected.to contain_exactly(secret_variable)
+ end
+ end
+
+ context 'when the ref is a protected branch' do
+ before do
+ create(:protected_branch, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+
+ context 'when the ref is a protected tag' do
+ before do
+ create(:protected_tag, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+ end
+
+ describe '#protected_for?' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.protected_for?('ref') }
+
+ context 'when the ref is not protected' do
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'when the ref is a protected branch' do
+ before do
+ create(:protected_branch, name: 'ref', project: project)
+ end
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when the ref is a protected tag' do
+ before do
+ create(:protected_tag, name: 'ref', project: project)
+ end
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+ end
+
describe '#update_project_statistics' do
let(:project) { create(:empty_project) }
@@ -1749,11 +1973,14 @@ describe Project, models: true do
end
describe 'inside_path' do
- let!(:project1) { create(:empty_project) }
+ let!(:project1) { create(:empty_project, namespace: create(:namespace, path: 'name_pace')) }
let!(:project2) { create(:empty_project) }
+ let!(:project3) { create(:empty_project, namespace: create(:namespace, path: 'namespace')) }
let!(:path) { project1.namespace.full_path }
- it { expect(Project.inside_path(path)).to eq([project1]) }
+ it 'returns correct project' do
+ expect(Project.inside_path(path)).to eq([project1])
+ end
end
describe '#route_map_for' do
@@ -1841,7 +2068,9 @@ describe Project, models: true do
describe '#parent_changed?' do
let(:project) { create(:empty_project) }
- before { project.namespace_id = 7 }
+ before do
+ project.namespace_id = 7
+ end
it { expect(project.parent_changed?).to be_truthy }
end
@@ -1899,21 +2128,96 @@ describe Project, models: true do
describe '#http_url_to_repo' do
let(:project) { create :empty_project }
- context 'when no user is given' do
- it 'returns the url to the repo without a username' do
- url = project.http_url_to_repo
+ it 'returns the url to the repo without a username' do
+ expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
+ expect(project.http_url_to_repo).not_to include('@')
+ end
+ end
- expect(url).to eq(project.http_url_to_repo)
- expect(url).not_to include('@')
+ describe '#pipeline_status' do
+ let(:project) { create(:project) }
+ it 'builds a pipeline status' do
+ 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
+
+ describe '.public_or_visible_to_user' do
+ let!(:user) { create(:user) }
+
+ let!(:private_project) do
+ create(:empty_project, :private, creator: user, namespace: user.namespace)
+ end
+
+ let!(:public_project) { create(:empty_project, :public) }
+
+ context 'with a user' do
+ let(:projects) do
+ Project.all.public_or_visible_to_user(user)
+ end
+
+ it 'includes projects the user has access to' do
+ expect(projects).to include(private_project)
+ end
+
+ it 'includes projects the user can see' do
+ expect(projects).to include(public_project)
end
end
- context 'when user is given' do
- it 'returns the url to the repo with the username' do
- user = build_stubbed(:user)
+ context 'without a user' do
+ it 'only includes public projects' do
+ projects = Project.all.public_or_visible_to_user
- expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@})
+ expect(projects).to eq([public_project])
end
end
end
+
+ describe '#remove_private_deploy_keys' do
+ it 'removes the private deploy keys of a project' do
+ project = create(:empty_project)
+
+ private_key = create(:deploy_key, public: false)
+ public_key = create(:deploy_key, public: true)
+
+ create(:deploy_keys_project, deploy_key: private_key, project: project)
+ create(:deploy_keys_project, deploy_key: public_key, project: project)
+
+ project.remove_private_deploy_keys
+
+ expect(project.deploy_keys.where(public: false).any?).to eq(false)
+ expect(project.deploy_keys.where(public: true).any?).to eq(true)
+ 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_team_spec.rb b/spec/models/project_team_spec.rb
index 942eeab251d..49f2f8c0ad1 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -81,7 +81,7 @@ describe ProjectTeam, models: true do
user = create(:user)
project.add_guest(user)
- expect(project.team.members).to contain_exactly(user)
+ expect(project.team.members).to contain_exactly(user, project.owner)
end
it 'returns project members of a specified level' do
@@ -100,7 +100,8 @@ describe ProjectTeam, models: true do
group_access: Gitlab::Access::GUEST
)
- expect(project.team.members).to contain_exactly(group_member.user)
+ expect(project.team.members)
+ .to contain_exactly(group_member.user, project.owner)
end
it 'returns invited members of a group of a specified level' do
@@ -137,7 +138,10 @@ describe ProjectTeam, models: true do
describe '#find_member' do
context 'personal project' do
- let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:project) do
+ create(:empty_project, :public, :access_requestable)
+ end
+
let(:requester) { create(:user) }
before do
@@ -200,7 +204,9 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) }
context 'personal project' do
- let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:project) do
+ create(:empty_project, :public, :access_requestable)
+ end
context 'when project is not shared with group' do
before do
@@ -234,7 +240,9 @@ describe ProjectTeam, models: true do
it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) }
context 'but share_with_group_lock is true' do
- before { project.namespace.update(share_with_group_lock: true) }
+ before do
+ project.namespace.update(share_with_group_lock: true)
+ end
it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) }
it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) }
@@ -244,7 +252,9 @@ describe ProjectTeam, models: true do
context 'group project' do
let(:group) { create(:group, :access_requestable) }
- let!(:project) { create(:empty_project, group: group) }
+ let!(:project) do
+ create(:empty_project, group: group)
+ end
before do
group.add_master(master)
@@ -265,8 +275,15 @@ describe ProjectTeam, models: true do
let(:group) { create(:group) }
let(:developer) { create(:user) }
let(:master) { create(:user) }
- let(:personal_project) { create(:empty_project, namespace: developer.namespace) }
- let(:group_project) { create(:empty_project, namespace: group) }
+
+ let(:personal_project) do
+ create(:empty_project, namespace: developer.namespace)
+ end
+
+ let(:group_project) do
+ create(:empty_project, namespace: group)
+ end
+
let(:members_project) { create(:empty_project) }
let(:shared_project) { create(:empty_project) }
@@ -312,69 +329,105 @@ describe ProjectTeam, models: true do
end
end
- shared_examples_for "#max_member_access_for_users" do |enable_request_store|
- describe "#max_member_access_for_users" do
- before do
- RequestStore.begin! if enable_request_store
+ shared_examples 'max member access for users' do
+ let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:second_group) { create(:group) }
+
+ let(:master) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:promoted_guest) { create(:user) }
+
+ let(:group_developer) { create(:user) }
+ let(:second_developer) { create(:user) }
+
+ let(:user_without_access) { create(:user) }
+ let(:second_user_without_access) { create(:user) }
+
+ let(:users) do
+ [master, reporter, promoted_guest, guest, group_developer, second_developer, user_without_access].map(&:id)
+ end
+
+ let(:expected) do
+ {
+ master.id => Gitlab::Access::MASTER,
+ reporter.id => Gitlab::Access::REPORTER,
+ promoted_guest.id => Gitlab::Access::DEVELOPER,
+ guest.id => Gitlab::Access::GUEST,
+ group_developer.id => Gitlab::Access::DEVELOPER,
+ second_developer.id => Gitlab::Access::MASTER,
+ user_without_access.id => Gitlab::Access::NO_ACCESS
+ }
+ end
+
+ before do
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(promoted_guest)
+ project.add_guest(guest)
+
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_master(promoted_guest)
+ group.add_developer(group_developer)
+ group.add_developer(second_developer)
+
+ project.project_group_links.create(
+ group: second_group,
+ group_access: Gitlab::Access::MASTER
+ )
+
+ second_group.add_master(second_developer)
+ end
+
+ it 'returns correct roles for different users' do
+ expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+ end
+ end
+
+ describe '#max_member_access_for_user_ids' do
+ context 'with RequestStore enabled', :request_store do
+ include_examples 'max member access for users'
+
+ def access_levels(users)
+ project.team.max_member_access_for_user_ids(users)
end
- after do
- if enable_request_store
- RequestStore.end!
- RequestStore.clear!
- end
+ it 'does not perform extra queries when asked for users who have already been found' do
+ access_levels(users)
+
+ expect { access_levels(users) }.not_to exceed_query_limit(0)
+
+ expect(access_levels(users)).to eq(expected)
end
- it 'returns correct roles for different users' do
- master = create(:user)
- reporter = create(:user)
- promoted_guest = create(:user)
- guest = create(:user)
- project = create(:empty_project)
+ it 'only requests the extra users when uncached users are passed' do
+ new_user = create(:user)
+ second_new_user = create(:user)
+ all_users = users + [new_user.id, second_new_user.id]
- project.add_master(master)
- project.add_reporter(reporter)
- project.add_guest(promoted_guest)
- project.add_guest(guest)
+ expected_all = expected.merge(new_user.id => Gitlab::Access::NO_ACCESS,
+ second_new_user.id => Gitlab::Access::NO_ACCESS)
- group = create(:group)
- group_developer = create(:user)
- second_developer = create(:user)
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::DEVELOPER)
-
- group.add_master(promoted_guest)
- group.add_developer(group_developer)
- group.add_developer(second_developer)
-
- second_group = create(:group)
- project.project_group_links.create(
- group: second_group,
- group_access: Gitlab::Access::MASTER)
- second_group.add_master(second_developer)
-
- users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id)
-
- expected = {
- master.id => Gitlab::Access::MASTER,
- reporter.id => Gitlab::Access::REPORTER,
- promoted_guest.id => Gitlab::Access::DEVELOPER,
- guest.id => Gitlab::Access::GUEST,
- group_developer.id => Gitlab::Access::DEVELOPER,
- second_developer.id => Gitlab::Access::MASTER
- }
-
- expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+ access_levels(users)
+
+ queries = ActiveRecord::QueryRecorder.new { access_levels(all_users) }
+
+ expect(queries.count).to eq(1)
+ expect(queries.log_message).to match(/\W#{new_user.id}\W/)
+ expect(queries.log_message).to match(/\W#{second_new_user.id}\W/)
+ expect(queries.log_message).not_to match(/\W#{promoted_guest.id}\W/)
+ expect(access_levels(all_users)).to eq(expected_all)
end
end
- end
- describe '#max_member_access_for_users with RequestStore' do
- it_behaves_like "#max_member_access_for_users", true
- end
-
- describe '#max_member_access_for_users without RequestStore' do
- it_behaves_like "#max_member_access_for_users", false
+ context 'with RequestStore disabled' do
+ include_examples 'max member access for users'
+ end
end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 58b57bd4fef..1f314791479 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -8,7 +8,10 @@ describe ProjectWiki, models: true do
let(:project_wiki) { ProjectWiki.new(project, user) }
subject { project_wiki }
- before { project_wiki.wiki }
+
+ before do
+ project_wiki.wiki
+ end
describe "#path_with_namespace" do
it "returns the project path with namespace with the .wiki extension" do
@@ -35,10 +38,13 @@ describe ProjectWiki, models: true do
end
describe "#http_url_to_repo" do
- it "provides the full http url to the repo" do
- gitlab_url = Gitlab.config.gitlab.url
- repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git"
- expect(subject.http_url_to_repo).to eq(repo_http_url)
+ let(:project) { create :empty_project }
+
+ it 'returns the full http url to the repo' do
+ expected_url = "#{Gitlab.config.gitlab.url}/#{subject.path_with_namespace}.git"
+
+ expect(project_wiki.http_url_to_repo).to eq(expected_url)
+ expect(project_wiki.http_url_to_repo).not_to include('@')
end
end
@@ -143,15 +149,15 @@ describe ProjectWiki, models: true do
describe '#find_file' do
before do
file = Gollum::File.new(subject.wiki)
- allow_any_instance_of(Gollum::Wiki).
- to receive(:file).with('image.jpg', 'master', true).
- and_return(file)
- allow_any_instance_of(Gollum::File).
- to receive(:mime_type).
- and_return('image/jpeg')
- allow_any_instance_of(Gollum::Wiki).
- to receive(:file).with('non-existant', 'master', true).
- and_return(nil)
+ allow_any_instance_of(Gollum::Wiki)
+ .to receive(:file).with('image.jpg', 'master', true)
+ .and_return(file)
+ allow_any_instance_of(Gollum::File)
+ .to receive(:mime_type)
+ .and_return('image/jpeg')
+ allow_any_instance_of(Gollum::Wiki)
+ .to receive(:file).with('non-existant', 'master', true)
+ .and_return(nil)
end
after do
@@ -200,9 +206,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
@@ -227,9 +236,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
@@ -245,17 +257,20 @@ 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
describe '#create_repo!' do
it 'creates a repository' do
- expect(subject).to receive(:init_repo).
- with(subject.path_with_namespace).
- and_return(true)
+ expect(subject).to receive(:init_repo)
+ .with(subject.path_with_namespace)
+ .and_return(true)
expect(subject.repository).to receive(:after_create)
@@ -263,6 +278,24 @@ describe ProjectWiki, models: true do
end
end
+ describe '#ensure_repository' do
+ it 'creates the repository if it not exist' do
+ allow(subject).to receive(:repository_exists?).and_return(false)
+
+ expect(subject).to receive(:create_repo!)
+
+ subject.ensure_repository
+ end
+
+ it 'does not create the repository if it exists' do
+ allow(subject).to receive(:repository_exists?).and_return(true)
+
+ expect(subject).not_to receive(:create_repo!)
+
+ subject.ensure_repository
+ end
+ end
+
describe '#hook_attrs' do
it 'returns a hash with values' do
expect(subject.hook_attrs).to be_a Hash
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 274e4f00a0a..7635b0868e7 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)
@@ -350,6 +347,17 @@ describe Repository, models: true do
expect(blob.data).to eq('Changelog!')
end
+ it 'creates new file and dir when file_path has a forward slash' do
+ expect do
+ repository.create_file(user, 'new_dir/new_file.txt', 'File!',
+ message: 'Create new_file with new_dir',
+ branch_name: 'master')
+ end.to change { repository.commits('master').count }.by(1)
+
+ expect(repository.tree('master', 'new_dir').path).to eq('new_dir')
+ expect(repository.blob_at('master', 'new_dir/new_file.txt').data).to eq('File!')
+ end
+
it 'respects the autocrlf setting' do
repository.create_file(user, 'hello.txt', "Hello,\r\nWorld",
message: 'Add hello world',
@@ -553,39 +561,39 @@ describe Repository, models: true do
end
end
- describe "#changelog", caching: true do
+ describe "#changelog", :use_clean_rails_memory_store_caching 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
- describe "#license_blob", caching: true do
+ describe "#license_blob", :use_clean_rails_memory_store_caching do
before do
repository.delete_file(
user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
@@ -596,8 +604,8 @@ describe Repository, models: true do
user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
- allow(repository).to receive(:file_on_head).
- and_raise(Rugged::ReferenceError)
+ allow(repository).to receive(:file_on_head)
+ .and_raise(Rugged::ReferenceError)
expect(repository.license_blob).to be_nil
end
@@ -616,7 +624,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|
@@ -630,7 +638,7 @@ describe Repository, models: true do
end
end
- describe '#license_key', caching: true do
+ describe '#license_key', :use_clean_rails_memory_store_caching do
before do
repository.delete_file(user, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
@@ -646,7 +654,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 +670,45 @@ describe Repository, models: true do
end
end
- describe "#gitlab_ci_yml", caching: true do
+ 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", :use_clean_rails_memory_store_caching 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
@@ -749,8 +790,8 @@ describe Repository, models: true do
context 'when pre hooks were successful' do
it 'runs without errors' do
- expect_any_instance_of(GitHooksService).to receive(:execute).
- with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature')
+ expect_any_instance_of(GitHooksService).to receive(:execute)
+ .with(user, project, old_rev, blank_sha, 'refs/heads/feature')
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
end
@@ -792,14 +833,9 @@ describe Repository, models: true do
before do
service = GitHooksService.new
expect(GitHooksService).to receive(:new).and_return(service)
- expect(service).to receive(:execute).
- with(
- user,
- repository.path_to_repo,
- old_rev,
- new_rev,
- 'refs/heads/feature').
- and_yield(service).and_return(true)
+ expect(service).to receive(:execute)
+ .with(user, project, old_rev, new_rev, 'refs/heads/feature')
+ .and_yield(service).and_return(true)
end
it 'runs without errors' do
@@ -893,8 +929,8 @@ describe Repository, models: true do
expect(repository).not_to receive(:expire_emptiness_caches)
expect(repository).to receive(:expire_branches_cache)
- GitOperationService.new(user, repository).
- with_branch('new-feature') do
+ GitOperationService.new(user, repository)
+ .with_branch('new-feature') do
new_rev
end
end
@@ -977,8 +1013,8 @@ describe Repository, models: true do
end
it 'does nothing' do
- expect(repository.raw_repository).not_to receive(:autocrlf=).
- with(:input)
+ expect(repository.raw_repository).not_to receive(:autocrlf=)
+ .with(:input)
GitOperationService.new(nil, repository).send(:update_autocrlf_option)
end
@@ -997,9 +1033,9 @@ describe Repository, models: true do
end
it 'caches the output' do
- expect(repository.raw_repository).to receive(:empty?).
- once.
- and_return(false)
+ expect(repository.raw_repository).to receive(:empty?)
+ .once
+ .and_return(false)
repository.empty?
repository.empty?
@@ -1012,9 +1048,9 @@ describe Repository, models: true do
end
it 'caches the output' do
- expect(repository.raw_repository).to receive(:root_ref).
- once.
- and_return('master')
+ expect(repository.raw_repository).to receive(:root_ref)
+ .once
+ .and_return('master')
repository.root_ref
repository.root_ref
@@ -1025,9 +1061,9 @@ describe Repository, models: true do
it 'expires the root reference cache' do
repository.root_ref
- expect(repository.raw_repository).to receive(:root_ref).
- once.
- and_return('foo')
+ expect(repository.raw_repository).to receive(:root_ref)
+ .once
+ .and_return('foo')
repository.expire_root_ref_cache
@@ -1041,17 +1077,17 @@ describe Repository, models: true do
let(:cache) { repository.send(:cache) }
it 'expires the cache for all branches' do
- expect(cache).to receive(:expire).
- at_least(repository.branches.length * 2).
- times
+ expect(cache).to receive(:expire)
+ .at_least(repository.branches.length * 2)
+ .times
repository.expire_branch_cache
end
it 'expires the cache for all branches when the root branch is given' do
- expect(cache).to receive(:expire).
- at_least(repository.branches.length * 2).
- times
+ expect(cache).to receive(:expire)
+ .at_least(repository.branches.length * 2)
+ .times
repository.expire_branch_cache(repository.root_ref)
end
@@ -1083,28 +1119,40 @@ describe Repository, models: true do
end
end
- describe :skip_merged_commit do
+ describe 'skip_merges option' do
subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } }
it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') }
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 +1320,6 @@ describe Repository, models: true do
:changelog,
:license,
:contributing,
- :version,
:gitignore,
:koding,
:gitlab_ci,
@@ -1293,19 +1340,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
@@ -1313,12 +1350,12 @@ describe Repository, models: true do
describe '#after_push_commit' do
it 'expires statistics caches' do
- expect(repository).to receive(:expire_statistics_caches).
- and_call_original
+ expect(repository).to receive(:expire_statistics_caches)
+ .and_call_original
- expect(repository).to receive(:expire_branch_cache).
- with('master').
- and_call_original
+ expect(repository).to receive(:expire_branch_cache)
+ .with('master')
+ .and_call_original
repository.after_push_commit('master')
end
@@ -1382,20 +1419,30 @@ 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
describe '#expire_branches_cache' do
it 'expires the cache' do
- expect(repository).to receive(:expire_method_caches).
- with(%i(branch_names branch_count)).
- and_call_original
+ expect(repository).to receive(:expire_method_caches)
+ .with(%i(branch_names branch_count))
+ .and_call_original
repository.expire_branches_cache
end
@@ -1403,9 +1450,9 @@ describe Repository, models: true do
describe '#expire_tags_cache' do
it 'expires the cache' do
- expect(repository).to receive(:expire_method_caches).
- with(%i(tag_names tag_count)).
- and_call_original
+ expect(repository).to receive(:expire_method_caches)
+ .with(%i(tag_names tag_count))
+ .and_call_original
repository.expire_tags_cache
end
@@ -1416,11 +1463,11 @@ describe Repository, models: true do
let(:user) { build_stubbed(:user) }
it 'creates the tag using rugged' do
- expect(repository.rugged.tags).to receive(:create).
- with('8.5', repository.commit('master').id,
+ expect(repository.rugged.tags).to receive(:create)
+ .with('8.5', repository.commit('master').id,
hash_including(message: 'foo',
- tagger: hash_including(name: user.name, email: user.email))).
- and_call_original
+ tagger: hash_including(name: user.name, email: user.email)))
+ .and_call_original
repository.add_tag(user, '8.5', 'master', 'foo')
end
@@ -1433,12 +1480,12 @@ describe Repository, models: true do
it 'passes commit SHA to pre-receive and update hooks,\
and tag SHA to post-receive hook' do
- pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', repository.path_to_repo)
- update_hook = Gitlab::Git::Hook.new('update', repository.path_to_repo)
- post_receive_hook = Gitlab::Git::Hook.new('post-receive', repository.path_to_repo)
+ pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
+ update_hook = Gitlab::Git::Hook.new('update', project)
+ post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
- allow(Gitlab::Git::Hook).to receive(:new).
- and_return(pre_receive_hook, update_hook, post_receive_hook)
+ allow(Gitlab::Git::Hook).to receive(:new)
+ .and_return(pre_receive_hook, update_hook, post_receive_hook)
allow(pre_receive_hook).to receive(:trigger).and_call_original
allow(update_hook).to receive(:trigger).and_call_original
@@ -1449,12 +1496,12 @@ describe Repository, models: true do
commit_sha = repository.commit('master').id
tag_sha = tag.target
- expect(pre_receive_hook).to have_received(:trigger).
- with(anything, anything, commit_sha, anything)
- expect(update_hook).to have_received(:trigger).
- with(anything, anything, commit_sha, anything)
- expect(post_receive_hook).to have_received(:trigger).
- with(anything, anything, tag_sha, anything)
+ expect(pre_receive_hook).to have_received(:trigger)
+ .with(anything, anything, commit_sha, anything)
+ expect(update_hook).to have_received(:trigger)
+ .with(anything, anything, commit_sha, anything)
+ expect(post_receive_hook).to have_received(:trigger)
+ .with(anything, anything, tag_sha, anything)
end
end
@@ -1488,25 +1535,25 @@ describe Repository, models: true do
describe '#avatar' do
it 'returns nil if repo does not exist' do
- expect(repository).to receive(:file_on_head).
- and_raise(Rugged::ReferenceError)
+ expect(repository).to receive(:file_on_head)
+ .and_raise(Rugged::ReferenceError)
expect(repository.avatar).to eq(nil)
end
it 'returns the first avatar file found in the repository' do
- expect(repository).to receive(:file_on_head).
- with(:avatar).
- and_return(double(:tree, path: 'logo.png'))
+ expect(repository).to receive(:file_on_head)
+ .with(:avatar)
+ .and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png')
end
it 'caches the output' do
- expect(repository).to receive(:file_on_head).
- with(:avatar).
- once.
- and_return(double(:tree, path: 'logo.png'))
+ expect(repository).to receive(:file_on_head)
+ .with(:avatar)
+ .once
+ .and_return(double(:tree, path: 'logo.png'))
2.times { expect(repository.avatar).to eq('logo.png') }
end
@@ -1564,26 +1611,26 @@ describe Repository, models: true do
end
end
- describe '#contribution_guide', caching: true do
+ describe '#contribution_guide', :use_clean_rails_memory_store_caching do
it 'returns and caches the output' do
- expect(repository).to receive(:file_on_head).
- with(:contributing).
- and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md')).
- once
+ expect(repository).to receive(:file_on_head)
+ .with(:contributing)
+ .and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md'))
+ .once
2.times do
- expect(repository.contribution_guide).
- to be_an_instance_of(Gitlab::Git::Tree)
+ expect(repository.contribution_guide)
+ .to be_an_instance_of(Gitlab::Git::Tree)
end
end
end
- describe '#gitignore', caching: true do
+ describe '#gitignore', :use_clean_rails_memory_store_caching do
it 'returns and caches the output' do
- expect(repository).to receive(:file_on_head).
- with(:gitignore).
- and_return(Gitlab::Git::Tree.new(path: '.gitignore')).
- once
+ expect(repository).to receive(:file_on_head)
+ .with(:gitignore)
+ .and_return(Gitlab::Git::Tree.new(path: '.gitignore'))
+ .once
2.times do
expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree)
@@ -1591,12 +1638,12 @@ describe Repository, models: true do
end
end
- describe '#koding_yml', caching: true do
+ describe '#koding_yml', :use_clean_rails_memory_store_caching do
it 'returns and caches the output' do
- expect(repository).to receive(:file_on_head).
- with(:koding).
- and_return(Gitlab::Git::Tree.new(path: '.koding.yml')).
- once
+ expect(repository).to receive(:file_on_head)
+ .with(:koding)
+ .and_return(Gitlab::Git::Tree.new(path: '.koding.yml'))
+ .once
2.times do
expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree)
@@ -1604,26 +1651,36 @@ describe Repository, models: true do
end
end
- describe '#readme', caching: true do
+ describe '#readme', :use_clean_rails_memory_store_caching 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
describe '#expire_statistics_caches' do
it 'expires the caches' do
- expect(repository).to receive(:expire_method_caches).
- with(%i(size commit_count))
+ expect(repository).to receive(:expire_method_caches)
+ .with(%i(size commit_count))
repository.expire_statistics_caches
end
@@ -1640,8 +1697,8 @@ describe Repository, models: true do
describe '#expire_all_method_caches' do
it 'expires the caches of all methods' do
- expect(repository).to receive(:expire_method_caches).
- with(Repository::CACHED_METHODS)
+ expect(repository).to receive(:expire_method_caches)
+ .with(Repository::CACHED_METHODS)
repository.expire_all_method_caches
end
@@ -1666,8 +1723,8 @@ describe Repository, models: true do
context 'with an existing repository' do
it 'returns a Gitlab::Git::Tree' do
- expect(repository.file_on_head(:readme)).
- to be_an_instance_of(Gitlab::Git::Tree)
+ expect(repository.file_on_head(:readme))
+ .to be_an_instance_of(Gitlab::Git::Tree)
end
end
end
@@ -1765,7 +1822,7 @@ describe Repository, models: true do
end
end
- describe '#cache_method_output', caching: true do
+ describe '#cache_method_output', :use_clean_rails_memory_store_caching do
context 'with a non-existing repository' do
let(:value) do
repository.cache_method_output(:cats, fallback: 10) do
@@ -1805,12 +1862,13 @@ 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))
+ expect(repository).to receive(:expire_method_caches)
+ .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 +1909,46 @@ describe Repository, models: true do
end
end
end
+
+ describe '#is_ancestor?' do
+ let(:commit) { repository.commit }
+ let(:ancestor) { commit.parents.first }
+
+ context 'with Gitaly enabled' do
+ it 'it is an ancestor' do
+ expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true)
+ end
+
+ it 'it is not an ancestor' do
+ expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false)
+ end
+
+ it 'returns false on nil-values' do
+ expect(repository.is_ancestor?(nil, commit.id)).to eq(false)
+ expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false)
+ expect(repository.is_ancestor?(nil, nil)).to eq(false)
+ end
+ end
+
+ context 'with Gitaly disabled' do
+ before do
+ allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false)
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false)
+ end
+
+ it 'it is an ancestor' do
+ expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true)
+ end
+
+ it 'it is not an ancestor' do
+ expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false)
+ end
+
+ it 'returns false on nil-values' do
+ expect(repository.is_ancestor?(nil, commit.id)).to eq(false)
+ expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false)
+ expect(repository.is_ancestor?(nil, nil)).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 0b222022e62..1754253e0f2 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -1,33 +1,78 @@
require 'spec_helper'
describe Route, models: true do
- let!(:group) { create(:group, path: 'gitlab', name: 'gitlab') }
- 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 do
+ expect(route).to be_persisted
+ end
+
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) }
+ let!(:another_group) { create(:group, path: 'other') }
+ let!(:similar_group) { create(:group, path: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'another', name: 'another', parent: similar_group) }
+
+ it 'returns correct routes' do
+ expect(Route.inside_path('git_lab')).to match_array([nested_group.route, deep_nested_group.route])
+ end
+ end
+
describe '#rename_descendants' 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) }
let!(:similar_group) { create(:group, path: 'gitlab-org', name: 'gitlab-org') }
+ let!(:another_group) { create(:group, path: 'gittlab', name: 'gitllab') }
+ let!(:another_group_nested) { create(:group, path: 'git_lab', name: 'git_lab', parent: another_group) }
context 'path update' do
context 'when route name is set' do
- before { route.update_attributes(path: 'bar') }
+ before do
+ route.update_attributes(path: 'bar')
+ end
- 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
expect(described_class.exists?(path: 'gitlab-org')).to be_truthy
+ expect(described_class.exists?(path: 'gittlab')).to be_truthy
+ expect(described_class.exists?(path: 'gittlab/git_lab')).to be_truthy
end
end
@@ -40,17 +85,107 @@ 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
- before { route.update_attributes(name: 'bar') }
+ it 'updates children routes with new path' do
+ route.update_attributes(name: 'bar')
- it "updates children routes with new path" do
expect(described_class.exists?(name: 'bar')).to be_truthy
expect(described_class.exists?(name: 'bar / test')).to be_truthy
expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy
expect(described_class.exists?(name: 'gitlab-org')).to be_truthy
end
+
+ it 'handles a rename from nil' do
+ # Note: using `update_columns` to skip all validation and callbacks
+ route.update_columns(name: nil)
+
+ expect { route.update_attributes(name: 'bar') }
+ .to change { route.name }.from(nil).to('bar')
+ 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 219ab1989ea..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) }
@@ -198,4 +157,59 @@ describe Snippet, models: true do
expect(snippet.participants).to include(note1.author, note2.author)
end
end
+
+ describe '#check_for_spam' do
+ let(:snippet) { create :snippet, visibility_level: visibility_level }
+
+ subject do
+ snippet.assign_attributes(title: title)
+ snippet.check_for_spam?
+ end
+
+ context 'when public and spammable attributes changed' do
+ let(:visibility_level) { Snippet::PUBLIC }
+ let(:title) { 'woo' }
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when private' do
+ let(:visibility_level) { Snippet::PRIVATE }
+ let(:title) { snippet.title }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+
+ it 'returns true when switching to public' do
+ snippet.save!
+ snippet.visibility_level = Snippet::PUBLIC
+
+ expect(snippet.check_for_spam?).to be_truthy
+ end
+ end
+
+ context 'when spammable attributes have not changed' do
+ let(:visibility_level) { Snippet::PUBLIC }
+ let(:title) { snippet.title }
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ 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/system_note_metadata_spec.rb b/spec/models/system_note_metadata_spec.rb
new file mode 100644
index 00000000000..d238e28209a
--- /dev/null
+++ b/spec/models/system_note_metadata_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe SystemNoteMetadata, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:note) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:note) }
+
+ context 'when action type is invalid' do
+ subject do
+ build(:system_note_metadata, note: build(:note), action: 'invalid_type' )
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when action type is valid' do
+ subject do
+ build(:system_note_metadata, note: build(:note), action: 'merge' )
+ end
+
+ it { is_expected.to be_valid }
+ 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/upload_spec.rb b/spec/models/upload_spec.rb
index 4c832c87d6a..2dea2c6015f 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -54,8 +54,8 @@ describe Upload, type: :model do
uploader: 'AvatarUploader'
)
- expect { described_class.remove_path(__FILE__) }.
- to change { described_class.count }.from(1).to(0)
+ expect { described_class.remove_path(__FILE__) }
+ .to change { described_class.count }.from(1).to(0)
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index adb5b538922..a1d6d7e6e0b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -13,6 +13,10 @@ describe User, models: true do
it { is_expected.to include_module(TokenAuthenticatable) }
end
+ describe 'delegations' do
+ it { is_expected.to delegate_method(:path).to(:namespace).with_prefix }
+ end
+
describe 'associations' do
it { is_expected.to have_one(:namespace) }
it { is_expected.to have_many(:snippets).dependent(:destroy) }
@@ -22,13 +26,10 @@ describe User, models: true do
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
it { is_expected.to have_many(:events).dependent(:destroy) }
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(:issues).dependent(:destroy) }
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 +38,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 +101,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
@@ -81,6 +122,7 @@ describe User, models: true do
it { is_expected.to validate_numericality_of(:projects_limit) }
it { is_expected.to allow_value(0).for(:projects_limit) }
it { is_expected.not_to allow_value(-1).for(:projects_limit) }
+ it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) }
it { is_expected.to validate_length_of(:bio).is_at_most(255) }
@@ -210,22 +252,6 @@ describe User, models: true do
end
end
end
-
- describe 'ghost users' do
- it 'does not allow a non-blocked ghost user' do
- user = build(:user, :ghost)
- user.state = 'active'
-
- expect(user).to be_invalid
- end
-
- it 'allows a blocked ghost user' do
- user = build(:user, :ghost)
- user.state = 'blocked'
-
- expect(user).to be_valid
- end
- end
end
describe "scopes" do
@@ -303,7 +329,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?) }
@@ -322,6 +348,35 @@ describe User, models: true do
end
end
+ describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state 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) }
@@ -376,22 +431,10 @@ describe User, models: true do
end
describe '#generate_password' do
- it "executes callback when force_random_password specified" do
- user = build(:user, force_random_password: true)
- expect(user).to receive(:generate_password)
- user.save
- end
-
it "does not generate password by default" do
user = create(:user, password: 'abcdefghe')
expect(user.password).to eq('abcdefghe')
end
-
- it "generates password when forcing random password" do
- allow(Devise).to receive(:friendly_token).and_return('123456789')
- user = create(:user, password: 'abcdefg', force_random_password: true)
- expect(user.password).to eq('12345678')
- end
end
describe 'authentication token' do
@@ -401,6 +444,56 @@ describe User, models: true do
end
end
+ describe 'ensure incoming email token' do
+ it 'has incoming email token' do
+ user = create(:user)
+ expect(user.incoming_email_token).not_to be_blank
+ end
+ end
+
+ describe '#ensure_user_rights_and_limits' do
+ describe 'with external user' do
+ let(:user) { create(:user, external: true) }
+
+ it 'receives callback when external changes' do
+ expect(user).to receive(:ensure_user_rights_and_limits)
+
+ user.update_attributes(external: false)
+ end
+
+ it 'ensures correct rights and limits for user' do
+ stub_config_setting(default_can_create_group: true)
+
+ expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true)
+ .and change { user.projects_limit }.to(current_application_settings.default_projects_limit)
+ end
+ end
+
+ describe 'without external user' do
+ let(:user) { create(:user, external: false) }
+
+ it 'receives callback when external changes' do
+ expect(user).to receive(:ensure_user_rights_and_limits)
+
+ user.update_attributes(external: true)
+ end
+
+ it 'ensures correct rights and limits for user' do
+ expect { user.update_attributes(external: true) }.to change { user.can_create_group }.to(false)
+ .and change { user.projects_limit }.to(0)
+ end
+ end
+ end
+
+ describe 'rss token' do
+ it 'ensures an rss token on read' do
+ user = create(:user, rss_token: nil)
+ rss_token = user.rss_token
+ expect(rss_token).not_to be_blank
+ expect(user.reload.rss_token).to eq rss_token
+ end
+ end
+
describe '#recently_sent_password_reset?' do
it 'is false when reset_password_sent_at is nil' do
user = build_stubbed(:user, reset_password_sent_at: nil)
@@ -572,21 +665,11 @@ describe User, models: true do
it { expect(User.without_projects).to include user_without_project2 }
end
- describe '.not_in_project' do
- before do
- User.delete_all
- @user = create :user
- @project = create(:empty_project)
- end
-
- it { expect(User.not_in_project(@project)).to include(@user, @project.owner) }
- end
-
describe 'user creation' 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 }
@@ -637,7 +720,7 @@ describe User, models: true do
protocol_and_expectation = {
'http' => false,
'ssh' => true,
- '' => true,
+ '' => true
}
protocol_and_expectation.each do |protocol, expected|
@@ -671,50 +754,60 @@ describe User, models: true do
end
describe '.search' do
- let(:user) { create(:user) }
+ let!(:user) { create(:user, name: 'user', username: 'usern', email: 'email@gmail.com') }
+ let!(:user2) { create(:user, name: 'user name', username: 'username', email: 'someemail@gmail.com') }
- it 'returns users with a matching name' do
- expect(described_class.search(user.name)).to eq([user])
- end
+ describe 'name matching' do
+ it 'returns users with a matching name with exact match first' do
+ expect(described_class.search(user.name)).to eq([user, user2])
+ end
- it 'returns users with a partially matching name' do
- expect(described_class.search(user.name[0..2])).to eq([user])
- end
+ it 'returns users with a partially matching name' do
+ expect(described_class.search(user.name[0..2])).to eq([user, user2])
+ end
- it 'returns users with a matching name regardless of the casing' do
- expect(described_class.search(user.name.upcase)).to eq([user])
+ it 'returns users with a matching name regardless of the casing' do
+ expect(described_class.search(user2.name.upcase)).to eq([user2])
+ end
end
- it 'returns users with a matching Email' do
- expect(described_class.search(user.email)).to eq([user])
- end
+ describe 'email matching' do
+ it 'returns users with a matching Email' do
+ expect(described_class.search(user.email)).to eq([user, user2])
+ end
- it 'returns users with a partially matching Email' do
- expect(described_class.search(user.email[0..2])).to eq([user])
- end
+ it 'returns users with a partially matching Email' do
+ expect(described_class.search(user.email[0..2])).to eq([user, user2])
+ end
- it 'returns users with a matching Email regardless of the casing' do
- expect(described_class.search(user.email.upcase)).to eq([user])
+ it 'returns users with a matching Email regardless of the casing' do
+ expect(described_class.search(user2.email.upcase)).to eq([user2])
+ end
end
- it 'returns users with a matching username' do
- expect(described_class.search(user.username)).to eq([user])
- end
+ describe 'username matching' do
+ it 'returns users with a matching username' do
+ expect(described_class.search(user.username)).to eq([user, user2])
+ end
- it 'returns users with a partially matching username' do
- expect(described_class.search(user.username[0..2])).to eq([user])
- end
+ it 'returns users with a partially matching username' do
+ expect(described_class.search(user.username[0..2])).to eq([user, user2])
+ end
- it 'returns users with a matching username regardless of the casing' do
- expect(described_class.search(user.username.upcase)).to eq([user])
+ it 'returns users with a matching username regardless of the casing' do
+ expect(described_class.search(user2.username.upcase)).to eq([user2])
+ end
end
end
describe '.search_with_secondary_emails' do
delegate :search_with_secondary_emails, to: :described_class
- let!(:user) { create(:user) }
- let!(:email) { create(:email) }
+ let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) }
+ let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) }
+ let!(:email) do
+ create(:email, user: another_user, email: 'alias@example.com')
+ end
it 'returns users with a matching name' do
expect(search_with_secondary_emails(user.name)).to eq([user])
@@ -826,8 +919,8 @@ describe User, models: true do
describe '.find_by_username!' do
it 'raises RecordNotFound' do
- expect { described_class.find_by_username!('JohnDoe') }.
- to raise_error(ActiveRecord::RecordNotFound)
+ expect { described_class.find_by_username!('JohnDoe') }
+ .to raise_error(ActiveRecord::RecordNotFound)
end
it 'is case-insensitive' do
@@ -836,6 +929,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) }
@@ -861,6 +1023,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/-/system/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 }
@@ -979,6 +1159,18 @@ describe User, models: true do
end
end
+ describe '#sanitize_attrs' do
+ let(:user) { build(:user, name: 'test & user', skype: 'test&user') }
+
+ it 'encodes HTML entities in the Skype attribute' do
+ expect { user.sanitize_attrs }.to change { user.skype }.to('test&amp;user')
+ end
+
+ it 'does not encode HTML entities in the name attribute' do
+ expect { user.sanitize_attrs }.not_to change { user.name }
+ end
+ end
+
describe '#starred?' do
it 'determines if user starred a project' do
user = create :user
@@ -1361,25 +1553,6 @@ describe User, models: true do
end
end
- describe '#viewable_starred_projects' do
- let(:user) { create(:user) }
- let(:public_project) { create(:empty_project, :public) }
- let(:private_project) { create(:empty_project, :private) }
- let(:private_viewable_project) { create(:empty_project, :private) }
-
- before do
- private_viewable_project.team << [user, Gitlab::Access::MASTER]
-
- [public_project, private_project, private_viewable_project].each do |project|
- user.toggle_star(project)
- end
- end
-
- it 'returns only starred projects the user can view' do
- expect(user.viewable_starred_projects).not_to include(private_project)
- end
- end
-
describe '#projects_with_reporter_access_limited_to' do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
@@ -1403,8 +1576,8 @@ describe User, models: true do
end
it 'returns the projects when using an ActiveRecord relation' do
- projects = user.
- projects_with_reporter_access_limited_to(Project.select(:id))
+ projects = user
+ .projects_with_reporter_access_limited_to(Project.select(:id))
expect(projects).to eq([project1])
end
@@ -1416,40 +1589,114 @@ describe User, models: true do
end
end
- describe '#nested_groups' do
+ describe '#all_expanded_groups' do
+ # foo/bar would also match foo/barbaz instead of just foo/bar and foo/bar/baz
let!(:user) { create(:user) }
- let!(:group) { create(:group) }
- let!(:nested_group) { create(:group, parent: group) }
- before do
- group.add_owner(user)
+ # group
+ # _______ (foo) _______
+ # | |
+ # | |
+ # nested_group_1 nested_group_2
+ # (bar) (barbaz)
+ # | |
+ # | |
+ # nested_group_1_1 nested_group_2_1
+ # (baz) (baz)
+ #
+ let!(:group) { create :group }
+ 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' }
+
+ subject { user.all_expanded_groups }
+
+ context 'user is not a member of any group' do
+ 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
- # Add more data to ensure method does not include wrong groups
- create(:group).add_owner(create(:user))
+ 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
- it { expect(user.nested_groups).to eq([nested_group]) }
- end
+ context 'user is member of the top group' do
+ before do
+ group.add_owner(user)
+ end
- describe '#nested_groups_projects' do
- let!(:user) { create(:user) }
- let!(:group) { create(:group) }
- let!(:nested_group) { create(:group, parent: group) }
- let!(:project) { create(:empty_project, namespace: group) }
- let!(:nested_project) { create(:empty_project, namespace: nested_group) }
+ if Group.supports_nested_groups?
+ 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
+ else
+ it 'returns the top-level groups' do
+ is_expected.to match_array [group]
+ end
+ end
+ end
- before do
- group.add_owner(user)
+ context 'user is member of the first child (internal node), branch 1', :nested_groups do
+ before do
+ nested_group_1.add_owner(user)
+ end
+
+ 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', :nested_groups do
+ before do
+ nested_group_2.add_owner(user)
+ end
- # Add more data to ensure method does not include wrong projects
- other_project = create(:empty_project, namespace: create(:group, :nested))
- other_project.add_developer(create(:user))
+ it 'returns the groups in the hierarchy' do
+ is_expected.to match_array [
+ group,
+ nested_group_2, nested_group_2_1
+ ]
+ end
end
- it { expect(user.nested_groups_projects).to eq([nested_project]) }
+ context 'user is member of the last child (leaf node)', :nested_groups do
+ before do
+ nested_group_1_1.add_owner(user)
+ end
+
+ 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 '#refresh_authorized_projects', redis: true do
+ describe '#refresh_authorized_projects', clean_gitlab_redis_shared_state: true do
let(:project1) { create(:empty_project) }
let(:project2) { create(:empty_project) }
let(:user) { create(:user) }
@@ -1466,10 +1713,6 @@ describe User, models: true do
expect(user.project_authorizations.count).to eq(2)
end
- it 'sets the authorized_projects_populated column' do
- expect(user.authorized_projects_populated).to eq(true)
- end
-
it 'stores the correct access levels' do
expect(user.project_authorizations.where(access_level: Gitlab::Access::GUEST).exists?).to eq(true)
expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true)
@@ -1509,6 +1752,20 @@ describe User, models: true do
end
end
+ describe '#full_private_access?' do
+ it 'returns false for regular user' do
+ user = build(:user)
+
+ expect(user.full_private_access?).to be_falsy
+ end
+
+ it 'returns true for admin user' do
+ user = build(:user, :admin)
+
+ expect(user.full_private_access?).to be_truthy
+ end
+ end
+
describe '.ghost' do
it "creates a ghost user if one isn't already present" do
ghost = User.ghost
@@ -1544,5 +1801,157 @@ 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', :nested_groups 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', :nested_groups 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
+
+ describe '#allow_password_authentication?' do
+ context 'regular user' do
+ let(:user) { build(:user) }
+
+ it 'returns true when sign-in is enabled' do
+ expect(user.allow_password_authentication?).to be_truthy
+ end
+
+ it 'returns false when sign-in is disabled' do
+ stub_application_setting(password_authentication_enabled: false)
+
+ expect(user.allow_password_authentication?).to be_falsey
+ end
+ end
+
+ it 'returns false for ldap user' do
+ user = create(:omniauth_user, provider: 'ldapmain')
+
+ expect(user.allow_password_authentication?).to be_falsey
+ end
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index d76d94c3186..4f462044532 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -61,8 +61,8 @@ describe WikiPage, models: true do
actual_order =
grouped_entries.map do |page_or_dir|
get_slugs(page_or_dir)
- end.
- flatten
+ end
+ .flatten
expect(actual_order).to eq(expected_order)
end
end