diff options
Diffstat (limited to 'spec/support/shared_examples')
29 files changed, 823 insertions, 70 deletions
diff --git a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb index a99068ab678..370f2072705 100644 --- a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb @@ -94,16 +94,32 @@ RSpec.shared_examples 'chat slash commands service' do subject.trigger(params) end + shared_examples_for 'blocks command execution' do + it do + expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute) + + result = subject.trigger(params) + expect(result[:text]).to match(error_message) + end + end + context 'when user is blocked' do before do chat_name.user.block end - it 'blocks command execution' do - expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute) + it_behaves_like 'blocks command execution' do + let(:error_message) { 'you do not have access to the GitLab project' } + end + end - result = subject.trigger(params) - expect(result).to include(text: /^You are not allowed/) + context 'when user is deactivated' do + before do + chat_name.user.deactivate + end + + it_behaves_like 'blocks command execution' do + let(:error_message) { 'your account has been deactivated by your administrator' } end end end diff --git a/spec/support/shared_examples/ci/pipeline_email_examples.rb b/spec/support/shared_examples/ci/pipeline_email_examples.rb new file mode 100644 index 00000000000..f72d8af3c65 --- /dev/null +++ b/spec/support/shared_examples/ci/pipeline_email_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +shared_examples_for 'correct pipeline information for pipelines for merge requests' do + context 'when pipeline for merge request' do + let(:pipeline) { merge_request.all_pipelines.first } + + let(:merge_request) do + create(:merge_request, :with_detached_merge_request_pipeline, + source_project: project, + target_project: project) + end + + it 'renders a source ref of the pipeline' do + render + + expect(rendered).to have_content pipeline.source_ref + expect(rendered).not_to have_content pipeline.ref + end + end +end diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb index e2b4b50d41d..441d3f4ccb9 100644 --- a/spec/support/shared_examples/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/ci_trace_shared_examples.rb @@ -423,7 +423,7 @@ shared_examples_for 'trace with disabled live trace feature' do expect(build.job_artifacts_trace.file.filename).to eq('job.log') expect(File.exist?(src_path)).to be_falsy expect(src_checksum) - .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest) + .to eq(described_class.hexdigest(build.job_artifacts_trace.file.path)) expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum) end end @@ -449,7 +449,7 @@ shared_examples_for 'trace with disabled live trace feature' do expect(build.job_artifacts_trace.file.filename).to eq('job.log') expect(build.old_trace).to be_nil expect(src_checksum) - .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest) + .to eq(described_class.hexdigest(build.job_artifacts_trace.file.path)) expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum) end end @@ -787,7 +787,7 @@ shared_examples_for 'trace with enabled live trace feature' do expect(build.job_artifacts_trace.file.filename).to eq('job.log') expect(Ci::BuildTraceChunk.where(build: build)).not_to be_exist expect(src_checksum) - .to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).hexdigest) + .to eq(described_class.hexdigest(build.job_artifacts_trace.file.path)) expect(build.job_artifacts_trace.file_sha256).to eq(src_checksum) end end diff --git a/spec/support/shared_examples/common_system_notes_examples.rb b/spec/support/shared_examples/common_system_notes_examples.rb index 75f93a32d78..ca79603a022 100644 --- a/spec/support/shared_examples/common_system_notes_examples.rb +++ b/spec/support/shared_examples/common_system_notes_examples.rb @@ -27,3 +27,30 @@ shared_examples 'WIP notes creation' do |wip_action| expect(Note.second.note).to match('changed title') end end + +shared_examples_for 'a note with overridable created_at' do + let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) } + + it 'the note has the correct time' do + expect(subject.created_at).to eq Time.at(42) + end +end + +shared_examples_for 'a system note' do |params| + let(:expected_noteable) { noteable } + let(:commit_count) { nil } + + it 'has the correct attributes', :aggregate_failures do + exclude_project = !params.nil? && params[:exclude_project] + + expect(subject).to be_valid + expect(subject).to be_system + + expect(subject.noteable).to eq expected_noteable + expect(subject.project).to eq project unless exclude_project + expect(subject.author).to eq author + + expect(subject.system_note_metadata.action).to eq(action) + expect(subject.system_note_metadata.commit_count).to eq(commit_count) + end +end diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb index d3cadf2ba7c..5dea17069f9 100644 --- a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb +++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb @@ -25,13 +25,17 @@ shared_examples 'a controller that can serve LFS files' do |options = {}| context 'when lfs is enabled' do before do allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true) + allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) + allow(controller).to receive(:send_file) { controller.head :ok } end - context 'when project has access' do + def link_project(project) + project.lfs_objects << lfs_object + end + + context 'when the project is linked to the LfsObject' do before do - project.lfs_objects << lfs_object - allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true) - allow(controller).to receive(:send_file) { controller.head :ok } + link_project(project) end it 'serves the file' do @@ -76,13 +80,68 @@ shared_examples 'a controller that can serve LFS files' do |options = {}| end end - context 'when project does not have access' do + context 'when project is not linked to the LfsObject' do it 'does not serve the file' do subject expect(response).to have_gitlab_http_status(404) end end + + context 'when the project is part of a fork network' do + shared_examples 'a controller that correctly serves lfs files within a fork network' do + it do + expect(fork_network_member).not_to eq(fork_network.root_project) + end + + it 'does not serve the file if no members are linked to the LfsObject' do + subject + + expect(response).to have_gitlab_http_status(404) + end + + it 'serves the file when the fork network root is linked to the LfsObject' do + link_project(fork_network.root_project) + + subject + + expect(response).to have_gitlab_http_status(200) + end + + it 'serves the file when the fork network member is linked to the LfsObject' do + link_project(fork_network_member) + + subject + + expect(response).to have_gitlab_http_status(200) + end + end + + context 'when the project is the root of the fork network' do + let!(:fork_network) { create(:fork_network, root_project: project) } + let!(:fork_network_member) { create(:fork_network_member, fork_network: fork_network).project } + + before do + project.reload + end + + it_behaves_like 'a controller that correctly serves lfs files within a fork network' + end + + context 'when the project is a downstream member of the fork network' do + let!(:fork_network) { create(:fork_network) } + let!(:fork_network_member) do + create(:fork_network_member, project: project, fork_network: fork_network) + project + end + + before do + project.reload + end + + it_behaves_like 'a controller that correctly serves lfs files within a fork network' + end + end end context 'when lfs is not enabled' do diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index 4bc22861d58..c24418b2f90 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -104,7 +104,7 @@ shared_examples 'handle uploads' do context "when neither the uploader nor the model exists" do before do - allow_any_instance_of(Upload).to receive(:build_uploader).and_return(nil) + allow_any_instance_of(Upload).to receive(:retrieve_uploader).and_return(nil) allow(controller).to receive(:find_model).and_return(nil) end @@ -338,7 +338,7 @@ shared_examples 'handle uploads authorize' do it_behaves_like 'a valid response' do it 'responds with status 200, location of uploads remote store and object details' do - expect(json_response['TempPath']).to eq(uploader_class.workhorse_local_upload_path) + expect(json_response).not_to have_key('TempPath') expect(json_response['RemoteObject']).to have_key('ID') expect(json_response['RemoteObject']).to have_key('GetURL') expect(json_response['RemoteObject']).to have_key('StoreURL') diff --git a/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb new file mode 100644 index 00000000000..dce1dbe1cd1 --- /dev/null +++ b/spec/support/shared_examples/cycle_analytics_event_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +shared_examples_for 'cycle analytics event' do + let(:instance) { described_class.new({}) } + + it { expect(described_class.name).to be_a_kind_of(String) } + it { expect(described_class.identifier).to be_a_kind_of(Symbol) } + it { expect(instance.object_type.ancestors).to include(ApplicationRecord) } + it { expect(instance).to respond_to(:timestamp_projection) } + + describe '#apply_query_customization' do + it 'expects an ActiveRecord::Relation object as argument and returns a modified version of it' do + input_query = instance.object_type.all + + output_query = instance.apply_query_customization(input_query) + expect(output_query).to be_a_kind_of(ActiveRecord::Relation) + end + end +end diff --git a/spec/support/shared_examples/cycle_analytics_stage_examples.rb b/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb index 151f5325e84..afa035d039a 100644 --- a/spec/support/shared_examples/cycle_analytics_stage_examples.rb +++ b/spec/support/shared_examples/cycle_analytics_stage_shared_examples.rb @@ -46,13 +46,20 @@ shared_examples_for 'cycle analytics stage' do expect(stage).not_to be_valid expect(stage.errors.details[:end_event]).to eq([{ error: :not_allowed_for_the_given_start_event }]) end + + context 'disallows default stage names when creating custom stage' do + let(:invalid_params) { valid_params.merge(name: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first, custom: true) } + let(:stage) { described_class.new(invalid_params) } + + it { expect(stage).not_to be_valid } + end end - describe '#subject_model' do + describe '#subject_class' do it 'infers the model from the start event' do stage = described_class.new(valid_params) - expect(stage.subject_model).to eq(MergeRequest) + expect(stage.subject_class).to eq(MergeRequest) end end @@ -71,4 +78,30 @@ shared_examples_for 'cycle analytics stage' do expect(stage.end_event).to be_a_kind_of(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged) end end + + describe '#matches_with_stage_params?' do + let(:params) { Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_test_stage } + + it 'matches with default stage params' do + stage = described_class.new(params) + + expect(stage).to be_default_stage + expect(stage).to be_matches_with_stage_params(params) + end + + it "mismatches when the stage is custom" do + stage = described_class.new(params.merge(custom: true)) + + expect(stage).not_to be_default_stage + expect(stage).not_to be_matches_with_stage_params(params) + end + end + + describe '#parent_id' do + it "delegates to 'parent_name'_id attribute" do + stage = described_class.new(parent: parent) + + expect(stage.parent_id).to eq(parent.id) + end + end end diff --git a/spec/support/shared_examples/diff_file_collections.rb b/spec/support/shared_examples/diff_file_collections.rb index 367ddf06c28..4c64abd2a97 100644 --- a/spec/support/shared_examples/diff_file_collections.rb +++ b/spec/support/shared_examples/diff_file_collections.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true shared_examples 'diff statistics' do |test_include_stats_flag: true| + subject { described_class.new(diffable, collection_default_args) } + def stub_stats_find_by_path(path, stats_mock) expect_next_instance_of(Gitlab::Git::DiffStatsCollection) do |collection| allow(collection).to receive(:find_by_path).and_call_original @@ -10,8 +12,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true| context 'when should request diff stats' do it 'Repository#diff_stats is called' do - subject = described_class.new(diffable, collection_default_args) - expect(diffable.project.repository) .to receive(:diff_stats) .with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha) @@ -21,8 +21,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true| end it 'Gitlab::Diff::File is initialized with diff stats' do - subject = described_class.new(diffable, collection_default_args) - stats_mock = double(Gitaly::DiffStats, path: '.gitignore', additions: 758, deletions: 120) stub_stats_find_by_path(stub_path, stats_mock) @@ -37,8 +35,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true| it 'Repository#diff_stats is not called' do collection_default_args[:diff_options][:include_stats] = false - subject = described_class.new(diffable, collection_default_args) - expect(diffable.project.repository).not_to receive(:diff_stats) subject.diff_files diff --git a/spec/support/shared_examples/evidence_updated_exposed_fields.rb b/spec/support/shared_examples/evidence_updated_exposed_fields.rb new file mode 100644 index 00000000000..2a02fdd7666 --- /dev/null +++ b/spec/support/shared_examples/evidence_updated_exposed_fields.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +shared_examples 'updated exposed field' do + it 'creates another Evidence object' do + model.send("#{updated_field}=", updated_value) + + expect(model.evidence_summary_keys).to include(updated_field) + expect { model.save! }.to change(Evidence, :count).by(1) + expect(updated_json_field).to eq(updated_value) + end +end + +shared_examples 'updated non-exposed field' do + it 'does not create any Evidence object' do + model.send("#{updated_field}=", updated_value) + + expect(model.evidence_summary_keys).not_to include(updated_field) + expect { model.save! }.not_to change(Evidence, :count) + end +end + +shared_examples 'updated field on non-linked entity' do + it 'does not create any Evidence object' do + model.send("#{updated_field}=", updated_value) + + expect(model.evidence_summary_keys).to be_empty + expect { model.save! }.not_to change(Evidence, :count) + end +end diff --git a/spec/support/shared_examples/lfs_http_shared_examples.rb b/spec/support/shared_examples/lfs_http_shared_examples.rb new file mode 100644 index 00000000000..bcd30fe9654 --- /dev/null +++ b/spec/support/shared_examples/lfs_http_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +shared_examples 'LFS http 200 response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 200 } + end +end + +shared_examples 'LFS http 401 response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 401 } + end +end + +shared_examples 'LFS http 403 response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 403 } + let(:message) { 'Access forbidden. Check your access level.' } + end +end + +shared_examples 'LFS http 501 response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 501 } + let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' } + end +end + +shared_examples 'LFS http 404 response' do + it_behaves_like 'LFS http expected response code and message' do + let(:response_code) { 404 } + end +end + +shared_examples 'LFS http expected response code and message' do + let(:response_code) { } + let(:message) { } + + it 'responds with the expected response code and message' do + expect(response).to have_gitlab_http_status(response_code) + expect(json_response['message']).to eq(message) if message + end +end diff --git a/spec/support/shared_examples/lib/gitlab/ci/build/rules/rule/clause/clause_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/build/rules/rule/clause/clause_shared_examples.rb new file mode 100644 index 00000000000..1934fd584f3 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/ci/build/rules/rule/clause/clause_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a glob matching rule' do + using RSpec::Parameterized::TableSyntax + + where(:case_name, :globs, :files, :satisfied) do + 'exact top-level match' | ['Dockerfile'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true + 'exact top-level no match' | ['Dockerfile'] | { 'Gemfile' => '' } | false + 'pattern top-level match' | ['Docker*'] | { 'Dockerfile' => '', 'Gemfile' => '' } | true + 'pattern top-level no match' | ['Docker*'] | { 'Gemfile' => '' } | false + 'exact nested match' | ['project/build.properties'] | { 'project/build.properties' => '' } | true + 'exact nested no match' | ['project/build.properties'] | { 'project/README.md' => '' } | false + 'pattern nested match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/goproject.go' => '' } | true + 'pattern nested no match' | ['src/**/*.go'] | { 'src/gitlab.com/goproject/README.md' => '' } | false + 'ext top-level match' | ['*.go'] | { 'main.go' => '', 'cmd/goproject/main.go' => '' } | true + 'ext nested no match' | ['*.go'] | { 'cmd/goproject/main.go' => '' } | false + 'ext slash no match' | ['/*.go'] | { 'main.go' => '', 'cmd/goproject/main.go' => '' } | false + end + + with_them do + it { is_expected.to eq(satisfied) } + end +end diff --git a/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb new file mode 100644 index 00000000000..f26a8554055 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/import_export/project_tree_restorer_shared_examples.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# Shared examples for ProjectTreeRestorer (shared to allow the testing +# of EE-specific features) +RSpec.shared_examples 'restores project correctly' do |**results| + it 'restores the project' do + expect(shared.errors).to be_empty + expect(restored_project_json).to be_truthy + end + + it 'has labels' do + labels_size = results.fetch(:labels, 0) + + expect(project.labels.size).to eq(labels_size) + end + + it 'has label priorities' do + label_with_priorities = results[:label_with_priorities] + + if label_with_priorities + expect(project.labels.find_by(title: label_with_priorities).priorities).not_to be_empty + end + end + + it 'has milestones' do + expect(project.milestones.size).to eq(results.fetch(:milestones, 0)) + end + + it 'has issues' do + expect(project.issues.size).to eq(results.fetch(:issues, 0)) + end + + it 'does not set params that are excluded from import_export settings' do + expect(project.import_type).to be_nil + expect(project.creator_id).not_to eq 123 + end +end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index 5341aacb445..a6653f89377 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -11,6 +11,20 @@ shared_examples 'cluster application status specs' do |application_name| end end + describe '#status_states' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + subject { described_class.new(cluster: cluster) } + + it 'returns a hash of state values' do + expect(subject.status_states).to include(:installed) + end + + it 'returns an integer for installed state value' do + expect(subject.status_states[:installed]).to eq(3) + end + end + describe '.available' do subject { described_class.available } @@ -61,7 +75,7 @@ shared_examples 'cluster application status specs' do |application_name| subject.reload - expect(subject.version).to eq(subject.class.const_get(:VERSION)) + expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) end context 'application is updating' do @@ -90,13 +104,14 @@ shared_examples 'cluster application status specs' do |application_name| subject.reload - expect(subject.version).to eq(subject.class.const_get(:VERSION)) + expect(subject.version).to eq(subject.class.const_get(:VERSION, false)) end end end describe '#make_errored' do subject { create(application_name, :installing) } + let(:reason) { 'some errors' } it 'is errored' do diff --git a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb index 181b102e685..ba02da41b53 100644 --- a/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_version_shared_examples.rb @@ -12,7 +12,7 @@ shared_examples 'cluster application version specs' do |application_name| context 'version is the same as VERSION' do let(:application) { build(application_name) } - let(:version) { application.class.const_get(:VERSION) } + let(:version) { application.class.const_get(:VERSION, false) } it { is_expected.to be_falsey } end diff --git a/spec/support/shared_examples/models/clusters/providers/provider_status.rb b/spec/support/shared_examples/models/clusters/providers/provider_status.rb new file mode 100644 index 00000000000..63cb9a56f5b --- /dev/null +++ b/spec/support/shared_examples/models/clusters/providers/provider_status.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +shared_examples 'provider status' do |factory| + describe 'state_machine' do + context 'when any => [:created]' do + let(:provider) { build(factory, :creating) } + + it 'nullifies API credentials' do + expect(provider).to receive(:nullify_credentials).and_call_original + provider.make_created + + expect(provider).to be_created + end + end + + context 'when any => [:creating]' do + let(:provider) { build(factory) } + let(:operation_id) { 'operation-xxx' } + + it 'calls #assign_operation_id on the provider' do + expect(provider).to receive(:assign_operation_id).with(operation_id).and_call_original + + provider.make_creating(operation_id) + end + end + + context 'when any => [:errored]' do + let(:provider) { build(factory, :creating) } + let(:status_reason) { 'err msg' } + + it 'calls #nullify_credentials on the provider' do + expect(provider).to receive(:nullify_credentials).and_call_original + + provider.make_errored(status_reason) + end + + it 'sets a status reason' do + provider.make_errored(status_reason) + + expect(provider.status_reason).to eq('err msg') + end + + context 'when status_reason is nil' do + let(:provider) { build(factory, :errored) } + + it 'does not set status_reason' do + provider.make_errored(nil) + + expect(provider.status_reason).not_to be_nil + end + end + end + end + + describe '#on_creation?' do + using RSpec::Parameterized::TableSyntax + + subject { provider.on_creation? } + + where(:status, :result) do + :scheduled | true + :creating | true + :created | false + :errored | false + end + + with_them do + let(:provider) { build(factory, status) } + + it { is_expected.to eq result } + end + end +end diff --git a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb index 9604555c57d..4ebb5e35e0e 100644 --- a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb +++ b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + shared_examples_for 'matches_cross_reference_regex? fails fast' do it 'fails fast for long strings' do # took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172823 @@ -6,3 +8,59 @@ shared_examples_for 'matches_cross_reference_regex? fails fast' do end.not_to raise_error end end + +shared_examples_for 'validates description length with custom validation' do + let(:issuable) { build(:issue, description: 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1)) } + let(:context) { :update } + + subject { issuable.validate(context) } + + context 'when Issuable is a new record' do + it 'validates the maximum description length' do + subject + expect(issuable.errors[:description]).to eq(["is too long (maximum is #{::Issuable::DESCRIPTION_LENGTH_MAX} characters)"]) + end + + context 'on create' do + let(:context) { :create } + + it 'does not validate the maximum description length' do + allow(issuable).to receive(:description_max_length_for_new_records_is_valid).and_call_original + + subject + + expect(issuable).not_to have_received(:description_max_length_for_new_records_is_valid) + end + end + end + + context 'when Issuable is an existing record' do + before do + allow(issuable).to receive(:expire_etag_cache) # to skip the expire_etag_cache callback + + issuable.save!(validate: false) + end + + it 'does not validate the maximum description length' do + subject + expect(issuable.errors).not_to have_key(:description) + end + end +end + +shared_examples_for 'truncates the description to its allowed maximum length on import' do + before do + allow(issuable).to receive(:importing?).and_return(true) + end + + let(:issuable) { build(:issue, description: 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1)) } + + subject { issuable.validate(:create) } + + it 'truncates the description to its allowed maximum length' do + subject + + expect(issuable.description).to eq('x' * ::Issuable::DESCRIPTION_LENGTH_MAX) + expect(issuable.errors[:description]).to be_empty + end +end diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb index eb1ade03017..822836c771e 100644 --- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb +++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb @@ -41,7 +41,8 @@ shared_examples_for 'model with uploads' do |supports_fileuploads| end it 'deletes remote files' do - expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(uploads.map(&:path)) + expected_array = array_including(*uploads.map(&:path)) + expect_any_instance_of(Uploads::Fog).to receive(:delete_keys).with(expected_array) model_object.destroy end diff --git a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb index cb5460bde23..b4a8e3fca4d 100644 --- a/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/zoom_quick_actions_shared_examples.rb @@ -45,14 +45,6 @@ shared_examples 'zoom quick actions' do expect(page).to have_content('Failed to add a Zoom meeting') expect(page).not_to have_content(zoom_link) end - - context 'when feature flag disabled' do - before do - stub_feature_flags(issue_zoom_integration: false) - end - - include_examples 'skip silently' - end end context 'with Zoom link not at the end of the issue description' do @@ -92,14 +84,6 @@ shared_examples 'zoom quick actions' do expect(page).to have_content('Zoom meeting removed') expect(issue.reload.description).to eq("Text with #{zoom_link}") end - - context 'when feature flag disabled' do - before do - stub_feature_flags(issue_zoom_integration: false) - end - - include_examples 'skip silently' - end end context 'with a Zoom link not at the end of the description' do diff --git a/spec/support/shared_examples/relative_positioning_shared_examples.rb b/spec/support/shared_examples/relative_positioning_shared_examples.rb index b7382cea93c..99e62ebf422 100644 --- a/spec/support/shared_examples/relative_positioning_shared_examples.rb +++ b/spec/support/shared_examples/relative_positioning_shared_examples.rb @@ -84,6 +84,22 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(item1.relative_position).to be < item2.relative_position end + + context 'when there is no space' do + let(:item3) { create(factory, default_params) } + + before do + item1.update(relative_position: 1000) + item2.update(relative_position: 1001) + item3.update(relative_position: 1002) + end + + it 'moves items correctly' do + item3.move_before(item2) + + expect(item3.relative_position).to be_between(item1.reload.relative_position, item2.reload.relative_position).exclusive + end + end end describe '#move_after' do @@ -94,6 +110,22 @@ RSpec.shared_examples 'a class that supports relative positioning' do expect(item1.relative_position).to be > item2.relative_position end + + context 'when there is no space' do + let(:item3) { create(factory, default_params) } + + before do + item1.update(relative_position: 1000) + item2.update(relative_position: 1001) + item3.update(relative_position: 1002) + end + + it 'moves items correctly' do + item1.move_after(item2) + + expect(item1.relative_position).to be_between(item2.reload.relative_position, item3.reload.relative_position).exclusive + end + end end describe '#move_to_end' do @@ -196,7 +228,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do new_item.move_between(item1, item2) - expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position) + expect(new_item.relative_position).to be_between(item1.relative_position, item2.relative_position).exclusive end it 'uses rebalancing if there is no place' do @@ -208,7 +240,7 @@ RSpec.shared_examples 'a class that supports relative positioning' do new_item.move_between(item2, item3) new_item.save! - expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position) + expect(new_item.relative_position).to be_between(item2.relative_position, item3.relative_position).exclusive expect(item1.reload.relative_position).not_to eq(100) end diff --git a/spec/support/shared_examples/repo_type_shared_examples.rb b/spec/support/shared_examples/repo_type_shared_examples.rb new file mode 100644 index 00000000000..dc9e3a73346 --- /dev/null +++ b/spec/support/shared_examples/repo_type_shared_examples.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +shared_examples 'a repo type' do + describe "#identifier_for_subject" do + subject { described_class.identifier_for_subject(project) } + + it { is_expected.to eq(expected_identifier) } + end + + describe "#fetch_id" do + it "finds an id match in the identifier" do + expect(described_class.fetch_id(expected_identifier)).to eq(expected_id) + end + + it 'does not break on other identifiers' do + expect(described_class.fetch_id("wiki-noid")).to eq(nil) + end + end + + describe "#path_suffix" do + subject { described_class.path_suffix } + + it { is_expected.to eq(expected_suffix) } + end + + describe "#repository_for" do + it "finds the repository for the repo type" do + expect(described_class.repository_for(project)).to eq(expected_repository) + end + end +end diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index afc6f59b773..a2e38cfc60b 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -8,6 +8,14 @@ # * period_in_seconds # * period shared_examples_for 'rate-limited token-authenticated requests' do + let(:throttle_types) do + { + "throttle_protected_paths" => "throttle_authenticated_protected_paths_api", + "throttle_authenticated_api" => "throttle_authenticated_api", + "throttle_authenticated_web" => "throttle_authenticated_web" + } + end + before do # Set low limits settings_to_set[:"#{throttle_setting_prefix}_requests_per_period"] = requests_per_period @@ -84,7 +92,8 @@ shared_examples_for 'rate-limited token-authenticated requests' do request_method: 'GET', path: get_args.first, user_id: user.id, - username: user.username + username: user.username, + throttle_type: throttle_types[throttle_setting_prefix] } expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once @@ -116,6 +125,13 @@ end # * period_in_seconds # * period shared_examples_for 'rate-limited web authenticated requests' do + let(:throttle_types) do + { + "throttle_protected_paths" => "throttle_authenticated_protected_paths_web", + "throttle_authenticated_web" => "throttle_authenticated_web" + } + end + before do login_as(user) @@ -196,7 +212,8 @@ shared_examples_for 'rate-limited web authenticated requests' do request_method: 'GET', path: '/dashboard/snippets', user_id: user.id, - username: user.username + username: user.username, + throttle_type: throttle_types[throttle_setting_prefix] } expect(Gitlab::AuthLogger).to receive(:error).with(arguments).once diff --git a/spec/support/shared_examples/services/boards/boards_create_service.rb b/spec/support/shared_examples/services/boards/boards_create_service.rb index 19818a6091b..7fd69354c2d 100644 --- a/spec/support/shared_examples/services/boards/boards_create_service.rb +++ b/spec/support/shared_examples/services/boards/boards_create_service.rb @@ -17,7 +17,7 @@ shared_examples 'boards create service' do context 'when parent has a board' do before do - create(:board, parent: parent) + create(:board, resource_parent: parent) end it 'does not create a new board' do diff --git a/spec/support/shared_examples/services/boards/boards_list_service.rb b/spec/support/shared_examples/services/boards/boards_list_service.rb index 566e5050f8e..25dc2e04942 100644 --- a/spec/support/shared_examples/services/boards/boards_list_service.rb +++ b/spec/support/shared_examples/services/boards/boards_list_service.rb @@ -15,7 +15,7 @@ shared_examples 'boards list service' do context 'when parent has a board' do before do - create(:board, parent: parent) + create(:board, resource_parent: parent) end it 'does not create a new board' do @@ -24,7 +24,7 @@ shared_examples 'boards list service' do end it 'returns parent boards' do - board = create(:board, parent: parent) + board = create(:board, resource_parent: parent) expect(service.execute).to eq [board] end diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb index 455f7b117b2..f15128d3e13 100644 --- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } - RSpec.shared_examples 'slack or mattermost notifications' do |service_name| let(:chat_service) { described_class.new } let(:webhook_url) { 'https://example.gitlab.com/' } @@ -371,6 +369,48 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name| end end + context 'on a protected branch with protected branches defined using wildcards' do + before do + create(:protected_branch, project: project, name: '*-stable') + end + + let(:data) do + Gitlab::DataBuilder::Push.build( + project: project, + user: user, + ref: '1-stable' + ) + end + + context 'pushing tags' do + let(:data) do + Gitlab::DataBuilder::Push.build( + project: project, + user: user, + ref: "#{Gitlab::Git::TAG_REF_PREFIX}test" + ) + end + + it_behaves_like "triggered #{service_name} service", event_type: "push" + end + + context 'notification enabled only for default branch' do + it_behaves_like "untriggered #{service_name} service", event_type: "push", branches_to_be_notified: "default" + end + + context 'notification enabled only for protected branches' do + it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "protected" + end + + context 'notification enabled only for default and protected branches' do + it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "default_and_protected" + end + + context 'notification enabled for all branches' do + it_behaves_like "triggered #{service_name} service", event_type: "push", branches_to_be_notified: "all" + end + end + context 'on a neither protected nor default branch' do let(:data) do Gitlab::DataBuilder::Push.build( @@ -572,6 +612,36 @@ RSpec.shared_examples 'slack or mattermost notifications' do |service_name| end end + context 'on a protected branch with protected branches defined usin wildcards' do + before do + create(:protected_branch, project: project, name: '*-stable') + end + + let(:pipeline) do + create(:ci_pipeline, + project: project, status: :failed, + sha: project.commit.sha, ref: '1-stable') + end + + let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } + + context 'notification enabled only for default branch' do + it_behaves_like "untriggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default" + end + + context 'notification enabled only for protected branches' do + it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "protected" + end + + context 'notification enabled only for default and protected branches' do + it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "default_and_protected" + end + + context 'notification enabled for all branches' do + it_behaves_like "triggered #{service_name} service", event_type: "pipeline", branches_to_be_notified: "all" + end + end + context 'on a neither protected nor default branch' do let(:pipeline) do create(:ci_pipeline, diff --git a/spec/support/shared_examples/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/snippet_visibility_shared_examples.rb index b5321c6db34..e2089ee623a 100644 --- a/spec/support/shared_examples/snippet_visibility_shared_examples.rb +++ b/spec/support/shared_examples/snippet_visibility_shared_examples.rb @@ -11,13 +11,21 @@ RSpec.shared_examples 'snippet visibility' do set(:author) { create(:user) } set(:member) { create(:user) } set(:external) { create(:user, :external) } + set(:non_member) { create(:user) } + + set(:project) do + create(:project).tap do |project| + project.add_developer(author) + project.add_developer(member) + end + end context "For project snippets" do let!(:users) do { unauthenticated: nil, external: external, - non_member: create(:user), + non_member: non_member, member: member, author: author } @@ -211,14 +219,18 @@ RSpec.shared_examples 'snippet visibility' do end with_them do - let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel.level_value(project_type.to_s)) } + let!(:project_visibility) { project.update_column(:visibility_level, Gitlab::VisibilityLevel.level_value(project_type.to_s)) } let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, feature_visibility) } let!(:user) { users[user_type] } let!(:snippet) { create(:project_snippet, visibility_level: snippet_type, project: project, author: author) } - let!(:members) do - project.add_developer(author) - project.add_developer(member) - project.add_developer(external) if project.private? + let!(:external_member) do + member = project.project_member(external) + + if project.private? + project.add_developer(external) unless member + else + member.delete if member + end end context "For #{params[:project_type]} project and #{params[:user_type]} users" do @@ -256,7 +268,7 @@ RSpec.shared_examples 'snippet visibility' do { unauthenticated: nil, external: external, - non_member: create(:user), + non_member: non_member, author: author } end diff --git a/spec/support/shared_examples/trackable_shared_examples.rb b/spec/support/shared_examples/trackable_shared_examples.rb new file mode 100644 index 00000000000..6ad75a14d6b --- /dev/null +++ b/spec/support/shared_examples/trackable_shared_examples.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +shared_examples 'a Trackable Controller' do + describe '#track_event' do + before do + sign_in user + end + + context 'with no params' do + controller(described_class) do + def index + track_event + head :ok + end + end + + it 'tracks the action name' do + expect(Gitlab::Tracking).to receive(:event).with('AnonymousController', 'index', {}) + get :index + end + end + + context 'with params' do + controller(described_class) do + def index + track_event('some_event', category: 'SomeCategory', label: 'errorlabel') + head :ok + end + end + + it 'tracks with the specified param' do + expect(Gitlab::Tracking).to receive(:event).with('SomeCategory', 'some_event', label: 'errorlabel') + get :index + end + end + end +end diff --git a/spec/support/shared_examples/updating_mentions_shared_examples.rb b/spec/support/shared_examples/updating_mentions_shared_examples.rb index ef385f94cc2..9a8f8012762 100644 --- a/spec/support/shared_examples/updating_mentions_shared_examples.rb +++ b/spec/support/shared_examples/updating_mentions_shared_examples.rb @@ -1,11 +1,16 @@ # frozen_string_literal: true RSpec.shared_examples 'updating mentions' do |service_class| - let(:mentioned_user) { create(:user) } - let(:service_class) { service_class } + let(:service_class) { service_class } + let(:mentioned_user) { create(:user) } + let(:group_member1) { create(:user) } + let(:group_member2) { create(:user) } + let(:external_group) { create(:group, :private) } before do project.add_developer(mentioned_user) + group.add_developer(group_member1) + group.add_developer(group_member2) end def update_mentionable(opts) @@ -16,23 +21,74 @@ RSpec.shared_examples 'updating mentions' do |service_class| mentionable.reload end - context 'in title' do - before do - update_mentionable(title: mentioned_user.to_reference) + context 'when mentioning a different user' do + context 'in title' do + before do + update_mentionable(title: "For #{mentioned_user.to_reference}") + end + + it 'emails only the newly-mentioned user' do + should_only_email(mentioned_user) + end + end + + context 'in description' do + before do + update_mentionable(description: "For #{mentioned_user.to_reference}") + end + + it 'emails only the newly-mentioned user' do + should_only_email(mentioned_user) + end + end + end + + context 'when mentioning a user and a group with access to' do + shared_examples 'updating attribute with allowed mentions' do |attribute| + before do + update_mentionable( + { attribute => "For #{group.to_reference}, cc: #{mentioned_user.to_reference}" } + ) + end + + it 'emails group members' do + should_email(mentioned_user) + should_email(group_member1) + should_email(group_member2) + end + end + + context 'when group is public' do + it_behaves_like 'updating attribute with allowed mentions', :title + it_behaves_like 'updating attribute with allowed mentions', :description end - it 'emails only the newly-mentioned user' do - should_only_email(mentioned_user) + context 'when the group is private' do + before do + group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + end + + it_behaves_like 'updating attribute with allowed mentions', :title + it_behaves_like 'updating attribute with allowed mentions', :description end end - context 'in description' do - before do - update_mentionable(description: mentioned_user.to_reference) + context 'when mentioning a user and a group without access to' do + shared_examples 'updating attribute with not allowed mentions' do |attribute| + before do + update_mentionable( + { attribute => "For #{external_group.to_reference}, cc: #{mentioned_user.to_reference}" } + ) + end + + it 'emails mentioned user' do + should_only_email(mentioned_user) + end end - it 'emails only the newly-mentioned user' do - should_only_email(mentioned_user) + context 'when the group is private' do + it_behaves_like 'updating attribute with not allowed mentions', :title + it_behaves_like 'updating attribute with not allowed mentions', :description end end end diff --git a/spec/support/shared_examples/versioned_description_shared_examples.rb b/spec/support/shared_examples/versioned_description_shared_examples.rb new file mode 100644 index 00000000000..59124af19ec --- /dev/null +++ b/spec/support/shared_examples/versioned_description_shared_examples.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'versioned description' do + describe 'associations' do + it { is_expected.to have_many(:description_versions) } + end + + describe 'save_description_version' do + let(:factory_name) { described_class.name.underscore.to_sym } + let!(:model) { create(factory_name, description: 'Original description') } + + context 'when feature is enabled' do + before do + stub_feature_flags(save_description_versions: true) + end + + context 'when description was changed' do + before do + model.update!(description: 'New description') + end + + it 'saves the old and new description for the first update' do + expect(model.description_versions.first.description).to eq('Original description') + expect(model.description_versions.last.description).to eq('New description') + end + + it 'only saves the new description for subsequent updates' do + expect { model.update!(description: 'Another description') }.to change { model.description_versions.count }.by(1) + + expect(model.description_versions.last.description).to eq('Another description') + end + + it 'sets the new description version to `saved_description_version`' do + expect(model.saved_description_version).to eq(model.description_versions.last) + end + + it 'clears `saved_description_version` after another save that does not change description' do + model.save! + + expect(model.saved_description_version).to be_nil + end + end + + context 'when description was not changed' do + it 'does not save any description version' do + expect { model.save! }.not_to change { model.description_versions.count } + + expect(model.saved_description_version).to be_nil + end + end + end + + context 'when feature is disabled' do + before do + stub_feature_flags(save_description_versions: false) + end + + it 'does not save any description version' do + expect { model.update!(description: 'New description') }.not_to change { model.description_versions.count } + + expect(model.saved_description_version).to be_nil + end + end + end +end |