diff options
Diffstat (limited to 'spec')
114 files changed, 3569 insertions, 580 deletions
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index 58bb91a0c80..767fba7fd58 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -52,7 +52,7 @@ describe SendFileUpload do end context 'with attachment' do - subject { controller.send_upload(uploader, attachment: 'test.js') } + let(:send_attachment) { controller.send_upload(uploader, attachment: 'test.js') } it 'sends a file with content-type of text/plain' do expected_params = { @@ -62,7 +62,29 @@ describe SendFileUpload do } expect(controller).to receive(:send_file).with(uploader.path, expected_params) - subject + send_attachment + end + + context 'with a proxied file in object storage' do + before do + stub_uploads_object_storage(uploader: uploader_class) + uploader.object_store = ObjectStorage::Store::REMOTE + uploader.store!(temp_file) + allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true } + end + + it 'sends a file with a custom type' do + headers = double + expected_headers = %r(response-content-disposition=attachment%3Bfilename%3D%22test.js%22&response-content-type=application/javascript) + expect(Gitlab::Workhorse).to receive(:send_url).with(expected_headers).and_call_original + expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/) + + expect(controller).not_to receive(:send_file) + expect(controller).to receive(:headers) { headers } + expect(controller).to receive(:head).with(:ok) + + send_attachment + end end end @@ -80,7 +102,12 @@ describe SendFileUpload do it 'sends a file' do headers = double + expect(Gitlab::Workhorse).not_to receive(:send_url).with(/response-content-disposition/) + expect(Gitlab::Workhorse).not_to receive(:send_url).with(/response-content-type/) + expect(Gitlab::Workhorse).to receive(:send_url).and_call_original + expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/) + expect(controller).not_to receive(:send_file) expect(controller).to receive(:headers) { headers } expect(controller).to receive(:head).with(:ok) diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index ae49490f31c..65d6cd1a295 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -38,14 +38,6 @@ describe GroupsController do project end - context 'as html' do - it 'assigns whether or not a group has children' do - get :show, id: group.to_param - - expect(assigns(:has_children)).to be_truthy - end - end - context 'as atom' do it 'assigns events for all the projects in the group' do create(:event, project: project) diff --git a/spec/controllers/instance_statistics/cohorts_controller_spec.rb b/spec/controllers/instance_statistics/cohorts_controller_spec.rb index e4eedede93a..596d3c7abe5 100644 --- a/spec/controllers/instance_statistics/cohorts_controller_spec.rb +++ b/spec/controllers/instance_statistics/cohorts_controller_spec.rb @@ -3,5 +3,19 @@ require 'spec_helper' describe InstanceStatistics::CohortsController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + it_behaves_like 'instance statistics availability' + + it 'renders a 404 when the usage ping is disabled' do + stub_application_setting(usage_ping_enabled: false) + + get :index + + expect(response).to have_gitlab_http_status(:not_found) + end end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 42917d0d505..26a532ee01d 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -274,11 +274,43 @@ describe Projects::ClustersController do context 'when creates a cluster' do it 'creates a new cluster' do expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + + expect(project.clusters.first).to be_user + expect(project.clusters.first).to be_kubernetes + end + end + + context 'when creates a RBAC-enabled cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + namespace: 'aaa', + authorization_type: 'rbac' + } + } + } + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_user expect(project.clusters.first).to be_kubernetes + expect(project.clusters.first).to be_platform_kubernetes_rbac end end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index d9499d7e207..751919f9501 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -135,7 +135,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - context 'when requesting JSON with failed job' do + context 'when requesting JSON' do let(:merge_request) { create(:merge_request, source_project: project) } before do @@ -147,61 +147,51 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do get_show(id: job.id, format: :json) end - it 'exposes needed information' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z}) - expect(json_response['new_issue_path']).to include('/issues/new') + context 'when job failed' do + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) + expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + expect(json_response['new_issue_path']).to include('/issues/new') + end end - end - - context 'when request JSON for successful job' do - let(:merge_request) { create(:merge_request, source_project: project) } - let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - - before do - project.add_developer(user) - sign_in(user) - allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) + context 'when job has artifacts' do + context 'with not expiry date' do + let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - get_show(id: job.id, format: :json) - end + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) + expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) + expect(json_response['artifact']).not_to have_key('expired') + expect(json_response['artifact']).not_to have_key('expired_at') + end + end - it 'exposes needed information' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) - expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) - expect(json_response['artifact']).not_to have_key(:expired) - expect(json_response['artifact']).not_to have_key(:expired_at) - expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + context 'with expiry date' do + let(:job) { create(:ci_build, :success, :artifacts, :expired, pipeline: pipeline) } + + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']).not_to have_key('download_path') + expect(json_response['artifact']).not_to have_key('browse_path') + expect(json_response['artifact']['expired']).to eq(true) + expect(json_response['artifact']['expire_at']).not_to be_empty + end + end end - context 'when request JSON for successful job with expired artifacts' do - let(:merge_request) { create(:merge_request, source_project: project) } - let(:job) { create(:ci_build, :success, :artifacts, :expired, pipeline: pipeline) } - - before do - project.add_developer(user) - sign_in(user) - - allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) - - get_show(id: job.id, format: :json) - end + context 'when job has terminal' do + let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } - it 'exposes needed information' do + it 'exposes the terminal path' do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']).not_to have_key(:download_path) - expect(json_response['artifact']).not_to have_key(:browse_path) - expect(json_response['artifact']['expired']).to eq(true) - expect(json_response['artifact']['expire_at']).not_to be_empty - expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + expect(json_response['terminal_path']).to match(%r{/terminal}) end end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 1458113b90c..81badaac76b 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -154,7 +154,7 @@ describe Projects::NotesController do get :index, request_params expect(parsed_response[:notes].count).to eq(1) - expect(note_json[:id]).to eq(note.id) + expect(note_json[:id]).to eq(note.id.to_s) end it 'does not result in N+1 queries' do diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index 325ee53aafb..9802e4d5b1e 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -18,6 +18,20 @@ describe Projects::UploadsController do end end + context "when exception occurs" do + before do + allow(FileUploader).to receive(:workhorse_authorize).and_raise(SocketError.new) + sign_in(create(:user)) + end + + it "responds with status internal_server_error" do + post_authorize + + expect(response).to have_gitlab_http_status(500) + expect(response.body).to eq('Error uploading file') + end + end + def post_authorize(verified: true) request.headers.merge!(workhorse_internal_api_request_header) if verified diff --git a/spec/db/development/import_common_metrics_spec.rb b/spec/db/development/import_common_metrics_spec.rb new file mode 100644 index 00000000000..25061ef0887 --- /dev/null +++ b/spec/db/development/import_common_metrics_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Import metrics on development seed' do + subject { load Rails.root.join('db', 'fixtures', 'development', '99_common_metrics.rb') } + + it "imports all prometheus metrics" do + expect(PrometheusMetric.common).to be_empty + + subject + + expect(PrometheusMetric.common).not_to be_empty + end +end diff --git a/spec/db/importers/common_metrics_importer_spec.rb b/spec/db/importers/common_metrics_importer_spec.rb new file mode 100644 index 00000000000..16b59e1dfe8 --- /dev/null +++ b/spec/db/importers/common_metrics_importer_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'rails_helper' +require Rails.root.join("db", "importers", "common_metrics_importer.rb") + +describe Importers::PrometheusMetric do + it 'group enum equals ::PrometheusMetric' do + expect(described_class.groups).to eq(::PrometheusMetric.groups) + end + + it 'GROUP_TITLES equals ::PrometheusMetric' do + expect(described_class::GROUP_TITLES).to eq(::PrometheusMetric::GROUP_TITLES) + end +end + +describe Importers::CommonMetricsImporter do + subject { described_class.new } + + context "does import common_metrics.yml" do + let(:groups) { subject.content } + let(:metrics) { groups.map { |group| group['metrics'] }.flatten } + let(:queries) { metrics.map { |group| group['queries'] }.flatten } + let(:query_ids) { queries.map { |query| query['id'] } } + + before do + subject.execute + end + + it "has the same amount of groups" do + expect(PrometheusMetric.common.group(:group).count.count).to eq(groups.count) + end + + it "has the same amount of metrics" do + expect(PrometheusMetric.common.group(:group, :title).count.count).to eq(metrics.count) + end + + it "has the same amount of queries" do + expect(PrometheusMetric.common.count).to eq(queries.count) + end + + it "does not have duplicate IDs" do + expect(query_ids).to eq(query_ids.uniq) + end + + it "imports all IDs" do + expect(PrometheusMetric.common.pluck(:identifier)).to contain_exactly(*query_ids) + end + end + + context 'does import properly all fields' do + let(:query_identifier) { 'response-metric' } + let(:group) do + { + group: 'Response metrics (NGINX Ingress)', + metrics: [{ + title: "Throughput", + y_label: "Requests / Sec", + queries: [{ + id: query_identifier, + query_range: 'my-query', + unit: 'my-unit', + label: 'status code' + }] + }] + } + end + + before do + expect(subject).to receive(:content) { [group.deep_stringify_keys] } + end + + shared_examples 'stores metric' do + let(:metric) { PrometheusMetric.find_by(identifier: query_identifier) } + + it 'with all data' do + expect(metric.group).to eq('nginx_ingress') + expect(metric.title).to eq('Throughput') + expect(metric.y_label).to eq('Requests / Sec') + expect(metric.unit).to eq('my-unit') + expect(metric.legend).to eq('status code') + expect(metric.query).to eq('my-query') + end + end + + context 'if ID is missing' do + let(:query_identifier) { } + + it 'raises exception' do + expect { subject.execute }.to raise_error(described_class::MissingQueryId) + end + end + + context 'for existing common metric with different ID' do + let!(:existing_metric) { create(:prometheus_metric, :common, identifier: 'my-existing-metric') } + + before do + subject.execute + end + + it_behaves_like 'stores metric' do + it 'and existing metric is not changed' do + expect(metric).not_to eq(existing_metric) + end + end + end + + context 'when metric with ID exists ' do + let!(:existing_metric) { create(:prometheus_metric, :common, identifier: 'response-metric') } + + before do + subject.execute + end + + it_behaves_like 'stores metric' do + it 'and existing metric is changed' do + expect(metric).to eq(existing_metric) + end + end + end + end +end diff --git a/spec/db/production/import_common_metrics_spec.rb b/spec/db/production/import_common_metrics_spec.rb new file mode 100644 index 00000000000..1e4ff818a86 --- /dev/null +++ b/spec/db/production/import_common_metrics_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Import metrics on production seed' do + subject { load Rails.root.join('db', 'fixtures', 'production', '999_common_metrics.rb') } + + it "imports all prometheus metrics" do + expect(PrometheusMetric.common).to be_empty + + subject + + expect(PrometheusMetric.common).not_to be_empty + end +end diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index a6ff226fa75..9fef424e425 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -77,6 +77,14 @@ FactoryBot.define do pipeline.builds << build(:ci_build, :test_reports, pipeline: pipeline, project: pipeline.project) end end + + trait :auto_devops_source do + config_source { Ci::Pipeline.config_sources[:auto_devops_source] } + end + + trait :repository_source do + config_source { Ci::Pipeline.config_sources[:repository_source] } + end end end end diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb index 89f6ddebf6a..36ac2372204 100644 --- a/spec/factories/clusters/platforms/kubernetes.rb +++ b/spec/factories/clusters/platforms/kubernetes.rb @@ -16,5 +16,9 @@ FactoryBot.define do platform_kubernetes.ca_cert = File.read(pem_file) end end + + trait :rbac_enabled do + authorization_type :rbac + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index c0d819498e1..80801eb1082 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -244,6 +244,10 @@ FactoryBot.define do trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED } trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED } trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE } + + trait :auto_devops do + association :auto_devops, factory: :project_auto_devops + end end # Project with empty repository diff --git a/spec/factories/prometheus_metrics.rb b/spec/factories/prometheus_metrics.rb new file mode 100644 index 00000000000..c56644bfb96 --- /dev/null +++ b/spec/factories/prometheus_metrics.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :prometheus_metric, class: PrometheusMetric do + title 'title' + query 'avg(metric)' + y_label 'y_label' + unit 'm/s' + group :business + project + legend 'legend' + + trait :common do + common true + project nil + end + end +end diff --git a/spec/factories/resource_label_events.rb b/spec/factories/resource_label_events.rb index a67ad78c098..739ba901052 100644 --- a/spec/factories/resource_label_events.rb +++ b/spec/factories/resource_label_events.rb @@ -2,9 +2,12 @@ FactoryBot.define do factory :resource_label_event do - user { issue.project.creator } action :add label - issue + user { issuable&.author || create(:user) } + + after(:build) do |event, evaluator| + event.issue = create(:issue) unless event.issuable + end end end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index a3229fe1741..3c65b5898b4 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -336,6 +336,15 @@ describe 'Admin updates settings' do expect(find_field('ED25519 SSH keys').value).to eq(forbidden) end + it 'loads usage ping payload on click', :js do + expect(page).to have_button 'Preview payload' + + find('.js-usage-ping-payload-trigger').click + + expect(page).to have_selector '.js-usage-ping-payload' + expect(page).to have_button 'Hide payload' + end + def check_all_events page.check('Active') page.check('Push') diff --git a/spec/features/instance_statistics/cohorts_spec.rb b/spec/features/instance_statistics/cohorts_spec.rb index 573f8600be1..81fc5eff980 100644 --- a/spec/features/instance_statistics/cohorts_spec.rb +++ b/spec/features/instance_statistics/cohorts_spec.rb @@ -12,12 +12,4 @@ describe 'Cohorts page' do expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") end - - it 'shows usage data', :js do - visit instance_statistics_cohorts_path - - wait_for_requests - - expect(find('.js-syntax-highlight').text).not_to eq('') - end end diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb index a6c16b6a2a3..d8be554d734 100644 --- a/spec/features/instance_statistics/conversational_development_index_spec.rb +++ b/spec/features/instance_statistics/conversational_development_index_spec.rb @@ -16,13 +16,21 @@ describe 'Conversational Development Index' do end context 'when usage ping is disabled' do - it 'shows empty state' do + before do stub_application_setting(usage_ping_enabled: false) + end + it 'shows empty state' do visit instance_statistics_conversational_development_index_index_path expect(page).to have_content('Usage ping is not enabled') end + + it 'hides the intro callout' do + visit instance_statistics_conversational_development_index_index_path + + expect(page).not_to have_content 'Introducing Your Conversational Development Index' + end end context 'when there is no data to display' do diff --git a/spec/features/instance_statistics/instance_statistics.rb b/spec/features/instance_statistics/instance_statistics.rb new file mode 100644 index 00000000000..d03e6e68075 --- /dev/null +++ b/spec/features/instance_statistics/instance_statistics.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe 'Cohorts page', :js do + before do + sign_in(create(:admin)) + end + + it 'hides cohorts nav button when usage ping is disabled' do + stub_application_setting(usage_ping_enabled: false) + + visit instance_statistics_root_path + + expect(find('.nav-sidebar')).not_to have_content('Cohorts') + end + + it 'shows cohorts nav button when usage ping is enabled' do + stub_application_setting(usage_ping_enabled: true) + + visit instance_statistics_root_path + + expect(find('.nav-sidebar')).to have_content('Cohorts') + end +end diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb new file mode 100644 index 00000000000..40c452c991a --- /dev/null +++ b/spec/features/issues/resource_label_events_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'List issue resource label events', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project, author: user) } + let!(:label) { create(:label, project: project, title: 'foo') } + + context 'when user displays the issue' do + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue, note: 'some note') } + let!(:event) { create(:resource_label_event, user: user, issue: issue, label: label) } + + before do + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'shows both notes and resource label events' do + page.within('#notes') do + expect(find("#note_#{note.id}")).to have_content 'some note' + expect(find("#note_#{event.discussion_id}")).to have_content 'added foo label' + end + end + end + + context 'when user adds label to the issue' do + def toggle_labels(labels) + page.within '.labels' do + click_link 'Edit' + wait_for_requests + + labels.each { |label| click_link label } + + click_link 'Edit' + wait_for_requests + end + end + + before do + create(:label, project: project, title: 'bar') + project.add_developer(user) + + sign_in(user) + visit project_issue_path(project, issue) + wait_for_requests + end + + it 'shows add note for newly added labels' do + toggle_labels(%w(foo bar)) + visit project_issue_path(project, issue) + wait_for_requests + + page.within('#notes') do + expect(page).to have_content 'added bar foo labels' + end + end + end +end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index babf47cc341..ec968bfcf7d 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -38,6 +38,28 @@ describe 'User Cluster', :js do end end + context 'rbac_clusters feature flag is enabled' do + before do + stub_feature_flags(rbac_clusters: true) + + fill_in 'cluster_name', with: 'dev-cluster' + fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com' + fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token' + check 'cluster_platform_kubernetes_attributes_authorization_type' + click_button 'Add Kubernetes cluster' + end + + it 'user sees a cluster details page' do + expect(page).to have_content('Kubernetes cluster integration') + expect(page.find_field('cluster[name]').value).to eq('dev-cluster') + expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) + .to have_content('http://example.com') + expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value) + .to have_content('my-token') + expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked + end + end + context 'when user filled form with invalid parameters' do before do click_button 'Add Kubernetes cluster' diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index c6d85e5d22f..1ea4934cff1 100644 --- a/spec/features/projects/members/share_with_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Project > Members > Share with Group', :js do +describe 'Project > Members > Invite group', :js do include Select2Helper include ActionView::Helpers::DateHelper @@ -8,17 +8,17 @@ describe 'Project > Members > Share with Group', :js do describe 'Share with group lock' do shared_examples 'the project can be shared with groups' do - it 'the "Share with group" tab exists' do + it 'the "Invite group" tab exists' do visit project_settings_members_path(project) - expect(page).to have_selector('#share-with-group-tab') + expect(page).to have_selector('#invite-group-tab') end end shared_examples 'the project cannot be shared with groups' do - it 'the "Share with group" tab does not exist' do + it 'the "Invite group" tab does not exist' do visit project_settings_members_path(project) - expect(page).to have_selector('#add-member-tab') - expect(page).not_to have_selector('#share-with-group-tab') + expect(page).to have_selector('#invite-member-tab') + expect(page).not_to have_selector('#invite-group-tab') end end @@ -31,13 +31,13 @@ describe 'Project > Members > Share with Group', :js do sign_in(maintainer) end - context 'when the group has "Share with group lock" disabled' do + context 'when the group has "Invite group lock" disabled' do it_behaves_like 'the project can be shared with groups' it 'the project can be shared with another group' do visit project_settings_members_path(project) - click_on 'share-with-group-tab' + click_on 'invite-group-tab' select2 group_to_share_with.id, from: '#link_group_id' page.find('body').click @@ -49,7 +49,7 @@ describe 'Project > Members > Share with Group', :js do end end - context 'when the group has "Share with group lock" enabled' do + context 'when the group has "Invite group lock" enabled' do before do project.namespace.update_column(:share_with_group_lock, true) end @@ -69,12 +69,12 @@ describe 'Project > Members > Share with Group', :js do sign_in(maintainer) end - context 'when the root_group has "Share with group lock" disabled' do - context 'when the subgroup has "Share with group lock" disabled' do + context 'when the root_group has "Invite group lock" disabled' do + context 'when the subgroup has "Invite group lock" disabled' do it_behaves_like 'the project can be shared with groups' end - context 'when the subgroup has "Share with group lock" enabled' do + context 'when the subgroup has "Invite group lock" enabled' do before do subgroup.update_column(:share_with_group_lock, true) end @@ -83,16 +83,16 @@ describe 'Project > Members > Share with Group', :js do end end - context 'when the root_group has "Share with group lock" enabled' do + context 'when the root_group has "Invite group lock" enabled' do before do root_group.update_column(:share_with_group_lock, true) end - context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do + context 'when the subgroup has "Invite group lock" disabled (parent overridden)' do it_behaves_like 'the project can be shared with groups' end - context 'when the subgroup has "Share with group lock" enabled' do + context 'when the subgroup has "Invite group lock" enabled' do before do subgroup.update_column(:share_with_group_lock, true) end @@ -117,12 +117,12 @@ describe 'Project > Members > Share with Group', :js do visit project_settings_members_path(project) - click_on 'share-with-group-tab' + click_on 'invite-group-tab' select2 group.id, from: '#link_group_id' fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d') - click_on 'share-with-group-tab' + click_on 'invite-group-tab' find('.btn-create').click end @@ -150,7 +150,7 @@ describe 'Project > Members > Share with Group', :js do visit project_settings_members_path(project) - click_link 'Share with group' + click_link 'Invite group' find('.ajax-groups-select.select2-container') @@ -183,7 +183,7 @@ describe 'Project > Members > Share with Group', :js do it 'the groups dropdown does not show ancestors', :nested_groups do visit project_settings_members_path(project) - click_on 'share-with-group-tab' + click_on 'invite-group-tab' click_link 'Search for a group' page.within '.select2-drop' do diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb index 2f1824d7849..676659b90c3 100644 --- a/spec/features/projects/settings/user_manages_group_links_spec.rb +++ b/spec/features/projects/settings/user_manages_group_links_spec.rb @@ -26,13 +26,13 @@ describe 'Projects > Settings > User manages group links' do end end - it 'shares a project with a group', :js do - click_link('Share with group') + it 'invites a group to a project', :js do + click_link('Invite group') select2(group_market.id, from: '#link_group_id') select('Maintainer', from: 'link_group_access') - click_button('Share') + click_button('Invite') page.within('.project-members-groups') do expect(page).to have_content('Market') diff --git a/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb new file mode 100644 index 00000000000..baf715000a3 --- /dev/null +++ b/spec/features/projects/show/user_interacts_with_auto_devops_banner_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Project > Show > User interacts with auto devops implicitly enabled banner' do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + project.add_user(user, role) + sign_in(user) + end + + context 'when user does not have maintainer access' do + let(:role) { :developer } + + context 'when AutoDevOps is implicitly enabled' do + it 'does not display AutoDevOps implicitly enabled banner' do + expect(page).not_to have_css('.auto-devops-implicitly-enabled-banner') + end + end + end + + context 'when user has mantainer access' do + let(:role) { :maintainer } + + context 'when AutoDevOps is implicitly enabled' do + before do + stub_application_setting(auto_devops_enabled: true) + + visit project_path(project) + end + + it 'display AutoDevOps implicitly enabled banner' do + expect(page).to have_css('.auto-devops-implicitly-enabled-banner') + end + + context 'when user dismisses the banner', :js do + it 'does not display AutoDevOps implicitly enabled banner' do + find('.hide-auto-devops-implicitly-enabled-banner').click + wait_for_requests + visit project_path(project) + + expect(page).not_to have_css('.auto-devops-implicitly-enabled-banner') + end + end + end + + context 'when AutoDevOps is not implicitly enabled' do + before do + stub_application_setting(auto_devops_enabled: false) + + visit project_path(project) + end + + it 'does not display AutoDevOps implicitly enabled banner' do + expect(page).not_to have_css('.auto-devops-implicitly-enabled-banner') + end + end + end +end diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb index 8ae036cd29f..45e81e1c040 100644 --- a/spec/features/projects/tree/tree_show_spec.rb +++ b/spec/features/projects/tree/tree_show_spec.rb @@ -4,16 +4,30 @@ describe 'Projects tree', :js do let(:user) { create(:user) } let(:project) { create(:project, :repository) } + # This commit has a known state on the master branch of gitlab-test + let(:test_sha) { '7975be0116940bf2ad4321f79d02a55c5f7779aa' } + before do project.add_maintainer(user) sign_in(user) end it 'renders tree table without errors' do - visit project_tree_path(project, 'master') + visit project_tree_path(project, test_sha) + wait_for_requests + + expect(page).to have_selector('.tree-item') + expect(page).to have_content('add tests for .gitattributes custom highlighting') + expect(page).not_to have_selector('.flash-alert') + expect(page).not_to have_selector('.label-lfs', text: 'LFS') + end + + it 'renders tree table for a subtree without errors' do + visit project_tree_path(project, File.join(test_sha, 'files')) wait_for_requests expect(page).to have_selector('.tree-item') + expect(page).to have_content('add spaces in whitespace file') expect(page).not_to have_selector('.label-lfs', text: 'LFS') expect(page).not_to have_selector('.flash-alert') end diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb index 796d40cb625..c64abdc3619 100644 --- a/spec/finders/group_descendants_finder_spec.rb +++ b/spec/finders/group_descendants_finder_spec.rb @@ -108,6 +108,15 @@ describe GroupDescendantsFinder do end end end + + it 'does not include projects shared with the group' do + project = create(:project, namespace: group) + other_project = create(:project) + other_project.project_group_links.create(group: group, + group_access: ProjectGroupLink::MASTER) + + expect(finder.execute).to contain_exactly(project) + end end context 'with nested groups', :nested_groups do diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index 73eca83d788..b8c099250be 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -2,6 +2,7 @@ "allOf": [{ "$ref": "job.json" }], "description": "An extension of job.json with more detailed information", "properties": { - "artifact": { "$ref": "artifact.json" } + "artifact": { "$ref": "artifact.json" }, + "terminal_path": { "type": "string" } } } diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 81f1a97112f..f380ef450db 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -27,7 +27,7 @@ export const listObjDuplicate = { export const BoardsMockData = { GET: { - '/test/-/boards/1/lists/300/issues?id=300&page=1&=': { + '/test/-/boards/1/lists/300/issues?id=300&page=1': { issues: [ { title: 'Testing', diff --git a/spec/javascripts/boards/utils/query_data_spec.js b/spec/javascripts/boards/utils/query_data_spec.js deleted file mode 100644 index 922215ffc1d..00000000000 --- a/spec/javascripts/boards/utils/query_data_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import queryData from '~/boards/utils/query_data'; - -describe('queryData', () => { - it('parses path for label with trailing +', () => { - expect( - queryData('label_name[]=label%2B', {}), - ).toEqual({ - label_name: ['label+'], - }); - }); - - it('parses path for milestone with trailing +', () => { - expect( - queryData('milestone_title=A%2B', {}), - ).toEqual({ - milestone_title: 'A+', - }); - }); - - it('parses path for search terms with spaces', () => { - expect( - queryData('search=two+words', {}), - ).toEqual({ - search: 'two words', - }); - }); -}); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index 41d0dfd8939..b29a22da7c2 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -16,7 +16,7 @@ export default { expanded: true, notes: [ { - id: 1749, + id: '1749', type: 'DiffNote', attachment: null, author: { @@ -68,7 +68,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1753, + id: '1753', type: 'DiffNote', attachment: null, author: { @@ -120,7 +120,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1754, + id: '1754', type: 'DiffNote', attachment: null, author: { @@ -162,7 +162,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1755, + id: '1755', type: 'DiffNote', attachment: null, author: { @@ -204,7 +204,7 @@ export default { '/gitlab-org/gitlab-test/issues/new?discussion_to_resolve=6b232e05bea388c6b043ccc243ba505faac04ea8&merge_request_to_resolve_discussions_of=20', }, { - id: 1756, + id: '1756', type: 'DiffNote', attachment: null, author: { diff --git a/spec/javascripts/dropzone_input_spec.js b/spec/javascripts/dropzone_input_spec.js new file mode 100644 index 00000000000..0c6b1a8946d --- /dev/null +++ b/spec/javascripts/dropzone_input_spec.js @@ -0,0 +1,68 @@ +import $ from 'jquery'; +import dropzoneInput from '~/dropzone_input'; +import { TEST_HOST } from 'spec/test_constants'; + +const TEST_FILE = { + upload: {}, +}; +const TEST_UPLOAD_PATH = `${TEST_HOST}/upload/file`; +const TEST_ERROR_MESSAGE = 'A big error occurred!'; +const TEMPLATE = ( +`<form class="gfm-form" data-uploads-path="${TEST_UPLOAD_PATH}"> + <textarea class="js-gfm-input"></textarea> + <div class="uploading-error-message"></div> +</form>` +); + +describe('dropzone_input', () => { + let form; + let dropzone; + let xhr; + let oldXMLHttpRequest; + + beforeEach(() => { + form = $(TEMPLATE); + + dropzone = dropzoneInput(form); + + xhr = jasmine.createSpyObj(Object.keys(XMLHttpRequest.prototype)); + oldXMLHttpRequest = window.XMLHttpRequest; + window.XMLHttpRequest = () => xhr; + }); + + afterEach(() => { + window.XMLHttpRequest = oldXMLHttpRequest; + }); + + it('shows error message, when AJAX fails with json', () => { + xhr = { + ...xhr, + statusCode: 400, + readyState: 4, + responseText: JSON.stringify({ message: TEST_ERROR_MESSAGE }), + getResponseHeader: () => 'application/json', + }; + + dropzone.processFile(TEST_FILE); + + xhr.onload(); + + expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE); + }); + + it('shows error message, when AJAX fails with text', () => { + xhr = { + ...xhr, + statusCode: 400, + readyState: 4, + responseText: TEST_ERROR_MESSAGE, + getResponseHeader: () => 'text/plain', + }; + + dropzone.processFile(TEST_FILE); + + xhr.onload(); + + expect(form.find('.uploading-error-message').text()).toEqual(TEST_ERROR_MESSAGE); + }); +}); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 03d4b472b87..76933cf337b 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -9,9 +9,14 @@ import GroupsStore from '~/groups/store/groups_store'; import GroupsService from '~/groups/service/groups_service'; import { - mockEndpoint, mockGroups, mockSearchedGroups, - mockRawPageInfo, mockParentGroupItem, mockRawChildren, - mockChildren, mockPageInfo, + mockEndpoint, + mockGroups, + mockSearchedGroups, + mockRawPageInfo, + mockParentGroupItem, + mockRawChildren, + mockChildren, + mockPageInfo, } from '../mock_data'; const createComponent = (hideProjects = false) => { @@ -28,22 +33,23 @@ const createComponent = (hideProjects = false) => { }); }; -const returnServicePromise = (data, failed) => new Promise((resolve, reject) => { - if (failed) { - reject(data); - } else { - resolve({ - json() { - return data; - }, - }); - } -}); +const returnServicePromise = (data, failed) => + new Promise((resolve, reject) => { + if (failed) { + reject(data); + } else { + resolve({ + json() { + return data; + }, + }); + } + }); describe('AppComponent', () => { let vm; - beforeEach((done) => { + beforeEach(done => { Vue.component('group-folder', groupFolderComponent); Vue.component('group-item', groupItemComponent); @@ -94,7 +100,7 @@ describe('AppComponent', () => { }); describe('fetchGroups', () => { - it('should call `getGroups` with all the params provided', (done) => { + it('should call `getGroups` with all the params provided', done => { spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(mockGroups)); vm.fetchGroups({ @@ -110,8 +116,10 @@ describe('AppComponent', () => { }, 0); }); - it('should set headers to store for building pagination info when called with `updatePagination`', (done) => { - spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise({ headers: mockRawPageInfo })); + it('should set headers to store for building pagination info when called with `updatePagination`', done => { + spyOn(vm.service, 'getGroups').and.returnValue( + returnServicePromise({ headers: mockRawPageInfo }), + ); spyOn(vm, 'updatePagination'); vm.fetchGroups({ updatePagination: true }); @@ -122,7 +130,7 @@ describe('AppComponent', () => { }, 0); }); - it('should show flash error when request fails', (done) => { + it('should show flash error when request fails', done => { spyOn(vm.service, 'getGroups').and.returnValue(returnServicePromise(null, true)); spyOn($, 'scrollTo'); spyOn(window, 'Flash'); @@ -138,7 +146,7 @@ describe('AppComponent', () => { }); describe('fetchAllGroups', () => { - it('should fetch default set of groups', (done) => { + it('should fetch default set of groups', done => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); spyOn(vm, 'updatePagination').and.callThrough(); spyOn(vm, 'updateGroups').and.callThrough(); @@ -153,7 +161,7 @@ describe('AppComponent', () => { }, 0); }); - it('should fetch matching set of groups when app is loaded with search query', (done) => { + it('should fetch matching set of groups when app is loaded with search query', done => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockSearchedGroups)); spyOn(vm, 'updateGroups').and.callThrough(); @@ -173,7 +181,7 @@ describe('AppComponent', () => { }); describe('fetchPage', () => { - it('should fetch groups for provided page details and update window state', (done) => { + it('should fetch groups for provided page details and update window state', done => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); spyOn(vm, 'updateGroups').and.callThrough(); const mergeUrlParams = spyOnDependency(appComponent, 'mergeUrlParams').and.callThrough(); @@ -193,9 +201,13 @@ describe('AppComponent', () => { expect(vm.isLoading).toBe(false); expect($.scrollTo).toHaveBeenCalledWith(0); expect(mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); - expect(window.history.replaceState).toHaveBeenCalledWith({ - page: jasmine.any(String), - }, jasmine.any(String), jasmine.any(String)); + expect(window.history.replaceState).toHaveBeenCalledWith( + { + page: jasmine.any(String), + }, + jasmine.any(String), + jasmine.any(String), + ); expect(vm.updateGroups).toHaveBeenCalled(); done(); }, 0); @@ -211,7 +223,7 @@ describe('AppComponent', () => { groupItem.isChildrenLoading = false; }); - it('should fetch children of given group and expand it if group is collapsed and children are not loaded', (done) => { + it('should fetch children of given group and expand it if group is collapsed and children are not loaded', done => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockRawChildren)); spyOn(vm.store, 'setGroupChildren'); @@ -244,7 +256,7 @@ describe('AppComponent', () => { expect(groupItem.isOpen).toBe(false); }); - it('should set `isChildrenLoading` back to `false` if load request fails', (done) => { + it('should set `isChildrenLoading` back to `false` if load request fails', done => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true)); vm.toggleChildren(groupItem); @@ -272,7 +284,9 @@ describe('AppComponent', () => { expect(vm.groupLeaveConfirmationMessage).toBe(''); vm.showLeaveGroupModal(group, mockParentGroupItem); expect(vm.showModal).toBe(true); - expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`); + expect(vm.groupLeaveConfirmationMessage).toBe( + `Are you sure you want to leave the "${group.fullName}" group?`, + ); }); }); @@ -299,7 +313,7 @@ describe('AppComponent', () => { vm.targetParentGroup = groupItem; }); - it('hides modal confirmation leave group and remove group item from tree', (done) => { + it('hides modal confirmation leave group and remove group item from tree', done => { const notice = `You left the "${childGroupItem.fullName}" group.`; spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ notice })); spyOn(vm.store, 'removeGroup').and.callThrough(); @@ -318,9 +332,11 @@ describe('AppComponent', () => { }, 0); }); - it('should show error flash message if request failed to leave group', (done) => { + it('should show error flash message if request failed to leave group', done => { const message = 'An error occurred. Please try again.'; - spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 500 }, true)); + spyOn(vm.service, 'leaveGroup').and.returnValue( + returnServicePromise({ status: 500 }, true), + ); spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); @@ -335,9 +351,11 @@ describe('AppComponent', () => { }, 0); }); - it('should show appropriate error flash message if request forbids to leave group', (done) => { + it('should show appropriate error flash message if request forbids to leave group', done => { const message = 'Failed to leave the group. Please make sure you are not the only owner.'; - spyOn(vm.service, 'leaveGroup').and.returnValue(returnServicePromise({ status: 403 }, true)); + spyOn(vm.service, 'leaveGroup').and.returnValue( + returnServicePromise({ status: 403 }, true), + ); spyOn(vm.store, 'removeGroup').and.callThrough(); spyOn(window, 'Flash'); @@ -388,7 +406,7 @@ describe('AppComponent', () => { }); describe('created', () => { - it('should bind event listeners on eventHub', (done) => { + it('should bind event listeners on eventHub', done => { spyOn(eventHub, '$on'); const newVm = createComponent(); @@ -405,21 +423,21 @@ describe('AppComponent', () => { }); }); - it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', (done) => { + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `false`', done => { const newVm = createComponent(); newVm.$mount(); Vue.nextTick(() => { - expect(newVm.searchEmptyMessage).toBe('Sorry, no groups or projects matched your search'); + expect(newVm.searchEmptyMessage).toBe('No groups or projects matched your search'); newVm.$destroy(); done(); }); }); - it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', (done) => { + it('should initialize `searchEmptyMessage` prop with correct string when `hideProjects` is `true`', done => { const newVm = createComponent(true); newVm.$mount(); Vue.nextTick(() => { - expect(newVm.searchEmptyMessage).toBe('Sorry, no groups matched your search'); + expect(newVm.searchEmptyMessage).toBe('No groups matched your search'); newVm.$destroy(); done(); }); @@ -427,7 +445,7 @@ describe('AppComponent', () => { }); describe('beforeDestroy', () => { - it('should unbind event listeners on eventHub', (done) => { + it('should unbind event listeners on eventHub', done => { spyOn(eventHub, '$off'); const newVm = createComponent(); @@ -454,7 +472,7 @@ describe('AppComponent', () => { vm.$destroy(); }); - it('should render loading icon', (done) => { + it('should render loading icon', done => { vm.isLoading = true; Vue.nextTick(() => { expect(vm.$el.querySelector('.loading-animation')).toBeDefined(); @@ -463,7 +481,7 @@ describe('AppComponent', () => { }); }); - it('should render groups tree', (done) => { + it('should render groups tree', done => { vm.store.state.groups = [mockParentGroupItem]; vm.isLoading = false; vm.store.state.pageInfo = mockPageInfo; @@ -473,7 +491,7 @@ describe('AppComponent', () => { }); }); - it('renders modal confirmation dialog', (done) => { + it('renders modal confirmation dialog', done => { vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?'; vm.showModal = true; Vue.nextTick(() => { diff --git a/spec/javascripts/helpers/vue_resource_helper.js b/spec/javascripts/helpers/vue_resource_helper.js index 70b7ec4e574..0d1bf5e2e80 100644 --- a/spec/javascripts/helpers/vue_resource_helper.js +++ b/spec/javascripts/helpers/vue_resource_helper.js @@ -5,6 +5,7 @@ export const headersInterceptor = (request, next) => { response.headers.forEach((value, key) => { headers[key] = value; }); + // eslint-disable-next-line no-param-reassign response.headers = headers; }); }; diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index 41d8bfff7e7..b45ae5bbb0f 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -30,7 +30,7 @@ describe('Multi-file editor commit sidebar list item', () => { }); it('renders file path', () => { - expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path); + expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path); }); it('renders actionn button', () => { diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js index a5b906da8a1..e09ccbe2a63 100644 --- a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js @@ -29,7 +29,7 @@ describe('IDE stage file button', () => { }); it('renders button to discard & stage', () => { - expect(vm.$el.querySelectorAll('.btn').length).toBe(2); + expect(vm.$el.querySelectorAll('.btn-blank').length).toBe(2); }); it('calls store with stage button', () => { @@ -39,7 +39,7 @@ describe('IDE stage file button', () => { }); it('calls store with discard button', () => { - vm.$el.querySelector('.dropdown-menu button').click(); + vm.$el.querySelector('.btn-danger').click(); expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); }); diff --git a/spec/javascripts/ide/components/file_templates/bar_spec.js b/spec/javascripts/ide/components/file_templates/bar_spec.js new file mode 100644 index 00000000000..a688f7f69a6 --- /dev/null +++ b/spec/javascripts/ide/components/file_templates/bar_spec.js @@ -0,0 +1,117 @@ +import Vue from 'vue'; +import { createStore } from '~/ide/stores'; +import Bar from '~/ide/components/file_templates/bar.vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore, file } from '../../helpers'; + +describe('IDE file templates bar component', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(Bar); + }); + + beforeEach(() => { + const store = createStore(); + + store.state.openFiles.push({ + ...file('file'), + opened: true, + active: true, + }); + + vm = mountComponentWithStore(Component, { store }); + }); + + afterEach(() => { + vm.$destroy(); + resetStore(vm.$store); + }); + + describe('template type dropdown', () => { + it('renders dropdown component', () => { + expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type'); + }); + + it('calls setSelectedTemplateType when clicking item', () => { + spyOn(vm, 'setSelectedTemplateType').and.stub(); + + vm.$el.querySelector('.dropdown-content button').click(); + + expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }); + }); + }); + + describe('template dropdown', () => { + beforeEach(done => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + vm.$store.state.fileTemplates.selectedTemplateType = { + name: '.gitlab-ci.yml', + key: 'gitlab_ci_ymls', + }; + + vm.$nextTick(done); + }); + + it('renders dropdown component', () => { + expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template'); + }); + + it('calls fetchTemplate on click', () => { + spyOn(vm, 'fetchTemplate').and.stub(); + + vm.$el + .querySelectorAll('.dropdown-content')[1] + .querySelector('button') + .click(); + + expect(vm.fetchTemplate).toHaveBeenCalledWith({ + name: 'test', + }); + }); + }); + + it('shows undo button if updateSuccess is true', done => { + vm.$store.state.fileTemplates.updateSuccess = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none'); + + done(); + }); + }); + + it('calls undoFileTemplate when clicking undo button', () => { + spyOn(vm, 'undoFileTemplate').and.stub(); + + vm.$el.querySelector('.btn-default').click(); + + expect(vm.undoFileTemplate).toHaveBeenCalled(); + }); + + it('calls setSelectedTemplateType if activeFile name matches a template', done => { + const fileName = '.gitlab-ci.yml'; + + spyOn(vm, 'setSelectedTemplateType'); + vm.$store.state.openFiles[0].name = fileName; + + vm.setInitialType(); + + vm.$nextTick(() => { + expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({ + name: fileName, + key: 'gitlab_ci_ymls', + }); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/file_templates/dropdown_spec.js b/spec/javascripts/ide/components/file_templates/dropdown_spec.js new file mode 100644 index 00000000000..898796f4fa0 --- /dev/null +++ b/spec/javascripts/ide/components/file_templates/dropdown_spec.js @@ -0,0 +1,201 @@ +import $ from 'jquery'; +import Vue from 'vue'; +import { createStore } from '~/ide/stores'; +import Dropdown from '~/ide/components/file_templates/dropdown.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { resetStore } from '../../helpers'; + +describe('IDE file templates dropdown component', () => { + let Component; + let vm; + + beforeAll(() => { + Component = Vue.extend(Dropdown); + }); + + beforeEach(() => { + const store = createStore(); + + vm = createComponentWithStore(Component, store, { + label: 'Test', + }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + resetStore(vm.$store); + }); + + describe('async', () => { + beforeEach(() => { + vm.isAsyncData = true; + }); + + it('calls async store method on Bootstrap dropdown event', () => { + spyOn(vm, 'fetchTemplateTypes').and.stub(); + + $(vm.$el).trigger('show.bs.dropdown'); + + expect(vm.fetchTemplateTypes).toHaveBeenCalled(); + }); + + it('renders templates when async', done => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test'); + + done(); + }); + }); + + it('renders loading icon when isLoading is true', done => { + vm.$store.state.fileTemplates.isLoading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + it('searches template data', () => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + vm.searchable = true; + vm.search = 'hello'; + + expect(vm.outputData).toEqual([]); + }); + + it('does not filter data is searchable is false', () => { + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + vm.search = 'hello'; + + expect(vm.outputData).toEqual([ + { + name: 'test', + }, + ]); + }); + + it('calls clickItem on click', done => { + spyOn(vm, 'clickItem').and.stub(); + + vm.$store.state.fileTemplates.templates = [ + { + name: 'test', + }, + ]; + + vm.$nextTick(() => { + vm.$el.querySelector('.dropdown-content button').click(); + + expect(vm.clickItem).toHaveBeenCalledWith({ + name: 'test', + }); + + done(); + }); + }); + + it('renders input when searchable is true', done => { + vm.searchable = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); + + done(); + }); + }); + + it('does not render input when searchable is true & showLoading is true', done => { + vm.searchable = true; + vm.$store.state.fileTemplates.isLoading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input')).toBe(null); + + done(); + }); + }); + }); + + describe('sync', () => { + beforeEach(done => { + vm.data = [ + { + name: 'test sync', + }, + ]; + + vm.$nextTick(done); + }); + + it('renders props data', () => { + expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync'); + }); + + it('renders input when searchable is true', done => { + vm.searchable = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null); + + done(); + }); + }); + + it('calls clickItem on click', done => { + spyOn(vm, 'clickItem').and.stub(); + + vm.$nextTick(() => { + vm.$el.querySelector('.dropdown-content button').click(); + + expect(vm.clickItem).toHaveBeenCalledWith({ + name: 'test sync', + }); + + done(); + }); + }); + + it('searches template data', () => { + vm.searchable = true; + vm.search = 'hello'; + + expect(vm.outputData).toEqual([]); + }); + + it('does not filter data is searchable is false', () => { + vm.search = 'hello'; + + expect(vm.outputData).toEqual([ + { + name: 'test sync', + }, + ]); + }); + + it('renders dropdown title', done => { + vm.title = 'Test title'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title'); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 30cd92b2ca4..d09ccd7ac34 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -111,7 +111,7 @@ describe('RepoCommitSection', () => { .then(vm.$nextTick) .then(() => { expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain( - 'No changes', + 'There are no unstaged changes', ); }) .then(done) @@ -133,7 +133,7 @@ describe('RepoCommitSection', () => { }); it('discards a single file', done => { - vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click(); + vm.$el.querySelector('.multi-file-commit-list li:first-child .js-modal-primary-action').click(); Vue.nextTick(() => { expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js index c11c482fef8..3ce9c9fcda1 100644 --- a/spec/javascripts/ide/helpers.js +++ b/spec/javascripts/ide/helpers.js @@ -5,6 +5,7 @@ import commitState from '~/ide/stores/modules/commit/state'; import mergeRequestsState from '~/ide/stores/modules/merge_requests/state'; import pipelinesState from '~/ide/stores/modules/pipelines/state'; import branchesState from '~/ide/stores/modules/branches/state'; +import fileTemplatesState from '~/ide/stores/modules/file_templates/state'; export const resetStore = store => { const newState = { @@ -13,6 +14,7 @@ export const resetStore = store => { mergeRequests: mergeRequestsState(), pipelines: pipelinesState(), branches: branchesState(), + fileTemplates: fileTemplatesState(), }; store.replaceState(newState); }; diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index bca2033ff97..1ca811e996b 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -692,21 +692,6 @@ describe('IDE store file actions', () => { .then(done) .catch(done.fail); }); - - it('calls scrollToTab', done => { - const scrollToTabSpy = jasmine.createSpy('scrollToTab'); - const oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line - store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line - - store - .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) - .then(() => { - expect(scrollToTabSpy).toHaveBeenCalled(); - store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line - }) - .then(done) - .catch(done.fail); - }); }); describe('removePendingTab', () => { diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index d84f1717a61..c9a1158a14e 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -305,7 +305,11 @@ describe('Multi-file store actions', () => { describe('stageAllChanges', () => { it('adds all files from changedFiles to stagedFiles', done => { - store.state.changedFiles.push(file(), file('new')); + const openFile = { ...file(), path: 'test' }; + + store.state.openFiles.push(openFile); + store.state.stagedFiles.push(openFile); + store.state.changedFiles.push(openFile, file('new')); testAction( stageAllChanges, @@ -316,7 +320,12 @@ describe('Multi-file store actions', () => { { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path }, { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path }, ], - [], + [ + { + type: 'openPendingTab', + payload: { file: openFile, keyPrefix: 'staged' }, + }, + ], done, ); }); @@ -324,7 +333,11 @@ describe('Multi-file store actions', () => { describe('unstageAllChanges', () => { it('removes all files from stagedFiles after unstaging', done => { - store.state.stagedFiles.push(file(), file('new')); + const openFile = { ...file(), path: 'test' }; + + store.state.openFiles.push(openFile); + store.state.changedFiles.push(openFile); + store.state.stagedFiles.push(openFile, file('new')); testAction( unstageAllChanges, @@ -334,7 +347,12 @@ describe('Multi-file store actions', () => { { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path }, { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path }, ], - [], + [ + { + type: 'openPendingTab', + payload: { file: openFile, keyPrefix: 'unstaged' }, + }, + ], done, ); }); diff --git a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js index f831a9f0a5d..c29dd9f0d06 100644 --- a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js @@ -148,14 +148,66 @@ describe('IDE file templates actions', () => { }); describe('setSelectedTemplateType', () => { - it('commits SET_SELECTED_TEMPLATE_TYPE', done => { - testAction( - actions.setSelectedTemplateType, - 'test', - state, - [{ type: types.SET_SELECTED_TEMPLATE_TYPE, payload: 'test' }], - [], - done, + it('commits SET_SELECTED_TEMPLATE_TYPE', () => { + const commit = jasmine.createSpy('commit'); + const options = { + commit, + dispatch() {}, + rootGetters: { + activeFile: { + name: 'test', + prevPath: '', + }, + }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(commit).toHaveBeenCalledWith(types.SET_SELECTED_TEMPLATE_TYPE, { name: 'test' }); + }); + + it('dispatches discardFileChanges if prevPath matches templates name', () => { + const dispatch = jasmine.createSpy('dispatch'); + const options = { + commit() {}, + dispatch, + rootGetters: { + activeFile: { + name: 'test', + path: 'test', + prevPath: 'test', + }, + }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true }); + }); + + it('dispatches renameEntry if file name doesnt match', () => { + const dispatch = jasmine.createSpy('dispatch'); + const options = { + commit() {}, + dispatch, + rootGetters: { + activeFile: { + name: 'oldtest', + path: 'oldtest', + prevPath: '', + }, + }, + }; + + actions.setSelectedTemplateType(options, { name: 'test' }); + + expect(dispatch).toHaveBeenCalledWith( + 'renameEntry', + { + path: 'oldtest', + name: 'test', + }, + { root: true }, ); }); }); @@ -332,5 +384,20 @@ describe('IDE file templates actions', () => { expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false); }); + + it('dispatches discardFileChanges if file has prevPath', () => { + const dispatch = jasmine.createSpy('dispatch'); + const rootGetters = { + activeFile: { path: 'test', prevPath: 'newtest', raw: 'raw content' }, + }; + + actions.undoFileTemplate({ dispatch, commit() {}, rootGetters }); + + expect(dispatch.calls.mostRecent().args).toEqual([ + 'discardFileChanges', + 'test', + { root: true }, + ]); + }); }); }); diff --git a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js index e337c3f331b..17cb457881f 100644 --- a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js @@ -1,3 +1,5 @@ +import createState from '~/ide/stores/state'; +import { activityBarViews } from '~/ide/constants'; import * as getters from '~/ide/stores/modules/file_templates/getters'; describe('IDE file templates getters', () => { @@ -8,22 +10,49 @@ describe('IDE file templates getters', () => { }); describe('showFileTemplatesBar', () => { - it('finds template type by name', () => { + let rootState; + + beforeEach(() => { + rootState = createState(); + }); + + it('returns true if template is found and currentActivityView is edit', () => { + rootState.currentActivityView = activityBarViews.edit; + + expect( + getters.showFileTemplatesBar( + null, + { + templateTypes: getters.templateTypes(), + }, + rootState, + )('LICENSE'), + ).toBe(true); + }); + + it('returns false if template is found and currentActivityView is not edit', () => { + rootState.currentActivityView = activityBarViews.commit; + expect( - getters.showFileTemplatesBar(null, { - templateTypes: getters.templateTypes(), - })('LICENSE'), - ).toEqual({ - name: 'LICENSE', - key: 'licenses', - }); + getters.showFileTemplatesBar( + null, + { + templateTypes: getters.templateTypes(), + }, + rootState, + )('LICENSE'), + ).toBe(false); }); it('returns undefined if not found', () => { expect( - getters.showFileTemplatesBar(null, { - templateTypes: getters.templateTypes(), - })('test'), + getters.showFileTemplatesBar( + null, + { + templateTypes: getters.templateTypes(), + }, + rootState, + )('test'), ).toBe(undefined); }); }); diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js index 6ce76aaa03b..41dd3d3c67f 100644 --- a/spec/javascripts/ide/stores/mutations_spec.js +++ b/spec/javascripts/ide/stores/mutations_spec.js @@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => { expect(localState.entries.parentPath.tree.length).toBe(1); }); + + it('adds to openFiles if previously opened', () => { + localState.entries.oldPath.opened = true; + + mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' }); + + expect(localState.openFiles).toEqual([localState.entries.newPath]); + }); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index babad296f09..28151c7e658 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -29,24 +29,46 @@ describe('common_utils', () => { }); }); - describe('getUrlParamsArray', () => { - it('should return params array', () => { - expect(commonUtils.getUrlParamsArray() instanceof Array).toBe(true); + describe('urlParamsToArray', () => { + it('returns empty array for empty querystring', () => { + expect(commonUtils.urlParamsToArray('')).toEqual([]); + }); + + it('should decode params', () => { + expect( + commonUtils.urlParamsToArray('?label_name%5B%5D=test')[0], + ).toBe('label_name[]=test'); }); it('should remove the question mark from the search params', () => { - const paramsArray = commonUtils.getUrlParamsArray(); + const paramsArray = commonUtils.urlParamsToArray('?test=thing'); expect(paramsArray[0][0] !== '?').toBe(true); }); + }); - it('should decode params', () => { - window.history.pushState('', '', '?label_name%5B%5D=test'); + describe('urlParamsToObject', () => { + it('parses path for label with trailing +', () => { + expect( + commonUtils.urlParamsToObject('label_name[]=label%2B', {}), + ).toEqual({ + label_name: ['label+'], + }); + }); + it('parses path for milestone with trailing +', () => { expect( - commonUtils.getUrlParamsArray()[0], - ).toBe('label_name[]=test'); + commonUtils.urlParamsToObject('milestone_title=A%2B', {}), + ).toEqual({ + milestone_title: 'A+', + }); + }); - window.history.pushState('', '', '?'); + it('parses path for search terms with spaces', () => { + expect( + commonUtils.urlParamsToObject('search=two+words', {}), + ).toEqual({ + search: 'two words', + }); }); }); diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js index 19278312b6d..a837b71db0b 100644 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ b/spec/javascripts/monitoring/graph/flag_spec.js @@ -35,7 +35,7 @@ const defaultValuesComponent = { unitOfDisplay: 'ms', currentDataIndex: 0, legendTitle: 'Average', - currentCoordinates: [], + currentCoordinates: {}, }; const deploymentFlagData = { diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index a46a387a534..990619b4109 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -113,6 +113,9 @@ describe('Graph', () => { projectPath, }); + // simulate moving mouse over data series + component.seriesUnderMouse = component.timeSeries; + component.positionFlag(); expect(component.currentData).toBe(component.timeSeries[0].values[10]); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 52cc42cb53d..d7298cb3483 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -28,7 +28,7 @@ describe('issue_note_actions component', () => { canEdit: true, canAwardEmoji: true, canReportAsAbuse: true, - noteId: 539, + noteId: '539', noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', @@ -59,6 +59,20 @@ describe('issue_note_actions component', () => { expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); }); + it('should be possible to copy link to a note', () => { + expect(vm.$el.querySelector('.js-btn-copy-note-link')).not.toBeNull(); + }); + + it('should not show copy link action when `noteUrl` prop is empty', done => { + vm.noteUrl = ''; + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.js-btn-copy-note-link')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + it('should be possible to delete comment', () => { expect(vm.$el.querySelector('.js-note-delete')).toBeDefined(); }); @@ -77,7 +91,7 @@ describe('issue_note_actions component', () => { canEdit: false, canAwardEmoji: false, canReportAsAbuse: false, - noteId: 539, + noteId: '539', noteUrl: 'https://localhost:3000/group/project/merge_requests/1#note_1', reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', diff --git a/spec/javascripts/notes/components/note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js index 9d98ba219da..6a6a810acff 100644 --- a/spec/javascripts/notes/components/note_awards_list_spec.js +++ b/spec/javascripts/notes/components/note_awards_list_spec.js @@ -30,7 +30,7 @@ describe('note_awards_list component', () => { propsData: { awards: awardsMock, noteAuthorId: 2, - noteId: 545, + noteId: '545', canAwardEmoji: true, toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', }, @@ -70,7 +70,7 @@ describe('note_awards_list component', () => { propsData: { awards: awardsMock, noteAuthorId: 2, - noteId: 545, + noteId: '545', canAwardEmoji: false, toggleAwardPath: '/gitlab-org/gitlab-ce/notes/545/toggle_award_emoji', }, diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 95d400ab3df..147ffcf1b81 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -19,7 +19,7 @@ describe('issue_note_form component', () => { props = { isEditing: false, noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', - noteId: 545, + noteId: '545', }; vm = new Component({ @@ -32,6 +32,22 @@ describe('issue_note_form component', () => { vm.$destroy(); }); + describe('noteHash', () => { + it('returns note hash string based on `noteId`', () => { + expect(vm.noteHash).toBe(`#note_${props.noteId}`); + }); + + it('return note hash as `#` when `noteId` is empty', done => { + vm.noteId = ''; + Vue.nextTick() + .then(() => { + expect(vm.noteHash).toBe('#'); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('conflicts editing', () => { it('should show conflict message if note changes outside the component', done => { vm.isEditing = true; diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js index a3c6bf78988..379780f43a0 100644 --- a/spec/javascripts/notes/components/note_header_spec.js +++ b/spec/javascripts/notes/components/note_header_spec.js @@ -33,7 +33,7 @@ describe('note_header component', () => { }, createdAt: '2017-08-02T10:51:58.559Z', includeToggle: false, - noteId: 1394, + noteId: '1394', expanded: true, }, }).$mount(); @@ -47,6 +47,16 @@ describe('note_header component', () => { it('should render timestamp link', () => { expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); }); + + it('should not render user information when prop `author` is empty object', done => { + vm.author = {}; + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.note-header-author-name')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); }); describe('discussion', () => { @@ -66,7 +76,7 @@ describe('note_header component', () => { }, createdAt: '2017-08-02T10:51:58.559Z', includeToggle: true, - noteId: 1395, + noteId: '1395', expanded: true, }, }).$mount(); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 0423fcb6ec4..1f030e5af28 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -66,7 +66,7 @@ export const individualNote = { individual_note: true, notes: [ { - id: 1390, + id: '1390', attachment: { url: null, filename: null, @@ -111,7 +111,7 @@ export const individualNote = { }; export const note = { - id: 546, + id: '546', attachment: { url: null, filename: null, @@ -174,7 +174,7 @@ export const discussionMock = { expanded: true, notes: [ { - id: 1395, + id: '1395', attachment: { url: null, filename: null, @@ -211,7 +211,7 @@ export const discussionMock = { path: '/gitlab-org/gitlab-ce/notes/1395', }, { - id: 1396, + id: '1396', attachment: { url: null, filename: null, @@ -257,7 +257,7 @@ export const discussionMock = { path: '/gitlab-org/gitlab-ce/notes/1396', }, { - id: 1437, + id: '1437', attachment: { url: null, filename: null, @@ -308,7 +308,7 @@ export const discussionMock = { }; export const loggedOutnoteableData = { - id: 98, + id: '98', iid: 26, author_id: 1, description: '', @@ -358,7 +358,7 @@ export const collapseNotesMock = [ individual_note: true, notes: [ { - id: 1390, + id: '1390', attachment: null, author: { id: 1, @@ -393,7 +393,7 @@ export const collapseNotesMock = [ individual_note: true, notes: [ { - id: 1391, + id: '1391', attachment: null, author: { id: 1, @@ -433,7 +433,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { expanded: true, notes: [ { - id: 1390, + id: '1390', attachment: { url: null, filename: null, @@ -495,7 +495,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { expanded: true, notes: [ { - id: 1391, + id: '1391', attachment: { url: null, filename: null, @@ -544,7 +544,7 @@ export const INDIVIDUAL_NOTE_RESPONSE_MAP = { '/gitlab-org/gitlab-ce/notes/1471': { commands_changes: null, valid: true, - id: 1471, + id: '1471', attachment: null, author: { id: 1, @@ -600,7 +600,7 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = { expanded: true, notes: [ { - id: 1471, + id: '1471', attachment: { url: null, filename: null, @@ -671,7 +671,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 901, + id: '901', type: null, attachment: null, author: { @@ -718,7 +718,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 902, + id: '902', type: null, attachment: null, author: { @@ -765,7 +765,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 903, + id: '903', type: null, attachment: null, author: { @@ -809,7 +809,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 904, + id: '904', type: null, attachment: null, author: { @@ -854,7 +854,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 905, + id: '905', type: null, attachment: null, author: { @@ -898,7 +898,7 @@ export const notesWithDescriptionChanges = [ expanded: true, notes: [ { - id: 906, + id: '906', type: null, attachment: null, author: { @@ -945,7 +945,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 901, + id: '901', type: null, attachment: null, author: { @@ -992,7 +992,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 902, + id: '902', type: null, attachment: null, author: { @@ -1039,7 +1039,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 904, + id: '904', type: null, attachment: null, author: { @@ -1084,7 +1084,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 905, + id: '905', type: null, attachment: null, author: { @@ -1129,7 +1129,7 @@ export const collapsedSystemNotes = [ expanded: true, notes: [ { - id: 906, + id: '906', type: null, attachment: null, author: { diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/javascripts/vue_shared/components/notes/system_note_spec.js index 2a6015fe35f..adcb1c858aa 100644 --- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js +++ b/spec/javascripts/vue_shared/components/notes/system_note_spec.js @@ -9,7 +9,7 @@ describe('system note component', () => { beforeEach(() => { props = { note: { - id: 1424, + id: '1424', author: { id: 1, name: 'Root', diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb index 4463c011522..1ad7f3ff567 100644 --- a/spec/lib/banzai/filter/spaced_link_filter_spec.rb +++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb @@ -3,49 +3,73 @@ require 'spec_helper' describe Banzai::Filter::SpacedLinkFilter do include FilterSpecHelper - let(:link) { '[example](page slug)' } + let(:link) { '[example](page slug)' } + let(:image) { '' } - it 'converts slug with spaces to a link' do - doc = filter("See #{link}") + context 'when a link is detected' do + it 'converts slug with spaces to a link' do + doc = filter("See #{link}") - expect(doc.at_css('a').text).to eq 'example' - expect(doc.at_css('a')['href']).to eq 'page%20slug' - expect(doc.at_css('p')).to eq nil - end + expect(doc.at_css('a').text).to eq 'example' + expect(doc.at_css('a')['href']).to eq 'page%20slug' + expect(doc.at_css('a')['title']).to be_nil + expect(doc.at_css('p')).to be_nil + end - it 'converts slug with spaces and a title to a link' do - link = '[example](page slug "title")' - doc = filter("See #{link}") + it 'converts slug with spaces and a title to a link' do + link = '[example](page slug "title")' + doc = filter("See #{link}") - expect(doc.at_css('a').text).to eq 'example' - expect(doc.at_css('a')['href']).to eq 'page%20slug' - expect(doc.at_css('a')['title']).to eq 'title' - expect(doc.at_css('p')).to eq nil - end + expect(doc.at_css('a').text).to eq 'example' + expect(doc.at_css('a')['href']).to eq 'page%20slug' + expect(doc.at_css('a')['title']).to eq 'title' + expect(doc.at_css('p')).to be_nil + end - it 'does nothing when markdown_engine is redcarpet' do - exp = act = link - expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp - end + it 'does nothing when markdown_engine is redcarpet' do + exp = act = link + expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp + end + + it 'does nothing with empty text' do + link = '[](page slug)' + doc = filter("See #{link}") + + expect(doc.at_css('a')).to be_nil + end - it 'does nothing with empty text' do - link = '[](page slug)' - doc = filter("See #{link}") + it 'does nothing with an empty slug' do + link = '[example]()' + doc = filter("See #{link}") - expect(doc.at_css('a')).to eq nil + expect(doc.at_css('a')).to be_nil + end end - it 'does nothing with an empty slug' do - link = '[example]()' - doc = filter("See #{link}") + context 'when an image is detected' do + it 'converts slug with spaces to an iamge' do + doc = filter("See #{image}") + + expect(doc.at_css('img')['src']).to eq 'img%20test.jpg' + expect(doc.at_css('img')['alt']).to eq 'example' + expect(doc.at_css('p')).to be_nil + end + + it 'converts slug with spaces and a title to an image' do + image = '' + doc = filter("See #{image}") - expect(doc.at_css('a')).to eq nil + expect(doc.at_css('img')['src']).to eq 'img%20test.jpg' + expect(doc.at_css('img')['alt']).to eq 'example' + expect(doc.at_css('img')['title']).to eq 'title' + expect(doc.at_css('p')).to be_nil + end end it 'converts multiple URLs' do link1 = '[first](slug one)' link2 = '[second](http://example.com/slug two)' - doc = filter("See #{link1} and #{link2}") + doc = filter("See #{link1} and #{image} and #{link2}") found_links = doc.css('a') @@ -54,6 +78,12 @@ describe Banzai::Filter::SpacedLinkFilter do expect(found_links[0]['href']).to eq 'slug%20one' expect(found_links[1].text).to eq 'second' expect(found_links[1]['href']).to eq 'http://example.com/slug%20two' + + found_images = doc.css('img') + + expect(found_images.size).to eq(1) + expect(found_images[0]['src']).to eq 'img%20test.jpg' + expect(found_images[0]['alt']).to eq 'example' end described_class::IGNORE_PARENTS.each do |elem| diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 52b8c9be647..64ca3ec345d 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -178,4 +178,25 @@ describe Banzai::Pipeline::WikiPipeline do end end end + + describe 'videos' do + let(:namespace) { create(:namespace, name: "wiki_link_ns") } + let(:project) { create(:project, :public, name: "wiki_link_project", namespace: namespace) } + let(:project_wiki) { ProjectWiki.new(project, double(:user)) } + let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) } + + it 'generates video html structure' do + markdown = "" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video_file_name.mp4"') + end + + it 'rewrites and replaces video links names with white spaces to %20' do + markdown = "" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"') + end + end end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 1f35d1e4880..44568f2a653 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -338,6 +338,13 @@ describe Gitlab::ClosingIssueExtractor do end end + context "with an invalid keyword such as suffix insted of fix" do + it do + message = "suffix #{reference}" + expect(subject.closed_by_message(message)).to eq([]) + end + end + context 'with multiple references' do let(:other_issue) { create(:issue, project: project) } let(:third_issue) { create(:issue, project: project) } diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index 8e524f9b05a..9e351368b22 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -29,11 +29,15 @@ describe Gitlab::FileDetector do end it 'returns the type of a license file' do - %w(LICENSE LICENCE COPYING).each do |file| + %w(LICENSE LICENCE COPYING UNLICENSE UNLICENCE).each do |file| expect(described_class.type_of(file)).to eq(:license) end end + it 'returns nil for an UNCOPYING file' do + expect(described_class.type_of('UNCOPYING')).to be_nil + end + it 'returns the type of a version file' do expect(described_class.type_of('VERSION')).to eq(:version) end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 17348b01006..28c34e234f7 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -583,6 +583,37 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#find_remote_root_ref' do + it 'gets the remote root ref from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::RemoteService) + .to receive(:find_remote_root_ref).and_call_original + + expect(repository.find_remote_root_ref('origin')).to eq 'master' + end + + it 'returns UTF-8' do + expect(repository.find_remote_root_ref('origin')).to be_utf8 + end + + it 'returns nil when remote name is nil' do + expect_any_instance_of(Gitlab::GitalyClient::RemoteService) + .not_to receive(:find_remote_root_ref) + + expect(repository.find_remote_root_ref(nil)).to be_nil + end + + it 'returns nil when remote name is empty' do + expect_any_instance_of(Gitlab::GitalyClient::RemoteService) + .not_to receive(:find_remote_root_ref) + + expect(repository.find_remote_root_ref('')).to be_nil + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RemoteService, :find_remote_root_ref do + subject { repository.find_remote_root_ref('origin') } + end + end + describe "#log" do shared_examples 'repository log' do let(:commit_with_old_name) do diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index 54f2ea33f90..bcdf12a00a0 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -19,7 +19,14 @@ describe Gitlab::GitalyClient::CommitService do right_commit_id: commit.id, collapse_diffs: false, enforce_limits: true, - **Gitlab::Git::DiffCollection.collection_limits.to_h + # Tests limitation parameters explicitly + max_files: 100, + max_lines: 5000, + max_bytes: 512000, + safe_max_files: 100, + safe_max_lines: 5000, + safe_max_bytes: 512000, + max_patch_bytes: 102400 ) expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) @@ -37,7 +44,14 @@ describe Gitlab::GitalyClient::CommitService do right_commit_id: initial_commit.id, collapse_diffs: false, enforce_limits: true, - **Gitlab::Git::DiffCollection.collection_limits.to_h + # Tests limitation parameters explicitly + max_files: 100, + max_lines: 5000, + max_bytes: 512000, + safe_max_files: 100, + safe_max_lines: 5000, + safe_max_bytes: 512000, + max_patch_bytes: 102400 ) expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash)) diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb index f03c7e3f04b..9030a49983d 100644 --- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb @@ -45,6 +45,26 @@ describe Gitlab::GitalyClient::RemoteService do end end + describe '#find_remote_root_ref' do + it 'sends an find_remote_root_ref message and returns the root ref' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:find_remote_root_ref) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(ref: 'master')) + + expect(client.find_remote_root_ref('origin')).to eq 'master' + end + + it 'ensure ref is a valid UTF-8 string' do + expect_any_instance_of(Gitaly::RemoteService::Stub) + .to receive(:find_remote_root_ref) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(ref: "an_invalid_ref_\xE5")) + + expect(client.find_remote_root_ref('origin')).to eq "an_invalid_ref_Ã¥" + end + end + describe '#update_remote_mirror' do let(:ref_name) { 'remote_mirror_1' } let(:only_branches_matching) { ['my-branch', 'master'] } diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index b4269bd5786..ec2bdbe22e1 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -288,6 +288,7 @@ project: - fork_network_member - fork_network - custom_attributes +- prometheus_metrics - lfs_file_locks - project_badges - source_of_merge_requests @@ -303,6 +304,8 @@ award_emoji: - user priorities: - label +prometheus_metrics: +- project timelogs: - issue - merge_request @@ -321,3 +324,9 @@ metrics: - latest_closed_by - merged_by - pipeline +resource_label_events: +- user +- issue +- merge_request +- epic +- label diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 1b7fa11cb3c..eefd00e7383 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -331,6 +331,28 @@ }, "events": [] } + ], + "resource_label_events": [ + { + "id":244, + "action":"remove", + "issue_id":40, + "merge_request_id":null, + "label_id":2, + "user_id":1, + "created_at":"2018-08-28T08:24:00.494Z", + "label": { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "type": "ProjectLabel" + } + } ] }, { @@ -2515,6 +2537,17 @@ "events": [] } ], + "resource_label_events": [ + { + "id":243, + "action":"add", + "issue_id":null, + "merge_request_id":27, + "label_id":null, + "user_id":1, + "created_at":"2018-08-28T08:24:00.494Z" + } + ], "merge_request_diff": { "id": 27, "state": "collected", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index a88ac0a091e..3ff6be595a8 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -89,6 +89,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(ProtectedTag.first.create_access_levels).not_to be_empty end + it 'restores issue resource label events' do + expect(Issue.find_by(title: 'Voluptatem').resource_label_events).not_to be_empty + end + + it 'restores merge requests resource label events' do + expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty + end + context 'event at forth level of the tree' do let(:event) { Event.where(action: 6).first } diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index fec8a2af9ab..5dc372263ad 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -169,6 +169,14 @@ describe Gitlab::ImportExport::ProjectTreeSaver do expect(priorities.flatten).not_to be_empty end + it 'has issue resource label events' do + expect(saved_project_json['issues'].first['resource_label_events']).not_to be_empty + end + + it 'has merge request resource label events' do + expect(saved_project_json['merge_requests'].first['resource_label_events']).not_to be_empty + end + it 'saves the correct service type' do expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService') end @@ -291,6 +299,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do project: project, commit_id: ci_build.pipeline.sha) + create(:resource_label_event, label: project_label, issue: issue) + create(:resource_label_event, label: group_label, merge_request: merge_request) + create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' }) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 579f175c4a8..e9f1be172b0 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -555,6 +555,19 @@ ProjectCustomAttribute: - project_id - key - value +PrometheusMetric: +- id +- created_at +- updated_at +- project_id +- y_label +- unit +- legend +- title +- query +- group +- common +- identifier Badge: - id - link_url @@ -566,3 +579,11 @@ Badge: - type ProjectCiCdSetting: - group_runners_enabled +ResourceLabelEvent: +- id +- action +- issue_id +- merge_request_id +- label_id +- user_id +- created_at diff --git a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb new file mode 100644 index 00000000000..4a669408025 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::ClusterRoleBinding do + let(:cluster_role_binding) { described_class.new(name, cluster_role_name, subjects) } + let(:name) { 'cluster-role-binding-name' } + let(:cluster_role_name) { 'cluster-admin' } + + let(:subjects) { [{ kind: 'ServiceAccount', name: 'sa', namespace: 'ns' }] } + + describe '#generate' do + let(:role_ref) do + { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: cluster_role_name + } + end + + let(:resource) do + ::Kubeclient::Resource.new( + metadata: { name: name }, + roleRef: role_ref, + subjects: subjects + ) + end + + subject { cluster_role_binding.generate } + + it 'should build a Kubeclient Resource' do + is_expected.to eq(resource) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 341f71a3e49..25c3b37753d 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -5,9 +5,18 @@ describe Gitlab::Kubernetes::Helm::Api do let(:helm) { described_class.new(client) } let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) } - let(:application) { create(:clusters_applications_prometheus) } - - let(:command) { application.install_command } + let(:application_name) { 'app-name' } + let(:rbac) { false } + let(:files) { {} } + + let(:command) do + Gitlab::Kubernetes::Helm::InstallCommand.new( + name: application_name, + chart: 'chart-name', + rbac: rbac, + files: files + ) + end subject { helm } @@ -28,6 +37,8 @@ describe Gitlab::Kubernetes::Helm::Api do before do allow(client).to receive(:create_pod).and_return(nil) allow(client).to receive(:create_config_map).and_return(nil) + allow(client).to receive(:create_service_account).and_return(nil) + allow(client).to receive(:create_cluster_role_binding).and_return(nil) allow(namespace).to receive(:ensure_exists!).once end @@ -39,7 +50,7 @@ describe Gitlab::Kubernetes::Helm::Api do end context 'with a ConfigMap' do - let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application.name, application.files).generate } + let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application_name, files).generate } it 'creates a ConfigMap on kubeclient' do expect(client).to receive(:create_config_map).with(resource).once @@ -47,6 +58,96 @@ describe Gitlab::Kubernetes::Helm::Api do subject.install(command) end end + + context 'without a service account' do + it 'does not create a service account on kubeclient' do + expect(client).not_to receive(:create_service_account) + expect(client).not_to receive(:create_cluster_role_binding) + + subject.install(command) + end + end + + context 'with a service account' do + let(:command) { Gitlab::Kubernetes::Helm::InitCommand.new(name: application_name, files: files, rbac: rbac) } + + context 'rbac-enabled cluster' do + let(:rbac) { true } + + let(:service_account_resource) do + Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' }) + end + + let(:cluster_role_binding_resource) do + Kubeclient::Resource.new( + metadata: { name: 'tiller-admin' }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, + subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }] + ) + end + + context 'service account and cluster role binding does not exist' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + end + + it 'creates a service account, followed the cluster role binding on kubeclient' do + expect(client).to receive(:create_service_account).with(service_account_resource).once.ordered + expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered + + subject.install(command) + end + end + + context 'service account already exists' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_return(service_account_resource) + expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + end + + it 'updates the service account, followed by creating the cluster role binding' do + expect(client).to receive(:update_service_account).with(service_account_resource).once.ordered + expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered + + subject.install(command) + end + end + + context 'service account and cluster role binding already exists' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_return(service_account_resource) + expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_return(cluster_role_binding_resource) + end + + it 'updates the service account, followed by creating the cluster role binding' do + expect(client).to receive(:update_service_account).with(service_account_resource).once.ordered + expect(client).to receive(:update_cluster_role_binding).with(cluster_role_binding_resource).once.ordered + + subject.install(command) + end + end + + context 'a non-404 error is thrown' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'raises an error' do + expect { subject.install(command) }.to raise_error(Kubeclient::HttpError) + end + end + end + + context 'legacy abac cluster' do + it 'does not create a service account on kubeclient' do + expect(client).not_to receive(:create_service_account) + expect(client).not_to receive(:create_cluster_role_binding) + + subject.install(command) + end + end + end end describe '#status' do diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index d50616e95e8..aacae78be43 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -2,14 +2,24 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::BaseCommand do let(:application) { create(:clusters_applications_helm) } + let(:rbac) { false } + let(:test_class) do Class.new do include Gitlab::Kubernetes::Helm::BaseCommand + def initialize(rbac) + @rbac = rbac + end + def name "test-class-name" end + def rbac? + @rbac + end + def files { some: 'value' @@ -19,7 +29,7 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do end let(:base_command) do - test_class.new + test_class.new(rbac) end subject { base_command } @@ -34,6 +44,14 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do it 'should returns a kubeclient resoure with pod content for application' do is_expected.to be_an_instance_of ::Kubeclient::Resource end + + context 'when rbac is true' do + let(:rbac) { true } + + it 'also returns a kubeclient resource' do + is_expected.to be_an_instance_of ::Kubeclient::Resource + end + end end describe '#pod_name' do diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb index dcbc046cf00..72dc1817936 100644 --- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -2,9 +2,135 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::InitCommand do let(:application) { create(:clusters_applications_helm) } - let(:commands) { 'helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null' } + let(:rbac) { false } + let(:files) { {} } + let(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) } - subject { described_class.new(name: application.name, files: {}) } + let(:commands) do + <<~EOS + helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null + EOS + end + + subject { init_command } it_behaves_like 'helm commands' + + context 'on a rbac-enabled cluster' do + let(:rbac) { true } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller >/dev/null + EOS + end + end + end + + describe '#rbac?' do + subject { init_command.rbac? } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_truthy } + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_falsey } + end + end + + describe '#config_map_resource' do + let(:metadata) do + { + name: 'values-content-configuration-helm', + namespace: 'gitlab-managed-apps', + labels: { name: 'values-content-configuration-helm' } + } + end + + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) } + + subject { init_command.config_map_resource } + + it 'returns a KubeClient resource with config map content for the application' do + is_expected.to eq(resource) + end + end + + describe '#pod_resource' do + subject { init_command.pod_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a pod that uses the tiller serviceAccountName' do + expect(subject.spec.serviceAccountName).to eq('tiller') + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates a pod that uses the default serviceAccountName' do + expect(subject.spec.serviceAcccountName).to be_nil + end + end + end + + describe '#service_account_resource' do + let(:resource) do + Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' }) + end + + subject { init_command.service_account_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the tiller ServiceAccount' do + is_expected.to eq(resource) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end + + describe '#cluster_role_binding_resource' do + let(:resource) do + Kubeclient::Resource.new( + metadata: { name: 'tiller-admin' }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, + subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }] + ) + end + + subject { init_command.cluster_role_binding_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do + is_expected.to eq(resource) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 982e2f41043..f28941ce58f 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -3,14 +3,17 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do let(:files) { { 'ca.pem': 'some file content' } } let(:repository) { 'https://repository.example.com' } + let(:rbac) { false } let(:version) { '1.2.3' } let(:install_command) do described_class.new( name: 'app-name', chart: 'chart-name', + rbac: rbac, files: files, - version: version, repository: repository + version: version, + repository: repository ) end @@ -21,19 +24,76 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do <<~EOS helm init --client-only >/dev/null helm repo add app-name https://repository.example.com - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + #{helm_install_comand} + EOS + end + + let(:helm_install_comand) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --version 1.2.3 + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end + context 'when rbac is true' do + let(:rbac) { true } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --version 1.2.3 + --set rbac.create\\=true,rbac.enabled\\=true + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null + EOS + end + end + end + context 'when there is no repository' do let(:repository) { nil } it_behaves_like 'helm commands' do let(:commands) do <<~EOS - helm init --client-only >/dev/null - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + helm init --client-only >/dev/null + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --version 1.2.3 + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end @@ -45,9 +105,19 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do it_behaves_like 'helm commands' do let(:commands) do <<~EOS - helm init --client-only >/dev/null - helm repo add app-name https://repository.example.com - helm install chart-name --name app-name --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --version 1.2.3 + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end @@ -59,14 +129,63 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do it_behaves_like 'helm commands' do let(:commands) do <<~EOS - helm init --client-only >/dev/null - helm repo add app-name https://repository.example.com - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end end + describe '#rbac?' do + subject { install_command.rbac? } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_truthy } + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_falsey } + end + end + + describe '#pod_resource' do + subject { install_command.pod_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a pod that uses the tiller serviceAccountName' do + expect(subject.spec.serviceAccountName).to eq('tiller') + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates a pod that uses the default serviceAccountName' do + expect(subject.spec.serviceAcccountName).to be_nil + end + end + end + describe '#config_map_resource' do let(:metadata) do { @@ -84,4 +203,20 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do is_expected.to eq(resource) end end + + describe '#service_account_resource' do + subject { install_command.service_account_resource } + + it 'returns nothing' do + is_expected.to be_nil + end + end + + describe '#cluster_role_binding_resource' do + subject { install_command.cluster_role_binding_resource } + + it 'returns nothing' do + is_expected.to be_nil + end + end end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index ec64193c0b2..b333b334f36 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -5,8 +5,9 @@ describe Gitlab::Kubernetes::Helm::Pod do let(:app) { create(:clusters_applications_prometheus) } let(:command) { app.install_command } let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } + let(:service_account_name) { nil } - subject { described_class.new(command, namespace) } + subject { described_class.new(command, namespace, service_account_name: service_account_name) } context 'with a command' do it 'should generate a Kubeclient::Resource' do @@ -58,6 +59,20 @@ describe Gitlab::Kubernetes::Helm::Pod do expect(volume.configMap['items'].first['key']).to eq(:'values.yaml') expect(volume.configMap['items'].first['path']).to eq(:'values.yaml') end + + it 'should have no serviceAccountName' do + spec = subject.generate.spec + expect(spec.serviceAccountName).to be_nil + end + + context 'with a service_account_name' do + let(:service_account_name) { 'sa' } + + it 'should use the serviceAccountName provided' do + spec = subject.generate.spec + expect(spec.serviceAccountName).to eq(service_account_name) + end + end end end end diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb new file mode 100644 index 00000000000..9146729d139 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::KubeClient do + include KubernetesHelpers + + let(:api_url) { 'https://kubernetes.example.com/prefix' } + let(:api_groups) { ['api', 'apis/rbac.authorization.k8s.io'] } + let(:api_version) { 'v1' } + let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } } + + let(:client) { described_class.new(api_url, api_groups, api_version, kubeclient_options) } + + before do + stub_kubeclient_discover(api_url) + end + + describe '#hashed_clients' do + subject { client.hashed_clients } + + it 'has keys from api groups' do + expect(subject.keys).to match_array api_groups + end + + it 'has values of Kubeclient::Client' do + expect(subject.values).to all(be_an_instance_of Kubeclient::Client) + end + end + + describe '#clients' do + subject { client.clients } + + it 'is not empty' do + is_expected.to be_present + end + + it 'is an array of Kubeclient::Client objects' do + is_expected.to all(be_an_instance_of Kubeclient::Client) + end + + it 'has each API group url' do + expected_urls = api_groups.map { |group| "#{api_url}/#{group}" } + + expect(subject.map(&:api_endpoint).map(&:to_s)).to match_array(expected_urls) + end + + it 'has the kubeclient options' do + subject.each do |client| + expect(client.auth_options).to eq({ bearer_token: 'xyz' }) + end + end + + it 'has the api_version' do + subject.each do |client| + expect(client.instance_variable_get(:@api_version)).to eq('v1') + end + end + end + + describe '#core_client' do + subject { client.core_client } + + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client + end + + it 'has the core API endpoint' do + expect(subject.api_endpoint.to_s).to match(%r{\/api\Z}) + end + end + + describe '#rbac_client' do + subject { client.rbac_client } + + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client + end + + it 'has the RBAC API group endpoint' do + expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z}) + end + end + + describe '#extensions_client' do + subject { client.extensions_client } + + let(:api_groups) { ['apis/extensions'] } + + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client + end + + it 'has the extensions API group endpoint' do + expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z}) + end + end + + describe '#discover!' do + it 'makes a discovery request for each API group' do + client.discover! + + api_groups.each do |api_group| + discovery_url = api_url + '/' + api_group + '/v1' + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + end + + describe 'core API' do + let(:core_client) { client.core_client } + + [ + :get_pods, + :get_secrets, + :get_config_map, + :get_pod, + :get_namespace, + :get_service, + :get_service_account, + :delete_pod, + :create_config_map, + :create_namespace, + :create_pod, + :create_service_account, + :update_config_map, + :update_service_account + ].each do |method| + describe "##{method}" do + it 'delegates to the core client' do + expect(client).to delegate_method(method).to(:core_client) + end + + it 'responds to the method' do + expect(client).to respond_to method + end + end + end + end + + describe 'rbac API group' do + let(:rbac_client) { client.rbac_client } + + [ + :create_cluster_role_binding, + :get_cluster_role_binding, + :update_cluster_role_binding + ].each do |method| + describe "##{method}" do + it 'delegates to the rbac client' do + expect(client).to delegate_method(method).to(:rbac_client) + end + + it 'responds to the method' do + expect(client).to respond_to method + end + + context 'no rbac client' do + let(:api_groups) { ['api'] } + + it 'throws an error' do + expect { client.public_send(method) }.to raise_error(Module::DelegationError) + end + end + end + end + end + + describe 'extensions API group' do + let(:api_groups) { ['apis/extensions'] } + let(:api_version) { 'v1beta1' } + let(:extensions_client) { client.extensions_client } + + describe '#get_deployments' do + it 'delegates to the extensions client' do + expect(client).to delegate_method(:get_deployments).to(:extensions_client) + end + + it 'responds to the method' do + expect(client).to respond_to :get_deployments + end + + context 'no extensions client' do + let(:api_groups) { ['api'] } + let(:api_version) { 'v1' } + + it 'throws an error' do + expect { client.get_deployments }.to raise_error(Module::DelegationError) + end + end + end + end + + describe 'non-entity methods' do + it 'does not proxy for non-entity methods' do + expect(client.clients.first).to respond_to :proxy_url + + expect(client).not_to respond_to :proxy_url + end + + it 'throws an error' do + expect { client.proxy_url }.to raise_error(NoMethodError) + end + end + + describe '#get_pod_log' do + let(:core_client) { client.core_client } + + it 'is delegated to the core client' do + expect(client).to delegate_method(:get_pod_log).to(:core_client) + end + + context 'when no core client' do + let(:api_groups) { ['apis/extensions'] } + + it 'throws an error' do + expect { client.get_pod_log('pod-name') }.to raise_error(Module::DelegationError) + end + end + end + + describe '#watch_pod_log' do + let(:core_client) { client.core_client } + + it 'is delegated to the core client' do + expect(client).to delegate_method(:watch_pod_log).to(:core_client) + end + + context 'when no core client' do + let(:api_groups) { ['apis/extensions'] } + + it 'throws an error' do + expect { client.watch_pod_log('pod-name') }.to raise_error(Module::DelegationError) + end + end + end + + describe 'methods that do not exist on any client' do + it 'throws an error' do + expect { client.non_existent_method }.to raise_error(NoMethodError) + end + + it 'returns false for respond_to' do + expect(client.respond_to?(:non_existent_method)).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/kubernetes/service_account_spec.rb b/spec/lib/gitlab/kubernetes/service_account_spec.rb new file mode 100644 index 00000000000..8da9e932dc3 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/service_account_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::ServiceAccount do + let(:name) { 'a_service_account' } + let(:namespace_name) { 'a_namespace' } + let(:service_account) { described_class.new(name, namespace_name) } + + it { expect(service_account.name).to eq(name) } + it { expect(service_account.namespace_name).to eq(namespace_name) } + + describe '#generate' do + let(:resource) do + ::Kubeclient::Resource.new(metadata: { name: name, namespace: namespace_name }) + end + + subject { service_account.generate } + + it 'should build a Kubeclient Resource' do + is_expected.to eq(resource) + end + end +end diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb index 5589db92b1d..1a108003bc2 100644 --- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb +++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Prometheus::AdditionalMetricsParser do let(:parser_error_class) { Gitlab::Prometheus::ParsingError } describe '#load_groups_from_yaml' do - subject { described_class.load_groups_from_yaml } + subject { described_class.load_groups_from_yaml('dummy.yaml') } describe 'parsing sample yaml' do let(:sample_yaml) do diff --git a/spec/lib/gitlab/prometheus/metric_group_spec.rb b/spec/lib/gitlab/prometheus/metric_group_spec.rb new file mode 100644 index 00000000000..e7d16e73663 --- /dev/null +++ b/spec/lib/gitlab/prometheus/metric_group_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::Prometheus::MetricGroup do + describe '.common_metrics' do + let!(:project_metric) { create(:prometheus_metric) } + let!(:common_metric_group_a) { create(:prometheus_metric, :common, group: :aws_elb) } + let!(:common_metric_group_b_q1) { create(:prometheus_metric, :common, group: :kubernetes) } + let!(:common_metric_group_b_q2) { create(:prometheus_metric, :common, group: :kubernetes) } + + subject { described_class.common_metrics } + + it 'returns exactly two groups' do + expect(subject.map(&:name)).to contain_exactly( + 'Response metrics (AWS ELB)', 'System metrics (Kubernetes)') + end + + it 'returns exactly three metric queries' do + expect(subject.map(&:metrics).flatten.map(&:id)).to contain_exactly( + common_metric_group_a.id, common_metric_group_b_q1.id, + common_metric_group_b_q2.id) + end + end + + describe '.for_project' do + let!(:other_project) { create(:project) } + let!(:project_metric) { create(:prometheus_metric) } + let!(:common_metric) { create(:prometheus_metric, :common, group: :aws_elb) } + + subject do + described_class.for_project(other_project) + .map(&:metrics).flatten + .map(&:id) + end + + it 'returns exactly one common metric' do + is_expected.to contain_exactly(common_metric.id) + end + end +end diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb new file mode 100644 index 00000000000..7ffcef2baef --- /dev/null +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -0,0 +1,202 @@ +require 'spec_helper' + +describe Gitlab::TreeSummary do + using RSpec::Parameterized::TableSyntax + + let(:project) { create(:project, :empty_repo) } + let(:repo) { project.repository } + let(:commit) { repo.head_commit } + + let(:path) { nil } + let(:offset) { nil } + let(:limit) { nil } + + subject(:summary) { described_class.new(commit, project, path: path, offset: offset, limit: limit) } + + describe '#initialize' do + it 'defaults offset to 0' do + expect(summary.offset).to eq(0) + end + + it 'defaults limit to 25' do + expect(summary.limit).to eq(25) + end + end + + describe '#summarize' do + let(:project) { create(:project, :custom_repo, files: { 'a.txt' => '' }) } + + subject(:summarized) { summary.summarize } + + it 'returns an array of entries, and an array of commits' do + expect(summarized).to be_a(Array) + expect(summarized.size).to eq(2) + + entries, commits = *summarized + aggregate_failures do + expect(entries).to contain_exactly( + a_hash_including(file_name: 'a.txt', commit: have_attributes(id: commit.id)) + ) + + expect(commits).to match_array(entries.map { |entry| entry[:commit] }) + end + end + end + + describe '#summarize (entries)' do + let(:limit) { 2 } + + custom_files = { + 'a.txt' => '', + 'b.txt' => '', + 'directory/c.txt' => '' + } + + let(:project) { create(:project, :custom_repo, files: custom_files) } + let(:commit) { repo.head_commit } + + subject(:entries) { summary.summarize.first } + + it 'summarizes the entries within the window' do + is_expected.to contain_exactly( + a_hash_including(type: :tree, file_name: 'directory'), + a_hash_including(type: :blob, file_name: 'a.txt') + # b.txt is excluded by the limit + ) + end + + it 'references the commit and commit path in entries' do + entry = entries.first + expected_commit_path = Gitlab::Routing.url_helpers.project_commit_path(project, commit) + + expect(entry[:commit]).to be_a(::Commit) + expect(entry[:commit_path]).to eq expected_commit_path + end + + context 'in a good subdirectory' do + let(:path) { 'directory' } + + it 'summarizes the entries in the subdirectory' do + is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'c.txt')) + end + end + + context 'in a non-existent subdirectory' do + let(:path) { 'tmp' } + + it { is_expected.to be_empty } + end + + context 'custom offset and limit' do + let(:offset) { 2 } + + it 'returns entries from the offset' do + is_expected.to contain_exactly(a_hash_including(type: :blob, file_name: 'b.txt')) + end + end + end + + describe '#summarize (commits)' do + # This is a commit in the master branch of the gitlab-test repository that + # satisfies certain assumptions these tests depend on + let(:test_commit_sha) { '7975be0116940bf2ad4321f79d02a55c5f7779aa' } + let(:whitespace_commit_sha) { '66eceea0db202bb39c4e445e8ca28689645366c5' } + + let(:project) { create(:project, :repository) } + let(:commit) { repo.commit(test_commit_sha) } + let(:limit) { nil } + let(:entries) { summary.summarize.first } + + subject(:commits) do + summary.summarize.last + end + + it 'returns an Array of ::Commit objects' do + is_expected.not_to be_empty + is_expected.to all(be_kind_of(::Commit)) + end + + it 'deduplicates commits when multiple entries reference the same commit' do + expect(commits.size).to be < entries.size + end + + context 'in a subdirectory' do + let(:path) { 'files' } + + it 'returns commits for entries in the subdirectory' do + expect(commits).to satisfy_one { |c| c.id == whitespace_commit_sha } + end + end + end + + describe '#more?' do + let(:path) { 'tmp/more' } + + where(:num_entries, :offset, :limit, :expected_result) do + 0 | 0 | 0 | false + 0 | 0 | 1 | false + + 1 | 0 | 0 | true + 1 | 0 | 1 | false + 1 | 1 | 0 | false + 1 | 1 | 1 | false + + 2 | 0 | 0 | true + 2 | 0 | 1 | true + 2 | 0 | 2 | false + 2 | 0 | 3 | false + 2 | 1 | 0 | true + 2 | 1 | 1 | false + 2 | 2 | 0 | false + 2 | 2 | 1 | false + end + + with_them do + before do + create_file('dummy', path: 'other') if num_entries.zero? + 1.upto(num_entries) { |n| create_file(n, path: path) } + end + + subject { summary.more? } + + it { is_expected.to eq(expected_result) } + end + end + + describe '#next_offset' do + let(:path) { 'tmp/next_offset' } + + where(:num_entries, :offset, :limit, :expected_result) do + 0 | 0 | 0 | 0 + 0 | 0 | 1 | 1 + 0 | 1 | 0 | 1 + 0 | 1 | 1 | 1 + + 1 | 0 | 0 | 0 + 1 | 0 | 1 | 1 + 1 | 1 | 0 | 1 + 1 | 1 | 1 | 2 + end + + with_them do + before do + create_file('dummy', path: 'other') if num_entries.zero? + 1.upto(num_entries) { |n| create_file(n, path: path) } + end + + subject { summary.next_offset } + + it { is_expected.to eq(expected_result) } + end + end + + def create_file(unique, path:) + repo.create_file( + project.creator, + "#{path}/file-#{unique}.txt", + 'content', + message: "Commit message #{unique}", + branch_name: 'master' + ) + end +end diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb new file mode 100644 index 00000000000..fcc05ab3a0c --- /dev/null +++ b/spec/lib/gitlab/user_extractor_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UserExtractor do + let(:text) do + <<~TXT + This is a long texth that mentions some users. + @user-1, @user-2 and user@gitlab.org take a walk in the park. + There they meet @user-4 that was out with other-user@gitlab.org. + @user-1 thought it was late, so went home straight away + TXT + end + subject(:extractor) { described_class.new(text) } + + describe '#users' do + it 'returns an empty relation when nil was passed' do + extractor = described_class.new(nil) + + expect(extractor.users).to be_empty + expect(extractor.users).to be_a(ActiveRecord::Relation) + end + + it 'returns the user case insensitive for usernames' do + user = create(:user, username: "USER-4") + + expect(extractor.users).to include(user) + end + + it 'returns users by primary email' do + user = create(:user, email: 'user@gitlab.org') + + expect(extractor.users).to include(user) + end + + it 'returns users by secondary email' do + user = create(:email, email: 'other-user@gitlab.org').user + + expect(extractor.users).to include(user) + end + end + + describe '#matches' do + it 'includes all mentioned email adresses' do + expect(extractor.matches[:emails]).to contain_exactly('user@gitlab.org', 'other-user@gitlab.org') + end + + it 'includes all mentioned usernames' do + expect(extractor.matches[:usernames]).to contain_exactly('user-1', 'user-2', 'user-4') + end + end + + describe '#references' do + it 'includes all user-references once' do + expect(extractor.references).to contain_exactly('user-1', 'user-2', 'user@gitlab.org', 'user-4', 'other-user@gitlab.org') + end + end +end diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 632acd6eb46..9c308cc1be9 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -62,7 +62,7 @@ describe ObjectStorage::DirectUpload do expect(subject[:StoreURL]).to start_with(storage_url) expect(subject[:DeleteURL]).to start_with(storage_url) expect(subject[:CustomPutHeaders]).to be_truthy - expect(subject[:PutHeaders]).to eq({ 'Content-Type' => 'application/octet-stream' }) + expect(subject[:PutHeaders]).to eq({}) end end diff --git a/spec/mailers/emails/auto_devops_spec.rb b/spec/mailers/emails/auto_devops_spec.rb new file mode 100644 index 00000000000..839caf3f50e --- /dev/null +++ b/spec/mailers/emails/auto_devops_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Emails::AutoDevops do + include EmailSpec::Matchers + + describe '#auto_devops_disabled_email' do + let(:owner) { create(:user) } + let(:namespace) { create(:namespace, owner: owner) } + let(:project) { create(:project, :repository, :auto_devops) } + let(:pipeline) { create(:ci_pipeline, :failed, project: project) } + + subject { Notify.autodevops_disabled_email(pipeline, owner.email) } + + it 'sents email with correct subject' do + is_expected.to have_subject("#{project.name} | Auto DevOps pipeline was disabled for #{project.name}") + end + + it 'sents an email to the user' do + recipient = subject.header[:to].addrs.map(&:address).first + + expect(recipient).to eq(owner.email) + end + + it 'is sent as GitLab email' do + sender = subject.header[:from].addrs[0].address + + expect(sender).to match(/gitlab/) + end + end +end diff --git a/spec/migrations/import_common_metrics_spec.rb b/spec/migrations/import_common_metrics_spec.rb new file mode 100644 index 00000000000..1001629007c --- /dev/null +++ b/spec/migrations/import_common_metrics_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180831164910_import_common_metrics.rb') + +describe ImportCommonMetrics, :migration do + describe '#up' do + it "imports all prometheus metrics" do + expect(PrometheusMetric.common).to be_empty + + migrate! + + expect(PrometheusMetric.common).not_to be_empty + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 77b7332a761..14ccc2960bb 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1941,4 +1941,28 @@ describe Ci::Pipeline, :mailer do expect(pipeline.total_size).to eq(5) end end + + describe '#status' do + context 'when transitioning to failed' do + context 'when pipeline has autodevops as source' do + let(:pipeline) { create(:ci_pipeline, :running, :auto_devops_source) } + + it 'calls autodevops disable service' do + expect(AutoDevops::DisableWorker).to receive(:perform_async).with(pipeline.id) + + pipeline.drop + end + end + + context 'when pipeline has other source' do + let(:pipeline) { create(:ci_pipeline, :running, :repository_source) } + + it 'does not call auto devops disable service' do + expect(AutoDevops::DisableWorker).not_to receive(:perform_async) + + pipeline.drop + end + end + end + end end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index e5b2bdc8a4e..2c37cd20ecc 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -47,5 +47,19 @@ describe Clusters::Applications::Helm do cert = OpenSSL::X509::Certificate.new(subject.files[:'cert.pem']) expect(cert.not_after).to be > 999.years.from_now end + + describe 'rbac' do + context 'non rbac cluster' do + it { expect(subject).not_to be_rbac } + end + + context 'rbac cluster' do + before do + helm.cluster.platform_kubernetes.rbac! + end + + it { expect(subject).to be_rbac } + end + end end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 21f75ced8c3..c55953c8d22 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -88,9 +88,18 @@ describe Clusters::Applications::Ingress do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') expect(subject.version).to eq('0.23.0') + expect(subject).not_to be_rbac expect(subject.files).to eq(ingress.files) end + context 'on a rbac enabled cluster' do + before do + ingress.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 027b732681b..591a01d78a9 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -51,10 +51,19 @@ describe Clusters::Applications::Jupyter do expect(subject.name).to eq('jupyter') expect(subject.chart).to eq('jupyter/jupyterhub') expect(subject.version).to eq('v0.6') + expect(subject).not_to be_rbac expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/') expect(subject.files).to eq(jupyter.files) end + context 'on a rbac enabled cluster' do + before do + jupyter.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 26b75c75e1d..f34b4ece8db 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe Clusters::Applications::Prometheus do + include KubernetesHelpers + include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application status specs', :cluster_application_prometheus @@ -107,26 +109,14 @@ describe Clusters::Applications::Prometheus do end context 'cluster has kubeclient' do - let(:kubernetes_url) { 'http://example.com' } - let(:k8s_discover_response) do - { - resources: [ - { - name: 'service', - kind: 'Service' - } - ] - } - end - - let(:kube_client) { Kubeclient::Client.new(kubernetes_url) } + let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } + let(:kube_client) { subject.cluster.kubeclient.core_client } - let(:cluster) { create(:cluster) } - subject { create(:clusters_applications_prometheus, cluster: cluster) } + subject { create(:clusters_applications_prometheus) } before do - allow(kube_client.rest_client).to receive(:get).and_return(k8s_discover_response.to_json) - allow(subject.cluster).to receive(:kubeclient).and_return(kube_client) + subject.cluster.platform_kubernetes.namespace = 'a-namespace' + stub_kubeclient_discover(subject.cluster.platform_kubernetes.api_url) end it 'creates proxy prometheus rest client' do @@ -134,7 +124,7 @@ describe Clusters::Applications::Prometheus do end it 'creates proper url' do - expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80/proxy') + expect(subject.prometheus_client.url).to eq("#{kubernetes_url}/api/v1/namespaces/gitlab-managed-apps/services/prometheus-prometheus-server:80/proxy") end it 'copies options and headers from kube client to proxy client' do @@ -164,9 +154,18 @@ describe Clusters::Applications::Prometheus do expect(subject.name).to eq('prometheus') expect(subject.chart).to eq('stable/prometheus') expect(subject.version).to eq('6.7.3') + expect(subject).not_to be_rbac expect(subject.files).to eq(prometheus.files) end + context 'on a rbac enabled cluster' do + before do + prometheus.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index d84f125e246..eda8d519f60 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -46,10 +46,19 @@ describe Clusters::Applications::Runner do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') expect(subject.version).to eq('0.1.31') + expect(subject).not_to be_rbac expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.files).to eq(gitlab_runner.files) end + context 'on a rbac enabled cluster' do + before do + gitlab_runner.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 6f66515b45f..2727191eb9b 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -13,6 +13,10 @@ describe Clusters::Cluster do it { is_expected.to delegate_method(:status_reason).to(:provider) } it { is_expected.to delegate_method(:status_name).to(:provider) } it { is_expected.to delegate_method(:on_creation?).to(:provider) } + it { is_expected.to delegate_method(:active?).to(:platform_kubernetes).with_prefix } + it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix } + it { is_expected.to delegate_method(:installed?).to(:application_helm).with_prefix } + it { is_expected.to delegate_method(:installed?).to(:application_ingress).with_prefix } it { is_expected.to respond_to :project } describe '.enabled' do diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index ab7f89f9bf4..66198d5ee2b 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -92,6 +92,30 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end end + describe '#kubeclient' do + subject { kubernetes.kubeclient } + + let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace') } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) } + end + + describe '#rbac?' do + subject { kubernetes.rbac? } + + let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) } + + context 'when authorization type is rbac' do + let(:kubernetes) { build(:cluster_platform_kubernetes, :rbac_enabled, :configured) } + + it { is_expected.to be_truthy } + end + + context 'when authorization type is nil' do + it { is_expected.to be_falsey } + end + end + describe '#actual_namespace' do subject { kubernetes.actual_namespace } diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 5c0dfaeb4d3..1bf6c9b3404 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -3,186 +3,50 @@ require 'spec_helper' describe CaseSensitivity do describe '.iwhere' do let(:connection) { ActiveRecord::Base.connection } - let(:model) { Class.new { include CaseSensitivity } } - - describe 'using PostgreSQL' do - before do - allow(Gitlab::Database).to receive(:postgresql?).and_return(true) - allow(Gitlab::Database).to receive(:mysql?).and_return(false) - end - - describe 'with a single column/value pair' do - it 'returns the criteria for a column and a value' do - criteria = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('"foo"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar') - .and_return(criteria) - - expect(model.iwhere(foo: 'bar')).to eq(criteria) - end - - it 'returns the criteria for a column with a table, and a value' do - criteria = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('"foo"."bar"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar') - .and_return(criteria) - - expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria) - end - end - - describe 'with multiple column/value pairs' do - it 'returns the criteria for a column and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('"foo"') - - expect(connection).to receive(:quote_table_name) - .with(:bar) - .and_return('"bar"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz') - .and_return(final) - - got = model.iwhere(foo: 'bar', bar: 'baz') - - expect(got).to eq(final) - end - - it 'returns the criteria for a column with a table, and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('"foo"."bar"') - - expect(connection).to receive(:quote_table_name) - .with(:'foo.baz') - .and_return('"foo"."baz"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz') - .and_return(final) - - got = model.iwhere('foo.bar'.to_sym => 'bar', - 'foo.baz'.to_sym => 'baz') - - expect(got).to eq(final) - end + let(:model) do + Class.new(ActiveRecord::Base) do + include CaseSensitivity + self.table_name = 'namespaces' end end - describe 'using MySQL' do - before do - allow(Gitlab::Database).to receive(:postgresql?).and_return(false) - allow(Gitlab::Database).to receive(:mysql?).and_return(true) - end - - describe 'with a single column/value pair' do - it 'returns the criteria for a column and a value' do - criteria = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('`foo`') - - expect(model).to receive(:where) - .with(%q{`foo` = :value}, value: 'bar') - .and_return(criteria) + let!(:model_1) { model.create(path: 'mOdEl-1', name: 'mOdEl 1') } + let!(:model_2) { model.create(path: 'mOdEl-2', name: 'mOdEl 2') } - expect(model.iwhere(foo: 'bar')).to eq(criteria) - end + it 'finds a single instance by a single attribute regardless of case' do + expect(model.iwhere(path: 'MODEL-1')).to contain_exactly(model_1) + end - it 'returns the criteria for a column with a table, and a value' do - criteria = double(:criteria) + it 'finds multiple instances by a single attribute regardless of case' do + expect(model.iwhere(path: %w(MODEL-1 model-2))).to contain_exactly(model_1, model_2) + end - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('`foo`.`bar`') + it 'finds instances by multiple attributes' do + expect(model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1')) + .to contain_exactly(model_1) + end - expect(model).to receive(:where) - .with(%q{`foo`.`bar` = :value}, value: 'bar') - .and_return(criteria) + # Using `mysql` & `postgresql` metadata-tags here because both adapters build + # the query slightly differently + context 'for MySQL', :mysql do + it 'builds a simple query' do + query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql + expected_query = <<~QRY.strip + SELECT `namespaces`.* FROM `namespaces` WHERE (`namespaces`.`path` IN ('MODEL-1', 'model-2')) AND (`namespaces`.`name` = 'model 1') + QRY - expect(model.iwhere('foo.bar'.to_sym => 'bar')) - .to eq(criteria) - end + expect(query).to eq(expected_query) end + end - describe 'with multiple column/value pairs' do - it 'returns the criteria for a column and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('`foo`') - - expect(connection).to receive(:quote_table_name) - .with(:bar) - .and_return('`bar`') - - expect(model).to receive(:where) - .with(%q{`foo` = :value}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{`bar` = :value}, value: 'baz') - .and_return(final) - - got = model.iwhere(foo: 'bar', bar: 'baz') - - expect(got).to eq(final) - end - - it 'returns the criteria for a column with a table, and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('`foo`.`bar`') - - expect(connection).to receive(:quote_table_name) - .with(:'foo.baz') - .and_return('`foo`.`baz`') - - expect(model).to receive(:where) - .with(%q{`foo`.`bar` = :value}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{`foo`.`baz` = :value}, value: 'baz') - .and_return(final) - - got = model.iwhere('foo.bar'.to_sym => 'bar', - 'foo.baz'.to_sym => 'baz') + context 'for PostgreSQL', :postgresql do + it 'builds a query using LOWER' do + query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql + expected_query = <<~QRY.strip + SELECT \"namespaces\".* FROM \"namespaces\" WHERE (LOWER(\"namespaces\".\"path\") IN (LOWER('MODEL-1'), LOWER('model-2'))) AND (LOWER(\"namespaces\".\"name\") = LOWER('model 1')) + QRY - expect(got).to eq(final) - end + expect(query).to eq(expected_query) end end end diff --git a/spec/models/label_note_spec.rb b/spec/models/label_note_spec.rb new file mode 100644 index 00000000000..f69874d94aa --- /dev/null +++ b/spec/models/label_note_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe LabelNote do + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } + set(:label) { create(:label, project: project) } + set(:label2) { create(:label, project: project) } + let(:resource_parent) { project } + + context 'when resource is issue' do + set(:resource) { create(:issue, project: project) } + + it_behaves_like 'label note created from events' + end + + context 'when resource is merge request' do + set(:resource) { create(:merge_request, source_project: project, target_project: project) } + + it_behaves_like 'label note created from events' + end +end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 54f1a0e38a5..788b3179b01 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -231,12 +231,12 @@ describe JiraService do end it 'logs exception when transition id is not valid' do - allow(Rails.logger).to receive(:info) - WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise('Bad Request') + allow(@jira_service).to receive(:log_error) + WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request") @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) - expect(Rails.logger).to have_received(:info).with('JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request') + expect(@jira_service).to have_received(:log_error).with("Issue transition failed", error: "Bad Request", client_url: "http://jira.example.com") end it 'calls the api with jira_issue_transition_id' do diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb new file mode 100644 index 00000000000..a83a31ae88c --- /dev/null +++ b/spec/models/prometheus_metric_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe PrometheusMetric do + subject { build(:prometheus_metric) } + let(:other_project) { build(:project) } + + it { is_expected.to belong_to(:project) } + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:query) } + it { is_expected.to validate_presence_of(:group) } + + describe 'common metrics' do + using RSpec::Parameterized::TableSyntax + + where(:common, :project, :result) do + false | other_project | true + false | nil | false + true | other_project | false + true | nil | true + end + + with_them do + before do + subject.common = common + subject.project = project + end + + it { expect(subject.valid?).to eq(result) } + end + end + + describe '#query_series' do + using RSpec::Parameterized::TableSyntax + + where(:legend, :type) do + 'Some other legend' | NilClass + 'Status Code' | Array + end + + with_them do + before do + subject.legend = legend + end + + it { expect(subject.query_series).to be_a(type) } + end + end + + describe '#group_title' do + shared_examples 'group_title' do |group, title| + subject { build(:prometheus_metric, group: group).group_title } + + it "returns text #{title} for group #{group}" do + expect(subject).to eq(title) + end + end + + it_behaves_like 'group_title', :business, 'Business metrics (Custom)' + it_behaves_like 'group_title', :response, 'Response metrics (Custom)' + it_behaves_like 'group_title', :system, 'System metrics (Custom)' + end + + describe '#to_query_metric' do + it 'converts to queryable metric object' do + expect(subject.to_query_metric).to be_instance_of(Gitlab::Prometheus::Metric) + end + + it 'queryable metric object has title' do + expect(subject.to_query_metric.title).to eq(subject.title) + end + + it 'queryable metric object has y_label' do + expect(subject.to_query_metric.y_label).to eq(subject.y_label) + end + + it 'queryable metric has no required_metric' do + expect(subject.to_query_metric.required_metrics).to eq([]) + end + + it 'queryable metric has weight 0' do + expect(subject.to_query_metric.weight).to eq(0) + end + + it 'queryable metrics has query description' do + queries = [ + { + query_range: subject.query, + unit: subject.unit, + label: subject.legend + } + ] + + expect(subject.to_query_metric.queries).to eq(queries) + end + end +end diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb index 4756caa1b97..da6e1b5610d 100644 --- a/spec/models/resource_label_event_spec.rb +++ b/spec/models/resource_label_event_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ResourceLabelEvent, type: :model do - subject { build(:resource_label_event) } + subject { build(:resource_label_event, issue: issue) } let(:issue) { create(:issue) } let(:merge_request) { create(:merge_request) } @@ -16,8 +16,6 @@ RSpec.describe ResourceLabelEvent, type: :model do describe 'validations' do it { is_expected.to be_valid } - it { is_expected.to validate_presence_of(:label) } - it { is_expected.to validate_presence_of(:user) } describe 'Issuable validation' do it 'is invalid if issue_id and merge_request_id are missing' do @@ -45,4 +43,52 @@ RSpec.describe ResourceLabelEvent, type: :model do end end end + + describe '#expire_etag_cache' do + def expect_expiration(issue) + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:touch) + .with("/#{issue.project.namespace.to_param}/#{issue.project.to_param}/noteable/issue/#{issue.id}/notes") + end + + it 'expires resource note etag cache on event save' do + expect_expiration(subject.issuable) + + subject.save! + end + + it 'expires resource note etag cache on event destroy' do + subject.save! + + expect_expiration(subject.issuable) + + subject.destroy! + end + end + + describe '#outdated_markdown?' do + it 'returns true if label is missing and reference is not empty' do + subject.attributes = { reference: 'ref', label_id: nil } + + expect(subject.outdated_markdown?).to be true + end + + it 'returns true if reference is not set yet' do + subject.attributes = { reference: nil } + + expect(subject.outdated_markdown?).to be true + end + + it 'returns true markdown is outdated' do + subject.attributes = { cached_markdown_version: 0 } + + expect(subject.outdated_markdown?).to be true + end + + it 'returns false if label and reference are set' do + subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION } + + expect(subject.outdated_markdown?).to be false + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 029ad7f3e9f..25eecb3f909 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -345,4 +345,31 @@ describe Service do expect(service.api_field_names).to eq(['safe_field']) end end + + context 'logging' do + let(:project) { create(:project) } + let(:service) { create(:service, project: project) } + let(:test_message) { "test message" } + let(:arguments) do + { + service_class: service.class.name, + project_path: project.full_path, + project_id: project.id, + message: test_message, + additional_argument: 'some argument' + } + end + + it 'logs info messages using json logger' do + expect(Gitlab::JsonLogger).to receive(:info).with(arguments) + + service.log_info(test_message, additional_argument: 'some argument') + end + + it 'logs error messages using json logger' do + expect(Gitlab::JsonLogger).to receive(:error).with(arguments) + + service.log_error(test_message, additional_argument: 'some argument') + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fd99acb3bb2..2a7aff39240 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -405,6 +405,23 @@ describe User do end end end + + describe '.by_username' do + it 'finds users regardless of the case passed' do + user = create(:user, username: 'CaMeLcAsEd') + user2 = create(:user, username: 'UPPERCASE') + + expect(described_class.by_username(%w(CAMELCASED uppercase))) + .to contain_exactly(user, user2) + end + + it 'finds a single user regardless of the case passed' do + user = create(:user, username: 'CaMeLcAsEd') + + expect(described_class.by_username('CAMELCASED')) + .to contain_exactly(user) + end + end end describe "Respond to" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 85c93f35c20..6890f46c724 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -381,7 +381,7 @@ describe API::Internal do it do pull(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -391,13 +391,61 @@ describe API::Internal do it do push(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end end end + context "custom action" do + let(:access_checker) { double(Gitlab::GitAccess) } + let(:message) { 'CustomActionError message' } + let(:payload) do + { + 'action' => 'geo_proxy_to_primary', + 'data' => { + 'api_endpoints' => %w{geo/proxy_git_push_ssh/info_refs geo/proxy_git_push_ssh/push}, + 'gl_username' => 'testuser', + 'primary_repo' => 'http://localhost:3000/testuser/repo.git' + } + } + end + + let(:custom_action_result) { Gitlab::GitAccessResult::CustomAction.new(payload, message) } + + before do + project.add_guest(user) + expect(Gitlab::GitAccess).to receive(:new).with( + key, + project, + 'ssh', + { + authentication_abilities: [:read_project, :download_code, :push_code], + namespace_path: project.namespace.name, + project_path: project.path, + redirected_path: nil + } + ).and_return(access_checker) + expect(access_checker).to receive(:check).with( + 'git-receive-pack', + 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master' + ).and_return(custom_action_result) + end + + context "git push" do + it do + push(key, project) + + expect(response).to have_gitlab_http_status(300) + expect(json_response['status']).to be_truthy + expect(json_response['message']).to eql(message) + expect(json_response['payload']).to eql(payload) + expect(user.reload.last_activity_on).to be_nil + end + end + end + context "blocked user" do let(:personal_project) { create(:project, namespace: user.namespace) } @@ -409,7 +457,7 @@ describe API::Internal do it do pull(key, personal_project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -419,7 +467,7 @@ describe API::Internal do it do push(key, personal_project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -445,7 +493,7 @@ describe API::Internal do it do push(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey end end @@ -477,7 +525,7 @@ describe API::Internal do it do archive(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response["status"]).to be_falsey end end @@ -489,7 +537,7 @@ describe API::Internal do pull(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response["status"]).to be_falsey end end @@ -498,7 +546,7 @@ describe API::Internal do it do pull(OpenStruct.new(id: 0), project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response["status"]).to be_falsey end end @@ -511,7 +559,7 @@ describe API::Internal do it 'rejects the SSH push' do push(key, project) - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over SSH is not allowed' end @@ -519,7 +567,7 @@ describe API::Internal do it 'rejects the SSH pull' do pull(key, project) - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over SSH is not allowed' end @@ -533,7 +581,7 @@ describe API::Internal do it 'rejects the HTTP push' do push(key, project, 'http') - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over HTTP is not allowed' end @@ -541,7 +589,7 @@ describe API::Internal do it 'rejects the HTTP pull' do pull(key, project, 'http') - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over HTTP is not allowed' end @@ -571,14 +619,14 @@ describe API::Internal do it 'rejects the push' do push(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response['status']).to be_falsy end it 'rejects the SSH pull' do pull(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response['status']).to be_falsy end end diff --git a/spec/requests/api/resource_label_events_spec.rb b/spec/requests/api/resource_label_events_spec.rb new file mode 100644 index 00000000000..b7d4a5152cc --- /dev/null +++ b/spec/requests/api/resource_label_events_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::ResourceLabelEvents do + set(:user) { create(:user) } + set(:project) { create(:project, :public, :repository, namespace: user.namespace) } + set(:private_user) { create(:user) } + + before do + project.add_developer(user) + end + + shared_examples 'resource_label_events API' do |parent_type, eventable_type, id_name| + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events" do + it "returns an array of resource label events" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(event.id) + end + + it "returns a 404 error when eventable id not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/12345/resource_label_events", user) + + expect(response).to have_gitlab_http_status(404) + end + + it "returns 404 when not authorized" do + parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events", private_user) + + expect(response).to have_gitlab_http_status(404) + end + end + + describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_label_events/:event_id" do + it "returns a resource label event by id" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/#{event.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(event.id) + end + + it "returns a 404 error if resource label event not found" do + get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_label_events/12345", user) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'when eventable is an Issue' do + let(:issue) { create(:issue, project: project, author: user) } + + it_behaves_like 'resource_label_events API', 'projects', 'issues', 'iid' do + let(:parent) { project } + let(:eventable) { issue } + let!(:event) { create(:resource_label_event, issue: issue) } + end + end + + context 'when eventable is a Merge Request' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) } + + it_behaves_like 'resource_label_events API', 'projects', 'merge_requests', 'iid' do + let(:parent) { project } + let(:eventable) { merge_request } + let!(:event) { create(:resource_label_event, merge_request: merge_request) } + end + end +end diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb index dcf4503ef9c..fa1a421d528 100644 --- a/spec/services/issuable/common_system_notes_service_spec.rb +++ b/spec/services/issuable/common_system_notes_service_spec.rb @@ -12,12 +12,21 @@ describe Issuable::CommonSystemNotesService do it_behaves_like 'system note creation', { time_estimate: 5 }, 'changed time estimate' context 'when new label is added' do + let(:label) { create(:label, project: project) } + before do - label = create(:label, project: project) issuable.labels << label + issuable.save end - it_behaves_like 'system note creation', {}, /added ~\w+ label/ + it 'creates a resource label event' do + described_class.new(project, user).execute(issuable, []) + event = issuable.reload.resource_label_events.last + + expect(event).not_to be_nil + expect(event.label_id).to eq label.id + expect(event.user_id).to eq user.id + end end context 'when new milestone is assigned' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 609eef76d2c..b5767583952 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -122,6 +122,17 @@ describe Issues::MoveService do end end + context 'issue with resource label events' do + it 'assigns resource label events to new issue' do + old_issue.resource_label_events = create_list(:resource_label_event, 2, issue: old_issue) + + new_issue = move_service.execute(old_issue, new_project) + + expected = old_issue.resource_label_events.map(&:label_id) + expect(new_issue.resource_label_events.map(&:label_id)).to match_array(expected) + end + end + context 'generic issue' do include_context 'issue move executed' diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 5bcfef46b75..07aa8449a66 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -189,11 +189,12 @@ describe Issues::UpdateService, :mailer do expect(note.note).to include "assigned to #{user2.to_reference}" end - it 'creates system note about issue label edit' do - note = find_note('added ~') + it 'creates a resource label event' do + event = issue.resource_label_events.last - expect(note).not_to be_nil - expect(note.note).to include "added #{label.to_reference} label" + expect(event).not_to be_nil + expect(event.label_id).to eq label.id + expect(event.user_id).to eq user.id end it 'creates system note about title change' do diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index 0ced5d1b6d6..9f1da7d9419 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -13,6 +13,8 @@ describe MergeRequests::BuildService do let(:description) { nil } let(:source_branch) { 'feature-branch' } let(:target_branch) { 'master' } + let(:milestone_id) { nil } + let(:label_ids) { [] } let(:merge_request) { service.execute } let(:compare) { double(:compare, commits: commits) } let(:commit_1) { double(:commit_1, sha: 'f00ba7', safe_message: "Initial commit\n\nCreate the app") } @@ -25,7 +27,9 @@ describe MergeRequests::BuildService do source_branch: source_branch, target_branch: target_branch, source_project: source_project, - target_project: target_project) + target_project: target_project, + milestone_id: milestone_id, + label_ids: label_ids) end before do @@ -179,6 +183,33 @@ describe MergeRequests::BuildService do expect(merge_request.description).to eq(expected_description) end end + + context 'when the source branch matches an internal issue' do + let(:label) { create(:label, project: project) } + let(:milestone) { create(:milestone, project: project) } + let(:source_branch) { '123-fix-issue' } + + before do + issue.update!(iid: 123, labels: [label], milestone: milestone) + end + + it 'assigns the issue label and milestone' do + expect(merge_request.milestone).to eq(milestone) + expect(merge_request.labels).to match_array([label]) + end + + context 'when milestone_id and label_ids are shared in the params' do + let(:label2) { create(:label, project: project) } + let(:milestone2) { create(:milestone, project: project) } + let(:label_ids) { [label2.id] } + let(:milestone_id) { milestone2.id } + + it 'assigns milestone_id and label_ids instead of issue labels and milestone' do + expect(merge_request.milestone).to eq(milestone2) + expect(merge_request.labels).to match_array([label2]) + end + end + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index f0029af83cc..55dfab81c26 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -109,11 +109,12 @@ describe MergeRequests::UpdateService, :mailer do expect(note.note).to include "assigned to #{user2.to_reference}" end - it 'creates system note about merge_request label edit' do - note = find_note('added ~') + it 'creates a resource label event' do + event = merge_request.resource_label_events.last - expect(note).not_to be_nil - expect(note.note).to include "added #{label.to_reference} label" + expect(event).not_to be_nil + expect(event.label_id).to eq label.id + expect(event.user_id).to eq user.id end it 'creates system note about title change' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index c442f6fe32f..68a361fa882 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1969,6 +1969,23 @@ describe NotificationService, :mailer do end end + context 'Auto DevOps notifications' do + describe '#autodevops_disabled' do + let(:owner) { create(:user) } + let(:namespace) { create(:namespace, owner: owner) } + let(:project) { create(:project, :repository, :auto_devops, namespace: namespace) } + let(:pipeline_user) { create(:user) } + let(:pipeline) { create(:ci_pipeline, :failed, project: project, user: pipeline_user) } + + it 'emails project owner and user that triggered the pipeline' do + notification.autodevops_disabled(pipeline, [owner.email, pipeline_user.email]) + + should_email(owner) + should_email(pipeline_user) + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/projects/auto_devops/disable_service_spec.rb b/spec/services/projects/auto_devops/disable_service_spec.rb new file mode 100644 index 00000000000..76977d7a1a7 --- /dev/null +++ b/spec/services/projects/auto_devops/disable_service_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Projects::AutoDevops::DisableService, '#execute' do + let(:project) { create(:project, :repository, :auto_devops) } + let(:auto_devops) { project.auto_devops } + + subject { described_class.new(project).execute } + + context 'when Auto DevOps disabled at instance level' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it { is_expected.to be_falsy } + end + + context 'when Auto DevOps enabled at instance level' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + context 'when Auto DevOps explicitly enabled on project' do + before do + auto_devops.update!(enabled: true) + end + + it { is_expected.to be_falsy } + end + + context 'when Auto DevOps explicitly disabled on project' do + before do + auto_devops.update!(enabled: false) + end + + it { is_expected.to be_falsy } + end + + context 'when Auto DevOps is implicitly enabled' do + before do + auto_devops.update!(enabled: nil) + end + + context 'when is the first pipeline failure' do + before do + create(:ci_pipeline, :failed, :auto_devops_source, project: project) + end + + it 'should disable Auto DevOps for project' do + subject + + expect(auto_devops.enabled).to eq(false) + end + end + + context 'when it is not the first pipeline failure' do + before do + create_list(:ci_pipeline, 2, :failed, :auto_devops_source, project: project) + end + + it 'should explicitly disable Auto DevOps for project' do + subject + + expect(auto_devops.reload.enabled).to eq(false) + end + end + + context 'when an Auto DevOps pipeline has succeeded before' do + before do + create(:ci_pipeline, :success, :auto_devops_source, project: project) + end + + it 'should not disable Auto DevOps for project' do + subject + + expect(auto_devops.reload.enabled).to be_nil + end + end + end + + context 'when project does not have an Auto DevOps record related' do + let(:project) { create(:project, :repository) } + + before do + create(:ci_pipeline, :failed, :auto_devops_source, project: project) + end + + it 'should disable Auto DevOps for project' do + subject + auto_devops = project.reload.auto_devops + + expect(auto_devops.enabled).to eq(false) + end + + it 'should create a ProjectAutoDevops record' do + expect { subject }.to change { ProjectAutoDevops.count }.from(0).to(1) + end + end + end +end diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb index 41b0fb3eea3..4c9138fb1ef 100644 --- a/spec/services/resource_events/change_labels_service_spec.rb +++ b/spec/services/resource_events/change_labels_service_spec.rb @@ -18,6 +18,14 @@ describe ResourceEvents::ChangeLabelsService do expect(event.action).to eq(action) end + it 'expires resource note etag cache' do + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:touch) + .with("/#{resource.project.namespace.to_param}/#{resource.project.to_param}/noteable/issue/#{resource.id}/notes") + + described_class.new(resource, author).execute(added_labels: [labels[0]]) + end + context 'when adding a label' do let(:added) { [labels[0]] } let(:removed) { [] } diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb new file mode 100644 index 00000000000..0d333d541c9 --- /dev/null +++ b/spec/services/resource_events/merge_into_notes_service_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ResourceEvents::MergeIntoNotesService do + def create_event(params) + event_params = { action: :add, label: label, issue: resource, + user: user } + + create(:resource_label_event, event_params.merge(params)) + end + + def create_note(params) + opts = { noteable: resource, project: project } + + create(:note_on_issue, opts.merge(params)) + end + + set(:project) { create(:project) } + set(:user) { create(:user) } + set(:resource) { create(:issue, project: project) } + set(:label) { create(:label, project: project) } + set(:label2) { create(:label, project: project) } + let(:time) { Time.now } + + describe '#execute' do + it 'merges label events into notes in order of created_at' do + note1 = create_note(created_at: 4.days.ago) + note2 = create_note(created_at: 2.days.ago) + event1 = create_event(created_at: 3.days.ago) + event2 = create_event(created_at: 1.day.ago) + + notes = described_class.new(resource, user).execute([note1, note2]) + + expected = [note1, event1, note2, event2].map(&:discussion_id) + expect(notes.map(&:discussion_id)).to eq expected + end + + it 'squashes events with same time and author into single note' do + user2 = create(:user) + + create_event(created_at: time) + create_event(created_at: time, label: label2, action: :remove) + create_event(created_at: time, user: user2) + create_event(created_at: 1.day.ago, label: label2) + + notes = described_class.new(resource, user).execute() + + expected = [ + "added #{label.to_reference} label and removed #{label2.to_reference} label", + "added #{label.to_reference} label", + "added #{label2.to_reference} label" + ] + + expect(notes.count).to eq 3 + expect(notes.map(&:note)).to match_array expected + end + + it 'fetches only notes created after last_fetched_at' do + create_event(created_at: 4.days.ago) + event = create_event(created_at: 1.day.ago) + + notes = described_class.new(resource, user, + last_fetched_at: 2.days.ago.to_i).execute() + + expect(notes.count).to eq 1 + expect(notes.first.discussion_id).to eq event.discussion_id + end + end +end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 442de61f69b..d5d750e182b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -197,45 +197,6 @@ describe SystemNoteService do end end - describe '.change_label' do - subject { described_class.change_label(noteable, project, author, added, removed) } - - let(:labels) { create_list(:label, 2, project: project) } - let(:added) { [] } - let(:removed) { [] } - - it_behaves_like 'a system note' do - let(:action) { 'label' } - end - - context 'with added labels' do - let(:added) { labels } - let(:removed) { [] } - - it 'sets the note text' do - expect(subject.note).to eq "added ~#{labels[0].id} ~#{labels[1].id} labels" - end - end - - context 'with removed labels' do - let(:added) { [] } - let(:removed) { labels } - - it 'sets the note text' do - expect(subject.note).to eq "removed ~#{labels[0].id} ~#{labels[1].id} labels" - end - end - - context 'with added and removed labels' do - let(:added) { [labels[0]] } - let(:removed) { [labels[1]] } - - it 'sets the note text' do - expect(subject.note).to eq "added ~#{labels[0].id} and removed ~#{labels[1].id} labels" - end - end - end - describe '.change_milestone' do context 'for a project milestone' do subject { described_class.change_milestone(noteable, project, author, milestone) } @@ -725,7 +686,7 @@ describe SystemNoteService do let(:jira_tracker) { project.jira_service } let(:commit) { project.commit } let(:comment_url) { jira_api_comment_url(jira_issue.id) } - let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." } + let(:success_message) { "SUCCESS: Successfully posted to http://jira.example.net." } before do stub_jira_urls(jira_issue.id) diff --git a/spec/services/wikis/create_attachment_service_spec.rb b/spec/services/wikis/create_attachment_service_spec.rb index 3f4da873ce4..f5899f292c8 100644 --- a/spec/services/wikis/create_attachment_service_spec.rb +++ b/spec/services/wikis/create_attachment_service_spec.rb @@ -88,8 +88,30 @@ describe Wikis::CreateAttachmentService do end end - describe 'validations' do + describe '#parse_file_name' do context 'when file_name' do + context 'has white spaces' do + let(:file_name) { 'file with spaces' } + + it "replaces all of them with '_'" do + result = service.execute + + expect(result[:status]).to eq :success + expect(result[:result][:file_name]).to eq 'file_with_spaces' + end + end + + context 'has other invalid characters' do + let(:file_name) { "file\twith\tinvalid chars" } + + it "replaces all of them with '_'" do + result = service.execute + + expect(result[:status]).to eq :success + expect(result[:result][:file_name]).to eq 'file_with_invalid_chars' + end + end + context 'is not present' do let(:file_name) { nil } diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 683a64504a1..994a2aaef90 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -16,6 +16,7 @@ module KubernetesHelpers def stub_kubeclient_discover(api_url) WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body)) + WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body)) end def stub_kubeclient_pods(response = nil) @@ -66,7 +67,8 @@ module KubernetesHelpers "resources" => [ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, - { "name" => "secrets", "namespaced" => true, "kind" => "Secret" } + { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "services", "namespaced" => true, "kind" => "Service" } ] } end @@ -77,7 +79,20 @@ module KubernetesHelpers "resources" => [ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, - { "name" => "secrets", "namespaced" => true, "kind" => "Secret" } + { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "services", "namespaced" => true, "kind" => "Service" } + ] + } + end + + def kube_v1_rbac_authorization_discovery_body + { + "kind" => "APIResourceList", + "resources" => [ + { "name" => "clusterrolebindings", "namespaced" => false, "kind" => "ClusterRoleBinding" }, + { "name" => "clusterroles", "namespaced" => false, "kind" => "ClusterRole" }, + { "name" => "rolebindings", "namespaced" => true, "kind" => "RoleBinding" }, + { "name" => "roles", "namespaced" => true, "kind" => "Role" } ] } end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 21103771d1f..3f8e3ae5190 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -52,7 +52,8 @@ module TestEnv 'add_images_and_changes' => '010d106', 'update-gitlab-shell-v-6-0-1' => '2f61d70', 'update-gitlab-shell-v-6-0-3' => 'de78448', - '2-mb-file' => 'bf12d25' + '2-mb-file' => 'bf12d25', + 'with-codeowners' => '219560e' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/shared_examples/models/label_note_shared_examples.rb b/spec/support/shared_examples/models/label_note_shared_examples.rb new file mode 100644 index 00000000000..5803b3af74b --- /dev/null +++ b/spec/support/shared_examples/models/label_note_shared_examples.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +shared_examples 'label note created from events' do + def create_event(params = {}) + event_params = { action: :add, label: label, user: user } + resource_key = resource.class.name.underscore.to_s + event_params[resource_key] = resource + + build(:resource_label_event, event_params.merge(params)) + end + + def label_refs(events) + sorted_labels = events.map(&:label).compact.sort_by(&:title) + + sorted_labels.map { |l| l.to_reference}.join(' ') + end + + let(:time) { Time.now } + let(:local_label_ids) { [label.id, label2.id] } + + describe '.from_events' do + it 'returns system note with expected attributes' do + event = create_event + + note = described_class.from_events([event, create_event]) + + expect(note.system).to be true + expect(note.author_id).to eq event.user_id + expect(note.discussion_id).to eq event.discussion_id + expect(note.noteable).to eq event.issuable + expect(note.note).to be_present + expect(note.note_html).to be_present + end + + it 'updates markdown cache if reference is not set yet' do + event = create_event(reference: nil) + + described_class.from_events([event]) + + expect(event.reference).not_to be_nil + end + + it 'updates markdown cache if label was deleted' do + event = create_event(reference: 'some_ref', label: nil) + + described_class.from_events([event]) + + expect(event.reference).to eq '' + end + + it 'returns html note' do + events = [create_event(created_at: time)] + + note = described_class.from_events(events) + + expect(note.note_html).to include label.title + end + + it 'returns text note for added labels' do + events = [create_event(created_at: time), + create_event(created_at: time, label: label2), + create_event(created_at: time, label: nil)] + + note = described_class.from_events(events) + + expect(note.note).to eq "added #{label_refs(events)} + 1 deleted label" + end + + it 'returns text note for removed labels' do + events = [create_event(action: :remove, created_at: time), + create_event(action: :remove, created_at: time, label: label2), + create_event(action: :remove, created_at: time, label: nil)] + + note = described_class.from_events(events) + + expect(note.note).to eq "removed #{label_refs(events)} + 1 deleted label" + end + + it 'returns text note for added and removed labels' do + add_events = [create_event(created_at: time), + create_event(created_at: time, label: nil)] + + remove_events = [create_event(action: :remove, created_at: time), + create_event(action: :remove, created_at: time, label: nil)] + + note = described_class.from_events(add_events + remove_events) + + expect(note.note).to eq "added #{label_refs(add_events)} + 1 deleted label and removed #{label_refs(remove_events)} + 1 deleted label" + end + + it 'returns text note for cross-project label' do + other_label = create(:label) + event = create_event(label: other_label) + + note = described_class.from_events([event]) + + expect(note.note).to eq "added #{other_label.to_reference(resource_parent)} label" + end + + it 'returns text note for cross-group label' do + other_label = create(:group_label) + event = create_event(label: other_label) + + note = described_class.from_events([event]) + + expect(note.note).to eq "added #{other_label.to_reference(other_label.group, target_project: project, full: true)} label" + end + end +end diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb index cc2cca10f58..19794227d9f 100644 --- a/spec/tasks/gitlab/cleanup_rake_spec.rb +++ b/spec/tasks/gitlab/cleanup_rake_spec.rb @@ -6,6 +6,8 @@ describe 'gitlab:cleanup rake tasks' do end describe 'cleanup namespaces and repos' do + let(:gitlab_shell) { Gitlab::Shell.new } + let(:storage) { storages.keys.first } let(:storages) do { 'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage')) @@ -17,53 +19,56 @@ describe 'gitlab:cleanup rake tasks' do end before do - FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage')) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end after do - FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage')) + Gitlab::GitalyClient::StorageService.new(storage).delete_all_repositories end describe 'cleanup:repos' do before do - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/broken/project.git')) - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git')) + gitlab_shell.add_namespace(storage, 'broken/project.git') + gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git') end it 'moves it to an orphaned path' do - run_rake_task('gitlab:cleanup:repos') - repo_list = Dir['tmp/tests/default_storage/broken/*'] + now = Time.now + + Timecop.freeze(now) do + run_rake_task('gitlab:cleanup:repos') + repo_list = Gitlab::GitalyClient::StorageService.new(storage).list_directories(depth: 0) - expect(repo_list.first).to include('+orphaned+') + expect(repo_list.last).to include("broken+orphaned+#{now.to_i}") + end end it 'ignores @hashed repos' do run_rake_task('gitlab:cleanup:repos') - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy + expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true) end end describe 'cleanup:dirs' do it 'removes missing namespaces' do - FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_1/project.git")) - FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_2/project.git")) - allow(Namespace).to receive(:pluck).and_return('namespace_1') + gitlab_shell.add_namespace(storage, "namespace_1/project.git") + gitlab_shell.add_namespace(storage, "namespace_2/project.git") + allow(Namespace).to receive(:pluck).and_return(['namespace_1']) stub_env('REMOVE', 'true') run_rake_task('gitlab:cleanup:dirs') - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_1'))).to be_truthy - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_2'))).to be_falsey + expect(gitlab_shell.exists?(storage, 'namespace_1')).to be(true) + expect(gitlab_shell.exists?(storage, 'namespace_2')).to be(false) end it 'ignores @hashed directory' do - FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git')) + gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git') run_rake_task('gitlab:cleanup:dirs') - expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy + expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true) end end end diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb index 71fe2c353c0..eafbea07e10 100644 --- a/spec/uploaders/namespace_file_uploader_spec.rb +++ b/spec/uploaders/namespace_file_uploader_spec.rb @@ -26,6 +26,20 @@ describe NamespaceFileUploader do upload_path: IDENTIFIER end + context '.base_dir' do + it 'returns local storage base_dir without store param' do + expect(described_class.base_dir(group)).to eq("uploads/-/system/namespace/#{group.id}") + end + + it 'returns local storage base_dir when store param is Store::LOCAL' do + expect(described_class.base_dir(group, ObjectStorage::Store::LOCAL)).to eq("uploads/-/system/namespace/#{group.id}") + end + + it 'returns remote base_dir when store param is Store::REMOTE' do + expect(described_class.base_dir(group, ObjectStorage::Store::REMOTE)).to eq("namespace/#{group.id}") + end + end + describe "#migrate!" do before do uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt'))) diff --git a/spec/workers/auto_devops/disable_worker_spec.rb b/spec/workers/auto_devops/disable_worker_spec.rb new file mode 100644 index 00000000000..800a07e41a5 --- /dev/null +++ b/spec/workers/auto_devops/disable_worker_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe AutoDevops::DisableWorker, '#perform' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :auto_devops) } + let(:auto_devops) { project.auto_devops } + let(:pipeline) { create(:ci_pipeline, :failed, :auto_devops_source, project: project, user: user) } + + subject { described_class.new } + + before do + stub_application_setting(auto_devops_enabled: true) + auto_devops.update_attribute(:enabled, nil) + end + + it 'disables auto devops for project' do + subject.perform(pipeline.id) + + expect(auto_devops.reload.enabled).to eq(false) + end + + context 'when project owner is a user' do + let(:owner) { create(:user) } + let(:namespace) { create(:namespace, owner: owner) } + let(:project) { create(:project, :repository, :auto_devops, namespace: namespace) } + + it 'sends an email to pipeline user and project owner' do + expect(NotificationService).to receive_message_chain(:new, :autodevops_disabled).with(pipeline, [user.email, owner.email]) + + subject.perform(pipeline.id) + end + end + + context 'when project does not have owner' do + let(:group) { create(:group) } + let(:project) { create(:project, :repository, :auto_devops, namespace: group) } + + it 'sends an email to pipeline user' do + expect(NotificationService).to receive_message_chain(:new, :autodevops_disabled).with(pipeline, [user.email]) + + subject.perform(pipeline.id) + end + end + + context 'when pipeline is not related to a user and project does not have owner' do + let(:group) { create(:group) } + let(:project) { create(:project, :repository, :auto_devops, namespace: group) } + let(:pipeline) { create(:ci_pipeline, :failed, project: project) } + + it 'does not send an email' do + expect(NotificationService).not_to receive(:new) + + subject.perform(pipeline.id) + end + end +end |
