diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-28 18:09:27 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-28 18:09:27 +0300 |
commit | b8026fd558e7ec154c626208a33c1485aec8f4ea (patch) | |
tree | 367f99b19d437db07b0c88e74a4a1086f22684e3 /spec | |
parent | 73a14e23da782bbafad03a0cfe9fcab8f44f13f2 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r-- | spec/controllers/search_controller_spec.rb | 7 | ||||
-rw-r--r-- | spec/graphql/types/upload_type_spec.rb | 13 | ||||
-rw-r--r-- | spec/policies/upload_policy_spec.rb | 76 | ||||
-rw-r--r-- | spec/requests/api/graphql/mutations/uploads/delete_spec.rb | 74 | ||||
-rw-r--r-- | spec/requests/api/users_spec.rb | 14 | ||||
-rw-r--r-- | spec/services/ci/create_pipeline_service/rules_spec.rb | 1028 | ||||
-rw-r--r-- | spec/services/ci/create_pipeline_service_spec.rb | 1013 | ||||
-rw-r--r-- | spec/services/uploads/destroy_service_spec.rb | 103 | ||||
-rw-r--r-- | spec/support/shared_contexts/policies/group_policy_shared_context.rb | 1 | ||||
-rw-r--r-- | spec/support/shared_contexts/policies/project_policy_shared_context.rb | 1 | ||||
-rw-r--r-- | spec/workers/post_receive_spec.rb | 6 |
11 files changed, 1311 insertions, 1025 deletions
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 6922b0f3831..e03a8b7d321 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -393,6 +393,13 @@ RSpec.describe SearchController do get(:autocomplete, params: { term: 'foo@bar.com', scope: 'users' }) end end + + it 'can be filtered with params[:filter]' do + get :autocomplete, params: { term: 'setting', filter: 'generic' } + expect(response).to have_gitlab_http_status(:ok) + expect(json_response.count).to eq(1) + expect(json_response.first['label']).to match(/User settings/) + end end describe '#append_info_to_payload' do diff --git a/spec/graphql/types/upload_type_spec.rb b/spec/graphql/types/upload_type_spec.rb new file mode 100644 index 00000000000..2b959fbf105 --- /dev/null +++ b/spec/graphql/types/upload_type_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['FileUpload'] do + it { expect(described_class).to require_graphql_authorizations(:read_upload) } + + it 'has the expected fields' do + expected_fields = %w[id size path] + + expect(described_class).to include_graphql_fields(*expected_fields) + end +end diff --git a/spec/policies/upload_policy_spec.rb b/spec/policies/upload_policy_spec.rb new file mode 100644 index 00000000000..1169df0b300 --- /dev/null +++ b/spec/policies/upload_policy_spec.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe UploadPolicy do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:guest) { create(:user).tap { |user| group.add_guest(user) } } + let_it_be(:developer) { create(:user).tap { |user| group.add_developer(user) } } + let_it_be(:maintainer) { create(:user).tap { |user| group.add_maintainer(user) } } + let_it_be(:owner) { create(:user).tap { |user| group.add_owner(user) } } + let_it_be(:admin) { create(:admin) } + let_it_be(:non_member_user) { create(:user) } + + let(:upload_permissions) { [:read_upload, :destroy_upload] } + + shared_examples_for 'uploads policy' do + subject { described_class.new(current_user, upload) } + + context 'when user is guest' do + let(:current_user) { guest } + + it { is_expected.to be_disallowed(*upload_permissions) } + end + + context 'when user is developer' do + let(:current_user) { developer } + + it { is_expected.to be_disallowed(*upload_permissions) } + end + + context 'when user is maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(*upload_permissions) } + end + + context 'when user is owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(*upload_permissions) } + end + + context 'when user is admin' do + let(:current_user) { admin } + + it { is_expected.to be_disallowed(*upload_permissions) } + + context 'with admin mode', :enable_admin_mode do + it { is_expected.to be_allowed(*upload_permissions) } + end + end + end + + describe 'destroy_upload' do + context 'when deleting project upload' do + let_it_be(:upload) { create(:upload, model: project) } + + it_behaves_like 'uploads policy' + end + + context 'when deleting group upload' do + let_it_be(:upload) { create(:upload, model: group) } + + it_behaves_like 'uploads policy' + end + + context 'when deleting upload associated with other model' do + let_it_be(:upload) { create(:upload, model: maintainer) } + + subject { described_class.new(maintainer, upload) } + + it { is_expected.to be_disallowed(*upload_permissions) } + end + end +end diff --git a/spec/requests/api/graphql/mutations/uploads/delete_spec.rb b/spec/requests/api/graphql/mutations/uploads/delete_spec.rb new file mode 100644 index 00000000000..f44bf179397 --- /dev/null +++ b/spec/requests/api/graphql/mutations/uploads/delete_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Delete an upload' do + include GraphqlHelpers + + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:developer) { create(:user).tap { |user| group.add_developer(user) } } + let_it_be(:maintainer) { create(:user).tap { |user| group.add_maintainer(user) } } + + let(:extra_params) { {} } + let(:params) { { filename: File.basename(upload.path), secret: upload.secret }.merge(extra_params) } + let(:mutation) { graphql_mutation(:uploadDelete, params) } + let(:mutation_response) { graphql_mutation_response(:upload_delete) } + + shared_examples_for 'upload deletion' do + context 'when the user is not allowed to delete uploads' do + let(:current_user) { developer } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when the user is anonymous' do + let(:current_user) { nil } + + it_behaves_like 'a mutation that returns a top-level access error' + end + + context 'when user has permissions to delete uploads' do + let(:current_user) { maintainer } + + it 'deletes the upload' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['upload']).to include('id' => upload.to_global_id.to_s) + expect(mutation_response['errors']).to be_empty + end + + context 'when upload does not exist' do + let(:params) { { filename: 'invalid', secret: upload.secret }.merge(extra_params) } + + it 'returns an error' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['upload']).to be_nil + expect(mutation_response['errors']).to match_array([ + "The resource that you are attempting to access does not "\ + "exist or you don't have permission to perform this action." + ]) + end + end + end + end + + context 'when deleting project upload' do + let_it_be_with_reload(:upload) { create(:upload, :issuable_upload, model: project) } + + let(:extra_params) { { project_path: project.full_path } } + + it_behaves_like 'upload deletion' + end + + context 'when deleting group upload' do + let_it_be_with_reload(:upload) { create(:upload, :namespace_upload, model: group) } + + let(:extra_params) { { group_path: group.full_path } } + + it_behaves_like 'upload deletion' + end +end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 68d5fad8ff4..1c8b74558ae 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1184,7 +1184,7 @@ RSpec.describe API::Users do post api('/users', admin), params: { email: 'invalid email', - password: 'password', + password: User.random_password, name: 'test' } expect(response).to have_gitlab_http_status(:bad_request) @@ -1250,7 +1250,7 @@ RSpec.describe API::Users do post api('/users', admin), params: { email: 'test@example.com', - password: 'password', + password: User.random_password, username: 'test', name: 'foo' } @@ -1262,7 +1262,7 @@ RSpec.describe API::Users do params: { name: 'foo', email: 'test@example.com', - password: 'password', + password: User.random_password, username: 'foo' } end.to change { User.count }.by(0) @@ -1276,7 +1276,7 @@ RSpec.describe API::Users do params: { name: 'foo', email: 'foo@example.com', - password: 'password', + password: User.random_password, username: 'test' } end.to change { User.count }.by(0) @@ -1290,7 +1290,7 @@ RSpec.describe API::Users do params: { name: 'foo', email: 'foo@example.com', - password: 'password', + password: User.random_password, username: 'TEST' } end.to change { User.count }.by(0) @@ -1635,8 +1635,8 @@ RSpec.describe API::Users do context "with existing user" do before do - post api("/users", admin), params: { email: 'test@example.com', password: 'password', username: 'test', name: 'test' } - post api("/users", admin), params: { email: 'foo@bar.com', password: 'password', username: 'john', name: 'john' } + post api("/users", admin), params: { email: 'test@example.com', password: User.random_password, username: 'test', name: 'test' } + post api("/users", admin), params: { email: 'foo@bar.com', password: User.random_password, username: 'john', name: 'john' } @user = User.all.last end diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb index d0ce1c5aba8..88cec0cde9a 100644 --- a/spec/services/ci/create_pipeline_service/rules_spec.rb +++ b/spec/services/ci/create_pipeline_service/rules_spec.rb @@ -7,10 +7,38 @@ RSpec.describe Ci::CreatePipelineService do let(:ref) { 'refs/heads/master' } let(:source) { :push } let(:service) { described_class.new(project, user, { ref: ref }) } - let(:pipeline) { service.execute(source).payload } + let(:response) { execute_service } + let(:pipeline) { response.payload } let(:build_names) { pipeline.builds.pluck(:name) } + def execute_service(before: '00000000', variables_attributes: nil) + params = { ref: ref, before: before, after: project.commit(ref).sha, variables_attributes: variables_attributes } + + described_class + .new(project, user, params) + .execute(source) do |pipeline| + yield(pipeline) if block_given? + end + end + context 'job:rules' do + let(:regular_job) { find_job('regular-job') } + let(:rules_job) { find_job('rules-job') } + let(:delayed_job) { find_job('delayed-job') } + + def find_job(name) + pipeline.builds.find_by(name: name) + end + + shared_examples 'rules jobs are excluded' do + it 'only persists the job without rules' do + expect(pipeline).to be_persisted + expect(regular_job).to be_persisted + expect(rules_job).to be_nil + expect(delayed_job).to be_nil + end + end + before do stub_ci_pipeline_yaml_file(config) allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| @@ -95,10 +123,6 @@ RSpec.describe Ci::CreatePipelineService do end context 'with allow_failure and exit_codes', :aggregate_failures do - def find_job(name) - pipeline.builds.find_by(name: name) - end - let(:config) do <<-EOY job-1: @@ -280,6 +304,773 @@ RSpec.describe Ci::CreatePipelineService do end end end + + context 'with simple if: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + master-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - if: $CI_COMMIT_REF_NAME == "nonexistant-branch" + when: never + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + negligible-job: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: true + + delayed-job: + script: "echo See you later, World!" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: delayed + start_in: 1 hour + + never-job: + script: "echo Goodbye, World!" + rules: + - if: $CI_COMMIT_REF_NAME + when: never + EOY + end + + context 'with matches' do + it 'creates a pipeline with the vanilla and manual jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'regular-job', 'delayed-job', 'master-job', 'negligible-job' + ) + end + + it 'assigns job:when values to the builds' do + expect(find_job('regular-job').when).to eq('on_success') + expect(find_job('master-job').when).to eq('manual') + expect(find_job('negligible-job').when).to eq('on_success') + expect(find_job('delayed-job').when).to eq('delayed') + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('regular-job').allow_failure).to eq(false) + expect(find_job('master-job').allow_failure).to eq(false) + expect(find_job('negligible-job').allow_failure).to eq(true) + expect(find_job('delayed-job').allow_failure).to eq(false) + end + + it 'assigns start_in for delayed jobs' do + expect(delayed_job.options[:start_in]).to eq('1 hour') + end + end + + context 'with no matches' do + let(:ref) { 'refs/heads/feature' } + + it_behaves_like 'rules jobs are excluded' + end + end + + context 'with complex if: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + rules: + - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME + when: manual + allow_failure: true + EOY + end + + it 'matches the first rule' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + expect(regular_job.when).to eq('manual') + expect(regular_job.allow_failure).to eq(true) + end + end + end + + context 'changes:' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - changes: + - README.md + when: manual + - changes: + - app.rb + when: on_success + + delayed-job: + script: "echo See you later, World!" + rules: + - changes: + - README.md + when: delayed + start_in: 4 hours + + negligible-job: + script: "can be failed sometimes" + rules: + - changes: + - README.md + allow_failure: true + + README: + script: "I use variables for changes!" + rules: + - changes: + - $CI_JOB_NAME* + + changes-paths: + script: "I am using a new syntax!" + rules: + - changes: + paths: [README.md] + EOY + end + + context 'and matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it 'creates five jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths' + ) + end + + it 'sets when: for all jobs' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('manual') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('4 hours') + end + + it 'sets allow_failure: for negligible job' do + expect(find_job('negligible-job').allow_failure).to eq(true) + end + end + + context 'and matches the second rule' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb]) + end + end + + it 'includes both jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job', 'rules-job') + end + + it 'sets when: for the created rules job based on the second clause' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('on_success') + end + end + + context 'and does not match' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[useless_script.rb]) + end + end + + it_behaves_like 'rules jobs are excluded' + + it 'sets when: for the created job' do + expect(regular_job.when).to eq('on_success') + end + end + + context 'with paths and compare_to' do + let_it_be(:project) { create(:project, :empty_repo) } + let_it_be(:user) { project.first_owner } + + before_all do + project.repository.add_branch(user, 'feature_1', 'master') + + project.repository.create_file( + user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1' + ) + + project.repository.add_branch(user, 'feature_2', 'feature_1') + + project.repository.create_file( + user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_2' + ) + end + + let(:changed_file) { 'file2.txt' } + let(:ref) { 'feature_2' } + + let(:response) { execute_service(before: nil) } + + context 'for jobs rules' do + let(:config) do + <<-EOY + job1: + script: exit 0 + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} + + job2: + script: exit 0 + EOY + end + + context 'when there is no such compare_to ref' do + let(:compare_to) { 'invalid-branch' } + + it 'returns an error' do + expect(pipeline.errors.full_messages).to eq([ + 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref' + ]) + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + + context 'when the compare_to ref exists' do + let(:compare_to) { 'feature_1'} + + context 'when the rule matches' do + it 'creates job1 and job2' do + expect(build_names).to contain_exactly('job1', 'job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + + context 'when the rule does not match' do + let(:changed_file) { 'file1.txt' } + + it 'does not create job1' do + expect(build_names).to contain_exactly('job2') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(build_names).to contain_exactly('job1', 'job2') + end + end + end + end + end + + context 'for workflow rules' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [#{changed_file}] + compare_to: #{compare_to} + + job1: + script: exit 0 + EOY + end + + let(:compare_to) { 'feature_1'} + + context 'when the rule matches' do + it 'creates job1' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') + end + + context 'when the FF ci_rules_changes_compare is not enabled' do + before do + stub_feature_flags(ci_rules_changes_compare: false) + end + + it 'ignores compare_to and changes is always true' do + expect(pipeline).to be_created_successfully + expect(build_names).to contain_exactly('job1') + end + end + end + + context 'when the rule does not match' do + let(:changed_file) { 'file1.txt' } + + it 'does not create job1' do + expect(pipeline).not_to be_created_successfully + expect(build_names).to be_empty + end + end + end + end + end + + context 'mixed if: and changes: rules' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + allow_failure: true + rules: + - changes: + - README.md + when: manual + - if: $CI_COMMIT_REF_NAME == "master" + when: on_success + allow_failure: false + + delayed-job: + script: "echo See you later, World!" + rules: + - changes: + - README.md + when: delayed + start_in: 4 hours + allow_failure: true + - if: $CI_COMMIT_REF_NAME == "master" + when: delayed + start_in: 1 hour + EOY + end + + context 'and changes: matches before if' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it 'creates two jobs' do + expect(pipeline).to be_persisted + expect(build_names) + .to contain_exactly('regular-job', 'rules-job', 'delayed-job') + end + + it 'sets when: for all jobs' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('manual') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('4 hours') + end + + it 'sets allow_failure: for all jobs' do + expect(regular_job.allow_failure).to eq(false) + expect(rules_job.allow_failure).to eq(true) + expect(delayed_job.allow_failure).to eq(true) + end + end + + context 'and if: matches after changes' do + it 'includes both jobs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job') + end + + it 'sets when: for the created rules job based on the second clause' do + expect(regular_job.when).to eq('on_success') + expect(rules_job.when).to eq('on_success') + expect(delayed_job.when).to eq('delayed') + expect(delayed_job.options[:start_in]).to eq('1 hour') + end + end + + context 'and does not match' do + let(:ref) { 'refs/heads/wip' } + + it_behaves_like 'rules jobs are excluded' + + it 'sets when: for the created job' do + expect(regular_job.when).to eq('on_success') + end + end + end + + context 'mixed if: and changes: clauses' do + let(:config) do + <<-EOY + regular-job: + script: 'echo Hello, World!' + + rules-job: + script: "echo hello world, $CI_COMMIT_REF_NAME" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + changes: [README.md] + when: on_success + allow_failure: true + - if: $CI_COMMIT_REF_NAME =~ /master/ + changes: [app.rb] + when: manual + EOY + end + + context 'with if matches and changes matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[app.rb]) + end + end + + it 'persists all jobs' do + expect(pipeline).to be_persisted + expect(regular_job).to be_persisted + expect(rules_job).to be_persisted + expect(rules_job.when).to eq('manual') + expect(rules_job.allow_failure).to eq(false) + end + end + + context 'with if matches and no change matches' do + it_behaves_like 'rules jobs are excluded' + end + + context 'with change matches and no if matches' do + let(:ref) { 'refs/heads/feature' } + + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[README.md]) + end + end + + it_behaves_like 'rules jobs are excluded' + end + + context 'and no matches' do + let(:ref) { 'refs/heads/feature' } + + it_behaves_like 'rules jobs are excluded' + end + end + + context 'complex if: allow_failure usages' do + let(:config) do + <<-EOY + job-1: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: false + + job-2: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: false + + job-3: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: true + + job-4: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: false + + job-5: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + allow_failure: true + + job-6: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + allow_failure: false + - allow_failure: true + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6') + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('job-1').allow_failure).to eq(false) + expect(find_job('job-4').allow_failure).to eq(false) + expect(find_job('job-5').allow_failure).to eq(true) + expect(find_job('job-6').allow_failure).to eq(true) + end + end + + context 'complex if: allow_failure & when usages' do + let(:config) do + <<-EOY + job-1: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-2: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + allow_failure: true + + job-3: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-4: + script: "exit 1" + allow_failure: true + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + allow_failure: false + + job-5: + script: "exit 1" + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + when: manual + allow_failure: false + - when: always + allow_failure: true + + job-6: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-7: + script: "exit 1" + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ + when: manual + - when: :on_failure + allow_failure: true + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly( + 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7' + ) + end + + it 'assigns job:allow_failure values to the builds' do + expect(find_job('job-1').allow_failure).to eq(false) + expect(find_job('job-2').allow_failure).to eq(true) + expect(find_job('job-3').allow_failure).to eq(true) + expect(find_job('job-4').allow_failure).to eq(false) + expect(find_job('job-5').allow_failure).to eq(true) + expect(find_job('job-6').allow_failure).to eq(false) + expect(find_job('job-7').allow_failure).to eq(true) + end + + it 'assigns job:when values to the builds' do + expect(find_job('job-1').when).to eq('manual') + expect(find_job('job-2').when).to eq('manual') + expect(find_job('job-3').when).to eq('manual') + expect(find_job('job-4').when).to eq('manual') + expect(find_job('job-5').when).to eq('always') + expect(find_job('job-6').when).to eq('manual') + expect(find_job('job-7').when).to eq('on_failure') + end + end + + context 'deploy freeze period `if:` clause' do + # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" + let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') } + + context 'with 2 jobs' do + let(:config) do + <<-EOY + stages: + - test + - deploy + + test-job: + script: + - echo 'running TEST stage' + + deploy-job: + stage: deploy + script: + - echo 'running DEPLOY stage' + rules: + - if: $CI_DEPLOY_FREEZE == null + EOY + end + + context 'when outside freeze period' do + it 'creates two jobs' do + Timecop.freeze(2020, 4, 10, 22, 59) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('test-job', 'deploy-job') + end + end + end + + context 'when inside freeze period' do + it 'creates one job' do + Timecop.freeze(2020, 4, 10, 23, 1) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('test-job') + end + end + end + end + + context 'with 1 job' do + let(:config) do + <<-EOY + stages: + - deploy + + deploy-job: + stage: deploy + script: + - echo 'running DEPLOY stage' + rules: + - if: $CI_DEPLOY_FREEZE == null + EOY + end + + context 'when outside freeze period' do + it 'creates two jobs' do + Timecop.freeze(2020, 4, 10, 22, 59) do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('deploy-job') + end + end + end + + context 'when inside freeze period' do + it 'does not create the pipeline', :aggregate_failures do + Timecop.freeze(2020, 4, 10, 23, 1) do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + end + end + + context 'with when:manual' do + let(:config) do + <<-EOY + job-with-rules: + script: 'echo hey' + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + + job-when-with-rules: + script: 'echo hey' + when: manual + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + + job-when-with-rules-when: + script: 'echo hey' + when: manual + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: on_success + + job-with-rules-when: + script: 'echo hey' + rules: + - if: $CI_COMMIT_REF_NAME =~ /master/ + when: manual + + job-without-rules: + script: 'echo this is a job with NO rules' + EOY + end + + let(:job_with_rules) { find_job('job-with-rules') } + let(:job_when_with_rules) { find_job('job-when-with-rules') } + let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') } + let(:job_with_rules_when) { find_job('job-with-rules-when') } + let(:job_without_rules) { find_job('job-without-rules') } + + context 'when matching the rules' do + let(:ref) { 'refs/heads/master' } + + it 'adds the job-with-rules with a when:manual' do + expect(job_with_rules).to be_persisted + expect(job_when_with_rules).to be_persisted + expect(job_when_with_rules_when).to be_persisted + expect(job_with_rules_when).to be_persisted + expect(job_without_rules).to be_persisted + + expect(job_with_rules.when).to eq('on_success') + expect(job_when_with_rules.when).to eq('manual') + expect(job_when_with_rules_when.when).to eq('on_success') + expect(job_with_rules_when.when).to eq('manual') + expect(job_without_rules.when).to eq('on_success') + end + end + + context 'when there is no match to the rule' do + let(:ref) { 'refs/heads/wip' } + + it 'does not add job_with_rules' do + expect(job_with_rules).to be_nil + expect(job_when_with_rules).to be_nil + expect(job_when_with_rules_when).to be_nil + expect(job_with_rules_when).to be_nil + expect(job_without_rules).to be_persisted + end + end end end @@ -447,5 +1238,232 @@ RSpec.describe Ci::CreatePipelineService do end end end + + context 'with persisted variables' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $CI_COMMIT_REF_NAME == "master" + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + end + + context 'with no matches' do + let(:ref) { 'refs/heads/feature' } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'with pipeline variables' do + let(:pipeline) do + execute_service(variables_attributes: variables_attributes).payload + end + + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + let(:variables_attributes) do + [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + end + + context 'with no matches' do + let(:variables_attributes) { {} } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'with trigger variables' do + let(:pipeline) do + execute_service do |pipeline| + pipeline.variables.build(variables) + end.payload + end + + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + regular-job: + script: 'echo Hello, World!' + EOY + end + + context 'with matches' do + let(:variables) do + [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('regular-job') + end + + context 'when a job requires the same variable' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + build: + stage: build + script: 'echo build' + rules: + - if: $SOME_VARIABLE + + test1: + stage: test + script: 'echo test1' + needs: [build] + + test2: + stage: test + script: 'echo test2' + EOY + end + + it 'creates a pipeline' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('build', 'test1', 'test2') + end + end + end + + context 'with no matches' do + let(:variables) { {} } + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + + context 'when a job requires the same variable' do + let(:config) do + <<-EOY + workflow: + rules: + - if: $SOME_VARIABLE + + build: + stage: build + script: 'echo build' + rules: + - if: $SOME_VARIABLE + + test1: + stage: test + script: 'echo test1' + needs: [build] + + test2: + stage: test + script: 'echo test2' + EOY + end + + it 'does not create a pipeline', :aggregate_failures do + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + end + + context 'changes' do + shared_examples 'comparing file changes with workflow rules' do + context 'when matches' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[file1.md]) + end + end + + it 'creates the pipeline with a job' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job') + end + end + + context 'when does not match' do + before do + allow_next_instance_of(Ci::Pipeline) do |pipeline| + allow(pipeline).to receive(:modified_paths).and_return(%w[unknown]) + end + end + + it 'creates the pipeline with a job' do + expect(pipeline.errors.full_messages).to eq(['Pipeline filtered out by workflow rules.']) + expect(response).to be_error + expect(pipeline).not_to be_persisted + end + end + end + + context 'changes is an array' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: [file1.md] + + job: + script: exit 0 + EOY + end + + it_behaves_like 'comparing file changes with workflow rules' + end + + context 'changes:paths is an array' do + let(:config) do + <<-EOY + workflow: + rules: + - changes: + paths: [file1.md] + + job: + script: exit 0 + EOY + end + + it_behaves_like 'comparing file changes with workflow rules' + end + end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 6f89e10da7f..a9442b0dc68 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1866,1019 +1866,6 @@ RSpec.describe Ci::CreatePipelineService do end end end - - context 'when rules are used' do - let(:ref_name) { 'refs/heads/master' } - let(:response) { execute_service } - let(:pipeline) { response.payload } - let(:build_names) { pipeline.builds.pluck(:name) } - let(:regular_job) { find_job('regular-job') } - let(:rules_job) { find_job('rules-job') } - let(:delayed_job) { find_job('delayed-job') } - - context 'with when:manual' do - let(:config) do - <<-EOY - job-with-rules: - script: 'echo hey' - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - - job-when-with-rules: - script: 'echo hey' - when: manual - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - - job-when-with-rules-when: - script: 'echo hey' - when: manual - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: on_success - - job-with-rules-when: - script: 'echo hey' - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-without-rules: - script: 'echo this is a job with NO rules' - EOY - end - - let(:job_with_rules) { find_job('job-with-rules') } - let(:job_when_with_rules) { find_job('job-when-with-rules') } - let(:job_when_with_rules_when) { find_job('job-when-with-rules-when') } - let(:job_with_rules_when) { find_job('job-with-rules-when') } - let(:job_without_rules) { find_job('job-without-rules') } - - context 'when matching the rules' do - let(:ref_name) { 'refs/heads/master' } - - it 'adds the job-with-rules with a when:manual' do - expect(job_with_rules).to be_persisted - expect(job_when_with_rules).to be_persisted - expect(job_when_with_rules_when).to be_persisted - expect(job_with_rules_when).to be_persisted - expect(job_without_rules).to be_persisted - - expect(job_with_rules.when).to eq('on_success') - expect(job_when_with_rules.when).to eq('manual') - expect(job_when_with_rules_when.when).to eq('on_success') - expect(job_with_rules_when.when).to eq('manual') - expect(job_without_rules.when).to eq('on_success') - end - end - - context 'when there is no match to the rule' do - let(:ref_name) { 'refs/heads/wip' } - - it 'does not add job_with_rules' do - expect(job_with_rules).to be_nil - expect(job_when_with_rules).to be_nil - expect(job_when_with_rules_when).to be_nil - expect(job_with_rules_when).to be_nil - expect(job_without_rules).to be_persisted - end - end - end - - shared_examples 'rules jobs are excluded' do - it 'only persists the job without rules' do - expect(pipeline).to be_persisted - expect(regular_job).to be_persisted - expect(rules_job).to be_nil - expect(delayed_job).to be_nil - end - end - - def find_job(name) - pipeline.builds.find_by(name: name) - end - - before do - stub_ci_pipeline_yaml_file(config) - allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) - end - - context 'with simple if: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - master-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - if: $CI_COMMIT_REF_NAME == "nonexistant-branch" - when: never - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - negligible-job: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: true - - delayed-job: - script: "echo See you later, World!" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: delayed - start_in: 1 hour - - never-job: - script: "echo Goodbye, World!" - rules: - - if: $CI_COMMIT_REF_NAME - when: never - EOY - end - - context 'with matches' do - it 'creates a pipeline with the vanilla and manual jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'regular-job', 'delayed-job', 'master-job', 'negligible-job' - ) - end - - it 'assigns job:when values to the builds' do - expect(find_job('regular-job').when).to eq('on_success') - expect(find_job('master-job').when).to eq('manual') - expect(find_job('negligible-job').when).to eq('on_success') - expect(find_job('delayed-job').when).to eq('delayed') - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('regular-job').allow_failure).to eq(false) - expect(find_job('master-job').allow_failure).to eq(false) - expect(find_job('negligible-job').allow_failure).to eq(true) - expect(find_job('delayed-job').allow_failure).to eq(false) - end - - it 'assigns start_in for delayed jobs' do - expect(delayed_job.options[:start_in]).to eq('1 hour') - end - end - - context 'with no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it_behaves_like 'rules jobs are excluded' - end - end - - context 'with complex if: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - rules: - - if: $VAR == 'present' && $OTHER || $CI_COMMIT_REF_NAME - when: manual - allow_failure: true - EOY - end - - it 'matches the first rule' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - expect(regular_job.when).to eq('manual') - expect(regular_job.allow_failure).to eq(true) - end - end - - context 'with changes:' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - changes: - - README.md - when: manual - - changes: - - app.rb - when: on_success - - delayed-job: - script: "echo See you later, World!" - rules: - - changes: - - README.md - when: delayed - start_in: 4 hours - - negligible-job: - script: "can be failed sometimes" - rules: - - changes: - - README.md - allow_failure: true - - README: - script: "I use variables for changes!" - rules: - - changes: - - $CI_JOB_NAME* - - changes-paths: - script: "I am using a new syntax!" - rules: - - changes: - paths: [README.md] - EOY - end - - context 'and matches' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it 'creates five jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'regular-job', 'rules-job', 'delayed-job', 'negligible-job', 'README', 'changes-paths' - ) - end - - it 'sets when: for all jobs' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('manual') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('4 hours') - end - - it 'sets allow_failure: for negligible job' do - expect(find_job('negligible-job').allow_failure).to eq(true) - end - end - - context 'and matches the second rule' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[app.rb]) - end - - it 'includes both jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job', 'rules-job') - end - - it 'sets when: for the created rules job based on the second clause' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('on_success') - end - end - - context 'and does not match' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[useless_script.rb]) - end - - it_behaves_like 'rules jobs are excluded' - - it 'sets when: for the created job' do - expect(regular_job.when).to eq('on_success') - end - end - end - - context 'with changes: paths and compare_to' do - before_all do - project.repository.add_branch(user, 'feature_1', 'master') - - project.repository.create_file( - user, 'file1.txt', 'file 1', message: 'Create file1.txt', branch_name: 'feature_1' - ) - - project.repository.add_branch(user, 'feature_2', 'feature_1') - - project.repository.create_file( - user, 'file2.txt', 'file 2', message: 'Create file2.txt', branch_name: 'feature_2' - ) - end - - let(:changed_file) { 'file2.txt' } - let(:ref_name) { 'feature_2' } - - let(:response) { execute_service(ref: ref_name, before: nil, after: project.commit(ref_name).sha) } - - context 'for jobs rules' do - let(:config) do - <<-EOY - job1: - script: exit 0 - rules: - - changes: - paths: [#{changed_file}] - compare_to: #{compare_to} - - job2: - script: exit 0 - EOY - end - - context 'when there is no such compare_to ref' do - let(:compare_to) { 'invalid-branch' } - - it 'returns an error' do - expect(pipeline.errors.full_messages).to eq([ - 'Failed to parse rule for job1: rules:changes:compare_to is not a valid ref' - ]) - end - - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end - - it 'ignores compare_to and changes is always true' do - expect(build_names).to contain_exactly('job1', 'job2') - end - end - end - - context 'when the compare_to ref exists' do - let(:compare_to) { 'feature_1'} - - context 'when the rule matches' do - it 'creates job1 and job2' do - expect(build_names).to contain_exactly('job1', 'job2') - end - - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end - - it 'ignores compare_to and changes is always true' do - expect(build_names).to contain_exactly('job1', 'job2') - end - end - end - - context 'when the rule does not match' do - let(:changed_file) { 'file1.txt' } - - it 'does not create job1' do - expect(build_names).to contain_exactly('job2') - end - - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end - - it 'ignores compare_to and changes is always true' do - expect(build_names).to contain_exactly('job1', 'job2') - end - end - end - end - end - - context 'for workflow rules' do - let(:config) do - <<-EOY - workflow: - rules: - - changes: - paths: [#{changed_file}] - compare_to: #{compare_to} - - job1: - script: exit 0 - EOY - end - - let(:compare_to) { 'feature_1'} - - context 'when the rule matches' do - it 'creates job1' do - expect(pipeline).to be_created_successfully - expect(build_names).to contain_exactly('job1') - end - - context 'when the FF ci_rules_changes_compare is not enabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end - - it 'ignores compare_to and changes is always true' do - expect(pipeline).to be_created_successfully - expect(build_names).to contain_exactly('job1') - end - end - end - - context 'when the rule does not match' do - let(:changed_file) { 'file1.txt' } - - it 'does not create job1' do - expect(pipeline).not_to be_created_successfully - expect(build_names).to be_empty - end - end - end - end - - context 'with mixed if: and changes: rules' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - allow_failure: true - rules: - - changes: - - README.md - when: manual - - if: $CI_COMMIT_REF_NAME == "master" - when: on_success - allow_failure: false - - delayed-job: - script: "echo See you later, World!" - rules: - - changes: - - README.md - when: delayed - start_in: 4 hours - allow_failure: true - - if: $CI_COMMIT_REF_NAME == "master" - when: delayed - start_in: 1 hour - EOY - end - - context 'and changes: matches before if' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it 'creates two jobs' do - expect(pipeline).to be_persisted - expect(build_names) - .to contain_exactly('regular-job', 'rules-job', 'delayed-job') - end - - it 'sets when: for all jobs' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('manual') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('4 hours') - end - - it 'sets allow_failure: for all jobs' do - expect(regular_job.allow_failure).to eq(false) - expect(rules_job.allow_failure).to eq(true) - expect(delayed_job.allow_failure).to eq(true) - end - end - - context 'and if: matches after changes' do - it 'includes both jobs' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job', 'rules-job', 'delayed-job') - end - - it 'sets when: for the created rules job based on the second clause' do - expect(regular_job.when).to eq('on_success') - expect(rules_job.when).to eq('on_success') - expect(delayed_job.when).to eq('delayed') - expect(delayed_job.options[:start_in]).to eq('1 hour') - end - end - - context 'and does not match' do - let(:ref_name) { 'refs/heads/wip' } - - it_behaves_like 'rules jobs are excluded' - - it 'sets when: for the created job' do - expect(regular_job.when).to eq('on_success') - end - end - end - - context 'with mixed if: and changes: clauses' do - let(:config) do - <<-EOY - regular-job: - script: 'echo Hello, World!' - - rules-job: - script: "echo hello world, $CI_COMMIT_REF_NAME" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - changes: [README.md] - when: on_success - allow_failure: true - - if: $CI_COMMIT_REF_NAME =~ /master/ - changes: [app.rb] - when: manual - EOY - end - - context 'with if matches and changes matches' do - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[app.rb]) - end - - it 'persists all jobs' do - expect(pipeline).to be_persisted - expect(regular_job).to be_persisted - expect(rules_job).to be_persisted - expect(rules_job.when).to eq('manual') - expect(rules_job.allow_failure).to eq(false) - end - end - - context 'with if matches and no change matches' do - it_behaves_like 'rules jobs are excluded' - end - - context 'with change matches and no if matches' do - let(:ref_name) { 'refs/heads/feature' } - - before do - allow_any_instance_of(Ci::Pipeline) - .to receive(:modified_paths).and_return(%w[README.md]) - end - - it_behaves_like 'rules jobs are excluded' - end - - context 'and no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it_behaves_like 'rules jobs are excluded' - end - end - - context 'with complex if: allow_failure usages' do - let(:config) do - <<-EOY - job-1: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: false - - job-2: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: false - - job-3: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: true - - job-4: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: false - - job-5: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - allow_failure: true - - job-6: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - allow_failure: false - - allow_failure: true - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('job-1', 'job-4', 'job-5', 'job-6') - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('job-1').allow_failure).to eq(false) - expect(find_job('job-4').allow_failure).to eq(false) - expect(find_job('job-5').allow_failure).to eq(true) - expect(find_job('job-6').allow_failure).to eq(true) - end - end - - context 'with complex if: allow_failure & when usages' do - let(:config) do - <<-EOY - job-1: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-2: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - allow_failure: true - - job-3: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-4: - script: "exit 1" - allow_failure: true - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - allow_failure: false - - job-5: - script: "exit 1" - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - when: manual - allow_failure: false - - when: always - allow_failure: true - - job-6: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /master/ - when: manual - - job-7: - script: "exit 1" - allow_failure: false - rules: - - if: $CI_COMMIT_REF_NAME =~ /nonexistant-branch/ - when: manual - - when: :on_failure - allow_failure: true - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly( - 'job-1', 'job-2', 'job-3', 'job-4', 'job-5', 'job-6', 'job-7' - ) - end - - it 'assigns job:allow_failure values to the builds' do - expect(find_job('job-1').allow_failure).to eq(false) - expect(find_job('job-2').allow_failure).to eq(true) - expect(find_job('job-3').allow_failure).to eq(true) - expect(find_job('job-4').allow_failure).to eq(false) - expect(find_job('job-5').allow_failure).to eq(true) - expect(find_job('job-6').allow_failure).to eq(false) - expect(find_job('job-7').allow_failure).to eq(true) - end - - it 'assigns job:when values to the builds' do - expect(find_job('job-1').when).to eq('manual') - expect(find_job('job-2').when).to eq('manual') - expect(find_job('job-3').when).to eq('manual') - expect(find_job('job-4').when).to eq('manual') - expect(find_job('job-5').when).to eq('always') - expect(find_job('job-6').when).to eq('manual') - expect(find_job('job-7').when).to eq('on_failure') - end - end - - context 'with deploy freeze period `if:` clause' do - # '0 23 * * 5' == "At 23:00 on Friday."", '0 7 * * 1' == "At 07:00 on Monday."" - let!(:freeze_period) { create(:ci_freeze_period, project: project, freeze_start: '0 23 * * 5', freeze_end: '0 7 * * 1') } - - context 'with 2 jobs' do - let(:config) do - <<-EOY - stages: - - test - - deploy - - test-job: - script: - - echo 'running TEST stage' - - deploy-job: - stage: deploy - script: - - echo 'running DEPLOY stage' - rules: - - if: $CI_DEPLOY_FREEZE == null - EOY - end - - context 'when outside freeze period' do - it 'creates two jobs' do - Timecop.freeze(2020, 4, 10, 22, 59) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('test-job', 'deploy-job') - end - end - end - - context 'when inside freeze period' do - it 'creates one job' do - Timecop.freeze(2020, 4, 10, 23, 1) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('test-job') - end - end - end - end - - context 'with 1 job' do - let(:config) do - <<-EOY - stages: - - deploy - - deploy-job: - stage: deploy - script: - - echo 'running DEPLOY stage' - rules: - - if: $CI_DEPLOY_FREEZE == null - EOY - end - - context 'when outside freeze period' do - it 'creates two jobs' do - Timecop.freeze(2020, 4, 10, 22, 59) do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('deploy-job') - end - end - end - - context 'when inside freeze period' do - it 'does not create the pipeline', :aggregate_failures do - Timecop.freeze(2020, 4, 10, 23, 1) do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - end - end - - context 'with workflow rules with persisted variables' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $CI_COMMIT_REF_NAME == "master" - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - end - - context 'with no matches' do - let(:ref_name) { 'refs/heads/feature' } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - - context 'with workflow rules with pipeline variables' do - let(:pipeline) do - execute_service(variables_attributes: variables_attributes).payload - end - - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - let(:variables_attributes) do - [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - end - - context 'with no matches' do - let(:variables_attributes) { {} } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - - context 'with workflow rules with trigger variables' do - let(:pipeline) do - execute_service do |pipeline| - pipeline.variables.build(variables) - end.payload - end - - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - regular-job: - script: 'echo Hello, World!' - EOY - end - - context 'with matches' do - let(:variables) do - [{ key: 'SOME_VARIABLE', secret_value: 'SOME_VAR' }] - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('regular-job') - end - - context 'when a job requires the same variable' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - build: - stage: build - script: 'echo build' - rules: - - if: $SOME_VARIABLE - - test1: - stage: test - script: 'echo test1' - needs: [build] - - test2: - stage: test - script: 'echo test2' - EOY - end - - it 'creates a pipeline' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('build', 'test1', 'test2') - end - end - end - - context 'with no matches' do - let(:variables) { {} } - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - - context 'when a job requires the same variable' do - let(:config) do - <<-EOY - workflow: - rules: - - if: $SOME_VARIABLE - - build: - stage: build - script: 'echo build' - rules: - - if: $SOME_VARIABLE - - test1: - stage: test - script: 'echo test1' - needs: [build] - - test2: - stage: test - script: 'echo test2' - EOY - end - - it 'does not create a pipeline', :aggregate_failures do - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - end - - context 'with workflow rules changes' do - shared_examples 'comparing file changes with workflow rules' do - context 'when matches' do - before do - allow_next_instance_of(Ci::Pipeline) do |pipeline| - allow(pipeline).to receive(:modified_paths).and_return(%w[file1.md]) - end - end - - it 'creates the pipeline with a job' do - expect(pipeline).to be_persisted - expect(build_names).to contain_exactly('job') - end - end - - context 'when does not match' do - before do - allow_next_instance_of(Ci::Pipeline) do |pipeline| - allow(pipeline).to receive(:modified_paths).and_return(%w[unknown]) - end - end - - it 'creates the pipeline with a job' do - expect(pipeline.errors.full_messages).to eq(['Pipeline filtered out by workflow rules.']) - expect(response).to be_error - expect(pipeline).not_to be_persisted - end - end - end - - context 'changes is an array' do - let(:config) do - <<-EOY - workflow: - rules: - - changes: [file1.md] - - job: - script: exit 0 - EOY - end - - it_behaves_like 'comparing file changes with workflow rules' - end - - context 'changes:paths is an array' do - let(:config) do - <<-EOY - workflow: - rules: - - changes: - paths: [file1.md] - - job: - script: exit 0 - EOY - end - - it_behaves_like 'comparing file changes with workflow rules' - end - end - end end describe '#execute!' do diff --git a/spec/services/uploads/destroy_service_spec.rb b/spec/services/uploads/destroy_service_spec.rb new file mode 100644 index 00000000000..bb58da231b6 --- /dev/null +++ b/spec/services/uploads/destroy_service_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Uploads::DestroyService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be_with_reload(:upload) { create(:upload, :issuable_upload, model: project) } + + let(:filename) { File.basename(upload.path) } + let(:secret) { upload.secret } + let(:model) { project } + let(:service) { described_class.new(model, user) } + + describe '#execute' do + subject { service.execute(secret, filename) } + + shared_examples_for 'upload not found' do + it 'does not delete any upload' do + expect { subject }.not_to change { Upload.count } + end + + it 'returns an error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq("The resource that you are attempting to access does not "\ + "exist or you don't have permission to perform this action.") + end + end + + context 'when user is nil' do + let(:user) { nil } + + it_behaves_like 'upload not found' + end + + context 'when user cannot destroy upload' do + before do + project.add_developer(user) + end + + it_behaves_like 'upload not found' + end + + context 'when user can destroy upload' do + before do + project.add_maintainer(user) + end + + it 'deletes the upload' do + expect { subject }.to change { Upload.count }.by(-1) + end + + it 'returns success response' do + expect(subject[:status]).to eq(:success) + expect(subject[:upload]).to eq(upload) + end + + context 'when upload is not found' do + let(:filename) { 'not existing filename' } + + it_behaves_like 'upload not found' + end + + context 'when upload secret is not found' do + let(:secret) { 'aaaaaaaaaa' } + + it_behaves_like 'upload not found' + end + + context 'when upload secret has invalid format' do + let(:secret) { 'invalid' } + + it_behaves_like 'upload not found' + end + + context 'when unknown model is used' do + let(:model) { user } + + it 'raises an error' do + expect { subject }.to raise_exception(ArgumentError) + end + end + + context 'when upload belongs to other model' do + let_it_be(:upload) { create(:upload, :namespace_upload) } + + it_behaves_like 'upload not found' + end + + context 'when upload destroy fails' do + before do + allow(service).to receive(:find_upload).and_return(upload) + allow(upload).to receive(:destroy).and_return(false) + end + + it 'returns error' do + expect(subject[:status]).to eq(:error) + expect(subject[:message]).to eq('Upload could not be deleted.') + end + end + end + end +end diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index eec6e92c5fe..893d3702407 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -56,6 +56,7 @@ RSpec.shared_context 'GroupPolicy context' do admin_package create_projects create_cluster update_cluster admin_cluster add_cluster + destroy_upload ] end diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index 789b385c435..1d4731d9b39 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -62,6 +62,7 @@ RSpec.shared_context 'ProjectPolicy context' do admin_project admin_project_member admin_snippet admin_terraform_state admin_wiki create_deploy_token destroy_deploy_token push_to_delete_protected_branch read_deploy_token update_snippet + destroy_upload ] end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 4ddb793516f..d632ca39e44 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -452,6 +452,12 @@ RSpec.describe PostReceive do perform end + it 'updates the snippet model updated_at' do + expect(snippet).to receive(:touch) + + perform + end + it 'updates snippet statistics' do expect(Snippets::UpdateStatisticsService).to receive(:new).with(snippet).and_call_original |