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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 17:34:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 17:34:42 +0300
commit9f46488805e86b1bc341ea1620b866016c2ce5ed (patch)
treef9748c7e287041e37d6da49e0a29c9511dc34768 /spec/models
parentdfc92d081ea0332d69c8aca2f0e745cb48ae5e6d (diff)
Add latest changes from gitlab-org/gitlab@13-0-stable-ee
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/ability_spec.rb46
-rw-r--r--spec/models/alert_management/alert_spec.rb320
-rw-r--r--spec/models/application_setting_spec.rb14
-rw-r--r--spec/models/blob_spec.rb512
-rw-r--r--spec/models/blob_viewer/readme_spec.rb2
-rw-r--r--spec/models/broadcast_message_spec.rb18
-rw-r--r--spec/models/ci/build_spec.rb353
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb (renamed from spec/models/ci/daily_report_result_spec.rb)25
-rw-r--r--spec/models/ci/freeze_period_spec.rb50
-rw-r--r--spec/models/ci/freeze_period_status_spec.rb62
-rw-r--r--spec/models/ci/instance_variable_spec.rb93
-rw-r--r--spec/models/ci/job_artifact_spec.rb120
-rw-r--r--spec/models/ci/persistent_ref_spec.rb12
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb8
-rw-r--r--spec/models/ci/pipeline_spec.rb145
-rw-r--r--spec/models/ci/processable_spec.rb159
-rw-r--r--spec/models/ci/runner_spec.rb8
-rw-r--r--spec/models/ci/stage_spec.rb26
-rw-r--r--spec/models/clusters/applications/elastic_stack_spec.rb70
-rw-r--r--spec/models/clusters/applications/fluentd_spec.rb36
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb6
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb4
-rw-r--r--spec/models/clusters/cluster_spec.rb98
-rw-r--r--spec/models/commit_status_spec.rb44
-rw-r--r--spec/models/concerns/awardable_spec.rb41
-rw-r--r--spec/models/concerns/blocks_json_serialization_spec.rb7
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb52
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb4
-rw-r--r--spec/models/concerns/has_user_type_spec.rb86
-rw-r--r--spec/models/concerns/mentionable_spec.rb52
-rw-r--r--spec/models/concerns/noteable_spec.rb2
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb100
-rw-r--r--spec/models/concerns/redis_cacheable_spec.rb6
-rw-r--r--spec/models/concerns/spammable_spec.rb91
-rw-r--r--spec/models/container_repository_spec.rb12
-rw-r--r--spec/models/cycle_analytics/code_spec.rb2
-rw-r--r--spec/models/cycle_analytics/group_level_spec.rb44
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb2
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb2
-rw-r--r--spec/models/cycle_analytics/production_spec.rb2
-rw-r--r--spec/models/cycle_analytics/project_level_spec.rb2
-rw-r--r--spec/models/cycle_analytics/review_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/deploy_token_spec.rb4
-rw-r--r--spec/models/design_management/action_spec.rb105
-rw-r--r--spec/models/design_management/design_action_spec.rb98
-rw-r--r--spec/models/design_management/design_at_version_spec.rb426
-rw-r--r--spec/models/design_management/design_collection_spec.rb82
-rw-r--r--spec/models/design_management/design_spec.rb575
-rw-r--r--spec/models/design_management/repository_spec.rb58
-rw-r--r--spec/models/design_management/version_spec.rb342
-rw-r--r--spec/models/design_user_mention_spec.rb12
-rw-r--r--spec/models/diff_note_spec.rb18
-rw-r--r--spec/models/email_spec.rb20
-rw-r--r--spec/models/environment_spec.rb21
-rw-r--r--spec/models/event_spec.rb171
-rw-r--r--spec/models/group_spec.rb168
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/issue_spec.rb132
-rw-r--r--spec/models/iteration_spec.rb170
-rw-r--r--spec/models/jira_import_state_spec.rb1
-rw-r--r--spec/models/member_spec.rb22
-rw-r--r--spec/models/merge_request_diff_spec.rb59
-rw-r--r--spec/models/merge_request_spec.rb168
-rw-r--r--spec/models/metrics/users_starred_dashboard_spec.rb39
-rw-r--r--spec/models/milestone_note_spec.rb10
-rw-r--r--spec/models/milestone_spec.rb163
-rw-r--r--spec/models/namespace/root_storage_size_spec.rb67
-rw-r--r--spec/models/note_spec.rb40
-rw-r--r--spec/models/pages_domain_spec.rb6
-rw-r--r--spec/models/performance_monitoring/prometheus_dashboard_spec.rb102
-rw-r--r--spec/models/performance_monitoring/prometheus_metric_spec.rb59
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_group_spec.rb54
-rw-r--r--spec/models/performance_monitoring/prometheus_panel_spec.rb77
-rw-r--r--spec/models/personal_access_token_spec.rb23
-rw-r--r--spec/models/personal_snippet_spec.rb1
-rw-r--r--spec/models/plan_limits_spec.rb74
-rw-r--r--spec/models/plan_spec.rb17
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb12
-rw-r--r--spec/models/project_feature_spec.rb74
-rw-r--r--spec/models/project_repository_storage_move_spec.rb63
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb605
-rw-r--r--spec/models/project_services/irker_service_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb93
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb7
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb2
-rw-r--r--spec/models/project_services/webex_teams_service_spec.rb10
-rw-r--r--spec/models/project_snippet_spec.rb1
-rw-r--r--spec/models/project_spec.rb279
-rw-r--r--spec/models/project_wiki_spec.rb453
-rw-r--r--spec/models/release_spec.rb52
-rw-r--r--spec/models/remote_mirror_spec.rb52
-rw-r--r--spec/models/repository_spec.rb76
-rw-r--r--spec/models/resource_label_event_spec.rb3
-rw-r--r--spec/models/resource_milestone_event_spec.rb17
-rw-r--r--spec/models/resource_state_event_spec.rb14
-rw-r--r--spec/models/sent_notification_spec.rb22
-rw-r--r--spec/models/service_spec.rb32
-rw-r--r--spec/models/snippet_repository_spec.rb32
-rw-r--r--spec/models/snippet_spec.rb54
-rw-r--r--spec/models/spam_log_spec.rb27
-rw-r--r--spec/models/state_note_spec.rb29
-rw-r--r--spec/models/timelog_spec.rb6
-rw-r--r--spec/models/todo_spec.rb32
-rw-r--r--spec/models/tree_spec.rb21
-rw-r--r--spec/models/user_spec.rb165
-rw-r--r--spec/models/user_type_enums_spec.rb13
-rw-r--r--spec/models/wiki_page/meta_spec.rb87
-rw-r--r--spec/models/wiki_page_spec.rb161
-rw-r--r--spec/models/x509_commit_signature_spec.rb32
111 files changed, 6216 insertions, 2444 deletions
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 2bf971f553f..9ef77da6f43 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -74,13 +74,20 @@ describe Ability do
context 'using a private project' do
let(:project) { create(:project, :private) }
- it 'returns users that are administrators' do
+ it 'returns users that are administrators when admin mode is enabled', :enable_admin_mode do
user = build(:user, admin: true)
expect(described_class.users_that_can_read_project([user], project))
.to eq([user])
end
+ it 'does not return users that are administrators when admin mode is disabled' do
+ user = build(:user, admin: true)
+
+ expect(described_class.users_that_can_read_project([user], project))
+ .to eq([])
+ end
+
it 'returns external users if they are the project owner' do
user1 = build(:user, external: true)
user2 = build(:user, external: true)
@@ -145,7 +152,7 @@ describe Ability do
end
describe '.merge_requests_readable_by_user' do
- context 'with an admin' do
+ context 'with an admin when admin mode is enabled', :enable_admin_mode do
it 'returns all merge requests' do
user = build(:user, admin: true)
merge_request = build(:merge_request)
@@ -155,6 +162,19 @@ describe Ability do
end
end
+ context 'with an admin when admin mode is disabled' do
+ it 'returns merge_requests that are publicly visible' do
+ user = build(:user, admin: true)
+ hidden_merge_request = build(:merge_request)
+ visible_merge_request = build(:merge_request, source_project: build(:project, :public))
+
+ merge_requests = described_class
+ .merge_requests_readable_by_user([hidden_merge_request, visible_merge_request], user)
+
+ expect(merge_requests).to eq([visible_merge_request])
+ end
+ end
+
context 'without a user' do
it 'returns merge_requests that are publicly visible' do
hidden_merge_request = build(:merge_request)
@@ -217,7 +237,7 @@ describe Ability do
end
describe '.issues_readable_by_user' do
- context 'with an admin user' do
+ context 'with an admin when admin mode is enabled', :enable_admin_mode do
it 'returns all given issues' do
user = build(:user, admin: true)
issue = build(:issue)
@@ -227,6 +247,26 @@ describe Ability do
end
end
+ context 'with an admin when admin mode is disabled' do
+ it 'returns the issues readable by the admin' do
+ user = build(:user, admin: true)
+ issue = build(:issue)
+
+ expect(issue).to receive(:readable_by?).with(user).and_return(true)
+
+ expect(described_class.issues_readable_by_user([issue], user))
+ .to eq([issue])
+ end
+
+ it 'returns no issues when not given access' do
+ user = build(:user, admin: true)
+ issue = build(:issue)
+
+ expect(described_class.issues_readable_by_user([issue], user))
+ .to be_empty
+ end
+ end
+
context 'with a regular user' do
it 'returns the issues readable by the user' do
user = build(:user)
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
new file mode 100644
index 00000000000..1da0c6d4071
--- /dev/null
+++ b/spec/models/alert_management/alert_spec.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AlertManagement::Alert do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:issue) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:title) }
+ it { is_expected.to validate_presence_of(:events) }
+ it { is_expected.to validate_presence_of(:severity) }
+ it { is_expected.to validate_presence_of(:status) }
+ it { is_expected.to validate_presence_of(:started_at) }
+
+ it { is_expected.to validate_length_of(:title).is_at_most(200) }
+ it { is_expected.to validate_length_of(:description).is_at_most(1000) }
+ it { is_expected.to validate_length_of(:service).is_at_most(100) }
+ it { is_expected.to validate_length_of(:monitoring_tool).is_at_most(100) }
+
+ context 'when status is triggered' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, ended_at: Time.current) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'when status is acknowledged' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert, :acknowledged) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, :acknowledged, ended_at: Time.current) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'when status is resolved' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert, :resolved, ended_at: nil) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, :resolved, ended_at: Time.current) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ context 'when status is ignored' do
+ context 'when ended_at is blank' do
+ subject { build(:alert_management_alert, :ignored) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when ended_at is present' do
+ subject { build(:alert_management_alert, :ignored, ended_at: Time.current) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ describe 'fingerprint' do
+ let_it_be(:fingerprint) { 'fingerprint' }
+ let_it_be(:existing_alert) { create(:alert_management_alert, fingerprint: fingerprint) }
+ let(:new_alert) { build(:alert_management_alert, fingerprint: fingerprint, project: project) }
+
+ subject { new_alert }
+
+ context 'adding an alert with the same fingerprint' do
+ context 'same project' do
+ let(:project) { existing_alert.project }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'different project' do
+ let(:project) { create(:project) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+
+ describe 'hosts' do
+ subject(:alert) { build(:alert_management_alert, hosts: hosts) }
+
+ context 'over 255 total chars' do
+ let(:hosts) { ['111.111.111.111'] * 18 }
+
+ it { is_expected.not_to be_valid }
+ end
+
+ context 'under 255 chars' do
+ let(:hosts) { ['111.111.111.111'] * 17 }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+
+ describe 'enums' do
+ let(:severity_values) do
+ { critical: 0, high: 1, medium: 2, low: 3, info: 4, unknown: 5 }
+ end
+
+ it { is_expected.to define_enum_for(:severity).with_values(severity_values) }
+ end
+
+ describe 'scopes' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:triggered_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:ignored_alert) { create(:alert_management_alert, :ignored, project: project) }
+
+ describe '.for_iid' do
+ subject { AlertManagement::Alert.for_iid(triggered_alert.iid) }
+
+ it { is_expected.to match_array(triggered_alert) }
+ end
+
+ describe '.for_status' do
+ let(:status) { AlertManagement::Alert::STATUSES[:resolved] }
+
+ subject { AlertManagement::Alert.for_status(status) }
+
+ it { is_expected.to match_array(resolved_alert) }
+
+ context 'with multiple statuses' do
+ let(:status) { AlertManagement::Alert::STATUSES.values_at(:resolved, :ignored) }
+
+ it { is_expected.to match_array([resolved_alert, ignored_alert]) }
+ end
+ end
+
+ describe '.for_fingerprint' do
+ let_it_be(:fingerprint) { SecureRandom.hex }
+ let_it_be(:alert_with_fingerprint) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
+ let_it_be(:unrelated_alert_with_finger_print) { create(:alert_management_alert, fingerprint: fingerprint) }
+
+ subject { described_class.for_fingerprint(project, fingerprint) }
+
+ it { is_expected.to contain_exactly(alert_with_fingerprint) }
+ end
+
+ describe '.counts_by_status' do
+ subject { described_class.counts_by_status }
+
+ it do
+ is_expected.to eq(
+ triggered_alert.status => 1,
+ resolved_alert.status => 1,
+ ignored_alert.status => 1
+ )
+ end
+ end
+ end
+
+ describe '.search' do
+ let_it_be(:alert) do
+ create(:alert_management_alert,
+ title: 'Title',
+ description: 'Desc',
+ service: 'Service',
+ monitoring_tool: 'Monitor'
+ )
+ end
+
+ subject { AlertManagement::Alert.search(query) }
+
+ context 'does not contain search string' do
+ let(:query) { 'something else' }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'title includes query' do
+ let(:query) { alert.title.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+
+ context 'description includes query' do
+ let(:query) { alert.description.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+
+ context 'service includes query' do
+ let(:query) { alert.service.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+
+ context 'monitoring tool includes query' do
+ let(:query) { alert.monitoring_tool.upcase }
+
+ it { is_expected.to contain_exactly(alert) }
+ end
+ end
+
+ describe '#details' do
+ let(:payload) do
+ {
+ 'title' => 'Details title',
+ 'custom' => {
+ 'alert' => {
+ 'fields' => %w[one two]
+ }
+ },
+ 'yet' => {
+ 'another' => 'field'
+ }
+ }
+ end
+ let(:alert) { build(:alert_management_alert, title: 'Details title', payload: payload) }
+
+ subject { alert.details }
+
+ it 'renders the payload as inline hash' do
+ is_expected.to eq(
+ 'custom.alert.fields' => %w[one two],
+ 'yet.another' => 'field'
+ )
+ end
+ end
+
+ describe '#trigger' do
+ subject { alert.trigger }
+
+ context 'when alert is in triggered state' do
+ let(:alert) { create(:alert_management_alert) }
+
+ it 'does not change the alert status' do
+ expect { subject }.not_to change { alert.reload.status }
+ end
+ end
+
+ context 'when alert not in triggered state' do
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'changes the alert status to triggered' do
+ expect { subject }.to change { alert.triggered? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { alert.reload.ended_at }.to nil
+ end
+ end
+ end
+
+ describe '#acknowledge' do
+ subject { alert.acknowledge }
+
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'changes the alert status to acknowledged' do
+ expect { subject }.to change { alert.acknowledged? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { alert.reload.ended_at }.to nil
+ end
+ end
+
+ describe '#resolve' do
+ let!(:ended_at) { Time.current }
+
+ subject do
+ alert.ended_at = ended_at
+ alert.resolve
+ end
+
+ context 'when alert already resolved' do
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'does not change the alert status' do
+ expect { subject }.not_to change { alert.reload.status }
+ end
+ end
+
+ context 'when alert is not resolved' do
+ let(:alert) { create(:alert_management_alert) }
+
+ it 'changes alert status to "resolved"' do
+ expect { subject }.to change { alert.resolved? }.to(true)
+ end
+ end
+ end
+
+ describe '#ignore' do
+ subject { alert.ignore }
+
+ let(:alert) { create(:alert_management_alert, :resolved) }
+
+ it 'changes the alert status to ignored' do
+ expect { subject }.to change { alert.ignored? }.to(true)
+ end
+
+ it 'resets ended at' do
+ expect { subject }.to change { alert.reload.ended_at }.to nil
+ end
+ end
+end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 523e17f82c1..64308af38f9 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -91,6 +91,20 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value(nil).for(:namespace_storage_size_limit) }
it { is_expected.not_to allow_value(-1).for(:namespace_storage_size_limit) }
+ it { is_expected.to allow_value(300).for(:issues_create_limit) }
+ it { is_expected.not_to allow_value('three').for(:issues_create_limit) }
+ it { is_expected.not_to allow_value(nil).for(:issues_create_limit) }
+ it { is_expected.not_to allow_value(10.5).for(:issues_create_limit) }
+ it { is_expected.not_to allow_value(-1).for(:issues_create_limit) }
+
+ it { is_expected.to allow_value(0).for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value('abc').for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value(nil).for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value(10.5).for(:raw_blob_request_limit) }
+ it { is_expected.not_to allow_value(-1).for(:raw_blob_request_limit) }
+
+ it { is_expected.not_to allow_value(false).for(:hashed_storage_enabled) }
+
context 'grafana_url validations' do
before do
subject.instance_variable_set(:@parsed_grafana_url, nil)
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index a0193b29bb3..c2d6406c3fb 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -5,12 +5,17 @@ require 'spec_helper'
describe Blob do
include FakeBlobHelpers
- let(:project) { build(:project, lfs_enabled: true) }
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project) { build(:project) }
let(:personal_snippet) { build(:personal_snippet) }
let(:project_snippet) { build(:project_snippet, project: project) }
+ let(:repository) { project.repository }
+ let(:lfs_enabled) { true }
+
before do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ allow(repository).to receive(:lfs_enabled?) { lfs_enabled }
end
describe '.decorate' do
@@ -27,7 +32,7 @@ describe Blob do
it 'does not fetch blobs when none are accessed' do
expect(container.repository).not_to receive(:blobs_at)
- described_class.lazy(container, commit_id, 'CHANGELOG')
+ described_class.lazy(container.repository, commit_id, 'CHANGELOG')
end
it 'fetches all blobs for the same repository when one is accessed' do
@@ -36,10 +41,10 @@ describe Blob do
.once.and_call_original
expect(other_container.repository).not_to receive(:blobs_at)
- changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
- contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
+ changelog = described_class.lazy(container.repository, commit_id, 'CHANGELOG')
+ contributing = described_class.lazy(same_container.repository, commit_id, 'CONTRIBUTING.md')
- described_class.lazy(other_container, commit_id, 'CHANGELOG')
+ described_class.lazy(other_container.repository, commit_id, 'CHANGELOG')
# Access property so the values are loaded
changelog.id
@@ -47,14 +52,14 @@ describe Blob do
end
it 'does not include blobs from previous requests in later requests' do
- changelog = described_class.lazy(container, commit_id, 'CHANGELOG')
- contributing = described_class.lazy(same_container, commit_id, 'CONTRIBUTING.md')
+ changelog = described_class.lazy(container.repository, commit_id, 'CHANGELOG')
+ contributing = described_class.lazy(same_container.repository, commit_id, 'CONTRIBUTING.md')
# Access property so the values are loaded
changelog.id
contributing.id
- readme = described_class.lazy(container, commit_id, 'README.md')
+ readme = described_class.lazy(container.repository, commit_id, 'README.md')
expect(container.repository).to receive(:blobs_at)
.with([[commit_id, 'README.md']], blob_size_limit: blob_size_limit).once.and_call_original
@@ -128,399 +133,84 @@ describe Blob do
end
describe '#external_storage_error?' do
- shared_examples 'no error' do
- it do
- expect(blob.external_storage_error?).to be_falsey
- end
- end
-
- shared_examples 'returns error' do
- it do
- expect(blob.external_storage_error?).to be_truthy
- end
- end
+ subject { blob.external_storage_error? }
context 'if the blob is stored in LFS' do
- let(:blob) { fake_blob(path: 'file.pdf', lfs: true, container: container) }
-
- context 'when the project has LFS enabled' do
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'no error'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns error'
- end
+ let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
- context 'with project snippet' do
- let(:container) { project_snippet }
+ context 'when LFS is enabled' do
+ let(:lfs_enabled) { true }
- it_behaves_like 'no error'
- end
+ it { is_expected.to be_falsy }
end
- context 'when the project does not have LFS enabled' do
- before do
- project.lfs_enabled = false
- end
-
- context 'with project' do
- let(:container) { project }
+ context 'when LFS is not enabled' do
+ let(:lfs_enabled) { false }
- it_behaves_like 'returns error'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns error'
- end
+ it { is_expected.to be_truthy }
end
end
context 'if the blob is not stored in LFS' do
- let(:blob) { fake_blob(path: 'file.md', container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'no error'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'no error'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ let(:blob) { fake_blob(path: 'file.md') }
- it_behaves_like 'no error'
- end
+ it { is_expected.to be_falsy }
end
end
describe '#stored_externally?' do
+ subject { blob.stored_externally? }
+
context 'if the blob is stored in LFS' do
let(:blob) { fake_blob(path: 'file.pdf', lfs: true) }
- shared_examples 'returns true' do
- it do
- expect(blob.stored_externally?).to be_truthy
- end
- end
-
- shared_examples 'returns false' do
- it do
- expect(blob.stored_externally?).to be_falsey
- end
- end
-
- context 'when the project has LFS enabled' do
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
+ context 'when LFS is enabled' do
+ let(:lfs_enabled) { true }
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
+ it { is_expected.to be_truthy }
end
- context 'when the project does not have LFS enabled' do
- before do
- project.lfs_enabled = false
- end
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
+ context 'when LFS is not enabled' do
+ let(:lfs_enabled) { false }
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
+ it { is_expected.to be_falsy }
end
end
context 'if the blob is not stored in LFS' do
let(:blob) { fake_blob(path: 'file.md') }
- it 'returns false' do
- expect(blob.stored_externally?).to be_falsey
- end
+ it { is_expected.to be_falsy }
end
end
describe '#binary?' do
- shared_examples 'returns true' do
- it do
- expect(blob.binary?).to be_truthy
- end
- end
-
- shared_examples 'returns false' do
- it do
- expect(blob.binary?).to be_falsey
- end
- end
-
- context 'if the blob is stored externally' do
- let(:blob) { fake_blob(path: file, lfs: true) }
-
- context 'if the extension has a rich viewer' do
- context 'if the viewer is binary' do
- let(:file) { 'file.pdf' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
-
- context 'if the viewer is text-based' do
- let(:file) { 'file.md' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
+ context 'an lfs object' do
+ where(:filename, :is_binary) do
+ 'file.pdf' | true
+ 'file.md' | false
+ 'file.txt' | false
+ 'file.ics' | false
+ 'file.rb' | false
+ 'file.exe' | true
+ 'file.ini' | false
+ 'file.wtf' | true
end
- context "if the extension doesn't have a rich viewer" do
- context 'if the extension has a text mime type' do
- context 'if the extension is for a programming language' do
- let(:file) { 'file.txt' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
-
- context 'if the extension is not for a programming language' do
- let(:file) { 'file.ics' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ with_them do
+ let(:blob) { fake_blob(path: filename, lfs: true, container: project) }
- it_behaves_like 'returns false'
- end
- end
- end
-
- context 'if the extension has a binary mime type' do
- context 'if the extension is for a programming language' do
- let(:file) { 'file.rb' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
-
- context 'if the extension is not for a programming language' do
- let(:file) { 'file.exe' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
- end
-
- context 'if the extension has an unknown mime type' do
- context 'if the extension is for a programming language' do
- let(:file) { 'file.ini' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns false'
- end
- end
-
- context 'if the extension is not for a programming language' do
- let(:file) { 'file.wtf' }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
- end
+ it { expect(blob.binary?).to eq(is_binary) }
end
end
- context 'if the blob is not stored externally' do
- context 'if the blob is binary' do
- let(:blob) { fake_blob(path: 'file.pdf', binary: true, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
- end
-
- context 'if the blob is text-based' do
- let(:blob) { fake_blob(path: 'file.md', container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
+ context 'a non-lfs object' do
+ let(:blob) { fake_blob(path: 'anything', container: project) }
- context 'with project snippet' do
- let(:container) { project_snippet }
+ it 'delegates to binary_in_repo?' do
+ expect(blob).to receive(:binary_in_repo?) { :result }
- it_behaves_like 'returns false'
- end
+ expect(blob.binary?).to eq(:result)
end
end
end
@@ -569,9 +259,7 @@ describe Blob do
describe '#rich_viewer' do
context 'when the blob has an external storage error' do
- before do
- project.lfs_enabled = false
- end
+ let(:lfs_enabled) { false }
it 'returns nil' do
blob = fake_blob(path: 'file.pdf', lfs: true)
@@ -631,9 +319,7 @@ describe Blob do
describe '#auxiliary_viewer' do
context 'when the blob has an external storage error' do
- before do
- project.lfs_enabled = false
- end
+ let(:lfs_enabled) { false }
it 'returns nil' do
blob = fake_blob(path: 'LICENSE', lfs: true)
@@ -676,63 +362,21 @@ describe Blob do
end
describe '#rendered_as_text?' do
- shared_examples 'returns true' do
- it do
- expect(blob.rendered_as_text?(ignore_errors: ignore_errors)).to be_truthy
- end
- end
-
- shared_examples 'returns false' do
- it do
- expect(blob.rendered_as_text?(ignore_errors: ignore_errors)).to be_falsey
- end
- end
+ subject { blob.rendered_as_text?(ignore_errors: ignore_errors) }
context 'when ignoring errors' do
let(:ignore_errors) { true }
context 'when the simple viewer is text-based' do
- let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
+ let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes) }
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
+ it { is_expected.to be_truthy }
end
context 'when the simple viewer is binary' do
- let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ let(:blob) { fake_blob(path: 'file.pdf', binary: true, size: 100.megabytes) }
- it_behaves_like 'returns false'
- end
+ it { is_expected.to be_falsy }
end
end
@@ -740,47 +384,15 @@ describe Blob do
let(:ignore_errors) { false }
context 'when the viewer has render errors' do
- let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes, container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns false'
- end
-
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns false'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
+ let(:blob) { fake_blob(path: 'file.md', size: 100.megabytes) }
- it_behaves_like 'returns false'
- end
+ it { is_expected.to be_falsy }
end
context "when the viewer doesn't have render errors" do
- let(:blob) { fake_blob(path: 'file.md', container: container) }
-
- context 'with project' do
- let(:container) { project }
-
- it_behaves_like 'returns true'
- end
+ let(:blob) { fake_blob(path: 'file.md') }
- context 'with personal snippet' do
- let(:container) { personal_snippet }
-
- it_behaves_like 'returns true'
- end
-
- context 'with project snippet' do
- let(:container) { project_snippet }
-
- it_behaves_like 'returns true'
- end
+ it { is_expected.to be_truthy }
end
end
end
diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
index 6586adbc373..89bc5be94fb 100644
--- a/spec/models/blob_viewer/readme_spec.rb
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -40,7 +40,7 @@ describe BlobViewer::Readme do
context 'when the wiki is not empty' do
before do
- create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: 'Home page' })
+ create(:wiki_page, wiki: project.wiki, title: 'home', content: 'Home page')
end
it 'returns nil' do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 6cef81d6e44..127faa5e8e2 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -143,6 +143,24 @@ describe BroadcastMessage do
expect(subject.call('/group/groupname/issues').length).to eq(0)
end
+
+ it 'does not return message if target path has no wild card at the end' do
+ create(:broadcast_message, target_path: "*/issues", broadcast_type: broadcast_type)
+
+ expect(subject.call('/group/issues/test').length).to eq(0)
+ end
+
+ it 'does not return message if target path has wild card at the end' do
+ create(:broadcast_message, target_path: "/issues/*", broadcast_type: broadcast_type)
+
+ expect(subject.call('/group/issues/test').length).to eq(0)
+ end
+
+ it 'does return message if target path has wild card at the beginning and the end' do
+ create(:broadcast_message, target_path: "*/issues/*", broadcast_type: broadcast_type)
+
+ expect(subject.call('/group/issues/test').length).to eq(1)
+ end
end
describe '.current', :use_clean_rails_memory_store_caching do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index a4f3fa518c6..6605866d9c0 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -106,10 +106,14 @@ describe Ci::Build do
end
end
- describe '.with_artifacts_archive' do
- subject { described_class.with_artifacts_archive }
+ describe '.with_downloadable_artifacts' do
+ subject { described_class.with_downloadable_artifacts }
- context 'when job does not have an archive' do
+ before do
+ stub_feature_flags(drop_license_management_artifact: false)
+ end
+
+ context 'when job does not have a downloadable artifact' do
let!(:job) { create(:ci_build) }
it 'does not return the job' do
@@ -117,15 +121,23 @@ describe Ci::Build do
end
end
- context 'when job has a job artifact archive' do
- let!(:job) { create(:ci_build, :artifacts) }
+ ::Ci::JobArtifact::DOWNLOADABLE_TYPES.each do |type|
+ context "when job has a #{type} artifact" do
+ it 'returns the job' do
+ job = create(:ci_build)
+ create(
+ :ci_job_artifact,
+ file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
+ file_type: type,
+ job: job
+ )
- it 'returns the job' do
- is_expected.to include(job)
+ is_expected.to include(job)
+ end
end
end
- context 'when job has a job artifact trace' do
+ context 'when job has a non-downloadable artifact' do
let!(:job) { create(:ci_build, :trace_artifact) }
it 'does not return the job' do
@@ -1419,6 +1431,8 @@ describe Ci::Build do
subject { build.erase_erasable_artifacts! }
before do
+ stub_feature_flags(drop_license_management_artifact: false)
+
Ci::JobArtifact.file_types.keys.each do |file_type|
create(:ci_job_artifact, job: build, file_type: file_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[file_type.to_sym])
end
@@ -2367,12 +2381,14 @@ describe Ci::Build do
let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true, masked: false } }
let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } }
let(:job_jwt_var) { { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true } }
+ let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } }
before do
allow(build).to receive(:predefined_variables) { [build_pre_var] }
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
allow(build).to receive(:persisted_variables) { [] }
allow(build).to receive(:job_jwt_variables) { [job_jwt_var] }
+ allow(build).to receive(:dependency_variables) { [job_dependency_var] }
allow_any_instance_of(Project)
.to receive(:predefined_variables) { [project_pre_var] }
@@ -2390,6 +2406,7 @@ describe Ci::Build do
project_pre_var,
pipeline_pre_var,
build_yaml_var,
+ job_dependency_var,
{ key: 'secret', value: 'value', public: false, masked: false }])
end
end
@@ -2884,6 +2901,19 @@ describe Ci::Build do
it { is_expected.to include(deployment_variable) }
end
+ context 'when build has a freeze period' do
+ let(:freeze_variable) { { key: 'CI_DEPLOY_FREEZE', value: 'true', masked: false, public: true } }
+
+ before do
+ expect_next_instance_of(Ci::FreezePeriodStatus) do |freeze_period|
+ expect(freeze_period).to receive(:execute)
+ .and_return(true)
+ end
+ end
+
+ it { is_expected.to include(freeze_variable) }
+ end
+
context 'when project has default CI config path' do
let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: '.gitlab-ci.yml', public: true, masked: false } }
@@ -2987,6 +3017,15 @@ describe Ci::Build do
end
end
end
+
+ context 'when build has dependency which has dotenv variable' do
+ let!(:prepare) { create(:ci_build, pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: [prepare.name] }) }
+
+ let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
+
+ it { is_expected.to include(key: job_variable.key, value: job_variable.value, public: false, masked: false) }
+ end
end
describe '#scoped_variables' do
@@ -3049,71 +3088,36 @@ describe Ci::Build do
end
end
end
- end
- describe '#secret_group_variables' do
- subject { build.secret_group_variables }
-
- let!(:variable) { create(:ci_group_variable, protected: true, group: group) }
+ context 'with dependency variables' do
+ let!(:prepare) { create(:ci_build, name: 'prepare', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare'] }) }
- context 'when ref is branch' do
- let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
+ let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: prepare) }
- context 'when ref is protected' do
+ context 'FF ci_dependency_variables is enabled' do
before do
- create(:protected_branch, :developers_can_merge, name: 'master', project: project)
+ stub_feature_flags(ci_dependency_variables: true)
end
- it { is_expected.to include(variable) }
- end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
- end
- end
-
- context 'when ref is tag' do
- let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) }
-
- context 'when ref is protected' do
- before do
- create(:protected_tag, project: project, name: 'v*')
+ it 'inherits dependent variables' do
+ expect(build.scoped_variables.to_hash).to include(job_variable.key => job_variable.value)
end
-
- it { is_expected.to include(variable) }
- end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
end
- end
- context 'when ref is merge request' do
- let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
- let(:pipeline) { merge_request.pipelines_for_merge_request.first }
- let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
-
- context 'when ref is protected' do
+ context 'FF ci_dependency_variables is disabled' do
before do
- create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project)
+ stub_feature_flags(ci_dependency_variables: false)
end
- it 'does not return protected variables as it is not supported for merge request pipelines' do
- is_expected.not_to include(variable)
+ it 'does not inherit dependent variables' do
+ expect(build.scoped_variables.to_hash).not_to include(job_variable.key => job_variable.value)
end
end
-
- context 'when ref is not protected' do
- it { is_expected.not_to include(variable) }
- end
end
end
- describe '#secret_project_variables' do
- subject { build.secret_project_variables }
-
- let!(:variable) { create(:ci_variable, protected: true, project: project) }
-
+ shared_examples "secret CI variables" do
context 'when ref is branch' do
let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
@@ -3167,6 +3171,30 @@ describe Ci::Build do
end
end
+ describe '#secret_instance_variables' do
+ subject { build.secret_instance_variables }
+
+ let_it_be(:variable) { create(:ci_instance_variable, protected: true) }
+
+ include_examples "secret CI variables"
+ end
+
+ describe '#secret_group_variables' do
+ subject { build.secret_group_variables }
+
+ let_it_be(:variable) { create(:ci_group_variable, protected: true, group: group) }
+
+ include_examples "secret CI variables"
+ end
+
+ describe '#secret_project_variables' do
+ subject { build.secret_project_variables }
+
+ let_it_be(:variable) { create(:ci_variable, protected: true, project: project) }
+
+ include_examples "secret CI variables"
+ end
+
describe '#deployment_variables' do
let(:build) { create(:ci_build, environment: environment) }
let(:environment) { 'production' }
@@ -3217,6 +3245,29 @@ describe Ci::Build do
expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar')
end
end
+
+ context 'when overriding CI instance variables' do
+ before do
+ create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1')
+ group.variables.create!(key: 'MY_VAR', value: 'my value 2')
+ end
+
+ it 'returns a regular hash created using valid ordering' do
+ expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2')
+ expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
+ end
+ end
+
+ context 'when CI instance variables are disabled' do
+ before do
+ create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1')
+ stub_feature_flags(ci_instance_level_variables: false)
+ end
+
+ it 'does not include instance level variables' do
+ expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
+ end
+ end
end
describe '#any_unmet_prerequisites?' do
@@ -3293,6 +3344,41 @@ describe Ci::Build do
end
end
+ describe '#dependency_variables' do
+ subject { build.dependency_variables }
+
+ context 'when using dependencies' do
+ let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) }
+ let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['prepare1'] }) }
+
+ let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) }
+ let!(:job_variable_2) { create(:ci_job_variable, job: prepare1) }
+ let!(:job_variable_3) { create(:ci_job_variable, :dotenv_source, job: prepare2) }
+
+ it 'inherits only dependent variables' do
+ expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value)
+ end
+ end
+
+ context 'when using needs' do
+ let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) }
+ let!(:prepare2) { create(:ci_build, name: 'prepare2', pipeline: pipeline, stage_idx: 0) }
+ let!(:prepare3) { create(:ci_build, name: 'prepare3', pipeline: pipeline, stage_idx: 0) }
+ let!(:build) { create(:ci_build, pipeline: pipeline, stage_idx: 1, scheduling_type: 'dag') }
+ let!(:build_needs_prepare1) { create(:ci_build_need, build: build, name: 'prepare1', artifacts: true) }
+ let!(:build_needs_prepare2) { create(:ci_build_need, build: build, name: 'prepare2', artifacts: false) }
+
+ let!(:job_variable_1) { create(:ci_job_variable, :dotenv_source, job: prepare1) }
+ let!(:job_variable_2) { create(:ci_job_variable, :dotenv_source, job: prepare2) }
+ let!(:job_variable_3) { create(:ci_job_variable, :dotenv_source, job: prepare3) }
+
+ it 'inherits only needs with artifacts variables' do
+ expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value)
+ end
+ end
+ end
+
describe 'state transition: any => [:preparing]' do
let(:build) { create(:ci_build, :created) }
@@ -3822,8 +3908,68 @@ describe Ci::Build do
create(:ci_job_artifact, :junit_with_corrupted_data, job: build, project: build.project)
end
- it 'raises an error' do
- expect { subject }.to raise_error(Gitlab::Ci::Parsers::Test::Junit::JunitParserError)
+ it 'returns no test data and includes a suite_error message' do
+ expect { subject }.not_to raise_error
+
+ expect(test_reports.get_suite(build.name).total_count).to eq(0)
+ expect(test_reports.get_suite(build.name).success_count).to eq(0)
+ expect(test_reports.get_suite(build.name).failed_count).to eq(0)
+ expect(test_reports.get_suite(build.name).suite_error).to eq('JUnit XML parsing failed: 1:1: FATAL: Document is empty')
+ end
+ end
+ end
+ end
+
+ describe '#collect_accessibility_reports!' do
+ subject { build.collect_accessibility_reports!(accessibility_report) }
+
+ let(:accessibility_report) { Gitlab::Ci::Reports::AccessibilityReports.new }
+
+ it { expect(accessibility_report.urls).to eq({}) }
+
+ context 'when build has an accessibility report' do
+ context 'when there is an accessibility report with errors' do
+ before do
+ create(:ci_job_artifact, :accessibility, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to match_array(['https://about.gitlab.com/'])
+ expect(accessibility_report.errors_count).to eq(10)
+ expect(accessibility_report.scans_count).to eq(1)
+ expect(accessibility_report.passes_count).to eq(0)
+ end
+ end
+
+ context 'when there is an accessibility report without errors' do
+ before do
+ create(:ci_job_artifact, :accessibility_without_errors, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls.keys).to match_array(['https://pa11y.org/'])
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(1)
+ expect(accessibility_report.passes_count).to eq(1)
+ end
+ end
+
+ context 'when there is an accessibility report with an invalid url' do
+ before do
+ create(:ci_job_artifact, :accessibility_with_invalid_url, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the accessibility report' do
+ expect { subject }.not_to raise_error
+
+ expect(accessibility_report.urls).to be_empty
+ expect(accessibility_report.errors_count).to eq(0)
+ expect(accessibility_report.scans_count).to eq(0)
+ expect(accessibility_report.passes_count).to eq(0)
end
end
end
@@ -3876,6 +4022,48 @@ describe Ci::Build do
end
end
+ describe '#collect_terraform_reports!' do
+ let(:terraform_reports) { Gitlab::Ci::Reports::TerraformReports.new }
+
+ it 'returns an empty hash' do
+ expect(build.collect_terraform_reports!(terraform_reports).plans).to eq({})
+ end
+
+ context 'when build has a terraform report' do
+ context 'when there is a valid tfplan.json' do
+ before do
+ create(:ci_job_artifact, :terraform, job: build, project: build.project)
+ end
+
+ it 'parses blobs and add the results to the terraform report' do
+ expect { build.collect_terraform_reports!(terraform_reports) }.not_to raise_error
+
+ expect(terraform_reports.plans).to match(
+ a_hash_including(
+ 'tfplan.json' => a_hash_including(
+ 'create' => 0,
+ 'update' => 1,
+ 'delete' => 0
+ )
+ )
+ )
+ end
+ end
+
+ context 'when there is an invalid tfplan.json' do
+ before do
+ create(:ci_job_artifact, :terraform_with_corrupted_data, job: build, project: build.project)
+ end
+
+ it 'raises an error' do
+ expect { build.collect_terraform_reports!(terraform_reports) }.to raise_error(
+ Gitlab::Ci::Parsers::Terraform::Tfplan::TfplanParserError
+ )
+ end
+ end
+ end
+ end
+
describe '#report_artifacts' do
subject { build.report_artifacts }
@@ -3986,6 +4174,28 @@ describe Ci::Build do
it { is_expected.to include(:upload_multiple_artifacts) }
end
+
+ context 'when artifacts exclude is defined and the is feature enabled' do
+ let(:options) do
+ { artifacts: { exclude: %w[something] } }
+ end
+
+ context 'when a feature flag is enabled' do
+ before do
+ stub_feature_flags(ci_artifacts_exclude: true)
+ end
+
+ it { is_expected.to include(:artifacts_exclude) }
+ end
+
+ context 'when a feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_artifacts_exclude: false)
+ end
+
+ it { is_expected.not_to include(:artifacts_exclude) }
+ end
+ end
end
describe '#supported_runner?' do
@@ -4312,4 +4522,31 @@ describe Ci::Build do
it { is_expected.to be_nil }
end
end
+
+ describe '#degradation_threshold' do
+ subject { build.degradation_threshold }
+
+ context 'when threshold variable is defined' do
+ before do
+ build.yaml_variables = [
+ { key: 'SOME_VAR_1', value: 'SOME_VAL_1' },
+ { key: 'DEGRADATION_THRESHOLD', value: '5' },
+ { key: 'SOME_VAR_2', value: 'SOME_VAL_2' }
+ ]
+ end
+
+ it { is_expected.to eq(5) }
+ end
+
+ context 'when threshold variable is not defined' do
+ before do
+ build.yaml_variables = [
+ { key: 'SOME_VAR_1', value: 'SOME_VAL_1' },
+ { key: 'SOME_VAR_2', value: 'SOME_VAL_2' }
+ ]
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
end
diff --git a/spec/models/ci/daily_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index 61aa58c6692..d4c305c649a 100644
--- a/spec/models/ci/daily_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -2,14 +2,14 @@
require 'spec_helper'
-describe Ci::DailyReportResult do
+describe Ci::DailyBuildGroupReportResult do
describe '.upsert_reports' do
let!(:rspec_coverage) do
create(
- :ci_daily_report_result,
- title: 'rspec',
+ :ci_daily_build_group_report_result,
+ group_name: 'rspec',
date: '2020-03-09',
- value: 71.2
+ data: { coverage: 71.2 }
)
end
let!(:new_pipeline) { create(:ci_pipeline) }
@@ -19,20 +19,18 @@ describe Ci::DailyReportResult do
{
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
- param_type: described_class.param_types[rspec_coverage.param_type],
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
- title: 'rspec',
- value: 81.0
+ group_name: 'rspec',
+ data: { 'coverage' => 81.0 }
},
{
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
- param_type: described_class.param_types[rspec_coverage.param_type],
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
- title: 'karma',
- value: 87.0
+ group_name: 'karma',
+ data: { 'coverage' => 87.0 }
}
])
@@ -40,16 +38,15 @@ describe Ci::DailyReportResult do
expect(rspec_coverage).to have_attributes(
last_pipeline_id: new_pipeline.id,
- value: 81.0
+ data: { 'coverage' => 81.0 }
)
- expect(described_class.find_by_title('karma')).to have_attributes(
+ expect(described_class.find_by_group_name('karma')).to have_attributes(
project_id: rspec_coverage.project_id,
ref_path: rspec_coverage.ref_path,
- param_type: rspec_coverage.param_type,
last_pipeline_id: new_pipeline.id,
date: rspec_coverage.date,
- value: 87.0
+ data: { 'coverage' => 87.0 }
)
end
diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb
new file mode 100644
index 00000000000..f7f840c6696
--- /dev/null
+++ b/spec/models/ci/freeze_period_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::FreezePeriod, type: :model do
+ subject { build(:ci_freeze_period) }
+
+ let(:invalid_cron) { '0 0 0 * *' }
+
+ it { is_expected.to belong_to(:project) }
+
+ it { is_expected.to respond_to(:freeze_start) }
+ it { is_expected.to respond_to(:freeze_end) }
+ it { is_expected.to respond_to(:cron_timezone) }
+
+ describe 'cron validations' do
+ it 'allows valid cron patterns' do
+ freeze_period = build(:ci_freeze_period)
+
+ expect(freeze_period).to be_valid
+ end
+
+ it 'does not allow invalid cron patterns on freeze_start' do
+ freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron)
+
+ expect(freeze_period).not_to be_valid
+ end
+
+ it 'does not allow invalid cron patterns on freeze_end' do
+ freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron)
+
+ expect(freeze_period).not_to be_valid
+ end
+
+ it 'does not allow an invalid timezone' do
+ freeze_period = build(:ci_freeze_period, cron_timezone: 'invalid')
+
+ expect(freeze_period).not_to be_valid
+ end
+
+ context 'when cron contains trailing whitespaces' do
+ it 'strips the attribute' do
+ freeze_period = build(:ci_freeze_period, freeze_start: ' 0 0 * * * ')
+
+ expect(freeze_period).to be_valid
+ expect(freeze_period.freeze_start).to eq('0 0 * * *')
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/freeze_period_status_spec.rb b/spec/models/ci/freeze_period_status_spec.rb
new file mode 100644
index 00000000000..b700ec8c45f
--- /dev/null
+++ b/spec/models/ci/freeze_period_status_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Ci::FreezePeriodStatus do
+ let(:project) { create :project }
+ # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday.""
+ let(:friday_2300) { '0 23 * * 5' }
+ let(:monday_0700) { '0 7 * * 1' }
+
+ subject { described_class.new(project: project).execute }
+
+ shared_examples 'within freeze period' do |time|
+ it 'is frozen' do
+ Timecop.freeze(time) do
+ expect(subject).to be_truthy
+ end
+ end
+ end
+
+ shared_examples 'outside freeze period' do |time|
+ it 'is not frozen' do
+ Timecop.freeze(time) do
+ expect(subject).to be_falsy
+ end
+ end
+ end
+
+ describe 'single freeze period' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 7, 1)
+ end
+
+ describe 'multiple freeze periods' do
+ # '30 23 * * 5' == "At 23:30 on Friday."", '0 8 * * 1' == "At 08:00 on Monday.""
+ let(:friday_2330) { '30 23 * * 5' }
+ let(:monday_0800) { '0 8 * * 1' }
+
+ let!(:freeze_period_1) { create(:ci_freeze_period, project: project, freeze_start: friday_2300, freeze_end: monday_0700) }
+ let!(:freeze_period_2) { create(:ci_freeze_period, project: project, freeze_start: friday_2330, freeze_end: monday_0800) }
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 10, 22, 59)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 29)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 11, 10, 0)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 10, 23, 1)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 6, 59)
+
+ it_behaves_like 'within freeze period', Time.utc(2020, 4, 13, 7, 59)
+
+ it_behaves_like 'outside freeze period', Time.utc(2020, 4, 13, 8, 1)
+ end
+end
diff --git a/spec/models/ci/instance_variable_spec.rb b/spec/models/ci/instance_variable_spec.rb
new file mode 100644
index 00000000000..ff8676e1424
--- /dev/null
+++ b/spec/models/ci/instance_variable_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::InstanceVariable do
+ subject { build(:ci_instance_variable) }
+
+ it_behaves_like "CI variable"
+
+ it { is_expected.to include_module(Ci::Maskable) }
+ it { is_expected.to validate_uniqueness_of(:key).with_message(/\(\w+\) has already been taken/) }
+
+ describe '.unprotected' do
+ subject { described_class.unprotected }
+
+ context 'when variable is protected' do
+ before do
+ create(:ci_instance_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_instance_variable, protected: false) }
+
+ it 'returns the variable' do
+ is_expected.to contain_exactly(variable)
+ end
+ end
+ end
+
+ describe '.all_cached', :use_clean_rails_memory_store_caching do
+ let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) }
+ let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
+
+ it { expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) }
+
+ it 'memoizes the result' do
+ expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
+
+ 2.times do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+ end
+ end
+
+ it 'removes scopes' do
+ expect(described_class.unprotected.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+ end
+
+ it 'resets the cache when records are deleted' do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+
+ protected_variable.destroy
+
+ expect(described_class.all_cached).to contain_exactly(unprotected_variable)
+ end
+
+ it 'resets the cache when records are inserted' do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+
+ variable = create(:ci_instance_variable, protected: true)
+
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable, variable)
+ end
+
+ it 'resets the cache when the shared key is missing' do
+ expect(Rails.cache).to receive(:read).with(:ci_instance_variable_changed_at).twice.and_return(nil)
+ expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).thrice.and_call_original
+
+ 3.times do
+ expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
+ end
+ end
+ end
+
+ describe '.unprotected_cached', :use_clean_rails_memory_store_caching do
+ let_it_be(:unprotected_variable) { create(:ci_instance_variable, protected: false) }
+ let_it_be(:protected_variable) { create(:ci_instance_variable, protected: true) }
+
+ it { expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable) }
+
+ it 'memoizes the result' do
+ expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
+
+ 2.times do
+ expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 6f6ff3704b4..4cdc74d7a41 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -19,24 +19,8 @@ describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
- context 'with update_project_statistics_after_commit enabled' do
- before do
- stub_feature_flags(update_project_statistics_after_commit: true)
- end
-
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 107464) }
- end
- end
-
- context 'with update_project_statistics_after_commit disabled' do
- before do
- stub_feature_flags(update_project_statistics_after_commit: false)
- end
-
- it_behaves_like 'UpdateProjectStatistics' do
- subject { build(:ci_job_artifact, :archive, size: 107464) }
- end
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 107464) }
end
describe '.with_reports' do
@@ -70,6 +54,22 @@ describe Ci::JobArtifact do
end
end
+ describe '.accessibility_reports' do
+ subject { described_class.accessibility_reports }
+
+ context 'when there is an accessibility report' do
+ let(:artifact) { create(:ci_job_artifact, :accessibility) }
+
+ it { is_expected.to eq([artifact]) }
+ end
+
+ context 'when there are no accessibility report' do
+ let(:artifact) { create(:ci_job_artifact, :archive) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
describe '.coverage_reports' do
subject { described_class.coverage_reports }
@@ -86,6 +86,22 @@ describe Ci::JobArtifact do
end
end
+ describe '.terraform_reports' do
+ context 'when there is a terraform report' do
+ it 'return the job artifact' do
+ artifact = create(:ci_job_artifact, :terraform)
+
+ expect(described_class.terraform_reports).to eq([artifact])
+ end
+ end
+
+ context 'when there are no terraform reports' do
+ it 'return the an empty array' do
+ expect(described_class.terraform_reports).to eq([])
+ end
+ end
+ end
+
describe '.erasable' do
subject { described_class.erasable }
@@ -128,15 +144,26 @@ describe Ci::JobArtifact do
end
describe '.for_sha' do
+ let(:first_pipeline) { create(:ci_pipeline) }
+ let(:second_pipeline) { create(:ci_pipeline, project: first_pipeline.project, sha: Digest::SHA1.hexdigest(SecureRandom.hex)) }
+ let!(:first_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline)) }
+ let!(:second_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline)) }
+
it 'returns job artifacts for a given pipeline sha' do
- project = create(:project)
- first_pipeline = create(:ci_pipeline, project: project)
- second_pipeline = create(:ci_pipeline, project: project, sha: Digest::SHA1.hexdigest(SecureRandom.hex))
- first_artifact = create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline))
- second_artifact = create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline))
+ expect(described_class.for_sha(first_pipeline.sha, first_pipeline.project.id)).to eq([first_artifact])
+ expect(described_class.for_sha(second_pipeline.sha, first_pipeline.project.id)).to eq([second_artifact])
+ end
+ end
- expect(described_class.for_sha(first_pipeline.sha, project.id)).to eq([first_artifact])
- expect(described_class.for_sha(second_pipeline.sha, project.id)).to eq([second_artifact])
+ describe '.for_ref' do
+ let(:first_pipeline) { create(:ci_pipeline, ref: 'first_ref') }
+ let(:second_pipeline) { create(:ci_pipeline, ref: 'second_ref', project: first_pipeline.project) }
+ let!(:first_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: first_pipeline)) }
+ let!(:second_artifact) { create(:ci_job_artifact, job: create(:ci_build, pipeline: second_pipeline)) }
+
+ it 'returns job artifacts for a given pipeline ref' do
+ expect(described_class.for_ref(first_pipeline.ref, first_pipeline.project.id)).to eq([first_artifact])
+ expect(described_class.for_ref(second_pipeline.ref, first_pipeline.project.id)).to eq([second_artifact])
end
end
@@ -153,9 +180,9 @@ describe Ci::JobArtifact do
end
describe 'callbacks' do
- subject { create(:ci_job_artifact, :archive) }
-
describe '#schedule_background_upload' do
+ subject { create(:ci_job_artifact, :archive) }
+
context 'when object storage is disabled' do
before do
stub_artifacts_object_storage(enabled: false)
@@ -212,9 +239,35 @@ describe Ci::JobArtifact do
end
end
+ describe 'validates if file format is supported' do
+ subject { artifact }
+
+ let(:artifact) { build(:ci_job_artifact, file_type: :license_management, file_format: :raw) }
+
+ context 'when license_management is supported' do
+ before do
+ stub_feature_flags(drop_license_management_artifact: false)
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when license_management is not supported' do
+ before do
+ stub_feature_flags(drop_license_management_artifact: true)
+ end
+
+ it { is_expected.not_to be_valid }
+ end
+ end
+
describe 'validates file format' do
subject { artifact }
+ before do
+ stub_feature_flags(drop_license_management_artifact: false)
+ end
+
described_class::TYPE_AND_FORMAT_PAIRS.except(:trace).each do |file_type, file_format|
context "when #{file_type} type with #{file_format} format" do
let(:artifact) { build(:ci_job_artifact, file_type: file_type, file_format: file_format) }
@@ -351,19 +404,6 @@ describe Ci::JobArtifact do
describe 'file is being stored' do
subject { create(:ci_job_artifact, :archive) }
- context 'when object has nil store' do
- before do
- subject.update_column(:file_store, nil)
- subject.reload
- end
-
- it 'is stored locally' do
- expect(subject.file_store).to be(nil)
- expect(subject.file).to be_file_storage
- expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
- end
- end
-
context 'when existing object has local store' do
it 'is stored locally' do
expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
diff --git a/spec/models/ci/persistent_ref_spec.rb b/spec/models/ci/persistent_ref_spec.rb
index 4cece0664cf..89dd9b05331 100644
--- a/spec/models/ci/persistent_ref_spec.rb
+++ b/spec/models/ci/persistent_ref_spec.rb
@@ -45,18 +45,6 @@ describe Ci::PersistentRef do
expect(pipeline.persistent_ref).to be_exist
end
- context 'when depend_on_persistent_pipeline_ref feature flag is disabled' do
- before do
- stub_feature_flags(depend_on_persistent_pipeline_ref: false)
- end
-
- it 'does not create a persistent ref' do
- expect(project.repository).not_to receive(:create_ref)
-
- subject
- end
- end
-
context 'when sha does not exist in the repository' do
let(:sha) { 'not-exist' }
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 4ed4b7e38d8..9a10c7629b2 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -17,14 +17,18 @@ describe Ci::PipelineSchedule do
it { is_expected.to respond_to(:description) }
it { is_expected.to respond_to(:next_run_at) }
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:ci_pipeline_schedule) }
+ end
+
describe 'validations' do
- it 'does not allow invalid cron patters' do
+ it 'does not allow invalid cron patterns' 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
+ it 'does not allow invalid cron patterns' do
pipeline_schedule = build(:ci_pipeline_schedule, cron_timezone: 'invalid')
expect(pipeline_schedule).not_to be_valid
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 90412136c1d..4f53b6b4418 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -53,6 +53,29 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#set_status' do
+ where(:from_status, :to_status) do
+ from_status_names = described_class.state_machines[:status].states.map(&:name)
+ to_status_names = from_status_names - [:created] # we never want to transition into created
+
+ from_status_names.product(to_status_names)
+ end
+
+ with_them do
+ it do
+ pipeline.status = from_status.to_s
+
+ if from_status != to_status
+ expect(pipeline.set_status(to_status.to_s))
+ .to eq(true)
+ else
+ expect(pipeline.set_status(to_status.to_s))
+ .to eq(false), "loopback transitions are not allowed"
+ end
+ end
+ end
+ end
+
describe '.processables' do
before do
create(:ci_build, name: 'build', pipeline: pipeline)
@@ -364,6 +387,26 @@ describe Ci::Pipeline, :mailer do
end
end
+ context 'when pipeline has an accessibility report' do
+ subject { described_class.with_reports(Ci::JobArtifact.accessibility_reports) }
+
+ let(:pipeline_with_report) { create(:ci_pipeline, :with_accessibility_reports) }
+
+ it 'selects the pipeline' do
+ is_expected.to eq([pipeline_with_report])
+ end
+ end
+
+ context 'when pipeline has a terraform report' do
+ it 'selects the pipeline' do
+ pipeline_with_report = create(:ci_pipeline, :with_terraform_reports)
+
+ expect(described_class.with_reports(Ci::JobArtifact.terraform_reports)).to eq(
+ [pipeline_with_report]
+ )
+ end
+ end
+
context 'when pipeline does not have metrics reports' do
subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
@@ -699,6 +742,28 @@ describe Ci::Pipeline, :mailer do
)
end
end
+
+ describe 'variable CI_KUBERNETES_ACTIVE' do
+ context 'when pipeline.has_kubernetes_active? is true' do
+ before do
+ allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
+ end
+
+ it "is included with value 'true'" do
+ expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
+ end
+ end
+
+ context 'when pipeline.has_kubernetes_active? is false' do
+ before do
+ allow(pipeline).to receive(:has_kubernetes_active?).and_return(false)
+ end
+
+ it 'is not included' do
+ expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE')
+ end
+ end
+ end
end
describe '#protected_ref?' do
@@ -944,7 +1009,10 @@ describe Ci::Pipeline, :mailer do
context 'when using legacy stages' do
before do
- stub_feature_flags(ci_pipeline_persisted_stages: false)
+ stub_feature_flags(
+ ci_pipeline_persisted_stages: false,
+ ci_atomic_processing: false
+ )
end
it 'returns legacy stages in valid order' do
@@ -952,9 +1020,40 @@ describe Ci::Pipeline, :mailer do
end
end
+ context 'when using atomic processing' do
+ before do
+ stub_feature_flags(
+ ci_atomic_processing: true
+ )
+ end
+
+ context 'when pipelines is not complete' do
+ it 'returns stages in valid order' do
+ expect(subject).to all(be_a Ci::Stage)
+ expect(subject.map(&:name))
+ .to eq %w[sanity build test deploy cleanup]
+ end
+ end
+
+ context 'when pipeline is complete' do
+ before do
+ pipeline.succeed!
+ end
+
+ it 'returns stages in valid order' do
+ expect(subject).to all(be_a Ci::Stage)
+ expect(subject.map(&:name))
+ .to eq %w[sanity build test deploy cleanup]
+ end
+ end
+ end
+
context 'when using persisted stages' do
before do
- stub_feature_flags(ci_pipeline_persisted_stages: true)
+ stub_feature_flags(
+ ci_pipeline_persisted_stages: true,
+ ci_atomic_processing: false
+ )
end
context 'when pipelines is not complete' do
@@ -1119,8 +1218,8 @@ describe Ci::Pipeline, :mailer do
context "from #{status}" do
let(:from_status) { status }
- it 'schedules pipeline success worker' do
- expect(Ci::DailyReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)
+ it 'schedules daily build group report results worker' do
+ expect(Ci::DailyBuildGroupReportResultsWorker).to receive(:perform_in).with(10.minutes, pipeline.id)
pipeline.succeed
end
@@ -2307,7 +2406,7 @@ describe Ci::Pipeline, :mailer do
def have_requested_pipeline_hook(status)
have_requested(:post, stubbed_hostname(hook.url)).with do |req|
- json_body = JSON.parse(req.body)
+ json_body = Gitlab::Json.parse(req.body)
json_body['object_attributes']['status'] == status &&
json_body['builds'].length == 2
end
@@ -2755,6 +2854,42 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#accessibility_reports' do
+ subject { pipeline.accessibility_reports }
+
+ context 'when pipeline has multiple builds with accessibility reports' do
+ let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) }
+ let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) }
+
+ before do
+ create(:ci_job_artifact, :accessibility, job: build_rspec, project: project)
+ create(:ci_job_artifact, :accessibility_without_errors, job: build_golang, project: project)
+ end
+
+ it 'returns accessibility report with collected data' do
+ expect(subject.urls.keys).to match_array([
+ "https://pa11y.org/",
+ "https://about.gitlab.com/"
+ ])
+ end
+
+ context 'when builds are retried' do
+ let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) }
+ let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) }
+
+ it 'returns empty urls for accessibility reports' do
+ expect(subject.urls).to be_empty
+ end
+ end
+ end
+
+ context 'when pipeline does not have any builds with accessibility reports' do
+ it 'returns empty urls for accessibility reports' do
+ expect(subject.urls).to be_empty
+ end
+ end
+ end
+
describe '#coverage_reports' do
subject { pipeline.coverage_reports }
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 4490371bde5..e67f740279b 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -6,16 +6,12 @@ describe Ci::Processable do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
- let_it_be(:detached_merge_request_pipeline) do
- create(:ci_pipeline, :detached_merge_request_pipeline, :with_job, project: project)
- end
-
- let_it_be(:legacy_detached_merge_request_pipeline) do
- create(:ci_pipeline, :legacy_detached_merge_request_pipeline, :with_job, project: project)
- end
+ describe 'delegations' do
+ subject { Ci::Processable.new }
- let_it_be(:merged_result_pipeline) do
- create(:ci_pipeline, :merged_result_pipeline, :with_job, project: project)
+ it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
+ it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
+ it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
end
describe '#aggregated_needs_names' do
@@ -52,69 +48,28 @@ describe Ci::Processable do
end
describe 'validate presence of scheduling_type' do
- context 'on create' do
- let(:processable) do
- build(
- :ci_build, :created, project: project, pipeline: pipeline,
- importing: importing, scheduling_type: nil
- )
- end
-
- context 'when importing' do
- let(:importing) { true }
-
- context 'when validate_scheduling_type_of_processables is true' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: true)
- end
+ using RSpec::Parameterized::TableSyntax
- it 'does not validate' do
- expect(processable).to be_valid
- end
- end
-
- context 'when validate_scheduling_type_of_processables is false' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: false)
- end
-
- it 'does not validate' do
- expect(processable).to be_valid
- end
- end
- end
+ subject { build(:ci_build, project: project, pipeline: pipeline, importing: importing) }
- context 'when not importing' do
- let(:importing) { false }
-
- context 'when validate_scheduling_type_of_processables is true' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: true)
- end
-
- it 'validates' do
- expect(processable).not_to be_valid
- end
- end
-
- context 'when validate_scheduling_type_of_processables is false' do
- before do
- stub_feature_flags(validate_scheduling_type_of_processables: false)
- end
+ where(:importing, :should_validate) do
+ false | true
+ true | false
+ end
- it 'does not validate' do
- expect(processable).to be_valid
+ with_them do
+ context 'on create' do
+ it 'validates presence' do
+ if should_validate
+ is_expected.to validate_presence_of(:scheduling_type).on(:create)
+ else
+ is_expected.not_to validate_presence_of(:scheduling_type).on(:create)
end
end
end
- end
-
- context 'on update' do
- let(:processable) { create(:ci_build, :created, project: project, pipeline: pipeline) }
- it 'does not validate' do
- processable.scheduling_type = nil
- expect(processable).to be_valid
+ context 'on update' do
+ it { is_expected.not_to validate_presence_of(:scheduling_type).on(:update) }
end
end
end
@@ -147,6 +102,8 @@ describe Ci::Processable do
describe '#needs_attributes' do
let(:build) { create(:ci_build, :created, project: project, pipeline: pipeline) }
+ subject { build.needs_attributes }
+
context 'with needs' do
before do
create(:ci_build_need, build: build, name: 'test1')
@@ -154,7 +111,7 @@ describe Ci::Processable do
end
it 'returns all needs attributes' do
- expect(build.needs_attributes).to contain_exactly(
+ is_expected.to contain_exactly(
{ 'artifacts' => true, 'name' => 'test1' },
{ 'artifacts' => true, 'name' => 'test2' }
)
@@ -162,75 +119,7 @@ describe Ci::Processable do
end
context 'without needs' do
- it 'returns all needs attributes' do
- expect(build.needs_attributes).to be_empty
- end
- end
- end
-
- describe '#merge_request?' do
- subject { pipeline.processables.first.merge_request? }
-
- context 'in a detached merge request pipeline' do
- let(:pipeline) { detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request?) }
- end
-
- context 'in a legacy detached merge_request_pipeline' do
- let(:pipeline) { legacy_detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request?) }
- end
-
- context 'in a pipeline for merged results' do
- let(:pipeline) { merged_result_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request?) }
- end
- end
-
- describe '#merge_request_ref?' do
- subject { pipeline.processables.first.merge_request_ref? }
-
- context 'in a detached merge request pipeline' do
- let(:pipeline) { detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request_ref?) }
- end
-
- context 'in a legacy detached merge_request_pipeline' do
- let(:pipeline) { legacy_detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request_ref?) }
- end
-
- context 'in a pipeline for merged results' do
- let(:pipeline) { merged_result_pipeline }
-
- it { is_expected.to eq(pipeline.merge_request_ref?) }
- end
- end
-
- describe '#legacy_detached_merge_request_pipeline?' do
- subject { pipeline.processables.first.legacy_detached_merge_request_pipeline? }
-
- context 'in a detached merge request pipeline' do
- let(:pipeline) { detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) }
- end
-
- context 'in a legacy detached merge_request_pipeline' do
- let(:pipeline) { legacy_detached_merge_request_pipeline }
-
- it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) }
- end
-
- context 'in a pipeline for merged results' do
- let(:pipeline) { merged_result_pipeline }
-
- it { is_expected.to eq(pipeline.legacy_detached_merge_request_pipeline?) }
+ it { is_expected.to be_empty }
end
end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 2dedff7f15b..8b6a4fa6ade 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -270,7 +270,7 @@ describe Ci::Runner do
it { is_expected.to eq([@runner2])}
end
- describe '#online?' do
+ describe '#online?', :clean_gitlab_redis_cache do
let(:runner) { create(:ci_runner, :instance) }
subject { runner.online? }
@@ -332,7 +332,7 @@ describe Ci::Runner do
end
def stub_redis_runner_contacted_at(value)
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
cache_key = runner.send(:cache_attribute_key)
expect(redis).to receive(:get).with(cache_key)
.and_return({ contacted_at: value }.to_json).at_least(:once)
@@ -640,7 +640,7 @@ describe Ci::Runner do
end
def expect_redis_update
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
redis_key = runner.send(:cache_attribute_key)
expect(redis).to receive(:set).with(redis_key, anything, any_args)
end
@@ -664,7 +664,7 @@ describe Ci::Runner do
end
it 'cleans up the queue' do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
expect(redis.get(queue_key)).to be_nil
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 3aeaa27abce..a1549532559 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
describe Ci::Stage, :models do
- let(:stage) { create(:ci_stage_entity) }
+ let_it_be(:pipeline) { create(:ci_empty_pipeline) }
+ let(:stage) { create(:ci_stage_entity, pipeline: pipeline, project: pipeline.project) }
it_behaves_like 'having unique enum values'
@@ -55,6 +56,29 @@ describe Ci::Stage, :models do
end
end
+ describe '#set_status' do
+ where(:from_status, :to_status) do
+ from_status_names = described_class.state_machines[:status].states.map(&:name)
+ to_status_names = from_status_names - [:created] # we never want to transition into created
+
+ from_status_names.product(to_status_names)
+ end
+
+ with_them do
+ it do
+ stage.status = from_status.to_s
+
+ if from_status != to_status
+ expect(stage.set_status(to_status.to_s))
+ .to eq(true)
+ else
+ expect(stage.set_status(to_status.to_s))
+ .to eq(false), "loopback transitions are not allowed"
+ end
+ end
+ end
+ end
+
describe '#update_status' do
context 'when stage objects needs to be updated' do
before do
diff --git a/spec/models/clusters/applications/elastic_stack_spec.rb b/spec/models/clusters/applications/elastic_stack_spec.rb
index b0992c43d11..02ada219e32 100644
--- a/spec/models/clusters/applications/elastic_stack_spec.rb
+++ b/spec/models/clusters/applications/elastic_stack_spec.rb
@@ -19,10 +19,12 @@ describe Clusters::Applications::ElasticStack do
it 'is initialized with elastic stack arguments' do
expect(subject.name).to eq('elastic-stack')
- expect(subject.chart).to eq('stable/elastic-stack')
- expect(subject.version).to eq('1.9.0')
+ expect(subject.chart).to eq('elastic-stack/elastic-stack')
+ expect(subject.version).to eq('3.0.0')
+ expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject).to be_rbac
expect(subject.files).to eq(elastic_stack.files)
+ expect(subject.preinstall).to be_empty
end
context 'on a non rbac enabled cluster' do
@@ -33,15 +35,75 @@ describe Clusters::Applications::ElasticStack do
it { is_expected.not_to be_rbac }
end
+ context 'on versions older than 2' do
+ before do
+ elastic_stack.status = elastic_stack.status_states[:updating]
+ elastic_stack.version = "1.9.0"
+ end
+
+ it 'includes a preinstall script' do
+ expect(subject.preinstall).not_to be_empty
+ expect(subject.preinstall.first).to include("delete")
+ end
+ end
+
+ context 'on versions older than 3' do
+ before do
+ elastic_stack.status = elastic_stack.status_states[:updating]
+ elastic_stack.version = "2.9.0"
+ end
+
+ it 'includes a preinstall script' do
+ expect(subject.preinstall).not_to be_empty
+ expect(subject.preinstall.first).to include("delete")
+ end
+ end
+
context 'application failed to install previously' do
let(:elastic_stack) { create(:clusters_applications_elastic_stack, :errored, version: '0.0.1') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('1.9.0')
+ expect(subject.version).to eq('3.0.0')
end
end
end
+ describe '#chart_above_v2?' do
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, version: version) }
+
+ subject { elastic_stack.chart_above_v2? }
+
+ context 'on v1.9.0' do
+ let(:version) { '1.9.0' }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'on v2.0.0' do
+ let(:version) { '2.0.0' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#chart_above_v3?' do
+ let(:elastic_stack) { create(:clusters_applications_elastic_stack, version: version) }
+
+ subject { elastic_stack.chart_above_v3? }
+
+ context 'on v1.9.0' do
+ let(:version) { '1.9.0' }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'on v3.0.0' do
+ let(:version) { '3.0.0' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe '#uninstall_command' do
let!(:elastic_stack) { create(:clusters_applications_elastic_stack) }
@@ -57,7 +119,7 @@ describe Clusters::Applications::ElasticStack do
it 'specifies a post delete command to remove custom resource definitions' do
expect(subject.postdelete).to eq([
- 'kubectl delete pvc --selector release\\=elastic-stack'
+ 'kubectl delete pvc --selector app\\=elastic-stack-elasticsearch-master --namespace gitlab-managed-apps'
])
end
end
diff --git a/spec/models/clusters/applications/fluentd_spec.rb b/spec/models/clusters/applications/fluentd_spec.rb
index 7e9680b0ab4..4e9548990ed 100644
--- a/spec/models/clusters/applications/fluentd_spec.rb
+++ b/spec/models/clusters/applications/fluentd_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
describe Clusters::Applications::Fluentd do
- let(:fluentd) { create(:clusters_applications_fluentd) }
+ let(:waf_log_enabled) { true }
+ let(:cilium_log_enabled) { true }
+ let(:fluentd) { create(:clusters_applications_fluentd, waf_log_enabled: waf_log_enabled, cilium_log_enabled: cilium_log_enabled) }
include_examples 'cluster application core specs', :clusters_applications_fluentd
include_examples 'cluster application status specs', :clusters_applications_fluentd
@@ -47,4 +49,36 @@ describe Clusters::Applications::Fluentd do
expect(values).to include('output.conf', 'general.conf')
end
end
+
+ describe '#values' do
+ let(:modsecurity_log_path) { "/var/log/containers/*#{Clusters::Applications::Ingress::MODSECURITY_LOG_CONTAINER_NAME}*.log" }
+ let(:cilium_log_path) { "/var/log/containers/*#{described_class::CILIUM_CONTAINER_NAME}*.log" }
+
+ subject { fluentd.values }
+
+ context 'with both logs variables set to false' do
+ let(:waf_log_enabled) { false }
+ let(:cilium_log_enabled) { false }
+
+ it "raises ActiveRecord::RecordInvalid" do
+ expect {subject}.to raise_error(ActiveRecord::RecordInvalid)
+ end
+ end
+
+ context 'with both logs variables set to true' do
+ it { is_expected.to include("#{modsecurity_log_path},#{cilium_log_path}") }
+ end
+
+ context 'with waf_log_enabled set to true' do
+ let(:cilium_log_enabled) { false }
+
+ it { is_expected.to include(modsecurity_log_path) }
+ end
+
+ context 'with cilium_log_enabled set to true' do
+ let(:waf_log_enabled) { false }
+
+ it { is_expected.to include(cilium_log_path) }
+ end
+ end
end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index b070729ccac..8aee4eec0d3 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -220,6 +220,12 @@ describe Clusters::Applications::Ingress do
expect(subject.values).to include('extraContainers')
end
+ it 'executes command to tail modsecurity logs with -F option' do
+ args = YAML.safe_load(subject.values).dig('controller', 'extraContainers', 0, 'args')
+
+ expect(args).to eq(['/bin/sh', '-c', 'tail -F /var/log/modsec/audit.log'])
+ end
+
it 'includes livenessProbe for modsecurity sidecar container' do
probe_config = YAML.safe_load(subject.values).dig('controller', 'extraContainers', 0, 'livenessProbe')
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 3bc5088d1ab..937db9217f3 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -57,7 +57,7 @@ describe Clusters::Applications::Jupyter do
it 'is initialized with 4 arguments' do
expect(subject.name).to eq('jupyter')
expect(subject.chart).to eq('jupyter/jupyterhub')
- expect(subject.version).to eq('0.9.0-beta.2')
+ expect(subject.version).to eq('0.9.0')
expect(subject).to be_rbac
expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/')
@@ -76,7 +76,7 @@ describe Clusters::Applications::Jupyter do
let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') }
it 'is initialized with the locked version' do
- expect(subject.version).to eq('0.9.0-beta.2')
+ expect(subject.version).to eq('0.9.0')
end
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index db1d8672d1e..521ed98f637 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -590,6 +590,60 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe '#find_or_build_application' do
+ let_it_be(:cluster, reload: true) { create(:cluster) }
+
+ it 'rejects classes that are not applications' do
+ expect do
+ cluster.find_or_build_application(Project)
+ end.to raise_error(ArgumentError)
+ end
+
+ context 'when none of applications are created' do
+ it 'returns the new application', :aggregate_failures do
+ described_class::APPLICATIONS.values.each do |application_class|
+ application = cluster.find_or_build_application(application_class)
+
+ expect(application).to be_a(application_class)
+ expect(application).not_to be_persisted
+ end
+ end
+ end
+
+ context 'when application is persisted' do
+ let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
+ let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
+ let!(:cert_manager) { create(:clusters_applications_cert_manager, cluster: cluster) }
+ let!(:crossplane) { create(:clusters_applications_crossplane, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
+ let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
+ let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+ let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
+ let!(:elastic_stack) { create(:clusters_applications_elastic_stack, cluster: cluster) }
+ let!(:fluentd) { create(:clusters_applications_fluentd, cluster: cluster) }
+
+ it 'returns the persisted application', :aggregate_failures do
+ {
+ Clusters::Applications::Helm => helm,
+ Clusters::Applications::Ingress => ingress,
+ Clusters::Applications::CertManager => cert_manager,
+ Clusters::Applications::Crossplane => crossplane,
+ Clusters::Applications::Prometheus => prometheus,
+ Clusters::Applications::Runner => runner,
+ Clusters::Applications::Jupyter => jupyter,
+ Clusters::Applications::Knative => knative,
+ Clusters::Applications::ElasticStack => elastic_stack,
+ Clusters::Applications::Fluentd => fluentd
+ }.each do |application_class, expected_object|
+ application = cluster.find_or_build_application(application_class)
+
+ expect(application).to eq(expected_object)
+ expect(application).to be_persisted
+ end
+ end
+ end
+ end
+
describe '#allow_user_defined_namespace?' do
subject { cluster.allow_user_defined_namespace? }
@@ -889,9 +943,9 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
describe '#make_cleanup_errored!' do
- NON_ERRORED_STATES = Clusters::Cluster.state_machines[:cleanup_status].states.keys - [:cleanup_errored]
+ non_errored_states = Clusters::Cluster.state_machines[:cleanup_status].states.keys - [:cleanup_errored]
- NON_ERRORED_STATES.each do |state|
+ non_errored_states.each do |state|
it "transitions cleanup_status from #{state} to cleanup_errored" do
cluster = create(:cluster, state)
@@ -948,6 +1002,22 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
+ describe '#nodes' do
+ let(:cluster) { create(:cluster) }
+
+ subject { cluster.nodes }
+
+ it { is_expected.to be_nil }
+
+ context 'with a cached status' do
+ before do
+ stub_reactive_cache(cluster, nodes: [kube_node])
+ end
+
+ it { is_expected.to eq([kube_node]) }
+ end
+ end
+
describe '#calculate_reactive_cache' do
subject { cluster.calculate_reactive_cache }
@@ -956,6 +1026,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it 'does not populate the cache' do
expect(cluster).not_to receive(:retrieve_connection_status)
+ expect(cluster).not_to receive(:retrieve_nodes)
is_expected.to be_nil
end
@@ -964,12 +1035,12 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
context 'cluster is enabled' do
let(:cluster) { create(:cluster, :provided_by_user, :group) }
- context 'connection to the cluster is successful' do
- before do
- stub_kubeclient_discover(cluster.platform.api_url)
- end
+ before do
+ stub_kubeclient_nodes_and_nodes_metrics(cluster.platform.api_url)
+ end
- it { is_expected.to eq(connection_status: :connected) }
+ context 'connection to the cluster is successful' do
+ it { is_expected.to eq(connection_status: :connected, nodes: [kube_node.merge(kube_node_metrics)]) }
end
context 'cluster cannot be reached' do
@@ -978,7 +1049,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(SocketError)
end
- it { is_expected.to eq(connection_status: :unreachable) }
+ it { is_expected.to eq(connection_status: :unreachable, nodes: []) }
end
context 'cluster cannot be authenticated to' do
@@ -987,7 +1058,7 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(OpenSSL::X509::CertificateError.new("Certificate error"))
end
- it { is_expected.to eq(connection_status: :authentication_failure) }
+ it { is_expected.to eq(connection_status: :authentication_failure, nodes: []) }
end
describe 'Kubeclient::HttpError' do
@@ -999,18 +1070,18 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(Kubeclient::HttpError.new(error_code, error_message, nil))
end
- it { is_expected.to eq(connection_status: :authentication_failure) }
+ it { is_expected.to eq(connection_status: :authentication_failure, nodes: []) }
context 'generic timeout' do
let(:error_message) { 'Timed out connecting to server'}
- it { is_expected.to eq(connection_status: :unreachable) }
+ it { is_expected.to eq(connection_status: :unreachable, nodes: []) }
end
context 'gateway timeout' do
let(:error_message) { '504 Gateway Timeout for GET https://kubernetes.example.com/api/v1'}
- it { is_expected.to eq(connection_status: :unreachable) }
+ it { is_expected.to eq(connection_status: :unreachable, nodes: []) }
end
end
@@ -1020,11 +1091,12 @@ describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
.and_raise(StandardError)
end
- it { is_expected.to eq(connection_status: :unknown_failure) }
+ it { is_expected.to eq(connection_status: :unknown_failure, nodes: []) }
it 'notifies Sentry' do
expect(Gitlab::ErrorTracking).to receive(:track_exception)
.with(instance_of(StandardError), hash_including(cluster_id: cluster.id))
+ .twice
subject
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 73b81b2225a..05d3329215a 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -751,4 +751,48 @@ describe CommitStatus do
it { is_expected.to be_a(CommitStatusPresenter) }
end
+
+ describe '#recoverable?' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:commit_status) { create(:commit_status, :pending) }
+
+ subject(:recoverable?) { commit_status.recoverable? }
+
+ context 'when commit status is failed' do
+ before do
+ commit_status.drop!
+ end
+
+ where(:failure_reason, :recoverable) do
+ :script_failure | false
+ :missing_dependency_failure | false
+ :archived_failure | false
+ :scheduler_failure | false
+ :data_integrity_failure | false
+ :unknown_failure | true
+ :api_failure | true
+ :stuck_or_timeout_failure | true
+ :runner_system_failure | true
+ end
+
+ with_them do
+ context "when failure reason is #{params[:failure_reason]}" do
+ before do
+ commit_status.update_attribute(:failure_reason, failure_reason)
+ end
+
+ it { is_expected.to eq(recoverable) }
+ end
+ end
+ end
+
+ context 'when commit status is not failed' do
+ before do
+ commit_status.success!
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 76da42cf243..29f911fcb04 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -91,4 +91,45 @@ describe Awardable do
expect(issue.award_emoji).to eq issue.award_emoji.sort_by(&:id)
end
end
+
+ describe "#grouped_awards" do
+ context 'default award emojis' do
+ let(:issue_without_downvote) { create(:issue) }
+ let(:issue_with_downvote) do
+ issue_with_downvote = create(:issue)
+ create(:award_emoji, :downvote, awardable: issue_with_downvote)
+ issue_with_downvote
+ end
+
+ it "includes unused thumbs buttons by default" do
+ expect(issue_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ end
+
+ it "doesn't include unused thumbs buttons when disabled in project" do
+ issue_without_downvote.project.show_default_award_emojis = false
+
+ expect(issue_without_downvote.grouped_awards.keys.sort).to eq []
+ end
+
+ it "includes unused thumbs buttons when enabled in project" do
+ issue_without_downvote.project.show_default_award_emojis = true
+
+ expect(issue_without_downvote.grouped_awards.keys.sort).to eq %w(thumbsdown thumbsup)
+ end
+
+ it "doesn't include unused thumbs buttons in summary" do
+ expect(issue_without_downvote.grouped_awards(with_thumbs: false).keys).to eq []
+ end
+
+ it "includes used thumbs buttons when disabled in project" do
+ issue_with_downvote.project.show_default_award_emojis = false
+
+ expect(issue_with_downvote.grouped_awards.keys).to eq %w(thumbsdown)
+ end
+
+ it "includes used thumbs buttons in summary" do
+ expect(issue_with_downvote.grouped_awards(with_thumbs: false).keys).to eq %w(thumbsdown)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/blocks_json_serialization_spec.rb b/spec/models/concerns/blocks_json_serialization_spec.rb
index 0ef5be3cb61..32870461019 100644
--- a/spec/models/concerns/blocks_json_serialization_spec.rb
+++ b/spec/models/concerns/blocks_json_serialization_spec.rb
@@ -3,8 +3,11 @@
require 'spec_helper'
describe BlocksJsonSerialization do
- DummyModel = Class.new do
- include BlocksJsonSerialization
+ before do
+ stub_const('DummyModel', Class.new)
+ DummyModel.class_eval do
+ include BlocksJsonSerialization
+ end
end
it 'blocks as_json' do
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 697a9e98505..193144fcb0e 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -223,6 +223,10 @@ describe CacheMarkdownField, :clean_gitlab_redis_cache do
end
context 'when the markdown cache is up to date' do
+ before do
+ thing.try(:save)
+ end
+
it 'does not call #refresh_markdown_cache' do
expect(thing).not_to receive(:refresh_markdown_cache)
@@ -256,6 +260,54 @@ describe CacheMarkdownField, :clean_gitlab_redis_cache do
let(:klass) { ar_class }
it_behaves_like 'a class with cached markdown fields'
+
+ describe '#attribute_invalidated?' do
+ let(:thing) { klass.create(description: markdown, description_html: html, cached_markdown_version: cache_version) }
+
+ it 'returns true when cached_markdown_version is different' do
+ thing.cached_markdown_version += 1
+
+ expect(thing.attribute_invalidated?(:description_html)).to eq(true)
+ end
+
+ it 'returns true when markdown is changed' do
+ thing.description = updated_markdown
+
+ expect(thing.attribute_invalidated?(:description_html)).to eq(true)
+ end
+
+ it 'returns true when both markdown and HTML are changed' do
+ thing.description = updated_markdown
+ thing.description_html = updated_html
+
+ expect(thing.attribute_invalidated?(:description_html)).to eq(true)
+ end
+
+ it 'returns false when there are no changes' do
+ expect(thing.attribute_invalidated?(:description_html)).to eq(false)
+ end
+ end
+
+ context 'when cache version is updated' do
+ let(:old_version) { cache_version - 1 }
+ let(:old_html) { '<p data-sourcepos="1:1-1:5" dir="auto" class="some-old-class"><code>Foo</code></p>' }
+
+ let(:thing) do
+ # This forces the record to have outdated HTML. We can't use `create` because the `before_create` hook
+ # would re-render the HTML to the latest version
+ klass.create.tap do |thing|
+ thing.update_columns(description: markdown, description_html: old_html, cached_markdown_version: old_version)
+ end
+ end
+
+ it 'correctly updates cached HTML even if refresh_markdown_cache is called before updating the attribute' do
+ thing.refresh_markdown_cache
+
+ thing.update(description: updated_markdown)
+
+ expect(thing.description_html).to eq(updated_html)
+ end
+ end
end
context 'for other classes' do
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index d8f940a808e..56e0d044247 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -205,11 +205,11 @@ describe CacheableAttributes do
end
end
- it 'uses RequestStore in addition to Thread memory cache', :request_store do
+ it 'uses RequestStore in addition to process memory cache', :request_store do
# Warm up the cache
create(:application_setting).cache!
- expect(ApplicationSetting.cache_backend).to eq(Gitlab::ThreadMemoryCache.cache_backend)
+ expect(ApplicationSetting.cache_backend).to eq(Gitlab::ProcessMemoryCache.cache_backend)
expect(ApplicationSetting.cache_backend).to receive(:read).with(ApplicationSetting.cache_key).once.and_call_original
2.times { ApplicationSetting.current }
diff --git a/spec/models/concerns/has_user_type_spec.rb b/spec/models/concerns/has_user_type_spec.rb
new file mode 100644
index 00000000000..f12eee414f9
--- /dev/null
+++ b/spec/models/concerns/has_user_type_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe User do
+ specify 'types consistency checks', :aggregate_failures do
+ expect(described_class::USER_TYPES.keys)
+ .to match_array(%w[human ghost alert_bot project_bot support_bot service_user visual_review_bot migration_bot])
+ expect(described_class::USER_TYPES).to include(*described_class::BOT_USER_TYPES)
+ expect(described_class::USER_TYPES).to include(*described_class::NON_INTERNAL_USER_TYPES)
+ expect(described_class::USER_TYPES).to include(*described_class::INTERNAL_USER_TYPES)
+ end
+
+ describe 'scopes & predicates' do
+ User::USER_TYPES.keys.each do |type|
+ let_it_be(type) { create(:user, username: type, user_type: type) }
+ end
+ let(:bots) { User::BOT_USER_TYPES.map { |type| public_send(type) } }
+ let(:non_internal) { User::NON_INTERNAL_USER_TYPES.map { |type| public_send(type) } }
+ let(:everyone) { User::USER_TYPES.keys.map { |type| public_send(type) } }
+
+ describe '.humans' do
+ it 'includes humans only' do
+ expect(described_class.humans).to match_array([human])
+ end
+ end
+
+ describe '.bots' do
+ it 'includes all bots' do
+ expect(described_class.bots).to match_array(bots)
+ end
+ end
+
+ describe '.bots_without_project_bot' do
+ it 'includes all bots except project_bot' do
+ expect(described_class.bots_without_project_bot).to match_array(bots - [project_bot])
+ end
+ end
+
+ describe '.non_internal' do
+ it 'includes all non_internal users' do
+ expect(described_class.non_internal).to match_array(non_internal)
+ end
+ end
+
+ describe '.without_ghosts' do
+ it 'includes everyone except ghosts' do
+ expect(described_class.without_ghosts).to match_array(everyone - [ghost])
+ end
+ end
+
+ describe '.without_project_bot' do
+ it 'includes everyone except project_bot' do
+ expect(described_class.without_project_bot).to match_array(everyone - [project_bot])
+ end
+ end
+
+ describe '#bot?' do
+ it 'is true for all bot user types and false for others' do
+ expect(bots).to all(be_bot)
+
+ (everyone - bots).each do |user|
+ expect(user).not_to be_bot
+ end
+ end
+ end
+
+ describe '#human?' do
+ it 'is true for humans only' do
+ expect(human).to be_human
+ expect(alert_bot).not_to be_human
+ expect(User.new).to be_human
+ end
+ end
+
+ describe '#internal?' do
+ it 'is true for all internal user types and false for others' do
+ expect(everyone - non_internal).to all(be_internal)
+
+ non_internal.each do |user|
+ expect(user).not_to be_internal
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 13a3d1cdd82..03fd1c69654 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -3,14 +3,17 @@
require 'spec_helper'
describe Mentionable do
- class Example
- include Mentionable
+ before do
+ stub_const('Example', Class.new)
+ Example.class_eval do
+ include Mentionable
- attr_accessor :project, :message
- attr_mentionable :message
+ attr_accessor :project, :message
+ attr_mentionable :message
- def author
- nil
+ def author
+ nil
+ end
end
end
@@ -28,11 +31,11 @@ describe Mentionable do
end
describe '#any_mentionable_attributes_changed?' do
- Message = Struct.new(:text)
+ message = Struct.new(:text)
let(:mentionable) { Example.new }
let(:changes) do
- msg = Message.new('test')
+ msg = message.new('test')
changes = {}
changes[msg] = ['', 'some message']
@@ -325,3 +328,36 @@ describe Snippet, 'Mentionable' do
end
end
end
+
+describe PersonalSnippet, 'Mentionable' do
+ describe '#store_mentions!' do
+ it_behaves_like 'mentions in description', :personal_snippet
+ it_behaves_like 'mentions in notes', :personal_snippet do
+ let(:note) { create(:note_on_personal_snippet) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+
+ describe 'load mentions' do
+ it_behaves_like 'load mentions from DB', :personal_snippet do
+ let(:note) { create(:note_on_personal_snippet) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+end
+
+describe DesignManagement::Design do
+ describe '#store_mentions!' do
+ it_behaves_like 'mentions in notes', :design do
+ let(:note) { create(:diff_note_on_design) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+
+ describe 'load mentions' do
+ it_behaves_like 'load mentions from DB', :design do
+ let(:note) { create(:diff_note_on_design) }
+ let(:mentionable) { note.noteable }
+ end
+ end
+end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 097bc24d90f..5c8c5425ca7 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -241,7 +241,7 @@ describe Noteable do
describe '.resolvable_types' do
it 'exposes the replyable types' do
- expect(described_class.resolvable_types).to include('MergeRequest')
+ expect(described_class.resolvable_types).to include('MergeRequest', 'DesignManagement::Design')
end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 96a9c317fb8..cfca383e0b0 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -6,39 +6,47 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
include ExclusiveLeaseHelpers
include ReactiveCachingHelpers
- class CacheTest
- include ReactiveCaching
+ let(:cache_class_test) do
+ Class.new do
+ include ReactiveCaching
- self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
+ self.reactive_cache_key = ->(thing) { ["foo", thing.id] }
- self.reactive_cache_lifetime = 5.minutes
- self.reactive_cache_refresh_interval = 15.seconds
+ self.reactive_cache_lifetime = 5.minutes
+ self.reactive_cache_refresh_interval = 15.seconds
- attr_reader :id
+ attr_reader :id
- def self.primary_key
- :id
- end
+ def self.primary_key
+ :id
+ end
- def initialize(id, &blk)
- @id = id
- @calculator = blk
- end
+ def initialize(id, &blk)
+ @id = id
+ @calculator = blk
+ end
- def calculate_reactive_cache
- @calculator.call
- end
+ def calculate_reactive_cache
+ @calculator.call
+ end
- def result
- with_reactive_cache do |data|
- data
+ def result
+ with_reactive_cache do |data|
+ data
+ end
end
end
end
+ let(:external_dependency_cache_class_test) do
+ Class.new(cache_class_test) do
+ self.reactive_cache_work_type = :external_dependency
+ end
+ end
+
let(:calculation) { -> { 2 + 2 } }
let(:cache_key) { "foo:666" }
- let(:instance) { CacheTest.new(666, &calculation) }
+ let(:instance) { cache_class_test.new(666, &calculation) }
describe '#with_reactive_cache' do
before do
@@ -47,6 +55,18 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
subject(:go!) { instance.result }
+ shared_examples 'reactive worker call' do |worker_class|
+ let(:instance) do
+ test_class.new(666, &calculation)
+ end
+
+ it 'performs caching with correct worker' do
+ expect(worker_class).to receive(:perform_async).with(test_class, 666)
+
+ go!
+ end
+ end
+
shared_examples 'a cacheable value' do |cached_value|
before do
stub_reactive_cache(instance, cached_value)
@@ -73,10 +93,12 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it { is_expected.to be_nil }
- it 'refreshes cache' do
- expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
+ it_behaves_like 'reactive worker call', ReactiveCachingWorker do
+ let(:test_class) { cache_class_test }
+ end
- instance.with_reactive_cache { raise described_class::InvalidateReactiveCache }
+ it_behaves_like 'reactive worker call', ExternalServiceReactiveCachingWorker do
+ let(:test_class) { external_dependency_cache_class_test }
end
end
end
@@ -84,10 +106,12 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
context 'when cache is empty' do
it { is_expected.to be_nil }
- it 'enqueues a background worker to bootstrap the cache' do
- expect(ReactiveCachingWorker).to receive(:perform_async).with(CacheTest, 666)
+ it_behaves_like 'reactive worker call', ReactiveCachingWorker do
+ let(:test_class) { cache_class_test }
+ end
- go!
+ it_behaves_like 'reactive worker call', ExternalServiceReactiveCachingWorker do
+ let(:test_class) { external_dependency_cache_class_test }
end
it 'updates the cache lifespan' do
@@ -168,12 +192,14 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
context 'with custom reactive_cache_worker_finder' do
let(:args) { %w(arg1 arg2) }
- let(:instance) { CustomFinderCacheTest.new(666, &calculation) }
+ let(:instance) { custom_finder_cache_test.new(666, &calculation) }
- class CustomFinderCacheTest < CacheTest
- self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+ let(:custom_finder_cache_test) do
+ Class.new(cache_class_test) do
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
- def self.from_cache(*args); end
+ def self.from_cache(*args); end
+ end
end
before do
@@ -234,6 +260,18 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
go!
end
+ context 'when :external_dependency cache' do
+ let(:instance) do
+ external_dependency_cache_class_test.new(666, &calculation)
+ end
+
+ it 'enqueues a repeat worker' do
+ expect_reactive_cache_update_queued(instance, worker_klass: ExternalServiceReactiveCachingWorker)
+
+ go!
+ end
+ end
+
it 'calls a reactive_cache_updated only once if content did not change on subsequent update' do
expect(instance).to receive(:calculate_reactive_cache).twice
expect(instance).to receive(:reactive_cache_updated).once
@@ -262,7 +300,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
it_behaves_like 'ExceededReactiveCacheLimit'
context 'when reactive_cache_hard_limit is overridden' do
- let(:test_class) { Class.new(CacheTest) { self.reactive_cache_hard_limit = 3.megabytes } }
+ let(:test_class) { Class.new(cache_class_test) { self.reactive_cache_hard_limit = 3.megabytes } }
let(:instance) { test_class.new(666, &calculation) }
it_behaves_like 'successful cache'
diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb
index f88d64e2013..1cf6afcc167 100644
--- a/spec/models/concerns/redis_cacheable_spec.rb
+++ b/spec/models/concerns/redis_cacheable_spec.rb
@@ -31,7 +31,7 @@ describe RedisCacheable do
subject { instance.cached_attribute(payload.each_key.first) }
it 'gets the cache attribute' do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:get).with(cache_key)
.and_return(payload.to_json)
end
@@ -44,7 +44,7 @@ describe RedisCacheable do
subject { instance.cache_attributes(payload) }
it 'sets the cache attributes' do
- Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Redis::Cache.with do |redis|
expect(redis).to receive(:set).with(cache_key, payload.to_json, anything)
end
@@ -52,7 +52,7 @@ describe RedisCacheable do
end
end
- describe '#cached_attr_reader', :clean_gitlab_redis_shared_state do
+ describe '#cached_attr_reader', :clean_gitlab_redis_cache do
subject { instance.name }
before do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index b8537dd39f6..a8d27e174b7 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -39,43 +39,100 @@ describe Spammable do
describe '#invalidate_if_spam' do
using RSpec::Parameterized::TableSyntax
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ end
+
context 'when the model is spam' do
- where(:recaptcha_enabled, :error) do
- true | /solve the reCAPTCHA to proceed/
- false | /has been discarded/
+ subject { invalidate_if_spam(is_spam: true) }
+
+ it 'has an error related to spam on the model' do
+ expect(subject.errors.messages[:base]).to match_array /has been discarded/
end
+ end
- with_them do
- subject { invalidate_if_spam(true, recaptcha_enabled) }
+ context 'when the model needs recaptcha' do
+ subject { invalidate_if_spam(needs_recaptcha: true) }
- it 'has an error related to spam on the model' do
- expect(subject.errors.messages[:base]).to match_array error
- end
+ it 'has an error related to spam on the model' do
+ expect(subject.errors.messages[:base]).to match_array /solve the reCAPTCHA/
end
end
- context 'when the model is not spam' do
- [true, false].each do |enabled|
- let(:recaptcha_enabled) { enabled }
+ context 'if the model is spam and also needs recaptcha' do
+ subject { invalidate_if_spam(is_spam: true, needs_recaptcha: true) }
+
+ it 'has an error related to spam on the model' do
+ expect(subject.errors.messages[:base]).to match_array /solve the reCAPTCHA/
+ end
+ end
- subject { invalidate_if_spam(false, recaptcha_enabled) }
+ context 'when the model is not spam nor needs recaptcha' do
+ subject { invalidate_if_spam }
- it 'returns no error' do
- expect(subject.errors.messages[:base]).to be_empty
- end
+ it 'returns no error' do
+ expect(subject.errors.messages[:base]).to be_empty
end
end
- def invalidate_if_spam(is_spam, recaptcha_enabled)
- stub_application_setting(recaptcha_enabled: recaptcha_enabled)
+ context 'if recaptcha is not enabled and the model needs recaptcha' do
+ before do
+ stub_application_setting(recaptcha_enabled: false)
+ end
+
+ subject { invalidate_if_spam(needs_recaptcha: true) }
+ it 'has no errors' do
+ expect(subject.errors.messages[:base]).to match_array /has been discarded/
+ end
+ end
+
+ def invalidate_if_spam(is_spam: false, needs_recaptcha: false)
issue.tap do |i|
i.spam = is_spam
+ i.needs_recaptcha = needs_recaptcha
i.invalidate_if_spam
end
end
end
+ describe 'spam flags' do
+ before do
+ issue.spam = false
+ issue.needs_recaptcha = false
+ end
+
+ describe '#spam!' do
+ it 'adds only `spam` flag' do
+ issue.spam!
+
+ expect(issue.spam).to be_truthy
+ expect(issue.needs_recaptcha).to be_falsey
+ end
+ end
+
+ describe '#needs_recaptcha!' do
+ it 'adds `needs_recaptcha` flag' do
+ issue.needs_recaptcha!
+
+ expect(issue.spam).to be_falsey
+ expect(issue.needs_recaptcha).to be_truthy
+ end
+ end
+
+ describe '#clear_spam_flags!' do
+ it 'clears spam and recaptcha flags' do
+ issue.spam = true
+ issue.needs_recaptcha = true
+
+ issue.clear_spam_flags!
+
+ expect(issue).not_to be_spam
+ expect(issue.needs_recaptcha).to be_falsey
+ end
+ end
+ end
+
describe '#submittable_as_spam_by?' do
let(:admin) { build(:admin) }
let(:user) { build(:user) }
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 5bcd9dfd396..1eecefe5d4a 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -19,7 +19,7 @@ describe ContainerRepository do
.with(headers: { 'Accept' => ContainerRegistry::Client::ACCEPTED_TYPES.join(', ') })
.to_return(
status: 200,
- body: JSON.dump(tags: ['test_tag']),
+ body: Gitlab::Json.dump(tags: ['test_tag']),
headers: { 'Content-Type' => 'application/json' })
end
@@ -309,4 +309,14 @@ describe ContainerRepository do
it { is_expected.to eq([]) }
end
end
+
+ describe '.search_by_name' do
+ let!(:another_repository) do
+ create(:container_repository, name: 'my_foo_bar', project: project)
+ end
+
+ subject { described_class.search_by_name('my_image') }
+
+ it { is_expected.to contain_exactly(repository) }
+ end
end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 441f8265629..f6ab8e0ece6 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#code' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/group_level_spec.rb b/spec/models/cycle_analytics/group_level_spec.rb
deleted file mode 100644
index ac169ebc0cf..00000000000
--- a/spec/models/cycle_analytics/group_level_spec.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe CycleAnalytics::GroupLevel do
- let_it_be(:group) { create(:group)}
- let_it_be(:project) { create(:project, :repository, namespace: group) }
- let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
- let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
- let_it_be(:milestone) { create(:milestone, project: project) }
- let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
- let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
-
- subject { described_class.new(group: group, options: { from: from_date, current_user: user }) }
-
- describe '#permissions' do
- it 'returns true for all stages' do
- expect(subject.permissions.values.uniq).to eq([true])
- end
- end
-
- describe '#stats' do
- before do
- create_cycle(user, project, issue, mr, milestone, pipeline)
- deploy_master(user, project)
- end
-
- it 'returns medians for each stage for a specific group' do
- expect(subject.no_stats?).to eq(false)
- end
- end
-
- describe '#summary' do
- before do
- create_cycle(user, project, issue, mr, milestone, pipeline)
- deploy_master(user, project)
- end
-
- it 'returns medians for each stage for a specific group' do
- expect(subject.summary.map { |summary| summary[:value] }).to contain_exactly('0.1', '1', '1')
- end
- end
-end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 726f2f8b018..b4ab763e0e6 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#issue' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 3bd9f317ca7..6765b2e2cbc 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#plan' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 01d88bbeec9..2f2bcd63acd 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#production' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/project_level_spec.rb b/spec/models/cycle_analytics/project_level_spec.rb
index 2fc81777746..bb296351a29 100644
--- a/spec/models/cycle_analytics/project_level_spec.rb
+++ b/spec/models/cycle_analytics/project_level_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe CycleAnalytics::ProjectLevel do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let_it_be(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 50670188e85..25e8f1441d3 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#review' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index cf0695f175a..effbc7056cc 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#staging' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
subject { project_level }
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 24800aafca7..7e7ba4d9994 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -7,7 +7,7 @@ describe 'CycleAnalytics#test' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:from_date) { 10.days.ago }
- let_it_be(:user) { create(:user, :admin) }
+ let_it_be(:user) { project.owner }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
let!(:merge_request) { create_merge_request_closing_issue(user, project, issue) }
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index a2d4c046d46..819e2850644 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -72,8 +72,10 @@ describe DeployToken do
describe '#scopes' do
context 'with all the scopes' do
+ let_it_be(:deploy_token) { create(:deploy_token, :all_scopes) }
+
it 'returns scopes assigned to DeployToken' do
- expect(deploy_token.scopes).to eq([:read_repository, :read_registry])
+ expect(deploy_token.scopes).to eq(DeployToken::AVAILABLE_SCOPES)
end
end
diff --git a/spec/models/design_management/action_spec.rb b/spec/models/design_management/action_spec.rb
new file mode 100644
index 00000000000..753c31b1549
--- /dev/null
+++ b/spec/models/design_management/action_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::Action do
+ describe 'relations' do
+ it { is_expected.to belong_to(:design) }
+ it { is_expected.to belong_to(:version) }
+ end
+
+ describe 'scopes' do
+ describe '.most_recent' do
+ let_it_be(:design_a) { create(:design) }
+ let_it_be(:design_b) { create(:design) }
+ let_it_be(:design_c) { create(:design) }
+
+ let(:designs) { [design_a, design_b, design_c] }
+
+ before_all do
+ create(:design_version, designs: [design_a, design_b, design_c])
+ create(:design_version, designs: [design_a, design_b])
+ create(:design_version, designs: [design_a])
+ end
+
+ it 'finds the correct version for each design' do
+ dvs = described_class.where(design: designs)
+
+ expected = designs
+ .map(&:id)
+ .zip(dvs.order("version_id DESC").pluck(:version_id).uniq)
+
+ actual = dvs.most_recent.map { |dv| [dv.design_id, dv.version_id] }
+
+ expect(actual).to eq(expected)
+ end
+ end
+
+ describe '.up_to_version' do
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:design_b) { create(:design, issue: issue) }
+
+ # let bindings are not available in before(:all) contexts,
+ # so we need to redefine the array on each construction.
+ let_it_be(:oldest) { create(:design_version, designs: [design_a, design_b]) }
+ let_it_be(:middle) { create(:design_version, designs: [design_a, design_b]) }
+ let_it_be(:newest) { create(:design_version, designs: [design_a, design_b]) }
+
+ subject { described_class.where(design: issue.designs).up_to_version(version) }
+
+ context 'the version is nil' do
+ let(:version) { nil }
+
+ it 'returns all design_versions' do
+ is_expected.to have_attributes(size: 6)
+ end
+ end
+
+ context 'when given a Version instance' do
+ context 'the version is the most current' do
+ let(:version) { newest }
+
+ it { is_expected.to have_attributes(size: 6) }
+ end
+
+ context 'the version is the oldest' do
+ let(:version) { oldest }
+
+ it { is_expected.to have_attributes(size: 2) }
+ end
+
+ context 'the version is the middle one' do
+ let(:version) { middle }
+
+ it { is_expected.to have_attributes(size: 4) }
+ end
+ end
+
+ context 'when given a commit SHA' do
+ context 'the version is the most current' do
+ let(:version) { newest.sha }
+
+ it { is_expected.to have_attributes(size: 6) }
+ end
+
+ context 'the version is the oldest' do
+ let(:version) { oldest.sha }
+
+ it { is_expected.to have_attributes(size: 2) }
+ end
+
+ context 'the version is the middle one' do
+ let(:version) { middle.sha }
+
+ it { is_expected.to have_attributes(size: 4) }
+ end
+ end
+
+ context 'when given a String that is not a commit SHA' do
+ let(:version) { 'foo' }
+
+ it { expect { subject }.to raise_error(ArgumentError) }
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/design_action_spec.rb b/spec/models/design_management/design_action_spec.rb
new file mode 100644
index 00000000000..da4ad41dfcb
--- /dev/null
+++ b/spec/models/design_management/design_action_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::DesignAction do
+ describe 'validations' do
+ describe 'the design' do
+ let(:fail_validation) { raise_error(/design/i) }
+
+ it 'must not be nil' do
+ expect { described_class.new(nil, :create, :foo) }.to fail_validation
+ end
+ end
+
+ describe 'the action' do
+ let(:fail_validation) { raise_error(/action/i) }
+
+ it 'must not be nil' do
+ expect { described_class.new(double, nil, :foo) }.to fail_validation
+ end
+
+ it 'must be a known action' do
+ expect { described_class.new(double, :wibble, :foo) }.to fail_validation
+ end
+ end
+
+ describe 'the content' do
+ context 'content is necesary' do
+ let(:fail_validation) { raise_error(/needs content/i) }
+
+ %i[create update].each do |action|
+ it "must not be nil if the action is #{action}" do
+ expect { described_class.new(double, action, nil) }.to fail_validation
+ end
+ end
+ end
+
+ context 'content is forbidden' do
+ let(:fail_validation) { raise_error(/forbids content/i) }
+
+ it "must not be nil if the action is delete" do
+ expect { described_class.new(double, :delete, :foo) }.to fail_validation
+ end
+ end
+ end
+ end
+
+ describe '#gitaly_action' do
+ let(:path) { 'some/path/somewhere' }
+ let(:design) { OpenStruct.new(full_path: path) }
+
+ subject { described_class.new(design, action, content) }
+
+ context 'the action needs content' do
+ let(:action) { :create }
+ let(:content) { :foo }
+
+ it 'produces a good gitaly action' do
+ expect(subject.gitaly_action).to eq(
+ action: action,
+ file_path: path,
+ content: content
+ )
+ end
+ end
+
+ context 'the action forbids content' do
+ let(:action) { :delete }
+ let(:content) { nil }
+
+ it 'produces a good gitaly action' do
+ expect(subject.gitaly_action).to eq(action: action, file_path: path)
+ end
+ end
+ end
+
+ describe '#issue_id' do
+ let(:issue_id) { :foo }
+ let(:design) { OpenStruct.new(issue_id: issue_id) }
+
+ subject { described_class.new(design, :delete) }
+
+ it 'delegates to the design' do
+ expect(subject.issue_id).to eq(issue_id)
+ end
+ end
+
+ describe '#performed' do
+ let(:design) { double }
+
+ subject { described_class.new(design, :delete) }
+
+ it 'calls design#clear_version_cache when the action has been performed' do
+ expect(design).to receive(:clear_version_cache)
+
+ subject.performed
+ end
+ end
+end
diff --git a/spec/models/design_management/design_at_version_spec.rb b/spec/models/design_management/design_at_version_spec.rb
new file mode 100644
index 00000000000..f6fa8df243c
--- /dev/null
+++ b/spec/models/design_management/design_at_version_spec.rb
@@ -0,0 +1,426 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::DesignAtVersion do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue, reload: true) { create(:issue) }
+ let_it_be(:issue_b, reload: true) { create(:issue) }
+ let_it_be(:design, reload: true) { create(:design, issue: issue) }
+ let_it_be(:version) { create(:design_version, designs: [design]) }
+
+ describe '#id' do
+ subject { described_class.new(design: design, version: version) }
+
+ it 'combines design.id and version.id' do
+ expect(subject.id).to include(design.id.to_s, version.id.to_s)
+ end
+ end
+
+ describe '#==' do
+ it 'identifies objects created with the same parameters as equal' do
+ design = build_stubbed(:design, issue: issue)
+ version = build_stubbed(:design_version, designs: [design], issue: issue)
+
+ this = build_stubbed(:design_at_version, design: design, version: version)
+ other = build_stubbed(:design_at_version, design: design, version: version)
+
+ expect(this).to eq(other)
+ expect(other).to eq(this)
+ end
+
+ it 'identifies unequal objects as unequal, by virtue of their version' do
+ design = build_stubbed(:design, issue: issue)
+ version_a = build_stubbed(:design_version, designs: [design])
+ version_b = build_stubbed(:design_version, designs: [design])
+
+ this = build_stubbed(:design_at_version, design: design, version: version_a)
+ other = build_stubbed(:design_at_version, design: design, version: version_b)
+
+ expect(this).not_to eq(nil)
+ expect(this).not_to eq(design)
+ expect(this).not_to eq(other)
+ expect(other).not_to eq(this)
+ end
+
+ it 'identifies unequal objects as unequal, by virtue of their design' do
+ design_a = build_stubbed(:design, issue: issue)
+ design_b = build_stubbed(:design, issue: issue)
+ version = build_stubbed(:design_version, designs: [design_a, design_b])
+
+ this = build_stubbed(:design_at_version, design: design_a, version: version)
+ other = build_stubbed(:design_at_version, design: design_b, version: version)
+
+ expect(this).not_to eq(other)
+ expect(other).not_to eq(this)
+ end
+
+ it 'rejects objects with the same id and the wrong class' do
+ dav = build_stubbed(:design_at_version)
+
+ expect(dav).not_to eq(OpenStruct.new(id: dav.id))
+ end
+
+ it 'expects objects to be of the same type, not subtypes' do
+ subtype = Class.new(described_class)
+ dav = build_stubbed(:design_at_version)
+ other = subtype.new(design: dav.design, version: dav.version)
+
+ expect(dav).not_to eq(other)
+ end
+ end
+
+ describe 'status methods' do
+ let!(:design_a) { create(:design, issue: issue) }
+ let!(:design_b) { create(:design, issue: issue) }
+
+ let!(:version_a) do
+ create(:design_version, designs: [design_a])
+ end
+ let!(:version_b) do
+ create(:design_version, designs: [design_b])
+ end
+ let!(:version_mod) do
+ create(:design_version, modified_designs: [design_a, design_b])
+ end
+ let!(:version_c) do
+ create(:design_version, deleted_designs: [design_a])
+ end
+ let!(:version_d) do
+ create(:design_version, deleted_designs: [design_b])
+ end
+ let!(:version_e) do
+ create(:design_version, designs: [design_a])
+ end
+
+ describe 'a design before it has been created' do
+ subject { build(:design_at_version, design: design_b, version: version_a) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :not_created_yet' do
+ expect(subject).to have_attributes(status: :not_created_yet)
+ end
+ end
+
+ describe 'a design as of its creation' do
+ subject { build(:design_at_version, design: design_a, version: version_a) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+
+ describe 'a design after it has been created, but before deletion' do
+ subject { build(:design_at_version, design: design_b, version: version_c) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+
+ describe 'a design as of its modification' do
+ subject { build(:design_at_version, design: design_a, version: version_mod) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+
+ describe 'a design as of its deletion' do
+ subject { build(:design_at_version, design: design_a, version: version_c) }
+
+ it 'is deleted' do
+ expect(subject).to be_deleted
+ end
+
+ it 'has the status :deleted' do
+ expect(subject).to have_attributes(status: :deleted)
+ end
+ end
+
+ describe 'a design after its deletion' do
+ subject { build(:design_at_version, design: design_b, version: version_e) }
+
+ it 'is deleted' do
+ expect(subject).to be_deleted
+ end
+
+ it 'has the status :deleted' do
+ expect(subject).to have_attributes(status: :deleted)
+ end
+ end
+
+ describe 'a design on its recreation' do
+ subject { build(:design_at_version, design: design_a, version: version_e) }
+
+ it 'is not deleted' do
+ expect(subject).not_to be_deleted
+ end
+
+ it 'has the status :current' do
+ expect(subject).to have_attributes(status: :current)
+ end
+ end
+ end
+
+ describe 'validations' do
+ subject(:design_at_version) { build(:design_at_version) }
+
+ it { is_expected.to be_valid }
+
+ describe 'a design-at-version without a design' do
+ subject { described_class.new(design: nil, version: build(:design_version)) }
+
+ it { is_expected.to be_invalid }
+
+ it 'mentions the design in the errors' do
+ subject.valid?
+
+ expect(subject.errors[:design]).to be_present
+ end
+ end
+
+ describe 'a design-at-version without a version' do
+ subject { described_class.new(design: build(:design), version: nil) }
+
+ it { is_expected.to be_invalid }
+
+ it 'mentions the version in the errors' do
+ subject.valid?
+
+ expect(subject.errors[:version]).to be_present
+ end
+ end
+
+ describe 'design_and_version_belong_to_the_same_issue' do
+ context 'both design and version are supplied' do
+ subject(:design_at_version) { build(:design_at_version, design: design, version: version) }
+
+ context 'the design belongs to the same issue as the version' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'the design does not belong to the same issue as the version' do
+ let(:design) { create(:design) }
+ let(:version) { create(:design_version) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+
+ context 'the factory is just supplied with a design' do
+ let(:design) { create(:design) }
+
+ subject(:design_at_version) { build(:design_at_version, design: design) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'the factory is just supplied with a version' do
+ let(:version) { create(:design_version) }
+
+ subject(:design_at_version) { build(:design_at_version, version: version) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+
+ describe 'design_and_version_have_issue_id' do
+ subject(:design_at_version) { build(:design_at_version, design: design, version: version) }
+
+ context 'the design has no issue_id, because it is being imported' do
+ let(:design) { create(:design, :importing) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'the version has no issue_id, because it is being imported' do
+ let(:version) { create(:design_version, :importing) }
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'both the design and the version are being imported' do
+ let(:version) { create(:design_version, :importing) }
+ let(:design) { create(:design, :importing) }
+
+ it { is_expected.to be_invalid }
+ end
+ end
+ end
+
+ def id_of(design, version)
+ build(:design_at_version, design: design, version: version).id
+ end
+
+ describe '.instantiate' do
+ context 'when attrs are valid' do
+ subject do
+ described_class.instantiate(design: design, version: version)
+ end
+
+ it { is_expected.to be_a(described_class).and(be_valid) }
+ end
+
+ context 'when attrs are invalid' do
+ subject do
+ described_class.instantiate(
+ design: create(:design),
+ version: create(:design_version)
+ )
+ end
+
+ it 'raises a validation error' do
+ expect { subject }.to raise_error(ActiveModel::ValidationError)
+ end
+ end
+ end
+
+ describe '.lazy_find' do
+ let!(:version_a) do
+ create(:design_version, designs: create_list(:design, 3, issue: issue))
+ end
+ let!(:version_b) do
+ create(:design_version, designs: create_list(:design, 1, issue: issue))
+ end
+ let!(:version_c) do
+ create(:design_version, designs: create_list(:design, 1, issue: issue_b))
+ end
+
+ let(:id_a) { id_of(version_a.designs.first, version_a) }
+ let(:id_b) { id_of(version_a.designs.second, version_a) }
+ let(:id_c) { id_of(version_a.designs.last, version_a) }
+ let(:id_d) { id_of(version_b.designs.first, version_b) }
+ let(:id_e) { id_of(version_c.designs.first, version_c) }
+ let(:bad_id) { id_of(version_c.designs.first, version_a) }
+
+ def find(the_id)
+ described_class.lazy_find(the_id)
+ end
+
+ let(:db_calls) { 2 }
+
+ it 'issues fewer queries than the naive approach would' do
+ expect do
+ dav_a = find(id_a)
+ dav_b = find(id_b)
+ dav_c = find(id_c)
+ dav_d = find(id_d)
+ dav_e = find(id_e)
+ should_not_exist = find(bad_id)
+
+ expect(dav_a.version).to eq(version_a)
+ expect(dav_b.version).to eq(version_a)
+ expect(dav_c.version).to eq(version_a)
+ expect(dav_d.version).to eq(version_b)
+ expect(dav_e.version).to eq(version_c)
+ expect(should_not_exist).not_to be_present
+
+ expect(version_a.designs).to include(dav_a.design, dav_b.design, dav_c.design)
+ expect(version_b.designs).to include(dav_d.design)
+ expect(version_c.designs).to include(dav_e.design)
+ end.not_to exceed_query_limit(db_calls)
+ end
+ end
+
+ describe '.find' do
+ let(:results) { described_class.find(ids) }
+
+ # 2 versions, with 5 total designs on issue A, so 2*5 = 10
+ let!(:version_a) do
+ create(:design_version, designs: create_list(:design, 3, issue: issue))
+ end
+ let!(:version_b) do
+ create(:design_version, designs: create_list(:design, 2, issue: issue))
+ end
+ # 1 version, with 3 designs on issue B, so 1*3 = 3
+ let!(:version_c) do
+ create(:design_version, designs: create_list(:design, 3, issue: issue_b))
+ end
+
+ context 'invalid ids' do
+ let(:ids) do
+ version_b.designs.map { |d| id_of(d, version_c) }
+ end
+
+ describe '#count' do
+ it 'counts 0 records' do
+ expect(results.count).to eq(0)
+ end
+ end
+
+ describe '#empty?' do
+ it 'is empty' do
+ expect(results).to be_empty
+ end
+ end
+
+ describe '#to_a' do
+ it 'finds no records' do
+ expect(results.to_a).to eq([])
+ end
+ end
+ end
+
+ context 'valid ids' do
+ let(:red_herrings) { issue_b.designs.sample(2).map { |d| id_of(d, version_a) } }
+
+ let(:ids) do
+ a_ids = issue.designs.sample(2).map { |d| id_of(d, version_a) }
+ b_ids = issue.designs.sample(2).map { |d| id_of(d, version_b) }
+ c_ids = issue_b.designs.sample(2).map { |d| id_of(d, version_c) }
+
+ a_ids + b_ids + c_ids + red_herrings
+ end
+
+ before do
+ ids.size # force IDs
+ end
+
+ describe '#count' do
+ it 'counts 2 records' do
+ expect(results.count).to eq(6)
+ end
+
+ it 'issues at most two queries' do
+ expect { results.count }.not_to exceed_query_limit(2)
+ end
+ end
+
+ describe '#to_a' do
+ it 'finds 6 records' do
+ expect(results.size).to eq(6)
+ expect(results).to all(be_a(described_class))
+ end
+
+ it 'only returns records with matching IDs' do
+ expect(results.map(&:id)).to match_array(ids - red_herrings)
+ end
+
+ it 'only returns valid records' do
+ expect(results).to all(be_valid)
+ end
+
+ it 'issues at most two queries' do
+ expect { results.to_a }.not_to exceed_query_limit(2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/design_collection_spec.rb b/spec/models/design_management/design_collection_spec.rb
new file mode 100644
index 00000000000..bd48f742042
--- /dev/null
+++ b/spec/models/design_management/design_collection_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::DesignCollection do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue, reload: true) { create(:issue) }
+
+ subject(:collection) { described_class.new(issue) }
+
+ describe ".find_or_create_design!" do
+ it "finds an existing design" do
+ design = create(:design, issue: issue, filename: 'world.png')
+
+ expect(collection.find_or_create_design!(filename: 'world.png')).to eq(design)
+ end
+
+ it "creates a new design if one didn't exist" do
+ expect(issue.designs.size).to eq(0)
+
+ new_design = collection.find_or_create_design!(filename: 'world.png')
+
+ expect(issue.designs.size).to eq(1)
+ expect(new_design.filename).to eq('world.png')
+ expect(new_design.issue).to eq(issue)
+ end
+
+ it "only queries the designs once" do
+ create(:design, issue: issue, filename: 'hello.png')
+ create(:design, issue: issue, filename: 'world.jpg')
+
+ expect do
+ collection.find_or_create_design!(filename: 'hello.png')
+ collection.find_or_create_design!(filename: 'world.jpg')
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
+ describe "#versions" do
+ it "includes versions for all designs" do
+ version_1 = create(:design_version)
+ version_2 = create(:design_version)
+ other_version = create(:design_version)
+ create(:design, issue: issue, versions: [version_1])
+ create(:design, issue: issue, versions: [version_2])
+ create(:design, versions: [other_version])
+
+ expect(collection.versions).to contain_exactly(version_1, version_2)
+ end
+ end
+
+ describe "#repository" do
+ it "builds a design repository" do
+ expect(collection.repository).to be_a(DesignManagement::Repository)
+ end
+ end
+
+ describe '#designs_by_filename' do
+ let(:designs) { create_list(:design, 5, :with_file, issue: issue) }
+ let(:filenames) { designs.map(&:filename) }
+ let(:query) { subject.designs_by_filename(filenames) }
+
+ it 'finds all the designs with those filenames on this issue' do
+ expect(query).to have_attributes(size: 5)
+ end
+
+ it 'only makes a single query' do
+ designs.each(&:id)
+ expect { query }.not_to exceed_query_limit(1)
+ end
+
+ context 'some are deleted' do
+ before do
+ delete_designs(*designs.sample(2))
+ end
+
+ it 'takes deletion into account' do
+ expect(query).to have_attributes(size: 3)
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
new file mode 100644
index 00000000000..95782c1f674
--- /dev/null
+++ b/spec/models/design_management/design_spec.rb
@@ -0,0 +1,575 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::Design do
+ include DesignManagementTestHelpers
+
+ let_it_be(:issue) { create(:issue) }
+ let_it_be(:design1) { create(:design, :with_versions, issue: issue, versions_count: 1) }
+ let_it_be(:design2) { create(:design, :with_versions, issue: issue, versions_count: 1) }
+ let_it_be(:design3) { create(:design, :with_versions, issue: issue, versions_count: 1) }
+ let_it_be(:deleted_design) { create(:design, :with_versions, deleted: true) }
+
+ describe 'relations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:issue) }
+ it { is_expected.to have_many(:actions) }
+ it { is_expected.to have_many(:versions) }
+ it { is_expected.to have_many(:notes).dependent(:delete_all) }
+ it { is_expected.to have_many(:user_mentions) }
+ end
+
+ describe 'validations' do
+ subject(:design) { build(:design) }
+
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:issue) }
+ it { is_expected.to validate_presence_of(:filename) }
+ it { is_expected.to validate_uniqueness_of(:filename).scoped_to(:issue_id) }
+
+ it "validates that the extension is an image" do
+ design.filename = "thing.txt"
+ extensions = described_class::SAFE_IMAGE_EXT + described_class::DANGEROUS_IMAGE_EXT
+
+ expect(design).not_to be_valid
+ expect(design.errors[:filename].first).to eq(
+ "does not have a supported extension. Only #{extensions.to_sentence} are supported"
+ )
+ end
+
+ describe 'validating files with .svg extension' do
+ before do
+ design.filename = "thing.svg"
+ end
+
+ it "allows .svg files when feature flag is enabled" do
+ stub_feature_flags(design_management_allow_dangerous_images: true)
+
+ expect(design).to be_valid
+ end
+
+ it "does not allow .svg files when feature flag is disabled" do
+ stub_feature_flags(design_management_allow_dangerous_images: false)
+
+ expect(design).not_to be_valid
+ expect(design.errors[:filename].first).to eq(
+ "does not have a supported extension. Only #{described_class::SAFE_IMAGE_EXT.to_sentence} are supported"
+ )
+ end
+ end
+ end
+
+ describe 'scopes' do
+ describe '.visible_at_version' do
+ let(:versions) { DesignManagement::Version.where(issue: issue).ordered }
+ let(:found) { described_class.visible_at_version(version) }
+
+ context 'at oldest version' do
+ let(:version) { versions.last }
+
+ it 'finds the first design only' do
+ expect(found).to contain_exactly(design1)
+ end
+ end
+
+ context 'at version 2' do
+ let(:version) { versions.second }
+
+ it 'finds the first and second designs' do
+ expect(found).to contain_exactly(design1, design2)
+ end
+ end
+
+ context 'at latest version' do
+ let(:version) { versions.first }
+
+ it 'finds designs' do
+ expect(found).to contain_exactly(design1, design2, design3)
+ end
+ end
+
+ context 'when the argument is nil' do
+ let(:version) { nil }
+
+ it 'finds all undeleted designs' do
+ expect(found).to contain_exactly(design1, design2, design3)
+ end
+ end
+
+ describe 'one of the designs was deleted before the given version' do
+ before do
+ delete_designs(design2)
+ end
+
+ it 'is not returned' do
+ current_version = versions.first
+
+ expect(described_class.visible_at_version(current_version)).to contain_exactly(design1, design3)
+ end
+ end
+
+ context 'a re-created history' do
+ before do
+ delete_designs(design1, design2)
+ restore_designs(design1)
+ end
+
+ it 'is returned, though other deleted events are not' do
+ expect(described_class.visible_at_version(nil)).to contain_exactly(design1, design3)
+ end
+ end
+
+ # test that a design that has been modified at various points
+ # can be queried for correctly at different points in its history
+ describe 'dead or alive' do
+ let(:versions) { DesignManagement::Version.where(issue: issue).map { |v| [v, :alive] } }
+
+ before do
+ versions << [delete_designs(design1), :dead]
+ versions << [modify_designs(design2), :dead]
+ versions << [restore_designs(design1), :alive]
+ versions << [modify_designs(design3), :alive]
+ versions << [delete_designs(design1), :dead]
+ versions << [modify_designs(design2, design3), :dead]
+ versions << [restore_designs(design1), :alive]
+ end
+
+ it 'can establish the history at any point' do
+ history = versions.map(&:first).map do |v|
+ described_class.visible_at_version(v).include?(design1) ? :alive : :dead
+ end
+
+ expect(history).to eq(versions.map(&:second))
+ end
+ end
+ end
+
+ describe '.with_filename' do
+ it 'returns correct design when passed a single filename' do
+ expect(described_class.with_filename(design1.filename)).to eq([design1])
+ end
+
+ it 'returns correct designs when passed an Array of filenames' do
+ expect(
+ described_class.with_filename([design1, design2].map(&:filename))
+ ).to contain_exactly(design1, design2)
+ end
+ end
+
+ describe '.on_issue' do
+ it 'returns correct designs when passed a single issue' do
+ expect(described_class.on_issue(issue)).to match_array(issue.designs)
+ end
+
+ it 'returns correct designs when passed an Array of issues' do
+ expect(
+ described_class.on_issue([issue, deleted_design.issue])
+ ).to contain_exactly(design1, design2, design3, deleted_design)
+ end
+ end
+
+ describe '.current' do
+ it 'returns just the undeleted designs' do
+ delete_designs(design3)
+
+ expect(described_class.current).to contain_exactly(design1, design2)
+ end
+ end
+ end
+
+ describe '#visible_in?' do
+ let_it_be(:issue) { create(:issue) }
+
+ # It is expensive to re-create complex histories, so we do it once, and then
+ # assert that we can establish visibility at any given version.
+ it 'tells us when a design is visible' do
+ expected = []
+
+ first_design = create(:design, :with_versions, issue: issue, versions_count: 1)
+ prior_to_creation = first_design.versions.first
+ expected << [prior_to_creation, :not_created_yet, false]
+
+ v = modify_designs(first_design)
+ expected << [v, :not_created_yet, false]
+
+ design = create(:design, :with_versions, issue: issue, versions_count: 1)
+ created_in = design.versions.first
+ expected << [created_in, :created, true]
+
+ # The future state should not affect the result for any state, so we
+ # ensure that most states have a long future as well as a rich past
+ 2.times do
+ v = modify_designs(first_design)
+ expected << [v, :unaffected_visible, true]
+
+ v = modify_designs(design)
+ expected << [v, :modified, true]
+
+ v = modify_designs(first_design)
+ expected << [v, :unaffected_visible, true]
+
+ v = delete_designs(design)
+ expected << [v, :deleted, false]
+
+ v = modify_designs(first_design)
+ expected << [v, :unaffected_nv, false]
+
+ v = restore_designs(design)
+ expected << [v, :restored, true]
+ end
+
+ delete_designs(design) # ensure visibility is not corelated with current state
+
+ got = expected.map do |(v, sym, _)|
+ [v, sym, design.visible_in?(v)]
+ end
+
+ expect(got).to eq(expected)
+ end
+ end
+
+ describe '#to_ability_name' do
+ it { expect(described_class.new.to_ability_name).to eq('design') }
+ end
+
+ describe '#status' do
+ context 'the design is new' do
+ subject { build(:design) }
+
+ it { is_expected.to have_attributes(status: :new) }
+ end
+
+ context 'the design is current' do
+ subject { design1 }
+
+ it { is_expected.to have_attributes(status: :current) }
+ end
+
+ context 'the design has been deleted' do
+ subject { deleted_design }
+
+ it { is_expected.to have_attributes(status: :deleted) }
+ end
+ end
+
+ describe '#deleted?' do
+ context 'the design is new' do
+ let(:design) { build(:design) }
+
+ it 'is falsy' do
+ expect(design).not_to be_deleted
+ end
+ end
+
+ context 'the design is current' do
+ let(:design) { design1 }
+
+ it 'is falsy' do
+ expect(design).not_to be_deleted
+ end
+ end
+
+ context 'the design has been deleted' do
+ let(:design) { deleted_design }
+
+ it 'is truthy' do
+ expect(design).to be_deleted
+ end
+ end
+
+ context 'the design has been deleted, but was then re-created' do
+ let(:design) { create(:design, :with_versions, versions_count: 1, deleted: true) }
+
+ it 'is falsy' do
+ restore_designs(design)
+
+ expect(design).not_to be_deleted
+ end
+ end
+ end
+
+ describe "#new_design?" do
+ let(:design) { design1 }
+
+ it "is false when there are versions" do
+ expect(design1).not_to be_new_design
+ end
+
+ it "is true when there are no versions" do
+ expect(build(:design)).to be_new_design
+ end
+
+ it 'is false for deleted designs' do
+ expect(deleted_design).not_to be_new_design
+ end
+
+ it "does not cause extra queries when actions are loaded" do
+ design.actions.map(&:id)
+
+ expect { design.new_design? }.not_to exceed_query_limit(0)
+ end
+
+ it "implicitly caches values" do
+ expect do
+ design.new_design?
+ design.new_design?
+ end.not_to exceed_query_limit(1)
+ end
+
+ it "queries again when the clear_version_cache trigger has been called" do
+ expect do
+ design.new_design?
+ design.clear_version_cache
+ design.new_design?
+ end.not_to exceed_query_limit(2)
+ end
+
+ it "causes a single query when there versions are not loaded" do
+ design.reload
+
+ expect { design.new_design? }.not_to exceed_query_limit(1)
+ end
+ end
+
+ describe "#full_path" do
+ it "builds the full path for a design" do
+ design = build(:design, filename: "hello.jpg")
+ expected_path = "#{DesignManagement.designs_directory}/issue-#{design.issue.iid}/hello.jpg"
+
+ expect(design.full_path).to eq(expected_path)
+ end
+ end
+
+ describe '#diff_refs' do
+ let(:design) { create(:design, :with_file, versions_count: versions_count) }
+
+ context 'there are several versions' do
+ let(:versions_count) { 3 }
+
+ it "builds diff refs based on the first commit and it's for the design" do
+ expect(design.diff_refs.base_sha).to eq(design.versions.ordered.second.sha)
+ expect(design.diff_refs.head_sha).to eq(design.versions.ordered.first.sha)
+ end
+ end
+
+ context 'there is just one version' do
+ let(:versions_count) { 1 }
+
+ it 'builds diff refs based on the empty tree if there was only one version' do
+ design = create(:design, :with_file, versions_count: 1)
+
+ expect(design.diff_refs.base_sha).to eq(Gitlab::Git::BLANK_SHA)
+ expect(design.diff_refs.head_sha).to eq(design.diff_refs.head_sha)
+ end
+ end
+
+ it 'has no diff ref if new' do
+ design = build(:design)
+
+ expect(design.diff_refs).to be_nil
+ end
+ end
+
+ describe '#repository' do
+ it 'is a design repository' do
+ design = build(:design)
+
+ expect(design.repository).to be_a(DesignManagement::Repository)
+ end
+ end
+
+ describe '#note_etag_key' do
+ it 'returns a correct etag key' do
+ design = create(:design)
+
+ expect(design.note_etag_key).to eq(
+ ::Gitlab::Routing.url_helpers.designs_project_issue_path(design.project, design.issue, { vueroute: design.filename })
+ )
+ end
+ end
+
+ describe '#user_notes_count', :use_clean_rails_memory_store_caching do
+ let_it_be(:design) { create(:design, :with_file) }
+
+ subject { design.user_notes_count }
+
+ # Note: Cache invalidation tests are in `design_user_notes_count_service_spec.rb`
+
+ it 'returns a count of user-generated notes' do
+ create(:diff_note_on_design, noteable: design)
+
+ is_expected.to eq(1)
+ end
+
+ it 'does not count notes on other designs' do
+ second_design = create(:design, :with_file)
+ create(:diff_note_on_design, noteable: second_design)
+
+ is_expected.to eq(0)
+ end
+
+ it 'does not count system notes' do
+ create(:diff_note_on_design, system: true, noteable: design)
+
+ is_expected.to eq(0)
+ end
+ end
+
+ describe '#after_note_changed' do
+ subject { build(:design) }
+
+ it 'calls #delete_cache on DesignUserNotesCountService' do
+ expect_next_instance_of(DesignManagement::DesignUserNotesCountService) do |service|
+ expect(service).to receive(:delete_cache)
+ end
+
+ subject.after_note_changed(build(:note))
+ end
+
+ it 'does not call #delete_cache on DesignUserNotesCountService when passed a system note' do
+ expect(DesignManagement::DesignUserNotesCountService).not_to receive(:new)
+
+ subject.after_note_changed(build(:note, :system))
+ end
+ end
+
+ describe '.for_reference' do
+ let_it_be(:design_a) { create(:design) }
+ let_it_be(:design_b) { create(:design) }
+
+ it 'avoids extra queries when calling to_reference' do
+ designs = described_class.for_reference.where(id: [design_a.id, design_b.id]).to_a
+
+ expect { designs.map(&:to_reference) }.not_to exceed_query_limit(0)
+ end
+ end
+
+ describe '#to_reference' do
+ let(:namespace) { build(:namespace, path: 'sample-namespace') }
+ let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
+ let(:group) { create(:group, name: 'Group', path: 'sample-group') }
+ let(:issue) { build(:issue, iid: 1, project: project) }
+ let(:filename) { 'homescreen.jpg' }
+ let(:design) { build(:design, filename: filename, issue: issue, project: project) }
+
+ context 'when nil argument' do
+ let(:reference) { design.to_reference }
+
+ it 'uses the simple format' do
+ expect(reference).to eq "#1[homescreen.jpg]"
+ end
+
+ context 'when the filename contains spaces, hyphens, periods, single-quotes, underscores and colons' do
+ let(:filename) { %q{a complex filename: containing - _ : etc., but still 'simple'.gif} }
+
+ it 'uses the simple format' do
+ expect(reference).to eq "#1[#{filename}]"
+ end
+ end
+
+ context 'when the filename contains HTML angle brackets' do
+ let(:filename) { 'a <em>great</em> filename.jpg' }
+
+ it 'uses Base64 encoding' do
+ expect(reference).to eq "#1[base64:#{Base64.strict_encode64(filename)}]"
+ end
+ end
+
+ context 'when the filename contains quotation marks' do
+ let(:filename) { %q{a "great" filename.jpg} }
+
+ it 'uses enclosing quotes, with backslash encoding' do
+ expect(reference).to eq %q{#1["a \"great\" filename.jpg"]}
+ end
+ end
+
+ context 'when the filename contains square brackets' do
+ let(:filename) { %q{a [great] filename.jpg} }
+
+ it 'uses enclosing quotes' do
+ expect(reference).to eq %q{#1["a [great] filename.jpg"]}
+ end
+ end
+ end
+
+ context 'when full is true' do
+ it 'returns complete path to the issue' do
+ refs = [
+ design.to_reference(full: true),
+ design.to_reference(project, full: true),
+ design.to_reference(group, full: true)
+ ]
+
+ expect(refs).to all(eq 'sample-namespace/sample-project#1/designs[homescreen.jpg]')
+ end
+ end
+
+ context 'when full is false' do
+ it 'returns complete path to the issue' do
+ refs = [
+ design.to_reference(build(:project), full: false),
+ design.to_reference(group, full: false)
+ ]
+
+ expect(refs).to all(eq 'sample-namespace/sample-project#1[homescreen.jpg]')
+ end
+ end
+
+ context 'when same project argument' do
+ it 'returns bare reference' do
+ expect(design.to_reference(project)).to eq("#1[homescreen.jpg]")
+ end
+ end
+ end
+
+ describe 'reference_pattern' do
+ let(:match) { described_class.reference_pattern.match(ref) }
+ let(:ref) { design.to_reference }
+ let(:design) { build(:design, filename: filename) }
+
+ context 'simple_file_name' do
+ let(:filename) { 'simple-file-name.jpg' }
+
+ it 'matches :simple_file_name' do
+ expect(match[:simple_file_name]).to eq(filename)
+ end
+ end
+
+ context 'quoted_file_name' do
+ let(:filename) { 'simple "file" name.jpg' }
+
+ it 'matches :simple_file_name' do
+ expect(match[:escaped_filename].gsub(/\\"/, '"')).to eq(filename)
+ end
+ end
+
+ context 'Base64 name' do
+ let(:filename) { '<>.png' }
+
+ it 'matches base_64_encoded_name' do
+ expect(Base64.decode64(match[:base_64_encoded_name])).to eq(filename)
+ end
+ end
+ end
+
+ describe '.by_issue_id_and_filename' do
+ let_it_be(:issue_a) { create(:issue) }
+ let_it_be(:issue_b) { create(:issue) }
+
+ let_it_be(:design_a) { create(:design, issue: issue_a) }
+ let_it_be(:design_b) { create(:design, issue: issue_a) }
+ let_it_be(:design_c) { create(:design, issue: issue_b, filename: design_a.filename) }
+ let_it_be(:design_d) { create(:design, issue: issue_b, filename: design_b.filename) }
+
+ it_behaves_like 'a where_composite scope', :by_issue_id_and_filename do
+ let(:all_results) { [design_a, design_b, design_c, design_d] }
+ let(:first_result) { design_a }
+
+ let(:composite_ids) do
+ all_results.map { |design| { issue_id: design.issue_id, filename: design.filename } }
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/repository_spec.rb b/spec/models/design_management/repository_spec.rb
new file mode 100644
index 00000000000..996316eeec9
--- /dev/null
+++ b/spec/models/design_management/repository_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignManagement::Repository do
+ let(:project) { create(:project) }
+ let(:repository) { described_class.new(project) }
+
+ shared_examples 'returns parsed git attributes that enable LFS for all file types' do
+ it do
+ expect(subject.patterns).to be_a_kind_of(Hash)
+ expect(subject.patterns).to have_key('/designs/*')
+ expect(subject.patterns['/designs/*']).to eql(
+ { "filter" => "lfs", "diff" => "lfs", "merge" => "lfs", "text" => false }
+ )
+ end
+ end
+
+ describe "#info_attributes" do
+ subject { repository.info_attributes }
+
+ include_examples 'returns parsed git attributes that enable LFS for all file types'
+ end
+
+ describe '#attributes_at' do
+ subject { repository.attributes_at }
+
+ include_examples 'returns parsed git attributes that enable LFS for all file types'
+ end
+
+ describe '#gitattribute' do
+ it 'returns a gitattribute when path has gitattributes' do
+ expect(repository.gitattribute('/designs/file.txt', 'filter')).to eq('lfs')
+ end
+
+ it 'returns nil when path has no gitattributes' do
+ expect(repository.gitattribute('/invalid/file.txt', 'filter')).to be_nil
+ end
+ end
+
+ describe '#copy_gitattributes' do
+ it 'always returns regardless of whether given a valid or invalid ref' do
+ expect(repository.copy_gitattributes('master')).to be true
+ expect(repository.copy_gitattributes('invalid')).to be true
+ end
+ end
+
+ describe '#attributes' do
+ it 'confirms that all files are LFS enabled' do
+ %w(png zip anything).each do |filetype|
+ path = "/#{DesignManagement.designs_directory}/file.#{filetype}"
+ attributes = repository.attributes(path)
+
+ expect(attributes['filter']).to eq('lfs')
+ end
+ end
+ end
+end
diff --git a/spec/models/design_management/version_spec.rb b/spec/models/design_management/version_spec.rb
new file mode 100644
index 00000000000..ab6958ea94a
--- /dev/null
+++ b/spec/models/design_management/version_spec.rb
@@ -0,0 +1,342 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe DesignManagement::Version do
+ let_it_be(:issue) { create(:issue) }
+
+ describe 'relations' do
+ it { is_expected.to have_many(:actions) }
+ it { is_expected.to have_many(:designs).through(:actions) }
+
+ it 'constrains the designs relation correctly' do
+ design = create(:design)
+ version = create(:design_version, designs: [design])
+
+ expect { version.designs << design }.to raise_error(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'allows adding multiple versions to a single design' do
+ design = create(:design)
+ versions = create_list(:design_version, 2)
+
+ expect { versions.each { |v| design.versions << v } }
+ .not_to raise_error
+ end
+ end
+
+ describe 'validations' do
+ subject(:design_version) { build(:design_version) }
+
+ it { is_expected.to be_valid }
+ it { is_expected.to validate_presence_of(:author) }
+ it { is_expected.to validate_presence_of(:sha) }
+ it { is_expected.to validate_presence_of(:designs) }
+ it { is_expected.to validate_presence_of(:issue_id) }
+ it { is_expected.to validate_uniqueness_of(:sha).scoped_to(:issue_id).case_insensitive }
+ end
+
+ describe "scopes" do
+ let_it_be(:version_1) { create(:design_version) }
+ let_it_be(:version_2) { create(:design_version) }
+
+ describe ".for_designs" do
+ it "only returns versions related to the specified designs" do
+ _other_version = create(:design_version)
+ designs = [create(:design, versions: [version_1]),
+ create(:design, versions: [version_2])]
+
+ expect(described_class.for_designs(designs))
+ .to contain_exactly(version_1, version_2)
+ end
+ end
+
+ describe '.earlier_or_equal_to' do
+ it 'only returns versions created earlier or later than the given version' do
+ expect(described_class.earlier_or_equal_to(version_1)).to eq([version_1])
+ expect(described_class.earlier_or_equal_to(version_2)).to contain_exactly(version_1, version_2)
+ end
+
+ it 'can be passed either a DesignManagement::Version or an ID' do
+ [version_1, version_1.id].each do |arg|
+ expect(described_class.earlier_or_equal_to(arg)).to eq([version_1])
+ end
+ end
+ end
+
+ describe '.by_sha' do
+ it 'can find versions by sha' do
+ [version_1, version_2].each do |version|
+ expect(described_class.by_sha(version.sha)).to contain_exactly(version)
+ end
+ end
+ end
+ end
+
+ describe ".create_for_designs" do
+ def current_version_id(design)
+ design.send(:head_version).try(:id)
+ end
+
+ def as_actions(designs, action = :create)
+ designs.map do |d|
+ DesignManagement::DesignAction.new(d, action, action == :delete ? nil : :content)
+ end
+ end
+
+ let_it_be(:author) { create(:user) }
+ let_it_be(:design_a) { create(:design, issue: issue) }
+ let_it_be(:design_b) { create(:design, issue: issue) }
+ let_it_be(:designs) { [design_a, design_b] }
+
+ describe 'the error raised when there are no actions' do
+ let_it_be(:sha) { 'f00' }
+
+ def call_with_empty_actions
+ described_class.create_for_designs([], sha, author)
+ end
+
+ it 'raises CouldNotCreateVersion' do
+ expect { call_with_empty_actions }
+ .to raise_error(described_class::CouldNotCreateVersion)
+ end
+
+ it 'has an appropriate cause' do
+ expect { call_with_empty_actions }
+ .to raise_error(have_attributes(cause: ActiveRecord::RecordInvalid))
+ end
+
+ it 'provides extra data sentry can consume' do
+ extra_info = a_hash_including(sha: sha)
+
+ expect { call_with_empty_actions }
+ .to raise_error(have_attributes(sentry_extra_data: extra_info))
+ end
+ end
+
+ describe 'the error raised when the designs come from different issues' do
+ let_it_be(:sha) { 'f00' }
+ let_it_be(:designs) { create_list(:design, 2) }
+ let_it_be(:actions) { as_actions(designs) }
+
+ def call_with_mismatched_designs
+ described_class.create_for_designs(actions, sha, author)
+ end
+
+ it 'raises CouldNotCreateVersion' do
+ expect { call_with_mismatched_designs }
+ .to raise_error(described_class::CouldNotCreateVersion)
+ end
+
+ it 'has an appropriate cause' do
+ expect { call_with_mismatched_designs }
+ .to raise_error(have_attributes(cause: described_class::NotSameIssue))
+ end
+
+ it 'provides extra data sentry can consume' do
+ extra_info = a_hash_including(design_ids: designs.map(&:id))
+
+ expect { call_with_mismatched_designs }
+ .to raise_error(have_attributes(sentry_extra_data: extra_info))
+ end
+ end
+
+ it 'does not leave invalid versions around if creation fails' do
+ expect do
+ described_class.create_for_designs([], 'abcdef', author) rescue nil
+ end.not_to change { described_class.count }
+ end
+
+ it 'does not leave orphaned design-versions around if creation fails' do
+ actions = as_actions(designs)
+ expect do
+ described_class.create_for_designs(actions, '', author) rescue nil
+ end.not_to change { DesignManagement::Action.count }
+ end
+
+ it 'creates a version and links it to multiple designs' do
+ actions = as_actions(designs, :create)
+
+ version = described_class.create_for_designs(actions, 'abc', author)
+
+ expect(version.designs).to contain_exactly(*designs)
+ expect(designs.map(&method(:current_version_id))).to all(eq version.id)
+ end
+
+ it 'creates designs if they are new to git' do
+ actions = as_actions(designs, :create)
+
+ described_class.create_for_designs(actions, 'abc', author)
+
+ expect(designs.map(&:most_recent_action)).to all(be_creation)
+ end
+
+ it 'correctly associates the version with the issue' do
+ actions = as_actions(designs)
+
+ version = described_class.create_for_designs(actions, 'abc', author)
+
+ expect(version.issue).to eq(issue)
+ end
+
+ it 'correctly associates the version with the author' do
+ actions = as_actions(designs)
+
+ version = described_class.create_for_designs(actions, 'abc', author)
+
+ expect(version.author).to eq(author)
+ end
+
+ it 'modifies designs if git updated them' do
+ actions = as_actions(designs, :update)
+
+ described_class.create_for_designs(actions, 'abc', author)
+
+ expect(designs.map(&:most_recent_action)).to all(be_modification)
+ end
+
+ it 'deletes designs when the git action was delete' do
+ actions = as_actions(designs, :delete)
+
+ described_class.create_for_designs(actions, 'def', author)
+
+ expect(designs).to all(be_deleted)
+ end
+
+ it 're-creates designs if they are deleted' do
+ described_class.create_for_designs(as_actions(designs, :create), 'abc', author)
+ described_class.create_for_designs(as_actions(designs, :delete), 'def', author)
+
+ expect(designs).to all(be_deleted)
+
+ described_class.create_for_designs(as_actions(designs, :create), 'ghi', author)
+
+ expect(designs.map(&:most_recent_action)).to all(be_creation)
+ expect(designs).not_to include(be_deleted)
+ end
+
+ it 'changes the version of the designs' do
+ actions = as_actions([design_a])
+ described_class.create_for_designs(actions, 'before', author)
+
+ expect do
+ described_class.create_for_designs(actions, 'after', author)
+ end.to change { current_version_id(design_a) }
+ end
+ end
+
+ describe '#designs_by_event' do
+ context 'there is a single design' do
+ let_it_be(:design) { create(:design) }
+
+ shared_examples :a_correctly_categorised_design do |kind, category|
+ let_it_be(:version) { create(:design_version, kind => [design]) }
+
+ it 'returns a hash with a single key and the single design in that bucket' do
+ expect(version.designs_by_event).to eq(category => [design])
+ end
+ end
+
+ it_behaves_like :a_correctly_categorised_design, :created_designs, 'creation'
+ it_behaves_like :a_correctly_categorised_design, :modified_designs, 'modification'
+ it_behaves_like :a_correctly_categorised_design, :deleted_designs, 'deletion'
+ end
+
+ context 'there are a bunch of different designs in a variety of states' do
+ let_it_be(:version) do
+ create(:design_version,
+ created_designs: create_list(:design, 3),
+ modified_designs: create_list(:design, 4),
+ deleted_designs: create_list(:design, 5))
+ end
+
+ it 'puts them in the right buckets' do
+ expect(version.designs_by_event).to match(
+ a_hash_including(
+ 'creation' => have_attributes(size: 3),
+ 'modification' => have_attributes(size: 4),
+ 'deletion' => have_attributes(size: 5)
+ )
+ )
+ end
+
+ it 'does not suffer from N+1 queries' do
+ version.designs.map(&:id) # we don't care about the set-up queries
+ expect { version.designs_by_event }.not_to exceed_query_limit(2)
+ end
+ end
+ end
+
+ describe '#author' do
+ it 'returns the author' do
+ author = build(:user)
+ version = build(:design_version, author: author)
+
+ expect(version.author).to eq(author)
+ end
+
+ it 'returns nil if author_id is nil and version is not persisted' do
+ version = build(:design_version, author: nil)
+
+ expect(version.author).to eq(nil)
+ end
+
+ it 'retrieves author from the Commit if author_id is nil and version has been persisted' do
+ author = create(:user)
+ version = create(:design_version, :committed, author: author)
+ author.destroy
+ version.reload
+ commit = version.issue.project.design_repository.commit(version.sha)
+ commit_user = create(:user, email: commit.author_email, name: commit.author_name)
+
+ expect(version.author_id).to eq(nil)
+ expect(version.author).to eq(commit_user)
+ end
+ end
+
+ describe '#diff_refs' do
+ let(:project) { issue.project }
+
+ before do
+ expect(project.design_repository).to receive(:commit)
+ .once
+ .with(sha)
+ .and_return(commit)
+ end
+
+ subject { create(:design_version, issue: issue, sha: sha) }
+
+ context 'there is a commit in the repo by the SHA' do
+ let(:commit) { build(:commit) }
+ let(:sha) { commit.id }
+
+ it { is_expected.to have_attributes(diff_refs: commit.diff_refs) }
+
+ it 'memoizes calls to #diff_refs' do
+ expect(subject.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ context 'there is no commit in the repo by the SHA' do
+ let(:commit) { nil }
+ let(:sha) { Digest::SHA1.hexdigest("points to nothing") }
+
+ it { is_expected.to have_attributes(diff_refs: be_nil) }
+ end
+ end
+
+ describe '#reset' do
+ subject { create(:design_version, issue: issue) }
+
+ it 'removes memoized values' do
+ expect(subject).to receive(:commit).twice.and_return(nil)
+
+ subject.diff_refs
+ subject.diff_refs
+
+ subject.reset
+
+ subject.diff_refs
+ subject.diff_refs
+ end
+ end
+end
diff --git a/spec/models/design_user_mention_spec.rb b/spec/models/design_user_mention_spec.rb
new file mode 100644
index 00000000000..03c77c73c8d
--- /dev/null
+++ b/spec/models/design_user_mention_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DesignUserMention do
+ describe 'associations' do
+ it { is_expected.to belong_to(:design) }
+ it { is_expected.to belong_to(:note) }
+ end
+
+ it_behaves_like 'has user mentions'
+end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index b802c8ba506..65f06a5b270 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -287,6 +287,24 @@ describe DiffNote do
reply_diff_note.reload.diff_file
end
end
+
+ context 'when noteable is a Design' do
+ it 'does not return a diff file' do
+ diff_note = create(:diff_note_on_design)
+
+ expect(diff_note.diff_file).to be_nil
+ end
+ end
+ end
+
+ describe '#latest_diff_file' do
+ context 'when noteable is a Design' do
+ it 'does not return a diff file' do
+ diff_note = create(:diff_note_on_design)
+
+ expect(diff_note.latest_diff_file).to be_nil
+ end
+ end
end
describe "#diff_line" do
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index aa3a60b867a..f7b194abcee 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -3,8 +3,14 @@
require 'spec_helper'
describe Email do
+ describe 'modules' do
+ subject { described_class }
+
+ it { is_expected.to include_module(AsyncDeviseEmail) }
+ end
+
describe 'validations' do
- it_behaves_like 'an object with email-formated attributes', :email do
+ it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email do
subject { build(:email) }
end
end
@@ -45,4 +51,16 @@ describe Email do
expect(build(:email, user: user).username).to eq user.username
end
end
+
+ describe 'Devise emails' do
+ let!(:user) { create(:user) }
+
+ describe 'behaviour' do
+ it 'sends emails asynchronously' do
+ expect do
+ user.emails.create!(email: 'hello@hello.com')
+ end.to have_enqueued_job.on_queue('mailers')
+ end
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index d0305d878e3..c0b2a4ae984 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1311,4 +1311,25 @@ describe Environment, :use_clean_rails_memory_store_caching do
expect { environment.destroy }.to change { project.commit(deployment.ref_path) }.to(nil)
end
end
+
+ describe '.count_by_state' do
+ context 'when environments are not empty' do
+ let!(:environment1) { create(:environment, project: project, state: 'stopped') }
+ let!(:environment2) { create(:environment, project: project, state: 'available') }
+ let!(:environment3) { create(:environment, project: project, state: 'stopped') }
+
+ it 'returns the environments count grouped by state' do
+ expect(project.environments.count_by_state).to eq({ stopped: 2, available: 1 })
+ end
+
+ it 'returns the environments count grouped by state with zero value' do
+ environment2.update(state: 'stopped')
+ expect(project.environments.count_by_state).to eq({ stopped: 3, available: 0 })
+ end
+ end
+
+ it 'returns zero state counts when environments are empty' do
+ expect(project.environments.count_by_state).to eq({ stopped: 0, available: 0 })
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 3239c7a843a..ac89f8fe9e1 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -84,6 +84,21 @@ describe Event do
end
end
+ describe 'scopes' do
+ describe 'created_at' do
+ it 'can find the right event' do
+ time = 1.day.ago
+ event = create(:event, created_at: time)
+ false_positive = create(:event, created_at: 2.days.ago)
+
+ found = described_class.created_at(time)
+
+ expect(found).to include(event)
+ expect(found).not_to include(false_positive)
+ end
+ end
+ end
+
describe "Push event" do
let(:project) { create(:project, :private) }
let(:user) { project.owner }
@@ -195,11 +210,13 @@ describe Event do
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
let(:project_snippet) { create(:project_snippet, :public, project: project, author: author) }
let(:personal_snippet) { create(:personal_snippet, :public, author: author) }
+ let(:design) { create(:design, issue: issue, project: project) }
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) }
let(:note_on_project_snippet) { create(:note_on_project_snippet, author: author, noteable: project_snippet, project: project) }
let(:note_on_personal_snippet) { create(:note_on_personal_snippet, author: author, noteable: personal_snippet, project: nil) }
+ let(:note_on_design) { create(:note_on_design, author: author, noteable: design, project: project) }
let(:milestone_on_project) { create(:milestone, project: project) }
let(:event) do
described_class.new(project: project,
@@ -270,8 +287,16 @@ describe Event do
context 'private project' do
let(:project) { create(:project, :private, :repository) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member) }
+ end
end
end
end
@@ -283,6 +308,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to assignee and author', true
end
@@ -292,6 +318,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
+
include_examples 'visible to assignee and author', true
end
end
@@ -303,6 +330,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to assignee and author', true
end
@@ -312,6 +340,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
+
include_examples 'visible to assignee and author', true
end
@@ -319,8 +348,16 @@ describe Event do
let(:project) { private_project }
let(:target) { note_on_issue }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member) }
+ end
end
include_examples 'visible to assignee and author', false
@@ -345,8 +382,16 @@ describe Event do
context 'private project' do
let(:project) { private_project }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member) }
+ end
end
include_examples 'visible to assignee', false
@@ -363,16 +408,32 @@ describe Event do
context 'on public project with private issue tracker and merge requests' do
let(:project) { create(:project, :public, :issues_private, :merge_requests_private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member, :admin) }
+ end
end
end
context 'on private project' do
let(:project) { create(:project, :private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member, :admin) }
+ end
end
end
end
@@ -383,8 +444,16 @@ describe Event do
context 'on private project', :aggregate_failures do
let(:project) { create(:project, :wiki_repo) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all_except(:logged_out, :non_member, :admin) }
+ end
end
end
@@ -407,22 +476,42 @@ describe Event do
context 'on public project with private snippets' do
let(:project) { create(:project, :public, :snippets_private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ end
+ end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member) }
+ end
end
+
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
+
include_examples 'visible to author', false
end
context 'on private project' do
let(:project) { create(:project, :private) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member, :admin) }
+ end
end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:guest, :member) }
+ end
+ end
+
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
+
include_examples 'visible to author', false
end
end
@@ -433,6 +522,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to author', true
context 'on internal snippet' do
@@ -446,12 +536,47 @@ describe Event do
context 'on private snippet' do
let(:personal_snippet) { create(:personal_snippet, :private, author: author) }
- include_examples 'visibility examples' do
- let(:visibility) { visible_to_none_except(:admin) }
+ context 'when admin mode enabled', :enable_admin_mode do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:admin) }
+ end
end
+
+ context 'when admin mode disabled' do
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none }
+ end
+ end
+
include_examples 'visible to author', true
end
end
+
+ context 'design event' do
+ include DesignManagementTestHelpers
+
+ let(:target) { note_on_design }
+
+ before do
+ enable_design_management
+ end
+
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all }
+ end
+
+ include_examples 'visible to assignee and author', true
+
+ context 'the event refers to a design on a confidential issue' do
+ let(:design) { create(:design, issue: confidential_issue, project: project) }
+
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member, :admin) }
+ end
+
+ include_examples 'visible to assignee and author', true
+ end
+ end
end
describe 'wiki_page predicate scopes' do
@@ -483,6 +608,14 @@ describe Event do
expect(described_class.not_wiki_page).to match_array(non_wiki_events)
end
end
+
+ describe '.for_wiki_meta' do
+ it 'finds events for a given wiki page metadata object' do
+ event = events.select(&:wiki_page?).first
+
+ expect(described_class.for_wiki_meta(event.target)).to contain_exactly(event)
+ end
+ end
end
describe '#wiki_page and #wiki_page?' do
@@ -490,7 +623,7 @@ describe Event do
context 'for a wiki page event' do
let(:wiki_page) do
- create(:wiki_page, :with_real_page, project: project)
+ create(:wiki_page, project: project)
end
subject(:event) { create(:wiki_page_event, project: project, wiki_page: wiki_page) }
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 576ac880fca..a4e49f88115 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -24,6 +24,8 @@ describe Group do
it { is_expected.to have_many(:cluster_groups).class_name('Clusters::Group') }
it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') }
it { is_expected.to have_many(:container_repositories) }
+ it { is_expected.to have_many(:milestones) }
+ it { is_expected.to have_many(:iterations) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
@@ -553,114 +555,72 @@ describe Group do
group_access: GroupMember::DEVELOPER })
end
- context 'when feature flag share_group_with_group is enabled' do
- before do
- stub_feature_flags(share_group_with_group: true)
- end
-
- context 'with user in the group' do
- let(:user) { group_user }
-
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
- end
+ context 'with user in the group' do
+ let(:user) { group_user }
- context 'with lower group access level than max access level for share' do
- let(:user) { create(:user) }
-
- it 'returns correct access level' do
- group.add_reporter(user)
-
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
- end
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::DEVELOPER)
end
- context 'with user in the parent group' do
- let(:user) { parent_group_user }
+ context 'with lower group access level than max access level for share' do
+ let(:user) { create(:user) }
it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
- end
-
- context 'with user in the child group' do
- let(:user) { child_group_user }
+ group.add_reporter(user)
- it 'returns correct access level' do
expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::REPORTER)
end
end
+ end
- context 'unrelated project owner' do
- let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
- let!(:group) { create(:group, id: common_id) }
- let!(:unrelated_project) { create(:project, id: common_id) }
- let(:user) { unrelated_project.owner }
+ context 'with user in the parent group' do
+ let(:user) { parent_group_user }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- context 'user without accepted access request' do
- let!(:user) { create(:user) }
-
- before do
- create(:group_member, :developer, :access_request, user: user, group: group)
- end
+ context 'with user in the child group' do
+ let(:user) { child_group_user }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
- context 'when feature flag share_group_with_group is disabled' do
- before do
- stub_feature_flags(share_group_with_group: false)
- end
-
- context 'with user in the group' do
- let(:user) { group_user }
+ context 'unrelated project owner' do
+ let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
+ let!(:group) { create(:group, id: common_id) }
+ let!(:unrelated_project) { create(:project, id: common_id) }
+ let(:user) { unrelated_project.owner }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
+ end
- context 'with user in the parent group' do
- let(:user) { parent_group_user }
+ context 'user without accepted access request' do
+ let!(:user) { create(:user) }
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ before do
+ create(:group_member, :developer, :access_request, user: user, group: group)
end
- context 'with user in the child group' do
- let(:user) { child_group_user }
-
- it 'returns correct access level' do
- expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
- end
+ it 'returns correct access level' do
+ expect(shared_group_parent.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
+ expect(shared_group_child.max_member_access_for_user(user)).to eq(Gitlab::Access::NO_ACCESS)
end
end
end
@@ -672,8 +632,6 @@ describe Group do
let(:shared_group) { create(:group, :private, parent: shared_group_parent) }
before do
- stub_feature_flags(share_group_with_group: true)
-
group.add_owner(user)
create(:group_group_link, { shared_with_group: group,
@@ -701,6 +659,42 @@ describe Group do
end
end
+ describe '#members_from_self_and_ancestors_with_effective_access_level' do
+ let!(:group_parent) { create(:group, :private) }
+ let!(:group) { create(:group, :private, parent: group_parent) }
+ let!(:group_child) { create(:group, :private, parent: group) }
+
+ let!(:user) { create(:user) }
+
+ let(:parent_group_access_level) { Gitlab::Access::REPORTER }
+ let(:group_access_level) { Gitlab::Access::DEVELOPER }
+ let(:child_group_access_level) { Gitlab::Access::MAINTAINER }
+
+ before do
+ create(:group_member, user: user, group: group_parent, access_level: parent_group_access_level)
+ create(:group_member, user: user, group: group, access_level: group_access_level)
+ create(:group_member, user: user, group: group_child, access_level: child_group_access_level)
+ end
+
+ it 'returns effective access level for user' do
+ expect(group_parent.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => parent_group_access_level)
+ )
+ )
+ expect(group.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => group_access_level)
+ )
+ )
+ expect(group_child.members_from_self_and_ancestors_with_effective_access_level.as_json).to(
+ contain_exactly(
+ hash_including('user_id' => user.id, 'access_level' => child_group_access_level)
+ )
+ )
+ end
+ end
+
describe '#direct_and_indirect_members' do
let!(:group) { create(:group, :nested) }
let!(:sub_group) { create(:group, parent: group) }
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index a945f0d1516..ccf8171049d 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -11,6 +11,10 @@ describe ProjectHook do
it { is_expected.to validate_presence_of(:project) }
end
+ it_behaves_like 'includes Limitable concern' do
+ subject { build(:project_hook, project: create(: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/issue_spec.rb b/spec/models/issue_spec.rb
index e8103be0682..dd5ff3dbdde 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -7,13 +7,30 @@ describe Issue do
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
+ it { is_expected.to belong_to(:iteration) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:moved_to).class_name('Issue') }
+ it { is_expected.to have_one(:moved_from).class_name('Issue') }
it { is_expected.to belong_to(:duplicated_to).class_name('Issue') }
it { is_expected.to belong_to(:closed_by).class_name('User') }
it { is_expected.to have_many(:assignees) }
it { is_expected.to have_many(:user_mentions).class_name("IssueUserMention") }
+ it { is_expected.to have_many(:designs) }
+ it { is_expected.to have_many(:design_versions) }
it { is_expected.to have_one(:sentry_issue) }
+ it { is_expected.to have_one(:alert_management_alert) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
+
+ describe 'versions.most_recent' do
+ it 'returns the most recent version' do
+ issue = create(:issue)
+ create_list(:design_version, 2, issue: issue)
+ last_version = create(:design_version, issue: issue)
+
+ expect(issue.design_versions.most_recent).to eq(last_version)
+ end
+ end
end
describe 'modules' do
@@ -23,6 +40,8 @@ describe Issue do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
@@ -61,6 +80,18 @@ describe Issue do
end
end
+ describe '.with_alert_management_alerts' do
+ subject { described_class.with_alert_management_alerts }
+
+ it 'gets only issues with alerts' do
+ alert = create(:alert_management_alert, issue: create(:issue))
+ issue = create(:issue)
+
+ expect(subject).to contain_exactly(alert.issue)
+ expect(subject).not_to include(issue)
+ end
+ end
+
describe 'locking' do
using RSpec::Parameterized::TableSyntax
@@ -593,8 +624,15 @@ describe Issue do
context 'with an admin user' do
let(:user) { build(:admin) }
- it_behaves_like 'issue readable by user'
- it_behaves_like 'confidential issue readable by user'
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it_behaves_like 'issue readable by user'
+ it_behaves_like 'confidential issue readable by user'
+ end
+
+ context 'when admin mode is disabled' do
+ it_behaves_like 'issue not readable by user'
+ it_behaves_like 'confidential issue not readable by user'
+ end
end
context 'with an owner' do
@@ -713,13 +751,29 @@ describe Issue do
expect(issue.visible_to_user?(user)).to be_falsy
end
- it 'does not check the external webservice for admins' do
- issue = build(:issue)
- user = build(:admin)
+ context 'with an admin' do
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'does not check the external webservice' do
+ issue = build(:issue)
+ user = build(:admin)
- expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
- issue.visible_to_user?(user)
+ issue.visible_to_user?(user)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it 'checks the external service to determine if an issue is readable by the admin' do
+ project = build(:project, :public,
+ external_authorization_classification_label: 'a-label')
+ issue = build(:issue, project: project)
+ user = build(:admin)
+
+ expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
+ expect(issue.visible_to_user?(user)).to be_falsy
+ end
+ end
end
end
@@ -967,4 +1021,68 @@ describe Issue do
expect(issue.previous_updated_at).to eq(Time.new(2013, 02, 06))
end
end
+
+ describe '#design_collection' do
+ it 'returns a design collection' do
+ issue = build(:issue)
+ collection = issue.design_collection
+
+ expect(collection).to be_a(DesignManagement::DesignCollection)
+ expect(collection.issue).to eq(issue)
+ end
+ end
+
+ describe 'current designs' do
+ let(:issue) { create(:issue) }
+
+ subject { issue.designs.current }
+
+ context 'an issue has no designs' do
+ it { is_expected.to be_empty }
+ end
+
+ context 'an issue only has current designs' do
+ let!(:design_a) { create(:design, :with_file, issue: issue) }
+ let!(:design_b) { create(:design, :with_file, issue: issue) }
+ let!(:design_c) { create(:design, :with_file, issue: issue) }
+
+ it { is_expected.to include(design_a, design_b, design_c) }
+ end
+
+ context 'an issue only has deleted designs' do
+ let!(:design_a) { create(:design, :with_file, issue: issue, deleted: true) }
+ let!(:design_b) { create(:design, :with_file, issue: issue, deleted: true) }
+ let!(:design_c) { create(:design, :with_file, issue: issue, deleted: true) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'an issue has a mixture of current and deleted designs' do
+ let!(:design_a) { create(:design, :with_file, issue: issue) }
+ let!(:design_b) { create(:design, :with_file, issue: issue, deleted: true) }
+ let!(:design_c) { create(:design, :with_file, issue: issue) }
+
+ it { is_expected.to contain_exactly(design_a, design_c) }
+ end
+ end
+
+ describe '.with_label_attributes' do
+ subject { described_class.with_label_attributes(label_attributes) }
+
+ let(:label_attributes) { { title: 'hello world', description: 'hi' } }
+
+ it 'gets issues with given label attributes' do
+ label = create(:label, **label_attributes)
+ labeled_issue = create(:labeled_issue, project: label.project, labels: [label])
+
+ expect(subject).to include(labeled_issue)
+ end
+
+ it 'excludes issues without given label attributes' do
+ label = create(:label, title: 'GitLab', description: 'tanuki')
+ labeled_issue = create(:labeled_issue, project: label.project, labels: [label])
+
+ expect(subject).not_to include(labeled_issue)
+ end
+ end
end
diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb
new file mode 100644
index 00000000000..e5b7b746639
--- /dev/null
+++ b/spec/models/iteration_spec.rb
@@ -0,0 +1,170 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Iteration do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+
+ it_behaves_like 'a timebox', :iteration do
+ let(:timebox_table_name) { described_class.table_name.to_sym }
+ end
+
+ describe "#iid" do
+ it "is properly scoped on project and group" do
+ iteration1 = create(:iteration, project: project)
+ iteration2 = create(:iteration, project: project)
+ iteration3 = create(:iteration, group: group)
+ iteration4 = create(:iteration, group: group)
+ iteration5 = create(:iteration, project: project)
+
+ want = {
+ iteration1: 1,
+ iteration2: 2,
+ iteration3: 1,
+ iteration4: 2,
+ iteration5: 3
+ }
+ got = {
+ iteration1: iteration1.iid,
+ iteration2: iteration2.iid,
+ iteration3: iteration3.iid,
+ iteration4: iteration4.iid,
+ iteration5: iteration5.iid
+ }
+ expect(got).to eq(want)
+ end
+ end
+
+ context 'Validations' do
+ subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) }
+
+ describe '#dates_do_not_overlap' do
+ let_it_be(:existing_iteration) { create(:iteration, group: group, start_date: 4.days.from_now, due_date: 1.week.from_now) }
+
+ context 'when no Iteration dates overlap' do
+ let(:start_date) { 2.weeks.from_now }
+ let(:due_date) { 3.weeks.from_now }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when dates overlap' do
+ context 'same group' do
+ context 'when start_date is in range' do
+ let(:start_date) { 5.days.from_now }
+ let(:due_date) { 3.weeks.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
+ end
+ end
+
+ context 'when end_date is in range' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 6.days.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
+ end
+ end
+
+ context 'when both overlap' do
+ let(:start_date) { 5.days.from_now }
+ let(:due_date) { 6.days.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
+ end
+ end
+ end
+
+ context 'different group' do
+ let(:start_date) { 5.days.from_now }
+ let(:due_date) { 6.days.from_now }
+ let(:group) { create(:group) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+
+ describe '#future_date' do
+ context 'when dates are in the future' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 1.week.from_now }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when start_date is in the past' do
+ let(:start_date) { 1.week.ago }
+ let(:due_date) { 1.week.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:start_date]).to include('cannot be in the past')
+ end
+ end
+
+ context 'when due_date is in the past' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 1.week.ago }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:due_date]).to include('cannot be in the past')
+ end
+ end
+
+ context 'when start_date is over 500 years in the future' do
+ let(:start_date) { 501.years.from_now }
+ let(:due_date) { Time.now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:start_date]).to include('cannot be more than 500 years in the future')
+ end
+ end
+
+ context 'when due_date is over 500 years in the future' do
+ let(:start_date) { Time.now }
+ let(:due_date) { 501.years.from_now }
+
+ it 'is not valid' do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:due_date]).to include('cannot be more than 500 years in the future')
+ end
+ end
+ end
+ end
+
+ describe '.within_timeframe' do
+ let_it_be(:now) { Time.now }
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:iteration_1) { create(:iteration, project: project, start_date: now, due_date: 1.day.from_now) }
+ let_it_be(:iteration_2) { create(:iteration, project: project, start_date: 2.days.from_now, due_date: 3.days.from_now) }
+ let_it_be(:iteration_3) { create(:iteration, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
+
+ it 'returns iterations with start_date and/or end_date between timeframe' do
+ iterations = described_class.within_timeframe(2.days.from_now, 3.days.from_now)
+
+ expect(iterations).to match_array([iteration_2])
+ end
+
+ it 'returns iterations which starts before the timeframe' do
+ iterations = described_class.within_timeframe(1.day.from_now, 3.days.from_now)
+
+ expect(iterations).to match_array([iteration_1, iteration_2])
+ end
+
+ it 'returns iterations which ends after the timeframe' do
+ iterations = described_class.within_timeframe(3.days.from_now, 5.days.from_now)
+
+ expect(iterations).to match_array([iteration_2, iteration_3])
+ end
+ end
+end
diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb
index 4d91bf25b5e..99f9e035205 100644
--- a/spec/models/jira_import_state_spec.rb
+++ b/spec/models/jira_import_state_spec.rb
@@ -124,6 +124,7 @@ describe JiraImportState do
jira_import.schedule
expect(jira_import.jid).to eq('some-job-id')
+ expect(jira_import.scheduled_at).to be_within(1.second).of(Time.now)
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index eeb2350359c..a8d864ad3f0 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -241,10 +241,22 @@ describe Member do
expect(member).to be_persisted
end
- it 'sets members.created_by to the given current_user' do
- member = described_class.add_user(source, user, :maintainer, current_user: admin)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'sets members.created_by to the given admin current_user' do
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
- expect(member.created_by).to eq(admin)
+ expect(member.created_by).to eq(admin)
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ # Skipped because `Group#max_member_access_for_user` needs to be migrated to use admin mode
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/207950
+ xit 'rejects setting members.created_by to the given admin current_user' do
+ member = described_class.add_user(source, user, :maintainer, current_user: admin)
+
+ expect(member.created_by).not_to be_persisted
+ end
end
it 'sets members.expires_at to the given expires_at' do
@@ -353,7 +365,7 @@ describe Member do
end
end
- context 'when current_user can update member' do
+ context 'when current_user can update member', :enable_admin_mode do
it 'creates the member' do
expect(source.users).not_to include(user)
@@ -421,7 +433,7 @@ describe Member do
end
end
- context 'when current_user can update member' do
+ context 'when current_user can update member', :enable_admin_mode do
it 'updates the member' do
expect(source.users).to include(user)
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 016af4f269b..0839dde696a 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe MergeRequestDiff do
+ using RSpec::Parameterized::TableSyntax
+
include RepoHelpers
let(:diff_with_commits) { create(:merge_request).merge_request_diff }
@@ -125,18 +127,71 @@ describe MergeRequestDiff do
end
end
+ describe '#update_external_diff_store' do
+ let_it_be(:merge_request) { create(:merge_request) }
+
+ let(:diff) { merge_request.merge_request_diff }
+ let(:store) { diff.external_diff.object_store }
+
+ where(:change_stored_externally, :change_external_diff) do
+ false | false
+ false | true
+ true | false
+ true | true
+ end
+
+ with_them do
+ it do
+ diff.stored_externally = true if change_stored_externally
+ diff.external_diff = "new-filename" if change_external_diff
+
+ update_store = receive(:update_column).with(:external_diff_store, store)
+
+ if change_stored_externally || change_external_diff
+ expect(diff).to update_store
+ else
+ expect(diff).not_to update_store
+ end
+
+ diff.save!
+ end
+ end
+ end
+
describe '#migrate_files_to_external_storage!' do
+ let(:uploader) { ExternalDiffUploader }
+ let(:file_store) { uploader::Store::LOCAL }
+ let(:remote_store) { uploader::Store::REMOTE }
let(:diff) { create(:merge_request).merge_request_diff }
- it 'converts from in-database to external storage' do
+ it 'converts from in-database to external file storage' do
expect(diff).not_to be_stored_externally
stub_external_diffs_setting(enabled: true)
- expect(diff).to receive(:save!)
+
+ expect(diff).to receive(:save!).and_call_original
+
+ diff.migrate_files_to_external_storage!
+
+ expect(diff).to be_stored_externally
+ expect(diff.external_diff_store).to eq(file_store)
+ end
+
+ it 'converts from in-database to external object storage' do
+ expect(diff).not_to be_stored_externally
+
+ stub_external_diffs_setting(enabled: true)
+
+ # Without direct_upload: true, the files would be saved to disk, and a
+ # background job would be enqueued to move the file to object storage
+ stub_external_diffs_object_storage(uploader, direct_upload: true)
+
+ expect(diff).to receive(:save!).and_call_original
diff.migrate_files_to_external_storage!
expect(diff).to be_stored_externally
+ expect(diff.external_diff_store).to eq(remote_store)
end
it 'does nothing with an external diff' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index cbb837c139e..fc4590f7b22 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -18,6 +18,10 @@ describe MergeRequest do
it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:merge_request_diffs) }
it { is_expected.to have_many(:user_mentions).class_name("MergeRequestUserMention") }
+ it { is_expected.to belong_to(:milestone) }
+ it { is_expected.to belong_to(:iteration) }
+ it { is_expected.to have_many(:resource_milestone_events) }
+ it { is_expected.to have_many(:resource_state_events) }
context 'for forks' do
let!(:project) { create(:project) }
@@ -176,6 +180,8 @@ describe MergeRequest do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(Taskable) }
+ it { is_expected.to include_module(MilestoneEventable) }
+ it { is_expected.to include_module(StateEventable) }
it_behaves_like 'AtomicInternalId' do
let(:internal_id_attribute) { :iid }
@@ -1610,6 +1616,32 @@ describe MergeRequest do
end
end
+ describe '#has_accessibility_reports?' do
+ subject { merge_request.has_accessibility_reports? }
+
+ let(:project) { create(:project, :repository) }
+
+ context 'when head pipeline has an accessibility reports' do
+ let(:merge_request) { create(:merge_request, :with_accessibility_reports, source_project: project) }
+
+ it { is_expected.to be_truthy }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(accessibility_report_view: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when head pipeline does not have accessibility reports' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#has_coverage_reports?' do
subject { merge_request.has_coverage_reports? }
@@ -1628,6 +1660,26 @@ describe MergeRequest do
end
end
+ describe '#has_terraform_reports?' do
+ let_it_be(:project) { create(:project, :repository) }
+
+ context 'when head pipeline has terraform reports' do
+ it 'returns true' do
+ merge_request = create(:merge_request, :with_terraform_reports, source_project: project)
+
+ expect(merge_request.has_terraform_reports?).to be_truthy
+ end
+ end
+
+ context 'when head pipeline does not have terraform reports' do
+ it 'returns false' do
+ merge_request = create(:merge_request, source_project: project)
+
+ expect(merge_request.has_terraform_reports?).to be_falsey
+ end
+ end
+ end
+
describe '#calculate_reactive_cache' do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
@@ -1837,6 +1889,62 @@ describe MergeRequest do
end
end
+ describe '#compare_accessibility_reports' do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:merge_request, reload: true) { create(:merge_request, :with_accessibility_reports, source_project: project) }
+ let_it_be(:pipeline) { merge_request.head_pipeline }
+
+ subject { merge_request.compare_accessibility_reports }
+
+ context 'when head pipeline has accessibility reports' do
+ let(:job) do
+ create(:ci_build, options: { artifacts: { reports: { pa11y: ['accessibility.json'] } } }, pipeline: pipeline)
+ end
+
+ let(:artifacts_metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+ context 'when reactive cache worker is parsing results asynchronously' do
+ it 'returns parsing status' do
+ expect(subject[:status]).to eq(:parsing)
+ end
+ end
+
+ context 'when reactive cache worker is inline' do
+ before do
+ synchronous_reactive_cache(merge_request)
+ end
+
+ it 'returns parsed status' do
+ expect(subject[:status]).to eq(:parsed)
+ expect(subject[:data]).to be_present
+ end
+
+ context 'when an error occurrs' do
+ before do
+ merge_request.update!(head_pipeline: nil)
+ end
+
+ it 'returns an error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:status_reason]).to eq("This merge request does not have accessibility reports")
+ end
+ end
+
+ context 'when cached result is not latest' do
+ before do
+ allow_next_instance_of(Ci::CompareAccessibilityReportsService) do |service|
+ allow(service).to receive(:latest?).and_return(false)
+ end
+ end
+
+ it 'raises an InvalidateReactiveCache error' do
+ expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
+ end
+ end
+ end
+ end
+ end
+
describe '#all_commit_shas' do
context 'when merge request is persisted' do
let(:all_commit_shas) do
@@ -3678,41 +3786,41 @@ describe MergeRequest do
describe '#recent_visible_deployments' do
let(:merge_request) { create(:merge_request) }
- let(:environment) do
- create(:environment, project: merge_request.target_project)
- end
-
it 'returns visible deployments' do
+ envs = create_list(:environment, 3, project: merge_request.target_project)
+
created = create(
:deployment,
:created,
project: merge_request.target_project,
- environment: environment
+ environment: envs[0]
)
success = create(
:deployment,
:success,
project: merge_request.target_project,
- environment: environment
+ environment: envs[1]
)
failed = create(
:deployment,
:failed,
project: merge_request.target_project,
- environment: environment
+ environment: envs[2]
)
- merge_request.deployment_merge_requests.create!(deployment: created)
- merge_request.deployment_merge_requests.create!(deployment: success)
- merge_request.deployment_merge_requests.create!(deployment: failed)
+ merge_request_relation = MergeRequest.where(id: merge_request.id)
+ created.link_merge_requests(merge_request_relation)
+ success.link_merge_requests(merge_request_relation)
+ failed.link_merge_requests(merge_request_relation)
expect(merge_request.recent_visible_deployments).to eq([failed, success])
end
it 'only returns a limited number of deployments' do
20.times do
+ environment = create(:environment, project: merge_request.target_project)
deploy = create(
:deployment,
:success,
@@ -3720,7 +3828,7 @@ describe MergeRequest do
environment: environment
)
- merge_request.deployment_merge_requests.create!(deployment: deploy)
+ deploy.link_merge_requests(MergeRequest.where(id: merge_request.id))
end
expect(merge_request.recent_visible_deployments.count).to eq(10)
@@ -3728,40 +3836,28 @@ describe MergeRequest do
end
describe '#diffable_merge_ref?' do
- context 'diff_compare_with_head enabled' do
- context 'merge request can be merged' do
- context 'merge_to_ref is not calculated' do
- it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(false)
- end
- end
-
- context 'merge_to_ref is calculated' do
- before do
- MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
- end
-
- it 'returns true' do
- expect(subject.diffable_merge_ref?).to eq(true)
- end
+ context 'merge request can be merged' do
+ context 'merge_to_ref is not calculated' do
+ it 'returns true' do
+ expect(subject.diffable_merge_ref?).to eq(false)
end
end
- context 'merge request cannot be merged' do
- it 'returns false' do
- subject.mark_as_unchecked!
+ context 'merge_to_ref is calculated' do
+ before do
+ MergeRequests::MergeToRefService.new(subject.project, subject.author).execute(subject)
+ end
- expect(subject.diffable_merge_ref?).to eq(false)
+ it 'returns true' do
+ expect(subject.diffable_merge_ref?).to eq(true)
end
end
end
- context 'diff_compare_with_head disabled' do
- before do
- stub_feature_flags(diff_compare_with_head: { enabled: false, thing: subject.target_project })
- end
-
+ context 'merge request cannot be merged' do
it 'returns false' do
+ subject.mark_as_unchecked!
+
expect(subject.diffable_merge_ref?).to eq(false)
end
end
diff --git a/spec/models/metrics/users_starred_dashboard_spec.rb b/spec/models/metrics/users_starred_dashboard_spec.rb
new file mode 100644
index 00000000000..6cb14ae569e
--- /dev/null
+++ b/spec/models/metrics/users_starred_dashboard_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::UsersStarredDashboard do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project).inverse_of(:metrics_users_starred_dashboards) }
+ it { is_expected.to belong_to(:user).inverse_of(:metrics_users_starred_dashboards) }
+ end
+
+ describe 'validation' do
+ subject { build(:metrics_users_starred_dashboard) }
+
+ it { is_expected.to validate_presence_of(:user_id) }
+ it { is_expected.to validate_presence_of(:project_id) }
+ it { is_expected.to validate_presence_of(:dashboard_path) }
+ it { is_expected.to validate_length_of(:dashboard_path).is_at_most(255) }
+ it { is_expected.to validate_uniqueness_of(:dashboard_path).scoped_to(%i[user_id project_id]) }
+ end
+
+ context 'scopes' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:starred_dashboard_a) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: 'path_a') }
+ let_it_be(:starred_dashboard_b) { create(:metrics_users_starred_dashboard, project: project, dashboard_path: 'path_b') }
+ let_it_be(:starred_dashboard_c) { create(:metrics_users_starred_dashboard, dashboard_path: 'path_b') }
+
+ describe '#for_project' do
+ it 'selects only starred dashboards belonging to project' do
+ expect(described_class.for_project(project)).to contain_exactly starred_dashboard_a, starred_dashboard_b
+ end
+ end
+
+ describe '#for_project_dashboard' do
+ it 'selects only starred dashboards belonging to project with given dashboard path' do
+ expect(described_class.for_project_dashboard(project, 'path_b')).to contain_exactly starred_dashboard_b
+ end
+ end
+ end
+end
diff --git a/spec/models/milestone_note_spec.rb b/spec/models/milestone_note_spec.rb
index 9e77ef91bb2..aad65cf0346 100644
--- a/spec/models/milestone_note_spec.rb
+++ b/spec/models/milestone_note_spec.rb
@@ -14,5 +14,15 @@ describe MilestoneNote do
it_behaves_like 'a system note', exclude_project: true do
let(:action) { 'milestone' }
end
+
+ context 'with a remove milestone event' do
+ let(:milestone) { create(:milestone) }
+ let(:event) { create(:resource_milestone_event, action: :remove, issue: noteable, milestone: milestone) }
+
+ it 'creates the expected note' do
+ expect(subject.note_html).to include('removed milestone')
+ expect(subject.note_html).not_to include('changed milestone to')
+ end
+ end
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index ee4c35ebddd..e51108947a7 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -3,8 +3,10 @@
require 'spec_helper'
describe Milestone do
+ it_behaves_like 'a timebox', :milestone
+
describe 'MilestoneStruct#serializable_hash' do
- let(:predefined_milestone) { described_class::MilestoneStruct.new('Test Milestone', '#test', 1) }
+ let(:predefined_milestone) { described_class::TimeboxStruct.new('Test Milestone', '#test', 1) }
it 'presents the predefined milestone as a hash' do
expect(predefined_milestone.serializable_hash).to eq(
@@ -15,69 +17,11 @@ describe Milestone do
end
end
- describe 'modules' do
- context 'with a project' do
- it_behaves_like 'AtomicInternalId' do
- let(:internal_id_attribute) { :iid }
- let(:instance) { build(:milestone, project: build(:project), group: nil) }
- let(:scope) { :project }
- let(:scope_attrs) { { project: instance.project } }
- let(:usage) { :milestones }
- end
- end
-
- context 'with a group' do
- it_behaves_like 'AtomicInternalId' do
- let(:internal_id_attribute) { :iid }
- let(:instance) { build(:milestone, project: nil, group: build(:group)) }
- let(:scope) { :group }
- let(:scope_attrs) { { namespace: instance.group } }
- let(:usage) { :milestones }
- end
- end
- end
-
describe "Validation" do
before do
allow(subject).to receive(:set_iid).and_return(false)
end
- describe 'start_date' do
- it 'adds an error when start_date is greater then due_date' do
- milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:due_date]).to include("must be greater than start date")
- end
-
- it 'adds an error when start_date is greater than 9999-12-31' do
- milestone = build(:milestone, start_date: Date.new(10000, 1, 1))
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:start_date]).to include("date must not be after 9999-12-31")
- end
- end
-
- describe 'due_date' do
- it 'adds an error when due_date is greater than 9999-12-31' do
- milestone = build(:milestone, due_date: Date.new(10000, 1, 1))
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:due_date]).to include("date must not be after 9999-12-31")
- end
- end
-
- describe 'title' do
- it { is_expected.to validate_presence_of(:title) }
-
- it 'is invalid if title would be empty after sanitation' do
- milestone = build(:milestone, project: project, title: '<img src=x onerror=prompt(1)>')
-
- expect(milestone).not_to be_valid
- expect(milestone.errors[:title]).to include("can't be blank")
- end
- end
-
describe 'milestone_releases' do
let(:milestone) { build(:milestone, project: project) }
@@ -99,8 +43,6 @@ describe Milestone do
end
describe "Associations" do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to have_many(:issues) }
it { is_expected.to have_many(:releases) }
it { is_expected.to have_many(:milestone_releases) }
end
@@ -110,87 +52,6 @@ describe Milestone do
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
- describe "#title" do
- let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
-
- it "sanitizes title" do
- expect(milestone.title).to eq("foo & bar -> 2.2")
- end
- end
-
- describe '#merge_requests_enabled?' do
- context "per project" do
- it "is true for projects with MRs enabled" do
- project = create(:project, :merge_requests_enabled)
- milestone = create(:milestone, project: project)
-
- expect(milestone.merge_requests_enabled?).to be(true)
- end
-
- it "is false for projects with MRs disabled" do
- project = create(:project, :repository_enabled, :merge_requests_disabled)
- milestone = create(:milestone, project: project)
-
- expect(milestone.merge_requests_enabled?).to be(false)
- end
-
- it "is false for projects with repository disabled" do
- project = create(:project, :repository_disabled)
- milestone = create(:milestone, project: project)
-
- expect(milestone.merge_requests_enabled?).to be(false)
- end
- end
-
- context "per group" do
- let(:group) { create(:group) }
- let(:milestone) { create(:milestone, group: group) }
-
- it "is always true for groups, for performance reasons" do
- expect(milestone.merge_requests_enabled?).to be(true)
- end
- end
- end
-
- describe "unique milestone title" do
- context "per project" do
- it "does not accept the same title in a project twice" do
- new_milestone = described_class.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(:project)
- new_milestone = described_class.new(project: project, title: milestone.title)
-
- expect(new_milestone).to be_valid
- end
- end
-
- context "per group" do
- let(:group) { create(:group) }
- let(:milestone) { create(:milestone, group: group) }
-
- before do
- project.update(group: group)
- end
-
- it "does not accept the same title in a group twice" do
- new_milestone = described_class.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 = described_class.new(group: group, title: milestone.title)
-
- expect(new_milestone).not_to be_valid
- end
- end
- end
-
describe '.predefined_id?' do
it 'returns true for a predefined Milestone ID' do
expect(Milestone.predefined_id?(described_class::Upcoming.id)).to be true
@@ -619,4 +480,22 @@ describe Milestone do
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/issues/123") }
it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
end
+
+ describe '#parent' do
+ context 'with group' do
+ it 'returns the expected parent' do
+ group = create(:group)
+
+ expect(build(:milestone, group: group).parent).to eq(group)
+ end
+ end
+
+ context 'with project' do
+ it 'returns the expected parent' do
+ project = create(:project)
+
+ expect(build(:milestone, project: project).parent).to eq(project)
+ end
+ end
+ end
end
diff --git a/spec/models/namespace/root_storage_size_spec.rb b/spec/models/namespace/root_storage_size_spec.rb
new file mode 100644
index 00000000000..a8048b7f637
--- /dev/null
+++ b/spec/models/namespace/root_storage_size_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespace::RootStorageSize, type: :model do
+ let(:namespace) { create(:namespace) }
+ let(:current_size) { 50.megabytes }
+ let(:limit) { 100 }
+ let(:model) { described_class.new(namespace) }
+ let(:create_statistics) { create(:namespace_root_storage_statistics, namespace: namespace, storage_size: current_size)}
+
+ before do
+ create_statistics
+
+ stub_application_setting(namespace_storage_size_limit: limit)
+ end
+
+ describe '#above_size_limit?' do
+ subject { model.above_size_limit? }
+
+ context 'when limit is 0' do
+ let(:limit) { 0 }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when below limit' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when above limit' do
+ let(:current_size) { 101.megabytes }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ describe '#usage_ratio' do
+ subject { model.usage_ratio }
+
+ it { is_expected.to eq(0.5) }
+
+ context 'when limit is 0' do
+ let(:limit) { 0 }
+
+ it { is_expected.to eq(0) }
+ end
+
+ context 'when there are no root_storage_statistics' do
+ let(:create_statistics) { nil }
+
+ it { is_expected.to eq(0) }
+ end
+ end
+
+ describe '#current_size' do
+ subject { model.current_size }
+
+ it { is_expected.to eq(current_size) }
+ end
+
+ describe '#limit' do
+ subject { model.limit }
+
+ it { is_expected.to eq(limit.megabytes) }
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 74ec74e0def..6dd295ca915 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -105,6 +105,38 @@ describe Note do
end
end
+ describe 'callbacks' do
+ describe '#notify_after_create' do
+ it 'calls #after_note_created on the noteable' do
+ note = build(:note)
+
+ expect(note).to receive(:notify_after_create).and_call_original
+ expect(note.noteable).to receive(:after_note_created).with(note)
+
+ note.save!
+ end
+ end
+
+ describe '#notify_after_destroy' do
+ it 'calls #after_note_destroyed on the noteable' do
+ note = create(:note)
+
+ expect(note).to receive(:notify_after_destroy).and_call_original
+ expect(note.noteable).to receive(:after_note_destroyed).with(note)
+
+ note.destroy
+ end
+
+ it 'does not error if noteable is nil' do
+ note = create(:note)
+
+ expect(note).to receive(:notify_after_destroy).and_call_original
+ expect(note).to receive(:noteable).at_least(:once).and_return(nil)
+ expect { note.destroy }.not_to raise_error
+ end
+ end
+ end
+
describe "Commit notes" do
before do
allow(Gitlab::Git::KeepAround).to receive(:execute).and_call_original
@@ -751,6 +783,14 @@ describe Note do
end
end
+ describe '#for_design' do
+ it 'is true when the noteable is a design' do
+ note = build(:note, noteable: build(:design))
+
+ expect(note).to be_for_design
+ end
+ end
+
describe '#to_ability_name' do
it 'returns note' do
expect(build(:note).to_ability_name).to eq('note')
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index fa2648979e9..54747ddf525 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -620,7 +620,11 @@ describe PagesDomain do
create(:pages_domain, :letsencrypt, :with_expired_certificate)
end
- it 'contains only domains needing verification' do
+ let!(:domain_with_failed_auto_ssl) do
+ create(:pages_domain, auto_ssl_enabled: true, auto_ssl_failed: true)
+ end
+
+ it 'contains only domains needing ssl renewal' do
is_expected.to(
contain_exactly(
domain_with_user_provided_certificate_and_auto_ssl,
diff --git a/spec/models/performance_monitoring/prometheus_dashboard_spec.rb b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
new file mode 100644
index 00000000000..e6fc03a0fb6
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_dashboard_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusDashboard do
+ let(:json_content) do
+ {
+ "dashboard" => "Dashboard Title",
+ "templating" => {
+ "variables" => {
+ "variable1" => %w(value1 value2 value3)
+ }
+ },
+ "panel_groups" => [{
+ "group" => "Group Title",
+ "panels" => [{
+ "type" => "area-chart",
+ "title" => "Chart Title",
+ "y_label" => "Y-Axis",
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }]
+ }]
+ }]
+ }
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusDashboard object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusDashboard
+ expect(subject.dashboard).to eq(json_content['dashboard'])
+ expect(subject.panel_groups).to all(be_a PerformanceMonitoring::PrometheusPanelGroup)
+ end
+
+ describe 'validations' do
+ context 'when dashboard is missing' do
+ before do
+ json_content['dashboard'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when panel groups are missing' do
+ before do
+ json_content['panel_groups'] = []
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+ end
+ end
+
+ describe '.find_for' do
+ let(:project) { build_stubbed(:project) }
+ let(:user) { build_stubbed(:user) }
+ let(:environment) { build_stubbed(:environment) }
+ let(:path) { ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH }
+
+ context 'dashboard has been found' do
+ it 'uses dashboard finder to find and load dashboard data and returns dashboard instance', :aggregate_failures do
+ expect(Gitlab::Metrics::Dashboard::Finder).to receive(:find).with(project, user, environment: environment, dashboard_path: path).and_return(status: :success, dashboard: json_content)
+
+ dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
+
+ expect(dashboard_instance).to be_instance_of described_class
+ expect(dashboard_instance.environment).to be environment
+ expect(dashboard_instance.path).to be path
+ end
+ end
+
+ context 'dashboard has NOT been found' do
+ it 'returns nil' do
+ allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find).and_return(status: :error)
+
+ dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
+
+ expect(dashboard_instance).to be_nil
+ end
+ end
+ end
+
+ describe '#to_yaml' do
+ subject { prometheus_dashboard.to_yaml }
+
+ let(:prometheus_dashboard) { described_class.from_json(json_content) }
+ let(:expected_yaml) do
+ "---\npanel_groups:\n- panels:\n - metrics:\n - id: metric_of_ages\n unit: count\n label: Metric of Ages\n query: \n query_range: http_requests_total\n type: area-chart\n title: Chart Title\n y_label: Y-Axis\n weight: \n group: Group Title\n priority: \ndashboard: Dashboard Title\n"
+ end
+
+ it { is_expected.to eq(expected_yaml) }
+ end
+end
diff --git a/spec/models/performance_monitoring/prometheus_metric_spec.rb b/spec/models/performance_monitoring/prometheus_metric_spec.rb
new file mode 100644
index 00000000000..08288e5d993
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_metric_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusMetric do
+ let(:json_content) do
+ {
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusMetric object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusMetric
+ expect(subject.id).to eq(json_content['id'])
+ expect(subject.unit).to eq(json_content['unit'])
+ expect(subject.label).to eq(json_content['label'])
+ expect(subject.query_range).to eq(json_content['query_range'])
+ end
+
+ describe 'validations' do
+ context 'when unit is missing' do
+ before do
+ json_content['unit'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when query and query_range is missing' do
+ before do
+ json_content['query_range'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when query_range is missing but query is available' do
+ before do
+ json_content['query_range'] = nil
+ json_content['query'] = 'http_requests_total'
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { is_expected.to be_valid }
+ end
+ end
+ end
+end
diff --git a/spec/models/performance_monitoring/prometheus_panel_group_spec.rb b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
new file mode 100644
index 00000000000..2447bb5df94
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_panel_group_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusPanelGroup do
+ let(:json_content) do
+ {
+ "group" => "Group Title",
+ "panels" => [{
+ "type" => "area-chart",
+ "title" => "Chart Title",
+ "y_label" => "Y-Axis",
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }]
+ }]
+ }
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusPanelGroup object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusPanelGroup
+ expect(subject.group).to eq(json_content['group'])
+ expect(subject.panels).to all(be_a PerformanceMonitoring::PrometheusPanel)
+ end
+
+ describe 'validations' do
+ context 'when group is missing' do
+ before do
+ json_content['group'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when panels are missing' do
+ before do
+ json_content['panels'] = []
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+ end
+ end
+end
diff --git a/spec/models/performance_monitoring/prometheus_panel_spec.rb b/spec/models/performance_monitoring/prometheus_panel_spec.rb
new file mode 100644
index 00000000000..f5e04ec91e2
--- /dev/null
+++ b/spec/models/performance_monitoring/prometheus_panel_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PerformanceMonitoring::PrometheusPanel do
+ let(:json_content) do
+ {
+ "max_value" => 1,
+ "type" => "area-chart",
+ "title" => "Chart Title",
+ "y_label" => "Y-Axis",
+ "weight" => 1,
+ "metrics" => [{
+ "id" => "metric_of_ages",
+ "unit" => "count",
+ "label" => "Metric of Ages",
+ "query_range" => "http_requests_total"
+ }]
+ }
+ end
+
+ describe '#new' do
+ it 'accepts old schema format' do
+ expect { described_class.new(json_content) }.not_to raise_error
+ end
+
+ it 'accepts new schema format' do
+ expect { described_class.new(json_content.merge("y_axis" => { "precision" => 0 })) }.not_to raise_error
+ end
+ end
+
+ describe '.from_json' do
+ subject { described_class.from_json(json_content) }
+
+ it 'creates a PrometheusPanelGroup object' do
+ expect(subject).to be_a PerformanceMonitoring::PrometheusPanel
+ expect(subject.type).to eq(json_content['type'])
+ expect(subject.title).to eq(json_content['title'])
+ expect(subject.y_label).to eq(json_content['y_label'])
+ expect(subject.weight).to eq(json_content['weight'])
+ expect(subject.metrics).to all(be_a PerformanceMonitoring::PrometheusMetric)
+ end
+
+ describe 'validations' do
+ context 'when title is missing' do
+ before do
+ json_content['title'] = nil
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+
+ context 'when metrics are missing' do
+ before do
+ json_content['metrics'] = []
+ end
+
+ subject { described_class.from_json(json_content) }
+
+ it { expect { subject }.to raise_error(ActiveModel::ValidationError) }
+ end
+ end
+ end
+
+ describe '.id' do
+ it 'returns hexdigest of group_title, type and title as the panel id' do
+ group_title = 'Business Group'
+ panel_type = 'area-chart'
+ panel_title = 'New feature requests made'
+
+ expect(Digest::SHA2).to receive(:hexdigest).with("#{group_title}#{panel_type}#{panel_title}").and_return('hexdigest')
+ expect(described_class.new(title: panel_title, type: panel_type).id(group_title)).to eql 'hexdigest'
+ end
+ end
+end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index b16d1f58be5..596b11613b3 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -179,4 +179,27 @@ describe PersonalAccessToken do
end
end
end
+
+ describe '.simple_sorts' do
+ it 'includes overriden keys' do
+ expect(described_class.simple_sorts.keys).to include(*%w(expires_at_asc expires_at_desc))
+ end
+ end
+
+ describe 'ordering by expires_at' do
+ let_it_be(:earlier_token) { create(:personal_access_token, expires_at: 2.days.ago) }
+ let_it_be(:later_token) { create(:personal_access_token, expires_at: 1.day.ago) }
+
+ describe '.order_expires_at_asc' do
+ it 'returns ordered list in asc order of expiry date' do
+ expect(described_class.order_expires_at_asc).to match [earlier_token, later_token]
+ end
+ end
+
+ describe '.order_expires_at_desc' do
+ it 'returns ordered list in desc order of expiry date' do
+ expect(described_class.order_expires_at_desc).to match [later_token, earlier_token]
+ end
+ end
+ end
end
diff --git a/spec/models/personal_snippet_spec.rb b/spec/models/personal_snippet_spec.rb
index a055f107e33..fb96d6e8bc3 100644
--- a/spec/models/personal_snippet_spec.rb
+++ b/spec/models/personal_snippet_spec.rb
@@ -22,5 +22,6 @@ describe PersonalSnippet do
let(:stubbed_container) { build_stubbed(:personal_snippet) }
let(:expected_full_path) { "@snippets/#{container.id}" }
let(:expected_web_url_path) { "snippets/#{container.id}" }
+ let(:expected_repo_url_path) { expected_web_url_path }
end
end
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
new file mode 100644
index 00000000000..1366f088623
--- /dev/null
+++ b/spec/models/plan_limits_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PlanLimits do
+ let(:plan_limits) { create(:plan_limits) }
+ let(:model) { ProjectHook }
+ let(:count) { model.count }
+
+ before do
+ create(:project_hook)
+ end
+
+ context 'without plan limits configured' do
+ describe '#exceeded?' do
+ it 'does not exceed any relation offset' do
+ expect(plan_limits.exceeded?(:project_hooks, model)).to be false
+ expect(plan_limits.exceeded?(:project_hooks, count)).to be false
+ end
+ end
+ end
+
+ context 'with plan limits configured' do
+ before do
+ plan_limits.update!(project_hooks: 2)
+ end
+
+ describe '#exceeded?' do
+ it 'does not exceed the relation offset' do
+ expect(plan_limits.exceeded?(:project_hooks, model)).to be false
+ expect(plan_limits.exceeded?(:project_hooks, count)).to be false
+ end
+ end
+
+ context 'with boundary values' do
+ before do
+ create(:project_hook)
+ end
+
+ describe '#exceeded?' do
+ it 'does exceed the relation offset' do
+ expect(plan_limits.exceeded?(:project_hooks, model)).to be true
+ expect(plan_limits.exceeded?(:project_hooks, count)).to be true
+ end
+ end
+ end
+ end
+
+ context 'validates default values' do
+ let(:columns_with_zero) do
+ %w[
+ ci_active_pipelines
+ ci_pipeline_size
+ ci_active_jobs
+ ]
+ end
+
+ it "has positive values for enabled limits" do
+ attributes = plan_limits.attributes
+ attributes = attributes.except(described_class.primary_key)
+ attributes = attributes.except(described_class.reflections.values.map(&:foreign_key))
+ attributes = attributes.except(*columns_with_zero)
+
+ expect(attributes).to all(include(be_positive))
+ end
+
+ it "has zero values for disabled limits" do
+ attributes = plan_limits.attributes
+ attributes = attributes.slice(*columns_with_zero)
+
+ expect(attributes).to all(include(be_zero))
+ end
+ end
+end
diff --git a/spec/models/plan_spec.rb b/spec/models/plan_spec.rb
new file mode 100644
index 00000000000..3f3b8046232
--- /dev/null
+++ b/spec/models/plan_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Plan do
+ describe '#default?' do
+ subject { plan.default? }
+
+ Plan.default_plans.each do |plan|
+ context "when '#{plan}'" do
+ let(:plan) { build("#{plan}_plan".to_sym) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+end
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 312cbbb0948..86115a61aa7 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -54,17 +54,5 @@ describe ProjectCiCdSetting do
expect(project.reload.ci_cd_settings.default_git_depth).to eq(0)
end
-
- context 'when feature flag :ci_set_project_default_git_depth is disabled' do
- let(:project) { create(:project) }
-
- before do
- stub_feature_flags(ci_set_project_default_git_depth: { enabled: false } )
- end
-
- it 'does not set default value for new records' do
- expect(project.ci_cd_settings.default_git_depth).to eq(nil)
- end
- end
end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 38fba5ea071..e072cc21b38 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -31,27 +31,30 @@ describe ProjectFeature do
context 'when features are disabled' do
it "returns false" do
+ update_all_project_features(project, features, ProjectFeature::DISABLED)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
- expect(project.feature_available?(:issues, user)).to eq(false)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(false), "#{feature} failed"
end
end
end
context 'when features are enabled only for team members' do
it "returns false when user is not a team member" do
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(false)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(false), "#{feature} failed"
end
end
it "returns true when user is a team member" do
project.add_developer(user)
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(true)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(true), "#{feature} failed"
end
end
@@ -60,27 +63,41 @@ describe ProjectFeature do
project = create(:project, namespace: group)
group.add_developer(user)
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(true)
+ expect(project.feature_available?(feature.to_sym, user)).to eq(true), "#{feature} failed"
end
end
- it "returns true if user is an admin" do
- user.update_attribute(:admin, true)
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it "returns true if user is an admin" do
+ user.update_attribute(:admin, true)
- features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.feature_available?(:issues, user)).to eq(true)
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
+ features.each do |feature|
+ expect(project.feature_available?(feature.to_sym, user)).to eq(true), "#{feature} failed"
+ end
+ end
+ end
+
+ context 'when admin mode is disabled' do
+ it "returns false when user is an admin" do
+ user.update_attribute(:admin, true)
+
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
+ features.each do |feature|
+ expect(project.feature_available?(feature.to_sym, user)).to eq(false), "#{feature} failed"
+ end
end
end
end
context 'when feature is enabled for everyone' do
it "returns true" do
- features.each do |feature|
- expect(project.feature_available?(:issues, user)).to eq(true)
- end
+ expect(project.feature_available?(:issues, user)).to eq(true)
end
end
@@ -117,7 +134,7 @@ describe ProjectFeature do
features.each do |feature|
field = "#{feature}_access_level".to_sym
project_feature.update_attribute(field, ProjectFeature::ENABLED)
- expect(project_feature.valid?).to be_falsy
+ expect(project_feature.valid?).to be_falsy, "#{field} failed"
end
end
end
@@ -131,7 +148,7 @@ describe ProjectFeature do
field = "#{feature}_access_level".to_sym
project_feature.update_attribute(field, ProjectFeature::PUBLIC)
- expect(project_feature.valid?).to be_falsy
+ expect(project_feature.valid?).to be_falsy, "#{field} failed"
end
end
end
@@ -140,22 +157,24 @@ describe ProjectFeature do
let(:features) { %w(wiki builds merge_requests) }
it "returns false when feature is disabled" do
+ update_all_project_features(project, features, ProjectFeature::DISABLED)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::DISABLED)
- expect(project.public_send("#{feature}_enabled?")).to eq(false)
+ expect(project.public_send("#{feature}_enabled?")).to eq(false), "#{feature} failed"
end
end
it "returns true when feature is enabled only for team members" do
+ update_all_project_features(project, features, ProjectFeature::PRIVATE)
+
features.each do |feature|
- project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::PRIVATE)
- expect(project.public_send("#{feature}_enabled?")).to eq(true)
+ expect(project.public_send("#{feature}_enabled?")).to eq(true), "#{feature} failed"
end
end
it "returns true when feature is enabled for everyone" do
features.each do |feature|
- expect(project.public_send("#{feature}_enabled?")).to eq(true)
+ expect(project.public_send("#{feature}_enabled?")).to eq(true), "#{feature} failed"
end
end
end
@@ -198,7 +217,7 @@ describe ProjectFeature do
end
describe '#public_pages?' do
- it 'returns true if Pages access controll is not enabled' do
+ it 'returns true if Pages access control is not enabled' do
stub_config(pages: { access_control: false })
project_feature = described_class.new(pages_access_level: described_class::PRIVATE)
@@ -281,7 +300,7 @@ describe ProjectFeature do
it 'raises error if feature is invalid' do
expect do
described_class.required_minimum_access_level(:foos)
- end.to raise_error
+ end.to raise_error(ArgumentError)
end
end
@@ -294,4 +313,9 @@ describe ProjectFeature do
expect(described_class.required_minimum_access_level_for_private_project(:issues)).to eq(Gitlab::Access::GUEST)
end
end
+
+ def update_all_project_features(project, features, value)
+ project_feature_attributes = features.map { |f| ["#{f}_access_level", value] }.to_h
+ project.project_feature.update(project_feature_attributes)
+ end
end
diff --git a/spec/models/project_repository_storage_move_spec.rb b/spec/models/project_repository_storage_move_spec.rb
new file mode 100644
index 00000000000..146fc13bee0
--- /dev/null
+++ b/spec/models/project_repository_storage_move_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProjectRepositoryStorageMove, type: :model do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:state) }
+ it { is_expected.to validate_presence_of(:source_storage_name) }
+ it { is_expected.to validate_presence_of(:destination_storage_name) }
+
+ context 'source_storage_name inclusion' do
+ subject { build(:project_repository_storage_move, source_storage_name: 'missing') }
+
+ it "does not allow repository storages that don't match a label in the configuration" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:source_storage_name].first).to match(/is not included in the list/)
+ end
+ end
+
+ context 'destination_storage_name inclusion' do
+ subject { build(:project_repository_storage_move, destination_storage_name: 'missing') }
+
+ it "does not allow repository storages that don't match a label in the configuration" do
+ expect(subject).not_to be_valid
+ expect(subject.errors[:destination_storage_name].first).to match(/is not included in the list/)
+ end
+ end
+ end
+
+ describe 'state transitions' do
+ using RSpec::Parameterized::TableSyntax
+
+ context 'when in the default state' do
+ subject(:storage_move) { create(:project_repository_storage_move, project: project, destination_storage_name: 'test_second_storage') }
+
+ let(:project) { create(:project) }
+
+ before do
+ stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
+ end
+
+ context 'and transits to scheduled' do
+ it 'triggers ProjectUpdateRepositoryStorageWorker' do
+ expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage', storage_move.id)
+
+ storage_move.schedule!
+ end
+ end
+
+ context 'and transits to started' do
+ it 'does not allow the transition' do
+ expect { storage_move.start! }
+ .to raise_error(StateMachines::InvalidTransition)
+ end
+ 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 e99148d1d1f..7c3e48f572a 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -55,475 +55,324 @@ describe ChatMessage::PipelineMessage do
allow(Gitlab::UrlBuilder).to receive(:build).with(args[:user]).and_return("http://example.gitlab.com/hacker")
end
- context 'when the fancy_pipeline_slack_notifications feature flag is disabled' do
- before do
- stub_feature_flags(fancy_pipeline_slack_notifications: false)
- end
+ it 'returns an empty pretext' do
+ expect(subject.pretext).to be_empty
+ end
+
+ it "returns the pipeline summary in the activity's title" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed"
+ )
+ end
- it 'returns an empty pretext' do
- expect(subject.pretext).to be_empty
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
end
- it "returns the pipeline summary in the activity's title" do
+ it "returns the summary with a 'failed' status" do
expect(subject.activity[:title]).to eq(
"Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
" of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) passed"
+ " by The Hacker (hacker) has failed"
)
end
+ end
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
-
- it "returns the summary with a 'failed' status" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) failed"
- )
- end
- end
-
- context 'when no user is provided because the pipeline was triggered by the API' do
- before do
- args[:user] = nil
- end
-
- it "returns the summary with 'API' as the username" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by API passed"
- )
- end
- end
-
- it "returns a link to the project in the activity's subtitle" do
- expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
- end
-
- it "returns the build duration in the activity's text property" do
- expect(subject.activity[:text]).to eq("in 02:00:10")
- end
-
- it "returns the user's avatar image URL in the activity's image property" do
- expect(subject.activity[:image]).to eq("http://example.com/avatar")
- end
-
- context 'when the user does not have an avatar' do
- before do
- args[:user][:avatar_url] = nil
- end
-
- it "returns an empty string in the activity's image property" do
- expect(subject.activity[:image]).to be_empty
- end
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
end
- it "returns the pipeline summary as the attachment's text property" do
- expect(subject.attachments.first[:text]).to eq(
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of branch <http://example.gitlab.com/commits/develop|develop>" \
- " by The Hacker (hacker) passed in 02:00:10"
+ it "returns the summary with a 'passed with warnings' status" do
+ expect(subject.activity[:title]).to eq(
+ "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed with warnings"
)
end
-
- it "returns 'good' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('good')
- end
-
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
-
- it "returns 'danger' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('danger')
- end
- end
-
- context 'when rendering markdown' do
- before do
- args[:markdown] = true
- end
-
- it 'returns the pipeline summary as the attachments in markdown format' do
- expect(subject.attachments).to eq(
- "[project_name](http://example.gitlab.com):" \
- " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) passed in 02:00:10"
- )
- end
- end
-
- context 'when ref type is tag' do
- before do
- args[:object_attributes][:tag] = true
- args[:object_attributes][:ref] = 'new_tag'
- end
-
- it "returns the pipeline summary in the activity's title" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of tag [new_tag](http://example.gitlab.com/-/tags/new_tag)" \
- " by The Hacker (hacker) passed"
- )
- end
-
- it "returns the pipeline summary as the attachment's text property" do
- expect(subject.attachments.first[:text]).to eq(
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of tag <http://example.gitlab.com/-/tags/new_tag|new_tag>" \
- " by The Hacker (hacker) passed in 02:00:10"
- )
- end
-
- context 'when rendering markdown' do
- before do
- args[:markdown] = true
- end
-
- it 'returns the pipeline summary as the attachments in markdown format' do
- expect(subject.attachments).to eq(
- "[project_name](http://example.gitlab.com):" \
- " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of tag [new_tag](http://example.gitlab.com/-/tags/new_tag)" \
- " by The Hacker (hacker) passed in 02:00:10"
- )
- end
- end
- end
end
- context 'when the fancy_pipeline_slack_notifications feature flag is enabled' do
+ context 'when no user is provided because the pipeline was triggered by the API' do
before do
- stub_feature_flags(fancy_pipeline_slack_notifications: true)
- end
-
- it 'returns an empty pretext' do
- expect(subject.pretext).to be_empty
+ args[:user] = nil
end
- it "returns the pipeline summary in the activity's title" do
+ it "returns the summary with 'API' as the username" do
expect(subject.activity[:title]).to eq(
"Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
" of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has passed"
+ " by API has passed"
)
end
+ end
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
+ it "returns a link to the project in the activity's subtitle" do
+ expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
+ end
- it "returns the summary with a 'failed' status" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has failed"
- )
- end
- end
+ it "returns the build duration in the activity's text property" do
+ expect(subject.activity[:text]).to eq("in 02:00:10")
+ end
- context "when the pipeline passed with warnings" do
- before do
- args[:object_attributes][:detailed_status] = 'passed with warnings'
- end
+ it "returns the user's avatar image URL in the activity's image property" do
+ expect(subject.activity[:image]).to eq("http://example.com/avatar")
+ end
- it "returns the summary with a 'passed with warnings' status" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has passed with warnings"
- )
- end
+ context 'when the user does not have an avatar' do
+ before do
+ args[:user][:avatar_url] = nil
end
- context 'when no user is provided because the pipeline was triggered by the API' do
- before do
- args[:user] = nil
- end
-
- it "returns the summary with 'API' as the username" do
- expect(subject.activity[:title]).to eq(
- "Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by API has passed"
- )
- end
+ it "returns an empty string in the activity's image property" do
+ expect(subject.activity[:image]).to be_empty
end
+ end
- it "returns a link to the project in the activity's subtitle" do
- expect(subject.activity[:subtitle]).to eq("in [project_name](http://example.gitlab.com)")
- end
+ it "returns the pipeline summary as the attachment's fallback property" do
+ expect(subject.attachments.first[:fallback]).to eq(
+ "<http://example.gitlab.com|project_name>:" \
+ " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
+ " of branch <http://example.gitlab.com/commits/develop|develop>" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
+ end
- it "returns the build duration in the activity's text property" do
- expect(subject.activity[:text]).to eq("in 02:00:10")
- end
+ it "returns 'good' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('good')
+ end
- it "returns the user's avatar image URL in the activity's image property" do
- expect(subject.activity[:image]).to eq("http://example.com/avatar")
+ context "when the pipeline failed" do
+ before do
+ args[:object_attributes][:status] = 'failed'
end
- context 'when the user does not have an avatar' do
- before do
- args[:user][:avatar_url] = nil
- end
-
- it "returns an empty string in the activity's image property" do
- expect(subject.activity[:image]).to be_empty
- end
+ it "returns 'danger' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('danger')
end
+ end
- it "returns the pipeline summary as the attachment's fallback property" do
- expect(subject.attachments.first[:fallback]).to eq(
- "<http://example.gitlab.com|project_name>:" \
- " Pipeline <http://example.gitlab.com/pipelines/123|#123>" \
- " of branch <http://example.gitlab.com/commits/develop|develop>" \
- " by The Hacker (hacker) has passed in 02:00:10"
- )
+ context "when the pipeline passed with warnings" do
+ before do
+ args[:object_attributes][:detailed_status] = 'passed with warnings'
end
- it "returns 'good' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('good')
+ it "returns 'warning' as the attachment's color property" do
+ expect(subject.attachments.first[:color]).to eq('warning')
end
+ end
- context "when the pipeline failed" do
- before do
- args[:object_attributes][:status] = 'failed'
- end
+ it "returns the committer's name and username as the attachment's author_name property" do
+ expect(subject.attachments.first[:author_name]).to eq('The Hacker (hacker)')
+ end
- it "returns 'danger' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('danger')
- end
- end
+ it "returns the committer's avatar URL as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to eq('http://example.com/avatar')
+ end
- context "when the pipeline passed with warnings" do
- before do
- args[:object_attributes][:detailed_status] = 'passed with warnings'
- end
+ it "returns the committer's GitLab profile URL as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to eq('http://example.gitlab.com/hacker')
+ end
- it "returns 'warning' as the attachment's color property" do
- expect(subject.attachments.first[:color]).to eq('warning')
- end
+ context 'when no user is provided because the pipeline was triggered by the API' do
+ before do
+ args[:user] = nil
end
it "returns the committer's name and username as the attachment's author_name property" do
- expect(subject.attachments.first[:author_name]).to eq('The Hacker (hacker)')
+ expect(subject.attachments.first[:author_name]).to eq('API')
end
- it "returns the committer's avatar URL as the attachment's author_icon property" do
- expect(subject.attachments.first[:author_icon]).to eq('http://example.com/avatar')
+ it "returns nil as the attachment's author_icon property" do
+ expect(subject.attachments.first[:author_icon]).to be_nil
end
- it "returns the committer's GitLab profile URL as the attachment's author_link property" do
- expect(subject.attachments.first[:author_link]).to eq('http://example.gitlab.com/hacker')
+ it "returns nil as the attachment's author_link property" do
+ expect(subject.attachments.first[:author_link]).to be_nil
end
+ end
- context 'when no user is provided because the pipeline was triggered by the API' do
- before do
- args[:user] = nil
- end
+ it "returns the pipeline ID, status, and duration as the attachment's title property" do
+ expect(subject.attachments.first[:title]).to eq("Pipeline #123 has passed in 02:00:10")
+ end
- it "returns the committer's name and username as the attachment's author_name property" do
- expect(subject.attachments.first[:author_name]).to eq('API')
- end
+ it "returns the pipeline URL as the attachment's title_link property" do
+ expect(subject.attachments.first[:title_link]).to eq("http://example.gitlab.com/pipelines/123")
+ end
- it "returns nil as the attachment's author_icon property" do
- expect(subject.attachments.first[:author_icon]).to be_nil
- end
+ it "returns two attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(2)
+ end
- it "returns nil as the attachment's author_link property" do
- expect(subject.attachments.first[:author_link]).to be_nil
- end
- end
+ it "returns the commit message as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][0]).to eq({
+ title: "Branch",
+ value: "<http://example.gitlab.com/commits/develop|develop>",
+ short: true
+ })
+ end
- it "returns the pipeline ID, status, and duration as the attachment's title property" do
- expect(subject.attachments.first[:title]).to eq("Pipeline #123 has passed in 02:00:10")
- end
+ it "returns the ref name and link as the attachment's second field property" do
+ expect(subject.attachments.first[:fields][1]).to eq({
+ title: "Commit",
+ value: "<http://example.com/commit|A test commit message>",
+ short: true
+ })
+ end
- it "returns the pipeline URL as the attachment's title_link property" do
- expect(subject.attachments.first[:title_link]).to eq("http://example.gitlab.com/pipelines/123")
+ context "when a job in the pipeline fails" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "rspec", status: "failed", stage: "test" },
+ { id: 2, name: "karma", status: "success", stage: "test" }
+ ]
end
- it "returns two attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(2)
+ it "returns four attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(4)
end
- it "returns the commit message as the attachment's second field property" do
- expect(subject.attachments.first[:fields][0]).to eq({
- title: "Branch",
- value: "<http://example.gitlab.com/commits/develop|develop>",
+ it "returns the stage name and link to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stage",
+ value: "<http://example.gitlab.com/pipelines/123/failures|test>",
short: true
})
end
- it "returns the ref name and link as the attachment's second field property" do
- expect(subject.attachments.first[:fields][1]).to eq({
- title: "Commit",
- value: "<http://example.com/commit|A test commit message>",
+ it "returns the job name and link as the attachment's fourth field property" do
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed job",
+ value: "<http://example.gitlab.com/-/jobs/1|rspec>",
short: true
})
end
+ end
- context "when a job in the pipeline fails" do
- before do
- args[:builds] = [
- { id: 1, name: "rspec", status: "failed", stage: "test" },
- { id: 2, name: "karma", status: "success", stage: "test" }
- ]
- end
-
- it "returns four attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(4)
- end
-
- it "returns the stage name and link to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
- expect(subject.attachments.first[:fields][2]).to eq({
- title: "Failed stage",
- value: "<http://example.gitlab.com/pipelines/123/failures|test>",
- short: true
- })
- end
-
- it "returns the job name and link as the attachment's fourth field property" do
- expect(subject.attachments.first[:fields][3]).to eq({
- title: "Failed job",
- value: "<http://example.gitlab.com/-/jobs/1|rspec>",
- short: true
- })
+ context "when lots of jobs across multiple stages fail" do
+ before do
+ args[:builds] = (1..25).map do |i|
+ { id: i, name: "job-#{i}", status: "failed", stage: "stage-" + ((i % 3) + 1).to_s }
end
end
- context "when lots of jobs across multiple stages fail" do
- before do
- args[:builds] = (1..25).map do |i|
- { id: i, name: "job-#{i}", status: "failed", stage: "stage-" + ((i % 3) + 1).to_s }
- end
- end
+ it "returns the stage names and links to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Failed stages",
+ value: "<http://example.gitlab.com/pipelines/123/failures|stage-2>, <http://example.gitlab.com/pipelines/123/failures|stage-1>, <http://example.gitlab.com/pipelines/123/failures|stage-3>",
+ short: true
+ })
+ end
- it "returns the stage names and links to the 'Failed jobs' tab on the pipeline's page as the attachment's third field property" do
- expect(subject.attachments.first[:fields][2]).to eq({
- title: "Failed stages",
- value: "<http://example.gitlab.com/pipelines/123/failures|stage-2>, <http://example.gitlab.com/pipelines/123/failures|stage-1>, <http://example.gitlab.com/pipelines/123/failures|stage-3>",
- short: true
- })
+ it "returns the job names and links as the attachment's fourth field property" do
+ expected_jobs = 25.downto(16).map do |i|
+ "<http://example.gitlab.com/-/jobs/#{i}|job-#{i}>"
end
- it "returns the job names and links as the attachment's fourth field property" do
- expected_jobs = 25.downto(16).map do |i|
- "<http://example.gitlab.com/-/jobs/#{i}|job-#{i}>"
- end
+ expected_jobs << "and <http://example.gitlab.com/pipelines/123/failures|15 more>"
- expected_jobs << "and <http://example.gitlab.com/pipelines/123/failures|15 more>"
-
- expect(subject.attachments.first[:fields][3]).to eq({
- title: "Failed jobs",
- value: expected_jobs.join(", "),
- short: true
- })
- end
+ expect(subject.attachments.first[:fields][3]).to eq({
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ })
end
+ end
- context "when jobs succeed on retries" do
- before do
- args[:builds] = [
- { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
- { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
- { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 8, name: "job-1", status: "success", stage: "stage-1" }
- ]
- end
-
- it "do not return a job which succeeded on retry" do
- expected_jobs = [
- "<http://example.gitlab.com/-/jobs/3|job-3>",
- "<http://example.gitlab.com/-/jobs/2|job-2>"
- ]
-
- expect(subject.attachments.first[:fields][3]).to eq(
- title: "Failed jobs",
- value: expected_jobs.join(", "),
- short: true
- )
- end
+ context "when jobs succeed on retries" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
+ { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
+ { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 8, name: "job-1", status: "success", stage: "stage-1" }
+ ]
+ end
+
+ it "do not return a job which succeeded on retry" do
+ expected_jobs = [
+ "<http://example.gitlab.com/-/jobs/3|job-3>",
+ "<http://example.gitlab.com/-/jobs/2|job-2>"
+ ]
+
+ expect(subject.attachments.first[:fields][3]).to eq(
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ )
end
+ end
- context "when jobs failed even on retries" do
- before do
- args[:builds] = [
- { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
- { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
- { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
- { id: 8, name: "job-1", status: "failed", stage: "stage-1" }
- ]
- end
-
- it "returns only first instance of the failed job" do
- expected_jobs = [
- "<http://example.gitlab.com/-/jobs/3|job-3>",
- "<http://example.gitlab.com/-/jobs/2|job-2>",
- "<http://example.gitlab.com/-/jobs/1|job-1>"
- ]
-
- expect(subject.attachments.first[:fields][3]).to eq(
- title: "Failed jobs",
- value: expected_jobs.join(", "),
- short: true
- )
- end
+ context "when jobs failed even on retries" do
+ before do
+ args[:builds] = [
+ { id: 1, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 2, name: "job-2", status: "failed", stage: "stage-2" },
+ { id: 3, name: "job-3", status: "failed", stage: "stage-3" },
+ { id: 7, name: "job-1", status: "failed", stage: "stage-1" },
+ { id: 8, name: "job-1", status: "failed", stage: "stage-1" }
+ ]
+ end
+
+ it "returns only first instance of the failed job" do
+ expected_jobs = [
+ "<http://example.gitlab.com/-/jobs/3|job-3>",
+ "<http://example.gitlab.com/-/jobs/2|job-2>",
+ "<http://example.gitlab.com/-/jobs/1|job-1>"
+ ]
+
+ expect(subject.attachments.first[:fields][3]).to eq(
+ title: "Failed jobs",
+ value: expected_jobs.join(", "),
+ short: true
+ )
end
+ end
- context "when the CI config file contains a YAML error" do
- let(:has_yaml_errors) { true }
-
- it "returns three attachment fields" do
- expect(subject.attachments.first[:fields].count).to eq(3)
- end
+ context "when the CI config file contains a YAML error" do
+ let(:has_yaml_errors) { true }
- it "returns the YAML error deatils as the attachment's third field property" do
- expect(subject.attachments.first[:fields][2]).to eq({
- title: "Invalid CI config YAML file",
- value: "yaml error description here",
- short: false
- })
- end
+ it "returns three attachment fields" do
+ expect(subject.attachments.first[:fields].count).to eq(3)
end
- it "returns the project's name as the attachment's footer property" do
- expect(subject.attachments.first[:footer]).to eq("project_name")
+ it "returns the YAML error deatils as the attachment's third field property" do
+ expect(subject.attachments.first[:fields][2]).to eq({
+ title: "Invalid CI config YAML file",
+ value: "yaml error description here",
+ short: false
+ })
end
+ end
- it "returns the project's avatar URL as the attachment's footer_icon property" do
- expect(subject.attachments.first[:footer_icon]).to eq("http://example.com/project_avatar")
- end
+ it "returns the project's name as the attachment's footer property" do
+ expect(subject.attachments.first[:footer]).to eq("project_name")
+ end
- it "returns the pipeline's timestamp as the attachment's ts property" do
- expected_ts = Time.parse(args[:object_attributes][:finished_at]).to_i
- expect(subject.attachments.first[:ts]).to eq(expected_ts)
- end
+ it "returns the project's avatar URL as the attachment's footer_icon property" do
+ expect(subject.attachments.first[:footer_icon]).to eq("http://example.com/project_avatar")
+ end
- context 'when rendering markdown' do
- before do
- args[:markdown] = true
- end
+ it "returns the pipeline's timestamp as the attachment's ts property" do
+ expected_ts = Time.parse(args[:object_attributes][:finished_at]).to_i
+ expect(subject.attachments.first[:ts]).to eq(expected_ts)
+ end
- it 'returns the pipeline summary as the attachments in markdown format' do
- expect(subject.attachments).to eq(
- "[project_name](http://example.gitlab.com):" \
- " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
- " of branch [develop](http://example.gitlab.com/commits/develop)" \
- " by The Hacker (hacker) has passed in 02:00:10"
- )
- end
+ context 'when rendering markdown' do
+ before do
+ args[:markdown] = true
+ end
+
+ it 'returns the pipeline summary as the attachments in markdown format' do
+ expect(subject.attachments).to eq(
+ "[project_name](http://example.gitlab.com):" \
+ " Pipeline [#123](http://example.gitlab.com/pipelines/123)" \
+ " of branch [develop](http://example.gitlab.com/commits/develop)" \
+ " by The Hacker (hacker) has passed in 02:00:10"
+ )
end
end
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index badc964db16..88a93eef214 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -65,7 +65,7 @@ describe IrkerService do
conn = @irker_server.accept
conn.each_line do |line|
- msg = JSON.parse(line.chomp("\n"))
+ msg = Gitlab::Json.parse(line.chomp("\n"))
expect(msg.keys).to match_array(%w(to privmsg))
expect(msg['to']).to match_array(["irc://chat.freenode.net/#commits",
"irc://test.net/#test"])
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 32e6b5afce5..a0d36f0a238 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -69,11 +69,23 @@ describe JiraService do
end
describe '.reference_pattern' do
- it_behaves_like 'allows project key on reference pattern'
+ using RSpec::Parameterized::TableSyntax
- 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
+ where(:key, :result) do
+ '#123' | ''
+ '1#23#12' | ''
+ 'JIRA-1234A' | 'JIRA-1234'
+ 'JIRA-1234-some_tag' | 'JIRA-1234'
+ 'JIRA-1234_some_tag' | 'JIRA-1234'
+ 'EXT_EXT-1234' | 'EXT_EXT-1234'
+ 'EXT3_EXT-1234' | 'EXT3_EXT-1234'
+ '3EXT_EXT-1234' | ''
+ end
+
+ with_them do
+ specify do
+ expect(described_class.reference_pattern.match(key).to_s).to eq(result)
+ end
end
end
@@ -570,6 +582,79 @@ describe JiraService do
end
end
+ describe '#create_cross_reference_note' do
+ let_it_be(:user) { build_stubbed(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let(:jira_service) do
+ described_class.new(
+ project: project,
+ url: url,
+ username: username,
+ password: password
+ )
+ end
+ let(:jira_issue) { ExternalIssue.new('JIRA-123', project) }
+
+ subject { jira_service.create_cross_reference_note(jira_issue, resource, user) }
+
+ shared_examples 'creates a comment on Jira' do
+ let(:issue_url) { "#{url}/rest/api/2/issue/JIRA-123" }
+ let(:comment_url) { "#{issue_url}/comment" }
+ let(:remote_link_url) { "#{issue_url}/remotelink" }
+
+ before do
+ allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
+ stub_request(:get, issue_url).with(basic_auth: [username, password])
+ stub_request(:post, comment_url).with(basic_auth: [username, password])
+ stub_request(:post, remote_link_url).with(basic_auth: [username, password])
+ end
+
+ it 'creates a comment on Jira' do
+ subject
+
+ expect(WebMock).to have_requested(:post, comment_url).with(
+ body: /mentioned this issue in/
+ ).once
+ end
+ end
+
+ context 'when resource is a commit' do
+ let(:resource) { project.commit('master') }
+
+ context 'when disabled' do
+ before do
+ allow_next_instance_of(JiraService) do |instance|
+ allow(instance).to receive(:commit_events) { false }
+ end
+ end
+
+ it { is_expected.to eq('Events for commits are disabled.') }
+ end
+
+ context 'when enabled' do
+ it_behaves_like 'creates a comment on Jira'
+ end
+ end
+
+ context 'when resource is a merge request' do
+ let(:resource) { build_stubbed(:merge_request, source_project: project) }
+
+ context 'when disabled' do
+ before do
+ allow_next_instance_of(JiraService) do |instance|
+ allow(instance).to receive(:merge_requests_events) { false }
+ end
+ end
+
+ it { is_expected.to eq('Events for merge requests are disabled.') }
+ end
+
+ context 'when enabled' do
+ it_behaves_like 'creates a comment on Jira'
+ end
+ end
+ end
+
describe '#test' do
let(:jira_service) do
described_class.new(
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 87e482059f2..836181929e3 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -121,5 +121,12 @@ describe MattermostSlashCommandsService do
end
end
end
+
+ describe '#chat_responder' do
+ it 'returns the responder to use for Mattermost' do
+ expect(described_class.new.chat_responder)
+ .to eq(Gitlab::Chat::Responder::Mattermost)
+ end
+ end
end
end
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index d93b8a2cb40..425599c73d4 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -121,7 +121,7 @@ describe MicrosoftTeamsService do
message: "user created page: Awesome wiki_page"
}
end
- let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: opts) }
+ let(:wiki_page) { create(:wiki_page, wiki: project.wiki, **opts) }
let(:wiki_page_sample_data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') }
it "calls Microsoft Teams API" do
diff --git a/spec/models/project_services/webex_teams_service_spec.rb b/spec/models/project_services/webex_teams_service_spec.rb
new file mode 100644
index 00000000000..38977ef3b7d
--- /dev/null
+++ b/spec/models/project_services/webex_teams_service_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe WebexTeamsService do
+ it_behaves_like "chat service", "Webex Teams" do
+ let(:client_arguments) { webhook_url }
+ let(:content_key) { :markdown }
+ end
+end
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index 719a74f995d..c17a24dc7cf 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -38,5 +38,6 @@ describe ProjectSnippet do
let(:stubbed_container) { build_stubbed(:project_snippet) }
let(:expected_full_path) { "#{container.project.full_path}/@snippets/#{container.id}" }
let(:expected_web_url_path) { "#{container.project.full_path}/snippets/#{container.id}" }
+ let(:expected_repo_url_path) { expected_web_url_path }
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4e75ef4fc87..5f8b51c250d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6,6 +6,7 @@ describe Project do
include ProjectForksHelper
include GitHelpers
include ExternalAuthorizationServiceHelpers
+ using RSpec::Parameterized::TableSyntax
it_behaves_like 'having unique enum values'
@@ -20,6 +21,7 @@ describe Project do
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(:iterations) }
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(:delete_all) }
@@ -34,6 +36,7 @@ describe Project do
it { is_expected.to have_one(:mattermost_service) }
it { is_expected.to have_one(:hangouts_chat_service) }
it { is_expected.to have_one(:unify_circuit_service) }
+ it { is_expected.to have_one(:webex_teams_service) }
it { is_expected.to have_one(:packagist_service) }
it { is_expected.to have_one(:pushover_service) }
it { is_expected.to have_one(:asana_service) }
@@ -110,7 +113,10 @@ describe Project do
it { is_expected.to have_many(:source_pipelines) }
it { is_expected.to have_many(:prometheus_alert_events) }
it { is_expected.to have_many(:self_managed_prometheus_alert_events) }
+ it { is_expected.to have_many(:alert_management_alerts) }
it { is_expected.to have_many(:jira_imports) }
+ it { is_expected.to have_many(:metrics_users_starred_dashboards).inverse_of(:project) }
+ it { is_expected.to have_many(:repository_storage_moves) }
it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project, :repository, path: 'somewhere') }
@@ -118,6 +124,11 @@ describe Project do
let(:expected_full_path) { "#{container.namespace.full_path}/somewhere" }
end
+ it_behaves_like 'model with wiki' do
+ let(:container) { create(:project, :wiki_repo) }
+ let(:container_without_wiki) { create(:project) }
+ end
+
it 'has an inverse relationship with merge requests' do
expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
end
@@ -263,27 +274,6 @@ describe Project do
create(:project)
end
- describe 'wiki path conflict' do
- context "when the new path has been used by the wiki of other Project" do
- it 'has an error on the name attribute' do
- new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
-
- expect(new_project).not_to be_valid
- expect(new_project.errors[:name].first).to eq(_('has already been taken'))
- end
- end
-
- context "when the new wiki path has been used by the path of other Project" do
- it 'has an error on the name attribute' do
- project_with_wiki_suffix = create(:project, path: 'foo.wiki')
- new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
-
- expect(new_project).not_to be_valid
- expect(new_project.errors[:name].first).to eq(_('has already been taken'))
- end
- end
- end
-
context 'repository storages inclusion' do
let(:project2) { build(:project, repository_storage: 'missing') }
@@ -1791,6 +1781,7 @@ describe Project do
let(:project) { create(:project, :repository) }
let(:repo) { double(:repo, exists?: true) }
let(:wiki) { double(:wiki, exists?: true) }
+ let(:design) { double(:design, exists?: true) }
it 'expires the caches of the repository and wiki' do
# In EE, there are design repositories as well
@@ -1804,8 +1795,13 @@ describe Project do
.with('foo.wiki', project, shard: project.repository_storage, repo_type: Gitlab::GlRepository::WIKI)
.and_return(wiki)
+ allow(Repository).to receive(:new)
+ .with('foo.design', project, shard: project.repository_storage, repo_type: Gitlab::GlRepository::DESIGN)
+ .and_return(design)
+
expect(repo).to receive(:before_delete)
expect(wiki).to receive(:before_delete)
+ expect(design).to receive(:before_delete)
project.expire_caches_before_rename('foo')
end
@@ -2849,12 +2845,16 @@ describe Project do
end
it 'schedules the transfer of the repository to the new storage and locks the project' do
- expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage')
+ expect(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async).with(project.id, 'test_second_storage', anything)
project.change_repository_storage('test_second_storage')
project.save!
expect(project).to be_repository_read_only
+ expect(project.repository_storage_moves.last).to have_attributes(
+ source_storage_name: "default",
+ destination_storage_name: "test_second_storage"
+ )
end
it "doesn't schedule the transfer if the repository is already read-only" do
@@ -3139,6 +3139,45 @@ describe Project do
end
end
+ describe '#ci_instance_variables_for' do
+ let(:project) { create(:project) }
+
+ let!(:instance_variable) do
+ create(:ci_instance_variable, value: 'secret')
+ end
+
+ let!(:protected_instance_variable) do
+ create(:ci_instance_variable, :protected, value: 'protected')
+ end
+
+ subject { project.ci_instance_variables_for(ref: 'ref') }
+
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ context 'when the ref is not protected' do
+ before do
+ allow(project).to receive(:protected_for?).with('ref').and_return(false)
+ end
+
+ it 'contains only the CI variables' do
+ is_expected.to contain_exactly(instance_variable)
+ end
+ end
+
+ context 'when the ref is protected' do
+ before do
+ allow(project).to receive(:protected_for?).with('ref').and_return(true)
+ end
+
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(instance_variable, protected_instance_variable)
+ end
+ end
+ end
+
describe '#any_lfs_file_locks?', :request_store do
let_it_be(:project) { create(:project) }
@@ -3637,6 +3676,24 @@ describe Project do
expect(projects).to contain_exactly(public_project)
end
end
+
+ context 'with deploy token users' do
+ let_it_be(:private_project) { create(:project, :private) }
+
+ subject { described_class.all.public_or_visible_to_user(user) }
+
+ context 'deploy token user without project' do
+ let_it_be(:user) { create(:deploy_token) }
+
+ it { is_expected.to eq [] }
+ end
+
+ context 'deploy token user with project' do
+ let_it_be(:user) { create(:deploy_token, projects: [private_project]) }
+
+ it { is_expected.to include(private_project) }
+ end
+ end
end
describe '.ids_with_issuables_available_for' do
@@ -3760,7 +3817,7 @@ describe Project do
end
end
- describe '.filter_by_feature_visibility' do
+ describe '.filter_by_feature_visibility', :enable_admin_mode do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
using RSpec::Parameterized::TableSyntax
@@ -3955,16 +4012,6 @@ describe Project do
expect { project.remove_pages }.to change { pages_metadatum.reload.deployed }.from(true).to(false)
end
- it 'is a no-op when there is no namespace' do
- project.namespace.delete
- project.reload
-
- expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
- expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
-
- expect { project.remove_pages }.not_to change { pages_metadatum.reload.deployed }
- end
-
it 'is run when the project is destroyed' do
expect(project).to receive(:remove_pages).and_call_original
@@ -4716,20 +4763,6 @@ describe Project do
end
end
- describe '#wiki_repository_exists?' do
- it 'returns true when the wiki repository exists' do
- project = create(:project, :wiki_repo)
-
- expect(project.wiki_repository_exists?).to eq(true)
- end
-
- it 'returns false when the wiki repository does not exist' do
- project = create(:project)
-
- expect(project.wiki_repository_exists?).to eq(false)
- end
- end
-
describe '#write_repository_config' do
let_it_be(:project) { create(:project, :repository) }
@@ -5972,6 +6005,158 @@ describe Project do
end
end
+ describe '#validate_jira_import_settings!' do
+ include JiraServiceHelper
+
+ let_it_be(:project, reload: true) { create(:project) }
+
+ shared_examples 'raise Jira import error' do |message|
+ it 'returns error' do
+ expect { subject }.to raise_error(Projects::ImportService::Error, message)
+ end
+ end
+
+ shared_examples 'jira configuration base checks' do
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(jira_issue_import: false)
+ end
+
+ it_behaves_like 'raise Jira import error', 'Jira import feature is disabled.'
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when Jira service was not setup' do
+ it_behaves_like 'raise Jira import error', 'Jira integration not configured.'
+ end
+
+ context 'when Jira service exists' do
+ let!(:jira_service) { create(:jira_service, project: project, active: true) }
+
+ context 'when Jira connection is not valid' do
+ before do
+ WebMock.stub_request(:get, 'https://jira.example.com/rest/api/2/serverInfo')
+ .to_raise(JIRA::HTTPError.new(double(message: 'Some failure.')))
+ end
+
+ it_behaves_like 'raise Jira import error', 'Unable to connect to the Jira instance. Please check your Jira integration configuration.'
+ end
+ end
+ end
+ end
+
+ before do
+ stub_jira_service_test
+ end
+
+ context 'without user param' do
+ subject { project.validate_jira_import_settings! }
+
+ it_behaves_like 'jira configuration base checks'
+
+ context 'when jira connection is valid' do
+ let!(:jira_service) { create(:jira_service, project: project, active: true) }
+
+ it 'does not return any error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+
+ context 'with user param provided' do
+ let_it_be(:user) { create(:user) }
+
+ subject { project.validate_jira_import_settings!(user: user) }
+
+ context 'when user has permission to run import' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'jira configuration base checks'
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(jira_issue_import: true)
+ end
+
+ context 'when user does not have permissions to run the import' do
+ before do
+ create(:jira_service, project: project, active: true)
+
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'raise Jira import error', 'You do not have permissions to run the import.'
+ end
+
+ context 'when user has permission to run import' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ let!(:jira_service) { create(:jira_service, project: project, active: true) }
+
+ context 'when issues feature is disabled' do
+ let_it_be(:project, reload: true) { create(:project, :issues_disabled) }
+
+ it_behaves_like 'raise Jira import error', 'Cannot import because issues are not available in this project.'
+ end
+
+ context 'when everything is ok' do
+ it 'does not return any error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe '#design_management_enabled?' do
+ let(:project) { build(:project) }
+
+ where(:lfs_enabled, :hashed_storage_enabled, :expectation) do
+ false | false | false
+ true | false | false
+ false | true | false
+ true | true | true
+ end
+
+ with_them do
+ before do
+ expect(project).to receive(:lfs_enabled?).and_return(lfs_enabled)
+ allow(project).to receive(:hashed_storage?).with(:repository).and_return(hashed_storage_enabled)
+ end
+
+ it do
+ expect(project.design_management_enabled?).to be(expectation)
+ end
+ end
+ end
+
+ describe '#bots' do
+ subject { project.bots }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+ let_it_be(:user) { create(:user) }
+
+ before_all do
+ [project_bot, user].each do |member|
+ project.add_maintainer(member)
+ end
+ end
+
+ it { is_expected.to contain_exactly(project_bot) }
+ it { is_expected.not_to include(user) }
+ end
+
def finish_job(export_job)
export_job.start
export_job.finish
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 1b121b7dee1..a4181e3be9a 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -1,448 +1,35 @@
# frozen_string_literal: true
-require "spec_helper"
+require 'spec_helper'
describe ProjectWiki do
- let(:user) { create(:user, :commit_email) }
- let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
- let(:repository) { project.repository }
- let(:gitlab_shell) { Gitlab::Shell.new }
- let(:project_wiki) { described_class.new(project, user) }
- let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo', 'group/project.wiki') }
- let(:commit) { project_wiki.repository.head_commit }
+ it_behaves_like 'wiki model' do
+ let(:wiki_container) { create(:project, :wiki_repo, namespace: user.namespace) }
+ let(:wiki_container_without_repo) { create(:project, namespace: user.namespace) }
- subject { project_wiki }
+ it { is_expected.to delegate_method(:storage).to(:container) }
+ it { is_expected.to delegate_method(:repository_storage).to(:container) }
+ it { is_expected.to delegate_method(:hashed_storage?).to(:container) }
- it { is_expected.to delegate_method(:repository_storage).to :project }
- it { is_expected.to delegate_method(:hashed_storage?).to :project }
-
- describe "#full_path" do
- it "returns the project path with namespace with the .wiki extension" do
- expect(subject.full_path).to eq(project.full_path + '.wiki')
- end
-
- it 'returns the same value as #full_path' do
- expect(subject.full_path).to eq(subject.full_path)
- end
- end
-
- describe '#web_url' do
- it 'returns the full web URL to the wiki' do
- expect(subject.web_url).to eq(Gitlab::UrlBuilder.build(subject))
- end
- end
-
- describe "#url_to_repo" do
- it "returns the correct ssh url to the repo" do
- expect(subject.url_to_repo).to eq(Gitlab::RepositoryUrlBuilder.build(subject.repository.full_path, protocol: :ssh))
- end
- end
-
- describe "#ssh_url_to_repo" do
- it "equals #url_to_repo" do
- expect(subject.ssh_url_to_repo).to eq(subject.url_to_repo)
- end
- end
-
- describe "#http_url_to_repo" do
- it "returns the correct http url to the repo" do
- expect(subject.http_url_to_repo).to eq(Gitlab::RepositoryUrlBuilder.build(subject.repository.full_path, protocol: :http))
- end
- end
-
- describe "#wiki_base_path" do
- it "returns the wiki base path" do
- wiki_base_path = "#{Gitlab.config.gitlab.relative_url_root}/#{project.full_path}/-/wikis"
-
- expect(subject.wiki_base_path).to eq(wiki_base_path)
- end
- end
-
- describe "#wiki" do
- it "contains a Gitlab::Git::Wiki instance" do
- expect(subject.wiki).to be_a Gitlab::Git::Wiki
- end
-
- it "creates a new wiki repo if one does not yet exist" do
- expect(project_wiki.create_page("index", "test content")).to be_truthy
- end
-
- it "creates a new wiki repo with a default commit message" do
- expect(project_wiki.create_page("index", "test content", :markdown, "")).to be_truthy
-
- page = project_wiki.find_page('index')
-
- expect(page.last_version.message).to eq("#{user.username} created page: index")
- end
-
- it "raises CouldNotCreateWikiError if it can't create the wiki repository" do
- # Create a fresh project which will not have a wiki
- project_wiki = described_class.new(create(:project), user)
- expect(project_wiki.repository).to receive(:create_if_not_exists) { false }
-
- expect { project_wiki.send(:wiki) }.to raise_exception(ProjectWiki::CouldNotCreateWikiError)
- end
- end
-
- describe "#empty?" do
- context "when the wiki repository is empty" do
- describe '#empty?' do
- subject { super().empty? }
-
- it { is_expected.to be_truthy }
- end
- end
-
- context "when the wiki has pages" do
- before do
- project_wiki.create_page("index", "This is an awesome new Gollum Wiki")
- project_wiki.create_page("another-page", "This is another page")
- end
-
- describe '#empty?' do
- subject { super().empty? }
-
- it { is_expected.to be_falsey }
-
- it 'only instantiates a Wiki page once' do
- expect(WikiPage).to receive(:new).once.and_call_original
-
- subject
- end
- end
- end
- end
-
- describe "#list_pages" do
- let(:wiki_pages) { subject.list_pages }
-
- before do
- create_page("index", "This is an index")
- create_page("index2", "This is an index2")
- create_page("an index3", "This is an index3")
- end
-
- after do
- wiki_pages.each do |wiki_page|
- destroy_page(wiki_page.page)
- end
- end
-
- it "returns an array of WikiPage instances" do
- expect(wiki_pages.first).to be_a WikiPage
- end
-
- it 'does not load WikiPage content by default' do
- wiki_pages.each do |page|
- expect(page.content).to be_empty
- end
- end
-
- it 'returns all pages by default' do
- expect(wiki_pages.count).to eq(3)
- end
-
- context "with limit option" do
- it 'returns limited set of pages' do
- expect(subject.list_pages(limit: 1).count).to eq(1)
- end
- end
-
- context "with sorting options" do
- it 'returns pages sorted by title by default' do
- pages = ['an index3', 'index', 'index2']
-
- expect(subject.list_pages.map(&:title)).to eq(pages)
- expect(subject.list_pages(direction: "desc").map(&:title)).to eq(pages.reverse)
- end
-
- it 'returns pages sorted by created_at' do
- pages = ['index', 'index2', 'an index3']
-
- expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
- expect(subject.list_pages(sort: 'created_at', direction: "desc").map(&:title)).to eq(pages.reverse)
- end
- end
-
- context "with load_content option" do
- let(:pages) { subject.list_pages(load_content: true) }
-
- it 'loads WikiPage content' do
- expect(pages.first.content).to eq("This is an index3")
- expect(pages.second.content).to eq("This is an index")
- expect(pages.third.content).to eq("This is an index2")
- end
- end
- end
-
- describe "#find_page" do
- before do
- create_page("index page", "This is an awesome Gollum Wiki")
- end
-
- after do
- subject.list_pages.each { |page| destroy_page(page.page) }
- end
-
- it "returns the latest version of the page if it exists" do
- page = subject.find_page("index page")
- expect(page.title).to eq("index page")
- end
-
- it "returns nil if the page does not exist" do
- expect(subject.find_page("non-existent")).to eq(nil)
- end
-
- it "can find a page by slug" do
- page = subject.find_page("index-page")
- expect(page.title).to eq("index page")
- end
-
- it "returns a WikiPage instance" do
- page = subject.find_page("index page")
- expect(page).to be_a WikiPage
- end
-
- context 'pages with multibyte-character title' do
- before do
- create_page("autre pagé", "C'est un génial Gollum Wiki")
- end
-
- it "can find a page by slug" do
- page = subject.find_page("autre pagé")
- expect(page.title).to eq("autre pagé")
- end
- end
-
- context 'pages with invalidly-encoded content' do
- before do
- create_page("encoding is fun", "f\xFCr".b)
- end
-
- it "can find the page" do
- page = subject.find_page("encoding is fun")
- expect(page.content).to eq("fr")
+ describe '#disk_path' do
+ it 'returns the repository storage path' do
+ expect(subject.disk_path).to eq("#{subject.container.disk_path}.wiki")
end
end
- end
-
- describe '#find_sidebar' do
- before do
- create_page(described_class::SIDEBAR, 'This is an awesome Sidebar')
- end
-
- after do
- subject.list_pages.each { |page| destroy_page(page.page) }
- end
-
- it 'finds the page defined as _sidebar' do
- page = subject.find_page('_sidebar')
-
- expect(page.content).to eq('This is an awesome Sidebar')
- end
- end
- describe '#find_file' do
- let(:image) { File.open(Rails.root.join('spec', 'fixtures', 'big-image.png')) }
+ describe '#update_container_activity' do
+ it 'updates project activity' do
+ wiki_container.update!(
+ last_activity_at: nil,
+ last_repository_updated_at: nil
+ )
- before do
- subject.wiki # Make sure the wiki repo exists
+ subject.create_page('Test Page', 'This is content')
+ wiki_container.reload
- repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- subject.repository.path_to_repo
+ expect(wiki_container.last_activity_at).to be_within(1.minute).of(Time.now)
+ expect(wiki_container.last_repository_updated_at).to be_within(1.minute).of(Time.now)
end
-
- BareRepoOperations.new(repo_path).commit_file(image, 'image.png')
- end
-
- it 'returns the latest version of the file if it exists' do
- file = subject.find_file('image.png')
- expect(file.mime_type).to eq('image/png')
- end
-
- it 'returns nil if the page does not exist' do
- expect(subject.find_file('non-existent')).to eq(nil)
- end
-
- it 'returns a Gitlab::Git::WikiFile instance' do
- file = subject.find_file('image.png')
- expect(file).to be_a Gitlab::Git::WikiFile
- end
-
- it 'returns the whole file' do
- file = subject.find_file('image.png')
- image.rewind
-
- expect(file.raw_data.b).to eq(image.read.b)
- end
- end
-
- describe "#create_page" do
- after do
- destroy_page(subject.list_pages.first.page)
- end
-
- it "creates a new wiki page" do
- expect(subject.create_page("test page", "this is content")).not_to eq(false)
- expect(subject.list_pages.count).to eq(1)
- end
-
- it "returns false when a duplicate page exists" do
- subject.create_page("test page", "content")
- expect(subject.create_page("test page", "content")).to eq(false)
end
-
- it "stores an error message when a duplicate page exists" do
- 2.times { subject.create_page("test page", "content") }
- expect(subject.error_message).to match(/Duplicate page:/)
- end
-
- it "sets the correct commit message" do
- subject.create_page("test page", "some content", :markdown, "commit message")
- expect(subject.list_pages.first.page.version.message).to eq("commit message")
- end
-
- it 'sets the correct commit email' do
- subject.create_page('test page', 'content')
-
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
-
- it 'updates project activity' do
- 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
-
- describe "#update_page" do
- before do
- create_page("update-page", "some content")
- @gitlab_git_wiki_page = subject.wiki.page(title: "update-page")
- subject.update_page(
- @gitlab_git_wiki_page,
- content: "some other content",
- format: :markdown,
- message: "updated page"
- )
- @page = subject.list_pages(load_content: true).first.page
- end
-
- after do
- destroy_page(@page)
- end
-
- it "updates the content of the page" do
- expect(@page.raw_data).to eq("some other content")
- end
-
- it "sets the correct commit message" do
- expect(@page.version.message).to eq("updated page")
- end
-
- it 'sets the correct commit email' do
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
-
- it 'updates project activity' do
- subject.update_page(
- @gitlab_git_wiki_page,
- content: 'Yet more content',
- format: :markdown,
- message: '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
-
- describe "#delete_page" do
- before do
- create_page("index", "some content")
- @page = subject.wiki.page(title: "index")
- end
-
- it "deletes the page" do
- subject.delete_page(@page)
- expect(subject.list_pages.count).to eq(0)
- end
-
- it 'sets the correct commit email' do
- subject.delete_page(@page)
-
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- end
-
- it 'updates project activity' do
- 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 '#ensure_repository' do
- let(:project) { create(:project) }
-
- it 'creates the repository if it not exist' do
- expect(raw_repository.exists?).to eq(false)
-
- subject.ensure_repository
-
- expect(raw_repository.exists?).to eq(true)
- end
-
- it 'does not create the repository if it exists' do
- subject.wiki
- expect(raw_repository.exists?).to eq(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
- expect(subject.hook_attrs.keys).to contain_exactly(:web_url, :git_ssh_url, :git_http_url, :path_with_namespace, :default_branch)
- end
- end
-
- private
-
- def create_temp_repo(path)
- FileUtils.mkdir_p path
- system(*%W(#{Gitlab.config.git.bin_path} init --quiet --bare -- #{path}))
- end
-
- def remove_temp_repo(path)
- FileUtils.rm_rf path
- end
-
- def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.commit_email, "test commit")
- end
-
- def create_page(name, content)
- subject.wiki.write_page(name, :markdown, content, commit_details)
- end
-
- def destroy_page(page)
- subject.delete_page(page, "test commit")
end
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 8b1b738ab58..d72fd137f3f 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -111,26 +111,6 @@ RSpec.describe Release do
end
end
- describe '#notify_new_release' do
- context 'when a release is created' do
- it 'instantiates NewReleaseWorker to send notifications' do
- expect(NewReleaseWorker).to receive(:perform_async)
-
- create(:release)
- end
- end
-
- context 'when a release is updated' do
- let!(:release) { create(:release) }
-
- it 'does not send any new notification' do
- expect(NewReleaseWorker).not_to receive(:perform_async)
-
- release.update!(description: 'new description')
- end
- end
- end
-
describe '#name' do
context 'name is nil' do
before do
@@ -143,38 +123,6 @@ RSpec.describe Release do
end
end
- describe '#evidence_sha' do
- subject { release.evidence_sha }
-
- context 'when a release was created before evidence collection existed' do
- let!(:release) { create(:release) }
-
- it { is_expected.to be_nil }
- end
-
- context 'when a release was created with evidence collection' do
- let!(:release) { create(:release, :with_evidence) }
-
- it { is_expected.to eq(release.evidences.first.summary_sha) }
- end
- end
-
- describe '#evidence_summary' do
- subject { release.evidence_summary }
-
- context 'when a release was created before evidence collection existed' do
- let!(:release) { create(:release) }
-
- it { is_expected.to eq({}) }
- end
-
- context 'when a release was created with evidence collection' do
- let!(:release) { create(:release, :with_evidence) }
-
- it { is_expected.to eq(release.evidences.first.summary) }
- end
- end
-
describe '#milestone_titles' do
let(:release) { create(:release, :with_milestones) }
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 15b162ae87a..a87cdcf9344 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -143,22 +143,54 @@ describe RemoteMirror, :mailer do
end
describe '#update_repository' do
- let(:git_remote_mirror) { spy }
+ it 'performs update including options' do
+ git_remote_mirror = stub_const('Gitlab::Git::RemoteMirror', spy)
+ mirror = build(:remote_mirror)
- before do
- stub_const('Gitlab::Git::RemoteMirror', git_remote_mirror)
+ expect(mirror).to receive(:options_for_update).and_return(keep_divergent_refs: true)
+ mirror.update_repository
+
+ expect(git_remote_mirror).to have_received(:new).with(
+ mirror.project.repository.raw,
+ mirror.remote_name,
+ keep_divergent_refs: true
+ )
+ expect(git_remote_mirror).to have_received(:update)
end
+ end
- it 'includes the `keep_divergent_refs` setting' do
+ describe '#options_for_update' do
+ it 'includes the `keep_divergent_refs` option' do
mirror = build_stubbed(:remote_mirror, keep_divergent_refs: true)
- mirror.update_repository({})
+ options = mirror.options_for_update
- expect(git_remote_mirror).to have_received(:new).with(
- anything,
- mirror.remote_name,
- hash_including(keep_divergent_refs: true)
- )
+ expect(options).to include(keep_divergent_refs: true)
+ end
+
+ it 'includes the `only_branches_matching` option' do
+ branch = create(:protected_branch)
+ mirror = build_stubbed(:remote_mirror, project: branch.project, only_protected_branches: true)
+
+ options = mirror.options_for_update
+
+ expect(options).to include(only_branches_matching: [branch.name])
+ end
+
+ it 'includes the `ssh_key` option' do
+ mirror = build(:remote_mirror, :ssh, ssh_private_key: 'private-key')
+
+ options = mirror.options_for_update
+
+ expect(options).to include(ssh_key: 'private-key')
+ end
+
+ it 'includes the `known_hosts` option' do
+ mirror = build(:remote_mirror, :ssh, ssh_known_hosts: 'known-hosts')
+
+ options = mirror.options_for_update
+
+ expect(options).to include(known_hosts: 'known-hosts')
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index ca04bd7a28a..be626dd6e32 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -2874,4 +2874,80 @@ describe Repository do
expect(repository.submodule_links).to be_a(Gitlab::SubmoduleLinks)
end
end
+
+ describe '#lfs_enabled?' do
+ let_it_be(:project) { create(:project, :repository, :design_repo, lfs_enabled: true) }
+
+ subject { repository.lfs_enabled? }
+
+ context 'for a project repository' do
+ let(:repository) { project.repository }
+
+ it 'returns true when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when LFS is disabled' do
+ stub_lfs_setting(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a project wiki repository' do
+ let(:repository) { project.wiki.repository }
+
+ it 'returns true when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when LFS is disabled' do
+ stub_lfs_setting(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a project snippet repository' do
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:repository) { snippet.repository }
+
+ it 'returns false when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a personal snippet repository' do
+ let(:snippet) { create(:personal_snippet) }
+ let(:repository) { snippet.repository }
+
+ it 'returns false when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_falsy
+ end
+ end
+
+ context 'for a design repository' do
+ let(:repository) { project.design_repository }
+
+ it 'returns true when LFS is enabled' do
+ stub_lfs_setting(enabled: true)
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false when LFS is disabled' do
+ stub_lfs_setting(enabled: false)
+
+ is_expected.to be_falsy
+ end
+ end
+ end
end
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index ca887b485a2..a1a2150f461 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -15,9 +15,6 @@ RSpec.describe ResourceLabelEvent, type: :model do
it_behaves_like 'a resource event for merge requests'
describe 'associations' do
- it { is_expected.to belong_to(:user) }
- it { is_expected.to belong_to(:issue) }
- it { is_expected.to belong_to(:merge_request) }
it { is_expected.to belong_to(:label) }
end
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
index bf8672f95c9..3f8d8b4c1df 100644
--- a/spec/models/resource_milestone_event_spec.rb
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -78,4 +78,21 @@ describe ResourceMilestoneEvent, type: :model do
let(:query_method) { :remove? }
end
end
+
+ describe '#milestone_title' do
+ let(:milestone) { create(:milestone, title: 'v2.3') }
+ let(:event) { create(:resource_milestone_event, milestone: milestone) }
+
+ it 'returns the expected title' do
+ expect(event.milestone_title).to eq('v2.3')
+ end
+
+ context 'when milestone is nil' do
+ let(:event) { create(:resource_milestone_event, milestone: nil) }
+
+ it 'returns nil' do
+ expect(event.milestone_title).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
new file mode 100644
index 00000000000..986a13cbd0d
--- /dev/null
+++ b/spec/models/resource_state_event_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceStateEvent, type: :model do
+ subject { build(:resource_state_event, issue: issue) }
+
+ let(:issue) { create(:issue) }
+ let(:merge_request) { create(:merge_request) }
+
+ it_behaves_like 'a resource event'
+ it_behaves_like 'a resource event for issues'
+ it_behaves_like 'a resource event for merge requests'
+end
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index fedaae372c4..087bc957373 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -326,4 +326,26 @@ describe SentNotification do
end
end
end
+
+ describe "#position=" do
+ subject { build(:sent_notification, noteable: create(:issue)) }
+
+ it "doesn't accept non-hash JSON passed as a string" do
+ subject.position = "true"
+
+ expect(subject.attributes_before_type_cast["position"]).to be(nil)
+ end
+
+ it "does accept a position hash as a string" do
+ subject.position = '{ "base_sha": "test" }'
+
+ expect(subject.position.base_sha).to eq("test")
+ end
+
+ it "does accept a hash" do
+ subject.position = { "base_sha" => "test" }
+
+ expect(subject.position.base_sha).to eq("test")
+ end
+ end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index cb8122b6573..106f8def42d 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -87,6 +87,20 @@ describe Service do
end
end
+ describe '#operating?' do
+ it 'is false when the service is not active' do
+ expect(build(:service).operating?).to eq(false)
+ end
+
+ it 'is false when the service is not persisted' do
+ expect(build(:service, active: true).operating?).to eq(false)
+ end
+
+ it 'is true when the service is active and persisted' do
+ expect(create(:service, active: true).operating?).to eq(true)
+ end
+ end
+
describe '.confidential_note_hooks' do
it 'includes services where confidential_note_events is true' do
create(:service, active: true, confidential_note_events: true)
@@ -523,24 +537,6 @@ describe Service do
end
end
- describe "#deprecated?" do
- let(:project) { create(:project, :repository) }
-
- it 'returns false by default' do
- service = create(:service, project: project)
- expect(service.deprecated?).to be_falsy
- end
- end
-
- describe "#deprecation_message" do
- let(:project) { create(:project, :repository) }
-
- it 'is empty by default' do
- service = create(:service, project: project)
- expect(service.deprecation_message).to be_nil
- end
- end
-
describe '#api_field_names' do
let(:fake_service) do
Class.new(Service) do
diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb
index dc9f9a95d24..255f07ebfa5 100644
--- a/spec/models/snippet_repository_spec.rb
+++ b/spec/models/snippet_repository_spec.rb
@@ -202,6 +202,38 @@ describe SnippetRepository do
it_behaves_like 'snippet repository with file names', 'snippetfile10.txt', 'snippetfile11.txt'
end
+
+ shared_examples 'snippet repository with git errors' do |path, error|
+ let(:new_file) { { file_path: path, content: 'bar' } }
+
+ it 'raises a path specific error' do
+ expect do
+ snippet_repository.multi_files_action(user, data, commit_opts)
+ end.to raise_error(error)
+ end
+ end
+
+ context 'with git errors' do
+ it_behaves_like 'snippet repository with git errors', 'invalid://path/here', described_class::InvalidPathError
+ it_behaves_like 'snippet repository with git errors', '../../path/traversal/here', described_class::InvalidPathError
+ it_behaves_like 'snippet repository with git errors', 'README', described_class::CommitError
+
+ context 'when user name is invalid' do
+ let(:user) { create(:user, name: '.') }
+
+ it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
+ end
+
+ context 'when user email is empty' do
+ let(:user) { create(:user) }
+
+ before do
+ user.update_column(:email, '')
+ end
+
+ it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
+ end
+ end
end
def blob_at(snippet, path)
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 2061084d5ea..4d6586c1df4 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -180,22 +180,6 @@ describe Snippet do
end
end
- describe '.search_code' do
- let(:snippet) { create(:snippet, content: 'class Foo; end') }
-
- it 'returns snippets with matching content' do
- expect(described_class.search_code(snippet.content)).to eq([snippet])
- end
-
- it 'returns snippets with partially matching content' do
- expect(described_class.search_code('class')).to eq([snippet])
- end
-
- it 'returns snippets with matching content regardless of the casing' do
- expect(described_class.search_code('FOO')).to eq([snippet])
- end
- end
-
describe 'when default snippet visibility set to internal' do
using RSpec::Parameterized::TableSyntax
@@ -545,11 +529,11 @@ describe Snippet do
let(:snippet) { build(:snippet) }
it 'excludes secret_token from generated json' do
- expect(JSON.parse(to_json).keys).not_to include("secret_token")
+ expect(Gitlab::Json.parse(to_json).keys).not_to include("secret_token")
end
it 'does not override existing exclude option value' do
- expect(JSON.parse(to_json(except: [:id])).keys).not_to include("secret_token", "id")
+ expect(Gitlab::Json.parse(to_json(except: [:id])).keys).not_to include("secret_token", "id")
end
def to_json(params = {})
@@ -735,31 +719,35 @@ describe Snippet do
end
end
- describe '#versioned_enabled_for?' do
- let_it_be(:user) { create(:user) }
+ describe '#url_to_repo' do
+ subject { snippet.url_to_repo }
+
+ context 'with personal snippet' do
+ let(:snippet) { create(:personal_snippet) }
- subject { snippet.versioned_enabled_for?(user) }
+ it { is_expected.to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "snippets/#{snippet.id}.git") }
+ end
- context 'with repository and version_snippets enabled' do
- let!(:snippet) { create(:personal_snippet, :repository, author: user) }
+ context 'with project snippet' do
+ let(:snippet) { create(:project_snippet) }
- it { is_expected.to be_truthy }
+ it { is_expected.to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "#{snippet.project.full_path}/snippets/#{snippet.id}.git") }
end
+ end
- context 'without repository' do
- let!(:snippet) { create(:personal_snippet, author: user) }
+ describe '.max_file_limit' do
+ subject { described_class.max_file_limit(nil) }
- it { is_expected.to be_falsy }
+ it "returns #{Snippet::MAX_FILE_COUNT}" do
+ expect(subject).to eq Snippet::MAX_FILE_COUNT
end
- context 'without version_snippets feature disabled' do
- let!(:snippet) { create(:personal_snippet, :repository, author: user) }
+ context 'when feature flag :snippet_multiple_files is disabled' do
+ it "returns #{described_class::MAX_SINGLE_FILE_COUNT}" do
+ stub_feature_flags(snippet_multiple_files: false)
- before do
- stub_feature_flags(version_snippets: false)
+ expect(subject).to eq described_class::MAX_SINGLE_FILE_COUNT
end
-
- it { is_expected.to be_falsy }
end
end
end
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index 8ebd97de9ff..8d0f247b5d6 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -20,15 +20,30 @@ describe SpamLog do
expect { spam_log.remove_user(deleted_by: admin) }.to change { spam_log.user.blocked? }.to(true)
end
- it 'removes the user', :sidekiq_might_not_need_inline do
- spam_log = build(:spam_log)
- user = spam_log.user
+ context 'when admin mode is enabled', :enable_admin_mode do
+ it 'removes the user', :sidekiq_might_not_need_inline do
+ spam_log = build(:spam_log)
+ user = spam_log.user
+
+ perform_enqueued_jobs do
+ spam_log.remove_user(deleted_by: admin)
+ end
- perform_enqueued_jobs do
- spam_log.remove_user(deleted_by: admin)
+ expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
+ end
- expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ context 'when admin mode is disabled' do
+ it 'does not allow to remove the user', :sidekiq_might_not_need_inline do
+ spam_log = build(:spam_log)
+ user = spam_log.user
+
+ perform_enqueued_jobs do
+ spam_log.remove_user(deleted_by: admin)
+ end
+
+ expect(User.exists?(user.id)).to be(true)
+ end
end
end
diff --git a/spec/models/state_note_spec.rb b/spec/models/state_note_spec.rb
new file mode 100644
index 00000000000..d3409315e41
--- /dev/null
+++ b/spec/models/state_note_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe StateNote do
+ describe '.from_event' do
+ let_it_be(:author) { create(:user) }
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:noteable) { create(:issue, author: author, project: project) }
+
+ ResourceStateEvent.states.each do |state, _value|
+ context "with event state #{state}" do
+ let_it_be(:event) { create(:resource_state_event, issue: noteable, state: state, created_at: '2020-02-05') }
+
+ subject { described_class.from_event(event, resource: noteable, resource_parent: project) }
+
+ it_behaves_like 'a system note', exclude_project: true do
+ let(:action) { state.to_s }
+ end
+
+ it 'contains the expected values' do
+ expect(subject.author).to eq(author)
+ expect(subject.created_at).to eq(event.created_at)
+ expect(subject.note_html).to eq("<p dir=\"auto\">#{state}</p>")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index 33c1afad59f..ae1697fb7e6 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -56,12 +56,12 @@ RSpec.describe Timelog do
end
end
- describe 'between_dates' do
- it 'returns collection of timelogs within given dates' do
+ describe 'between_times' do
+ it 'returns collection of timelogs within given times' do
create(:timelog, spent_at: 65.days.ago)
timelog1 = create(:timelog, spent_at: 15.days.ago)
timelog2 = create(:timelog, spent_at: 5.days.ago)
- timelogs = described_class.between_dates(20.days.ago, 1.day.ago)
+ timelogs = described_class.between_times(20.days.ago, 1.day.ago)
expect(timelogs).to contain_exactly(timelog1, timelog2)
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 3f0c95b2513..e125f58399e 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -61,11 +61,13 @@ describe Todo do
describe '#done' do
it 'changes state to done' do
todo = create(:todo, state: :pending)
+
expect { todo.done }.to change(todo, :state).from('pending').to('done')
end
it 'does not raise error when is already done' do
todo = create(:todo, state: :done)
+
expect { todo.done }.not_to raise_error
end
end
@@ -73,15 +75,31 @@ describe Todo do
describe '#for_commit?' do
it 'returns true when target is a commit' do
subject.target_type = 'Commit'
+
expect(subject.for_commit?).to eq true
end
it 'returns false when target is an issuable' do
subject.target_type = 'Issue'
+
expect(subject.for_commit?).to eq false
end
end
+ describe '#for_design?' do
+ it 'returns true when target is a Design' do
+ subject.target_type = 'DesignManagement::Design'
+
+ expect(subject.for_design?).to eq(true)
+ end
+
+ it 'returns false when target is not a Design' do
+ subject.target_type = 'Issue'
+
+ expect(subject.for_design?).to eq(false)
+ end
+ end
+
describe '#target' do
context 'for commits' do
let(:project) { create(:project, :repository) }
@@ -108,6 +126,7 @@ describe Todo do
it 'returns the issuable for issuables' do
subject.target_id = issue.id
subject.target_type = issue.class.name
+
expect(subject.target).to eq issue
end
end
@@ -126,6 +145,7 @@ describe Todo do
it 'returns full reference for issuables' do
subject.target = issue
+
expect(subject.target_reference).to eq issue.to_reference(full: false)
end
end
@@ -389,5 +409,17 @@ describe Todo do
expect(described_class.update_state(:pending)).to be_empty
end
+
+ it 'updates updated_at' do
+ create(:todo, :pending)
+
+ Timecop.freeze(1.day.from_now) do
+ expected_update_date = Time.now.utc
+
+ ids = described_class.update_state(:done)
+
+ expect(Todo.where(id: ids).map(&:updated_at)).to all(be_like_time(expected_update_date))
+ end
+ end
end
end
diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb
index c2d5dfdf9c4..7dde8459f9a 100644
--- a/spec/models/tree_spec.rb
+++ b/spec/models/tree_spec.rb
@@ -9,15 +9,18 @@ describe Tree do
subject { described_class.new(repository, '54fcc214') }
describe '#readme' do
- class FakeBlob
- attr_reader :name
-
- def initialize(name)
- @name = name
- end
-
- def readme?
- name =~ /^readme/i
+ before do
+ stub_const('FakeBlob', Class.new)
+ FakeBlob.class_eval do
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def readme?
+ name =~ /^readme/i
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 8597397c3c6..94a3f6bafea 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe User, :do_not_mock_admin_mode do
+describe User do
include ProjectForksHelper
include TermsHelper
include ExclusiveLeaseHelpers
@@ -17,6 +17,7 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
it { is_expected.to include_module(BlocksJsonSerialization) }
+ it { is_expected.to include_module(AsyncDeviseEmail) }
end
describe 'delegations' do
@@ -54,6 +55,7 @@ describe User, :do_not_mock_admin_mode do
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
it { is_expected.to have_many(:custom_attributes).class_name('UserCustomAttribute') }
it { is_expected.to have_many(:releases).dependent(:nullify) }
+ it { is_expected.to have_many(:metrics_users_starred_dashboards).inverse_of(:user) }
describe "#bio" do
it 'syncs bio with `user_details.bio` on create' do
@@ -164,6 +166,18 @@ describe User, :do_not_mock_admin_mode do
end
end
+ describe 'Devise emails' do
+ let!(:user) { create(:user) }
+
+ describe 'behaviour' do
+ it 'sends emails asynchronously' do
+ expect do
+ user.update!(email: 'hello@hello.com')
+ end.to have_enqueued_job.on_queue('mailers').exactly(:twice)
+ end
+ end
+ end
+
describe 'validations' do
describe 'password' do
let!(:user) { create(:user) }
@@ -295,7 +309,7 @@ describe User, :do_not_mock_admin_mode do
subject { build(:user) }
end
- it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do
+ it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :public_email, :notification_email do
subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } }
end
@@ -538,18 +552,6 @@ describe User, :do_not_mock_admin_mode do
expect(user).to be_valid
end
- context 'when feature flag is turned off' do
- before do
- stub_feature_flags(email_restrictions: false)
- end
-
- it 'does accept the email address' do
- user = build(:user, email: 'info+1@test.com')
-
- expect(user).to be_valid
- end
- end
-
context 'when created_by_id is set' do
it 'does accept the email address' do
user = build(:user, email: 'info+1@test.com', created_by_id: 1)
@@ -813,7 +815,7 @@ describe User, :do_not_mock_admin_mode do
describe '.active_without_ghosts' do
let_it_be(:user1) { create(:user, :external) }
let_it_be(:user2) { create(:user, state: 'blocked') }
- let_it_be(:user3) { create(:user, ghost: true) }
+ let_it_be(:user3) { create(:user, :ghost) }
let_it_be(:user4) { create(:user) }
it 'returns all active users but ghost users' do
@@ -824,7 +826,7 @@ describe User, :do_not_mock_admin_mode do
describe '.without_ghosts' do
let_it_be(:user1) { create(:user, :external) }
let_it_be(:user2) { create(:user, state: 'blocked') }
- let_it_be(:user3) { create(:user, ghost: true) }
+ let_it_be(:user3) { create(:user, :ghost) }
it 'returns users without ghosts users' do
expect(described_class.without_ghosts).to match_array([user1, user2])
@@ -927,7 +929,6 @@ describe User, :do_not_mock_admin_mode do
user.tap { |u| u.update!(email: new_email) }.reload
end.to change(user, :unconfirmed_email).to(new_email)
end
-
it 'does not change :notification_email' do
expect do
user.tap { |u| u.update!(email: new_email) }.reload
@@ -3275,7 +3276,6 @@ describe User, :do_not_mock_admin_mode do
expect(ghost.namespace).not_to be_nil
expect(ghost.namespace).to be_persisted
expect(ghost.user_type).to eq 'ghost'
- expect(ghost.ghost).to eq true
end
it "does not create a second ghost user if one is already present" do
@@ -4077,7 +4077,7 @@ describe User, :do_not_mock_admin_mode do
context 'in single-user environment' do
it 'requires user consent after one week' do
- create(:user, ghost: true)
+ create(:user, :ghost)
expect(user.requires_usage_stats_consent?).to be true
end
@@ -4355,31 +4355,15 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe 'internal methods' do
- let_it_be(:user) { create(:user) }
- let_it_be(:ghost) { described_class.ghost }
- let_it_be(:alert_bot) { described_class.alert_bot }
- let_it_be(:project_bot) { create(:user, :project_bot) }
- let_it_be(:non_internal) { [user, project_bot] }
- let_it_be(:internal) { [ghost, alert_bot] }
+ describe '.active_without_ghosts' do
+ let_it_be(:user1) { create(:user, :external) }
+ let_it_be(:user2) { create(:user, state: 'blocked') }
+ let_it_be(:user3) { create(:user, :ghost) }
+ let_it_be(:user4) { create(:user, user_type: :support_bot) }
+ let_it_be(:user5) { create(:user, state: 'blocked', user_type: :support_bot) }
- it 'returns internal users' do
- expect(described_class.internal).to match_array(internal)
- expect(internal.all?(&:internal?)).to eq(true)
- end
-
- it 'returns non internal users' do
- expect(described_class.non_internal).to match_array(non_internal)
- expect(non_internal.all?(&:internal?)).to eq(false)
- end
-
- describe '#bot?' do
- it 'marks bot users' do
- expect(user.bot?).to eq(false)
- expect(ghost.bot?).to eq(false)
-
- expect(alert_bot.bot?).to eq(true)
- end
+ it 'returns all active users including active bots but ghost users' do
+ expect(described_class.active_without_ghosts).to match_array([user1, user4])
end
end
@@ -4417,19 +4401,6 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe 'bots & humans' do
- it 'returns corresponding users' do
- human = create(:user)
- bot = create(:user, :bot)
- project_bot = create(:user, :project_bot)
-
- expect(described_class.humans).to match_array([human])
- expect(described_class.bots).to match_array([bot, project_bot])
- expect(described_class.bots_without_project_bot).to match_array([bot])
- expect(described_class.with_project_bots).to match_array([human, project_bot])
- end
- end
-
describe '#hook_attrs' do
it 'includes name, username, avatar_url, and email' do
user = create(:user)
@@ -4458,45 +4429,6 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe '#gitlab_employee?' do
- using RSpec::Parameterized::TableSyntax
-
- subject { user.gitlab_employee? }
-
- where(:email, :is_com, :expected_result) do
- 'test@gitlab.com' | true | true
- 'test@example.com' | true | false
- 'test@gitlab.com' | false | false
- 'test@example.com' | false | false
- end
-
- with_them do
- let(:user) { build(:user, email: email) }
-
- before do
- allow(Gitlab).to receive(:com?).and_return(is_com)
- end
-
- it { is_expected.to be expected_result }
- end
-
- context 'when email is of Gitlab and is not confirmed' do
- let(:user) { build(:user, email: 'test@gitlab.com', confirmed_at: nil) }
-
- it { is_expected.to be false }
- end
-
- context 'when `:gitlab_employee_badge` feature flag is disabled' do
- let(:user) { build(:user, email: 'test@gitlab.com') }
-
- before do
- stub_feature_flags(gitlab_employee_badge: false)
- end
-
- it { is_expected.to be false }
- end
- end
-
describe '#current_highest_access_level' do
let_it_be(:user) { create(:user) }
@@ -4517,27 +4449,6 @@ describe User, :do_not_mock_admin_mode do
end
end
- describe '#organization' do
- using RSpec::Parameterized::TableSyntax
-
- let(:user) { build(:user, organization: 'ACME') }
-
- subject { user.organization }
-
- where(:gitlab_employee?, :expected_result) do
- true | 'GitLab'
- false | 'ACME'
- end
-
- with_them do
- before do
- allow(user).to receive(:gitlab_employee?).and_return(gitlab_employee?)
- end
-
- it { is_expected.to eql(expected_result) }
- end
- end
-
context 'when after_commit :update_highest_role' do
describe 'create user' do
subject { create(:user) }
@@ -4563,7 +4474,7 @@ describe User, :do_not_mock_admin_mode do
where(:attributes) do
[
{ state: 'blocked' },
- { ghost: true },
+ { user_type: :ghost },
{ user_type: :alert_bot }
]
end
@@ -4606,7 +4517,7 @@ describe User, :do_not_mock_admin_mode do
context 'when user is a ghost user' do
before do
- user.update(ghost: true)
+ user.update(user_type: :ghost)
end
it { is_expected.to be false }
@@ -4645,7 +4556,7 @@ describe User, :do_not_mock_admin_mode do
context 'when user is an internal user' do
before do
- user.update(ghost: true)
+ user.update(user_type: :ghost)
end
it { is_expected.to be User::LOGIN_FORBIDDEN }
@@ -4685,4 +4596,20 @@ describe User, :do_not_mock_admin_mode do
it_behaves_like 'does not require password to be present'
end
end
+
+ describe '#migration_bot' do
+ it 'creates the user if it does not exist' do
+ expect do
+ described_class.migration_bot
+ end.to change { User.where(user_type: :migration_bot).count }.by(1)
+ end
+
+ it 'does not create a new user if it already exists' do
+ described_class.migration_bot
+
+ expect do
+ described_class.migration_bot
+ end.not_to change { User.count }
+ end
+ end
end
diff --git a/spec/models/user_type_enums_spec.rb b/spec/models/user_type_enums_spec.rb
deleted file mode 100644
index 4f56e6ea96e..00000000000
--- a/spec/models/user_type_enums_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe UserTypeEnums do
- it '.types' do
- expect(described_class.types.keys).to include('alert_bot', 'project_bot', 'human', 'ghost')
- end
-
- it '.bots' do
- expect(described_class.bots.keys).to include('alert_bot', 'project_bot')
- end
-end
diff --git a/spec/models/wiki_page/meta_spec.rb b/spec/models/wiki_page/meta_spec.rb
index f9bfc31ba64..0255dd802cf 100644
--- a/spec/models/wiki_page/meta_spec.rb
+++ b/spec/models/wiki_page/meta_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe WikiPage::Meta do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :wiki_repo) }
let_it_be(:other_project) { create(:project) }
describe 'Associations' do
@@ -169,8 +169,11 @@ describe WikiPage::Meta do
described_class.find_or_create(last_known_slug, wiki_page)
end
- def create_previous_version(title = old_title, slug = last_known_slug)
- create(:wiki_page_meta, title: title, project: project, canonical_slug: slug)
+ def create_previous_version(title: old_title, slug: last_known_slug, date: wiki_page.version.commit.committed_date)
+ create(:wiki_page_meta,
+ title: title, project: project,
+ created_at: date, updated_at: date,
+ canonical_slug: slug)
end
def create_context
@@ -198,6 +201,8 @@ describe WikiPage::Meta do
title: wiki_page.title,
project: wiki_page.wiki.project
)
+ expect(meta.updated_at).to eq(wiki_page.version.commit.committed_date)
+ expect(meta.created_at).not_to be_after(meta.updated_at)
expect(meta.slugs.where(slug: last_known_slug)).to exist
expect(meta.slugs.canonical.where(slug: wiki_page.slug)).to exist
end
@@ -209,22 +214,32 @@ describe WikiPage::Meta do
end
end
- context 'the slug is too long' do
- let(:last_known_slug) { FFaker::Lorem.characters(2050) }
+ context 'there are problems' do
+ context 'the slug is too long' do
+ let(:last_known_slug) { FFaker::Lorem.characters(2050) }
- it 'raises an error' do
- expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ it 'raises an error' do
+ expect { find_record }.to raise_error ActiveRecord::ValueTooLong
+ end
end
- end
- context 'a conflicting record exists' do
- before do
- create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
- create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ context 'a conflicting record exists' do
+ before do
+ create(:wiki_page_meta, project: project, canonical_slug: last_known_slug)
+ create(:wiki_page_meta, project: project, canonical_slug: current_slug)
+ end
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ end
end
- it 'raises an error' do
- expect { find_record }.to raise_error(ActiveRecord::RecordInvalid)
+ context 'the wiki page is not valid' do
+ let(:wiki_page) { build(:wiki_page, project: project, title: nil) }
+
+ it 'raises an error' do
+ expect { find_record }.to raise_error(described_class::WikiPageInvalid)
+ end
end
end
@@ -258,6 +273,17 @@ describe WikiPage::Meta do
end
end
+ context 'the commit happened a day ago' do
+ before do
+ allow(wiki_page.version.commit).to receive(:committed_date).and_return(1.day.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # Identical to the base case.
+ let(:query_limit) { 5 }
+ end
+ end
+
context 'the last_known_slug is the same as the current slug, as on creation' do
let(:last_known_slug) { current_slug }
@@ -292,6 +318,33 @@ describe WikiPage::Meta do
end
end
+ context 'a record exists in the DB, but we need to update timestamps' do
+ let(:last_known_slug) { current_slug }
+ let(:old_title) { title }
+
+ before do
+ create_previous_version(date: 1.week.ago)
+ end
+
+ include_examples 'metadata examples' do
+ # We need the query, and the update
+ # SAVEPOINT active_record_2
+ #
+ # SELECT * FROM wiki_page_meta
+ # INNER JOIN wiki_page_slugs
+ # ON wiki_page_slugs.wiki_page_meta_id = wiki_page_meta.id
+ # WHERE wiki_page_meta.project_id = ?
+ # AND wiki_page_slugs.canonical = TRUE
+ # AND wiki_page_slugs.slug = ?
+ # LIMIT 2
+ #
+ # UPDATE wiki_page_meta SET updated_at = ?date WHERE id = ?id
+ #
+ # RELEASE SAVEPOINT active_record_2
+ let(:query_limit) { 4 }
+ end
+ end
+
context 'we need to update the slug, but not the title' do
let(:old_title) { title }
@@ -359,14 +412,14 @@ describe WikiPage::Meta do
end
context 'we want to change the slug back to a previous version' do
- let(:slug_1) { 'foo' }
- let(:slug_2) { 'bar' }
+ let(:slug_1) { generate(:sluggified_title) }
+ let(:slug_2) { generate(:sluggified_title) }
let(:wiki_page) { create(:wiki_page, title: slug_1, project: project) }
let(:last_known_slug) { slug_2 }
before do
- meta = create_previous_version(title, slug_1)
+ meta = create_previous_version(title: title, slug: slug_1)
meta.canonical_slug = slug_2
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index 718b386b3fd..201dc85daf8 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -3,20 +3,11 @@
require "spec_helper"
describe WikiPage do
- let(:project) { create(:project, :wiki_repo) }
- let(:user) { project.owner }
- let(:wiki) { ProjectWiki.new(project, user) }
-
- let(:new_page) do
- described_class.new(wiki).tap do |page|
- page.attributes = { title: 'test page', content: 'test content' }
- end
- end
-
- let(:existing_page) do
- create_page('test page', 'test content')
- wiki.find_page('test page')
- end
+ let_it_be(:user) { create(:user) }
+ let(:container) { create(:project, :wiki_repo) }
+ let(:wiki) { Wiki.for_container(container, user) }
+ let(:new_page) { build(:wiki_page, wiki: wiki, title: 'test page', content: 'test content') }
+ let(:existing_page) { create(:wiki_page, wiki: wiki, title: 'test page', content: 'test content', message: 'test commit') }
subject { new_page }
@@ -24,11 +15,8 @@ describe WikiPage do
stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => false)
end
- def enable_front_matter_for_project
- stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => {
- thing: project,
- enabled: true
- })
+ def enable_front_matter_for(thing)
+ stub_feature_flags(Gitlab::WikiPages::FrontMatterParser::FEATURE_FLAG => thing)
end
describe '.group_by_directory' do
@@ -41,13 +29,13 @@ describe WikiPage do
context 'when there are pages' do
before do
- create_page('dir_1/dir_1_1/page_3', 'content')
- create_page('page_1', 'content')
- create_page('dir_1/page_2', 'content')
- create_page('dir_2', 'page with dir name')
- create_page('dir_2/page_5', 'content')
- create_page('page_6', 'content')
- create_page('dir_2/page_4', 'content')
+ wiki.create_page('dir_1/dir_1_1/page_3', 'content')
+ wiki.create_page('page_1', 'content')
+ wiki.create_page('dir_1/page_2', 'content')
+ wiki.create_page('dir_2', 'page with dir name')
+ wiki.create_page('dir_2/page_5', 'content')
+ wiki.create_page('page_6', 'content')
+ wiki.create_page('dir_2/page_4', 'content')
end
let(:page_1) { wiki.find_page('page_1') }
@@ -114,7 +102,8 @@ describe WikiPage do
describe '#front_matter' do
let_it_be(:project) { create(:project) }
- let(:wiki_page) { create(:wiki_page, project: project, content: content) }
+ let(:container) { project }
+ let(:wiki_page) { create(:wiki_page, container: container, content: content) }
shared_examples 'a page without front-matter' do
it { expect(wiki_page).to have_attributes(front_matter: {}, content: content) }
@@ -153,9 +142,9 @@ describe WikiPage do
it_behaves_like 'a page without front-matter'
- context 'but enabled for the project' do
+ context 'but enabled for the container' do
before do
- enable_front_matter_for_project
+ enable_front_matter_for(container)
end
it_behaves_like 'a page with front-matter'
@@ -344,7 +333,7 @@ describe WikiPage do
context 'with an existing page title exceeding the limit' do
subject do
title = 'a' * (max_title + 1)
- create_page(title, 'content')
+ wiki.create_page(title, 'content')
wiki.find_page(title)
end
@@ -388,6 +377,20 @@ describe WikiPage do
expect(wiki.find_page("Index").message).to eq 'Custom Commit Message'
end
+
+ it 'if the title is preceded by a / it is removed' do
+ subject.create(attributes.merge(title: '/New Page'))
+
+ expect(wiki.find_page('New Page')).not_to be_nil
+ end
+ end
+
+ context "with invalid attributes" do
+ it 'does not create the page' do
+ subject.create(title: '')
+
+ expect(wiki.find_page('New Page')).to be_nil
+ end
end
end
@@ -410,14 +413,11 @@ describe WikiPage do
end
end
- describe "#update" do
- subject do
- create_page(title, "content")
- wiki.find_page(title)
- end
+ describe '#update' do
+ subject { create(:wiki_page, wiki: wiki, title: title) }
- it "updates the content of the page" do
- subject.update(content: "new content")
+ it 'updates the content of the page' do
+ subject.update(content: 'new content')
page = wiki.find_page(title)
expect([subject.content, page.content]).to all(eq('new content'))
@@ -429,24 +429,6 @@ describe WikiPage do
end
end
- describe '#create' do
- context 'with valid attributes' do
- it 'raises an error if a page with the same path already exists' do
- create_page('New Page', 'content')
- create_page('foo/bar', 'content')
-
- expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
- expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
- end
-
- it 'if the title is preceded by a / it is removed' do
- create_page('/New Page', 'content')
-
- expect(wiki.find_page('New Page')).not_to be_nil
- end
- end
- end
-
describe "#update" do
subject { existing_page }
@@ -514,9 +496,9 @@ describe WikiPage do
expect([subject, page]).to all(have_attributes(front_matter: be_empty, content: content))
end
- context 'but it is enabled for the project' do
+ context 'but it is enabled for the container' do
before do
- enable_front_matter_for_project
+ enable_front_matter_for(container)
end
it_behaves_like 'able to update front-matter'
@@ -556,7 +538,7 @@ describe WikiPage do
context 'when renaming a page' do
it 'raises an error if the page already exists' do
- create_page('Existing Page', 'content')
+ wiki.create_page('Existing Page', 'content')
expect { subject.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
expect(subject.title).to eq 'test page'
@@ -578,7 +560,7 @@ describe WikiPage do
context 'when moving a page' do
it 'raises an error if the page already exists' do
- create_page('foo/Existing Page', 'content')
+ wiki.create_page('foo/Existing Page', 'content')
expect { subject.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
expect(subject.title).to eq 'test page'
@@ -598,10 +580,7 @@ describe WikiPage do
end
context 'in subdir' do
- subject do
- create_page('foo/Existing Page', 'content')
- wiki.find_page('foo/Existing Page')
- end
+ subject { create(:wiki_page, wiki: wiki, title: 'foo/Existing Page') }
it 'moves the page to the root folder if the title is preceded by /' do
expect(subject.slug).to eq 'foo/Existing-Page'
@@ -639,7 +618,7 @@ describe WikiPage do
end
end
- describe "#destroy" do
+ describe "#delete" do
subject { existing_page }
it "deletes the page" do
@@ -671,10 +650,7 @@ describe WikiPage do
using RSpec::Parameterized::TableSyntax
let(:untitled_page) { described_class.new(wiki) }
- let(:directory_page) do
- create_page('parent directory/child page', 'test content')
- wiki.find_page('parent directory/child page')
- end
+ let(:directory_page) { create(:wiki_page, title: 'parent directory/child page') }
where(:page, :title, :changed) do
:untitled_page | nil | false
@@ -737,10 +713,7 @@ describe WikiPage do
end
context 'when the page is inside an actual directory' do
- subject do
- create_page('dir_1/dir_1_1/file', 'content')
- wiki.find_page('dir_1/dir_1_1/file')
- end
+ subject { create(:wiki_page, title: 'dir_1/dir_1_1/file') }
it 'returns the full directory hierarchy' do
expect(subject.directory).to eq('dir_1/dir_1_1')
@@ -787,6 +760,16 @@ describe WikiPage do
end
end
+ describe '#persisted?' do
+ it 'returns true for a persisted page' do
+ expect(existing_page).to be_persisted
+ end
+
+ it 'returns false for an unpersisted page' do
+ expect(new_page).not_to be_persisted
+ end
+ end
+
describe '#to_partial_path' do
it 'returns the relative path to the partial to be used' do
expect(subject.to_partial_path).to eq('projects/wikis/wiki_page')
@@ -812,23 +795,23 @@ describe WikiPage do
other_page = create(:wiki_page)
expect(subject.slug).not_to eq(other_page.slug)
- expect(subject.project).not_to eq(other_page.project)
+ expect(subject.container).not_to eq(other_page.container)
expect(subject).not_to eq(other_page)
end
- it 'returns false for page with different slug on same project' do
- other_page = create(:wiki_page, project: subject.project)
+ it 'returns false for page with different slug on same container' do
+ other_page = create(:wiki_page, container: subject.container)
expect(subject.slug).not_to eq(other_page.slug)
- expect(subject.project).to eq(other_page.project)
+ expect(subject.container).to eq(other_page.container)
expect(subject).not_to eq(other_page)
end
- it 'returns false for page with the same slug on a different project' do
+ it 'returns false for page with the same slug on a different container' do
other_page = create(:wiki_page, title: existing_page.slug)
expect(subject.slug).to eq(other_page.slug)
- expect(subject.project).not_to eq(other_page.project)
+ expect(subject.container).not_to eq(other_page.container)
expect(subject).not_to eq(other_page)
end
end
@@ -858,19 +841,21 @@ describe WikiPage do
end
end
- private
-
- def remove_temp_repo(path)
- FileUtils.rm_rf path
- end
+ describe '#version_commit_timestamp' do
+ context 'for a new page' do
+ it 'returns nil' do
+ expect(new_page.version_commit_timestamp).to be_nil
+ end
+ end
- def commit_details
- Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit")
+ context 'for page that exists' do
+ it 'returns the timestamp of the commit' do
+ expect(existing_page.version_commit_timestamp).to eq(existing_page.version.commit.committed_date)
+ end
+ end
end
- def create_page(name, content)
- wiki.wiki.write_page(name, :markdown, content, commit_details)
- end
+ private
def get_slugs(page_or_dir)
if page_or_dir.is_a? WikiPage
diff --git a/spec/models/x509_commit_signature_spec.rb b/spec/models/x509_commit_signature_spec.rb
index a2f72228a86..2efb77c96ad 100644
--- a/spec/models/x509_commit_signature_spec.rb
+++ b/spec/models/x509_commit_signature_spec.rb
@@ -9,6 +9,15 @@ RSpec.describe X509CommitSignature do
let(:x509_certificate) { create(:x509_certificate) }
let(:x509_signature) { create(:x509_commit_signature, commit_sha: commit_sha) }
+ let(:attributes) do
+ {
+ commit_sha: commit_sha,
+ project: project,
+ x509_certificate_id: x509_certificate.id,
+ verification_status: "verified"
+ }
+ end
+
it_behaves_like 'having unique enum values'
describe 'validation' do
@@ -23,15 +32,6 @@ RSpec.describe X509CommitSignature do
end
describe '.safe_create!' do
- let(:attributes) do
- {
- commit_sha: commit_sha,
- project: project,
- x509_certificate_id: x509_certificate.id,
- verification_status: "verified"
- }
- end
-
it 'finds a signature by commit sha if it existed' do
x509_signature
@@ -50,4 +50,18 @@ RSpec.describe X509CommitSignature do
expect(signature.x509_certificate_id).to eq(x509_certificate.id)
end
end
+
+ describe '#user' do
+ context 'if email is assigned to a user' do
+ let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
+
+ it 'returns user' do
+ expect(described_class.safe_create!(attributes).user).to eq(user)
+ end
+ end
+
+ it 'if email is not assigned to a user, return nil' do
+ expect(described_class.safe_create!(attributes).user).to be_nil
+ end
+ end
end